142 Commits

Author SHA1 Message Date
67a16bc604 Makes the addTitlePage command's output translatable
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2025-08-15 16:24:27 +02:00
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
0fe9ccb590 Bumps version for release
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2024-09-07 01:34:42 +02:00
1a7236cebd Updates KnarLib and fixes pom 2024-09-07 01:33:38 +02:00
e482e494f8 Fixes some warnings 2024-09-07 01:27:00 +02:00
0f76c8f869 Changes unsign behavior to work on 1.21 2024-09-07 01:26:52 +02:00
6b4e87d33a Bumps Spigot version 2024-09-07 01:16:49 +02:00
40b0fa0baf Updates dependencies
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2024-05-03 15:53:55 +02:00
31bb26b755 Makes it clear that use includes peekbookshelf
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2023-06-24 12:40:55 +02:00
0e134fcdab Bumps version back to dev version
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2023-06-24 12:34:18 +02:00
f5ba4db5ac Bumps version for release
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2023-06-24 12:15:22 +02:00
29d5247c9b Bumps KnarLib version
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2023-06-24 12:09:31 +02:00
c389c6fbcb Minor changes
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
Changes bookshelf peeking to require sneaking
Makes sure plain books are displayed in the book list
2023-06-24 12:03:37 +02:00
1158820f97 Fixes some issues
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
Fixes the API version being read incorrectly by Paper
Fixes the check for missing title or author for a book in a bookshelf
2023-06-23 18:10:45 +02:00
f9908db88f 1.20 update
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
Adds an optional feature which displays the contents of a bookshelf when left-clicked. Sneaking must be used to destroy the bookshelf when enabled.
Updates depreciated sign code.
Bumps the API version to 1.20
Builds against 1.20
2023-06-23 17:57:54 +02:00
738dbe6e30 Updates version to 1.3.4-SNAPSHOT
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good
2022-11-26 17:20:41 +01:00
3838750f64 Adds Jenkinsfile
Some checks failed
EpicKnarvik97/Books-Without-Borders/pipeline/head There was a failure building this commit
2022-11-26 15:54:17 +01:00
16954f22e3 Merge branch 'master' into knarlib 2022-11-26 15:52:17 +01:00
6359af02e6 Fixes table formatting 2022-11-14 00:49:58 +00:00
6ae15acc72 Bumps version to 1.3.3 2022-11-08 20:37:06 +01:00
2c80e4117e Lists real file names, in addition to converted file names 2022-11-08 20:36:17 +01:00
6d539a14df Fixes the files provided in the tab-completion not translating to actual files 2022-11-08 20:34:53 +01:00
51371736e4 Removes KnarLib.setPlugin call 2022-11-07 22:24:33 +01:00
2383dfd2ef Renames FileHelper to BookFileHelper
Makes sure books are saved and read as UTF-8
2022-11-07 09:23:25 +01:00
04d4bc8303 Uses KnarLib where possible 2022-11-07 01:43:38 +01:00
184f78d935 Adds a missing / to the README 2022-11-05 12:35:27 +01:00
95b0b42fd1 Bumps version to 1.3.2 2022-11-04 18:47:31 +01:00
73eb903517 Implements RGB support for setLore and setName 2022-11-04 18:39:58 +01:00
b577b1ff75 Bumps version to 1.3.1 2022-11-04 16:19:27 +01:00
c27756f7dc Clears author, generation and title data just in case 2022-11-04 16:19:15 +01:00
90bab4a148 Fixes various bugs in the new features
Fixes the clearBook command not being aborted when no book is held.
Adds missing clearBook success message
Fixes a bug where too many empty books would be taken.
Adds some missing null checks when looking for empty books.
2022-11-04 16:06:27 +01:00
f005b8f8e5 Adds clearBook command #7 and improves README 2022-11-04 13:01:56 +01:00
0fdfd81579 Prevents WIP books from being lost
This change adds a workaround for writable books used for payment. Only books that don't contain any text can be used for payment, thus preventing any books with important text from being lost.
2022-11-04 03:37:38 +01:00
6820b71dde Updates spigot version to 1.19.2 2022-11-04 03:07:32 +01:00
28b7849ff5 Tries to fix #9
Sets the amount of books to the same amount as the old book
2022-11-04 03:05:30 +01:00
e893684dec Prevents consumption of dye when clicking plugin signs 2022-08-20 14:54:40 +02:00
f1a8db4f5e Merge branch 'master' of https://git.knarcraft.net/EpicKnarvik97/Books-Without-Borders
 Conflicts:
	README.md
2022-08-20 14:16:12 +02:00
47fabe5806 Merge pull request 'Excapes <' (#8) from epicknarvik97-patch-1 into master
Reviewed-on: #8
2022-08-20 14:13:31 +02:00
ec6def15f9 Excapes < 2022-08-20 14:12:20 +02:00
13cbe883ed Adds missing mention of generation for alterbooks permission node 2022-08-14 11:36:46 +02:00
d2403d247b Adds bypassauthoronlysave permission to README 2022-08-11 01:16:19 +02:00
c6c018ee88 Adds permission to bypass author only save 2022-08-11 01:14:50 +02:00
8b47aeb8f2 Prevents some unintended usage of UUID for filenames 2022-08-11 00:53:29 +02:00
396c0a9d41 Updates version to 1.3.0, as a lot of changes have been performed 2022-08-10 18:39:21 +02:00
c995a4fc0f Stores author name as UUID if storing own books
This change basically always stores the player's own books under their UUID, thus preventing being denied access to their own books if they change their username. The UUID is converted back to the username on the fly, so it shouldn't be noticeable for the players.
2022-08-10 18:36:06 +02:00
40512dd771 Adds an option to only allow saving own books 2022-08-10 14:52:05 +02:00
5d340af6f2 Changes player folders to UUIDs to prevent problems if users change their names 2022-08-10 13:44:11 +02:00
70ad6390db Updates README a bit 2022-08-10 12:25:41 +02:00
2a5c5b310d Adds missing permission to README 2022-08-10 12:18:58 +02:00
94375eee4b Adds a command for changing book generation, and updates README #5 2022-08-10 02:49:44 +02:00
1eb9c370bc Fixes some messages not being sent using the correct methods 2022-08-10 01:04:54 +02:00
1100f181be Treats null book generation as the original 2022-08-10 00:55:30 +02:00
7467645bcd Fixes an incorrect boolean 2022-08-10 00:35:34 +02:00
11108011a5 Removes duplicate payment 2022-08-10 00:33:56 +02:00
ce249a93b3 Increases book generation for all loaded books #4 2022-08-10 00:26:23 +02:00
7c229fb459 Fixes root node of the new config options 2022-08-10 00:19:46 +02:00
f243bf32e7 Adds an option which mimics the vanilla book copying behavior 2022-08-10 00:16:33 +02:00
a7c284ade2 Adds an option for only allowing un-signing by the author 2022-08-09 17:21:55 +02:00
0d4d87373c Fixes redundancy in book filename generation, and saves Generation 2022-08-09 16:56:38 +02:00
542cd03bdc Fixes an inconsistency where "," is always used as separator for unsigned books 2022-08-09 16:07:37 +02:00
5e85dfd3e4 Displays no permission required as None instead of null 2022-08-05 14:15:30 +02:00
af0b0fd12e Updates version to 1.2.3 2022-08-05 12:03:46 +02:00
a963733734 Builds against 1.19.1 2022-08-05 12:01:08 +02:00
7dd201d0d0 Merge branch 'master' of https://git.knarcraft.net/EpicKnarvik97/Books-Without-Borders 2022-07-19 18:35:52 +02:00
a95d737414 Fixes an exception when trying to save a book without holding a book 2022-07-19 18:08:10 +02:00
0ce85af61b 1.2.2 Filtered tab completion and update notice 2022-02-19 20:06:35 +01:00
ed54ae84b5 Updates README with command and permission info 2022-01-18 14:46:55 +01:00
27d5980fa8 Updates version to 1.2.1 2022-01-17 18:15:34 +01:00
544f6f69fe Fixes a NullPointerException caused by getting the size of a null list 2022-01-17 18:13:48 +01:00
055d7fca60 Improves some command and permission descriptions 2022-01-17 13:10:53 +01:00
8f7cfc591f Updates version to 1.2 2022-01-17 12:03:58 +01:00
2aa25295af Improves some formatting and gets rid of BooksWithoutBordersSettings.java 2022-01-17 11:55:52 +01:00
d423b1e109 Adds a new option to format books when signed
Also cleans up config stuff a bit and moves config-related tasks to its own class
Moves book loading code to its own class
Adds a default config file to make the config file have comments
Adds missing command info about the formatBook command
Adds information about required permissions to the command info
2022-01-17 11:38:43 +01:00
7acaa9fc81 Adds a new /formatbook command 2022-01-17 00:47:10 +01:00
94589faba0 Updates to Minecraft 1.18 and Java 17 2022-01-17 00:46:52 +01:00
85 changed files with 6730 additions and 1893 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/

33
Jenkinsfile vendored Normal file
View File

@@ -0,0 +1,33 @@
pipeline {
agent any
tools {
jdk 'JDK17'
}
stages {
stage('Build') {
steps {
echo 'Building...'
sh 'mvn clean & mvn validate & mvn compile'
}
}
stage('Test') {
steps {
echo 'Testing...'
sh 'mvn test'
}
}
stage('Verify') {
steps {
echo 'Verifying...'
sh 'mvn verify -Dmaven.test.skip=true'
}
}
stage('Deploy') {
steps {
echo 'Deploying...'
sh 'mvn deploy -Dmaven.install.skip=true -Dmaven.test.skip=true'
archiveArtifacts artifacts: '**/target/*.jar', fingerprint: true
}
}
}
}

161
README.md
View File

@@ -1,10 +1,9 @@
# Books Without Borders # Books Without Borders
This is an attempt at a rewrite of the Books Without Borders plugin. This rewrite uses the source code given This is a rewrite of the Books Without Borders plugin. This rewrite originally used the source code given
at [the original bukkit page](https://dev.bukkit.org/projects/books-without-borders). I'm not planning any new features at [the original bukkit page](https://dev.bukkit.org/projects/books-without-borders). While the old plugin still worked
at this time. The only goal is to make it 1.17.1 compliant, but I'll make the code more maintainable along the way. the last time I checked, this plugin does not use any depreciated function calls, making sure it works for the
While the original version still works, it's using a lot of depreciated function calls which will most likely break in foreseeable future.
the future.
## Books without Borders! ## Books without Borders!
@@ -13,40 +12,158 @@ 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
* Encrypt books to prevent other players from reading them - Encrypt books to prevent other players from reading them
* Give, encrypt, or decrypt held books with signs - Give, encrypt, or decrypt held books with signs
* Give players books via command blocks - Give players books via command blocks
* Unsign or copy held books with a simple command - Unsign or copy held books with a simple command
* Give first time players a single book or a set of books when they join - Give first time players a single book or a set of books when they join
* 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`
- Clear a book, and start anew with `/clearbook`
- Formatting and color codes can be turned into formatting once any book is signed. This is enabled through a config
value
- Change generation of books. Create tattered books for your RPG server!
- Optionally, make it impossible to duplicate the original version of a book
- Optionally, hit a bookshelf while sneaking to display the contained books. 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:
An in-game description of available commands is available through the /bwb command.
| Command | Arguments | Permission | Description |
|----------------------|----------------------------------------------------------------------------------|-----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| /bookswithoutborders | None | | Displays information about commands (and permissions if the user has bookswithoutborders.admin) |
| /copybook | \<# of copies> | bookswithoutborders.copy | Copies the book the player is holding |
| /decryptbook | \<key> | bookswithoutborders.decrypt | Decrypts the book the player is holding. "key" is required and MUST be IDENTICAL to the key used to encrypt the held book |
| /deletebook | \<file name or number> | bookswithoutborders.delete | Deletes the specified file in the player's directory |
| /deletepublicbook | \<file name or number> | bookswithoutborders.admin | Same as deletebook, but deletes files in the public directory |
| /encryptbook | \<key> \[encryption style] | bookswithoutborders.encrypt | Encrypts the book the player is holding. "key" is required and can be any phrase or number excluding spaces. "style" is not required. Possible values are "DNA" or "" |
| /formatbook | None | bookswithoutborders.format | Formats the held written book (converts color and formatting codes to the corresponding formatted text) |
| /givebook | \<file name or number> \<playername> \[# of copies (num)] \[signed (true/false)] | bookswithoutborders.give | Gives the selected player a book from your personal directory |
| /givepublicbook | \<file name or number> \<playername> \[# of copies (num)] \[signed (true/false)] | bookswithoutborders.givepublic | Same as givebook, but uses books from the public directory |
| /loadbook | \<file name or number> \[# of copies] \[signed (true/false)] | bookswithoutborders.load | Creates a book from the specified file and gives it to the player. If no file is specified, a list of available files is returned. If true is specified, the book will be signed, if false it will be unsigned |
| /loadpublicbook | \<file name or number> \[# of copies] \[signed (true/false)] | bookswithoutborders.loadpublic | Same as loadbook, but views files in the public directory |
| /reload | None | bookswithoutborders.reload | Reloads BwB's configuration file |
| /savebook | \[overwrite (true/false)] | bookswithoutborders.save | Saves the book the player is holding to a text file in a private directory. If true is specified, a book of the same name by the same author will be overwritten by the new book |
| /savepublicbook | \[overwrite (true/false)] | bookswithoutborders.savepublic | Same as savebook, but saves files in the public directory |
| /setbookauthor | \<author> | bookswithoutborders.setauthor | Sets the author of the book the player is holding |
| /setbookgeneration | \<generation> | bookswithoutborders.setgeneration | Sets the generation of the held book (ORIGINAL, COPY_OF_ORIGINAL, COPY_OF_COPY, TATTERED) |
| /setbookprice | \<item/eco> \<quantity> | bookswithoutborders.setbookprice | Sets the per-book price to create a book via commands. If "Item", the item in the player's hand in the amount of \<quantity> will be the price. If "Eco", a Vault based economy will be used for price. If neither \<Item/Eco> nor \<quantity> are specified, the current price to create books will be removed. |
| /setlore | \<new lore> | bookswithoutborders.setlore | Sets the lore of the item the player is holding. Insert the lore_line_separator character to force a new line ("~" by default) |
| /settitle | \<title> | bookswithoutborders.settitle | Sets the title of the book/item the player is holding |
| /unsignbook | None | bookswithoutborders.unsign | Un-signs the book the player is holding |
| /clearbook | None | bookswithoutborders.clear | Removes all text from the held un-signed book |
| /setBookshelfData | \<delete/name/lore> \text> \[more text] | bookswithoutborders.editbookshelf | Sets the name/lore for a bookshelf which is shown when peeking at its contents. |
| /addBookTitlePage | \[page index] \[title~description] | bookswithoutborders.addtitlepage | Adds a blank page, title page or chapter page depending on input and whether the book is signed |
| /deleteBookPage | \<page> | bookswithoutborders.deletepage | Deletes one page from a book |
| /migrateBooks | None | bookswithoutborders.admin | Migrates all txt books to yml, and fixes any incorrect filenames. |
### Permissions:
| Node | Description |
|--------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------|
| bookswithoutborders.* | Grants all permissions |
| bookswithoutborders.admin | Grants all permissions |
| bookswithoutborders.use | bookswithoutborders.use - Allows player to use commands to save/load/delete in their personal directory, and peeking at bookshelves if enabled |
| bookswithoutborders.alterbooks | Allows player to change books' data such as lore/title/author/generation/formatting and un-signing books, and setting bookshelf data |
| bookswithoutborders.reload | Allows player to reload this plugin |
| bookswithoutborders.format | Allows a player to format a book |
| bookswithoutborders.save | Allows a player to save books to their personal directory |
| bookswithoutborders.load | Allows player to load books from their personal directory |
| bookswithoutborders.delete | Allows player to delete books from their personal directory |
| bookswithoutborders.unsign | Allows player to un-sign books |
| bookswithoutborders.copy | Allows player to copy books |
| bookswithoutborders.loadpublic | Allows player to load from the public directory |
| bookswithoutborders.savepublic | Allows player to save to the public directory |
| bookswithoutborders.encrypt | Allows player to encrypt books |
| bookswithoutborders.groupencrypt | Allows player to use group-based encryption |
| bookswithoutborders.decrypt | Allows player to decrypt books |
| bookswithoutborders.decrypt.agroup | Allows player to decrypt books group-encrypted for group "agroup" |
| bookswithoutborders.signs | Allows player to create signs that give/encrypt/decrypt books |
| bookswithoutborders.give | Allows player to give another player one of their privately saved books |
| bookswithoutborders.givepublic | Allows a player to give another player a book from the public directory |
| bookswithoutborders.settitle | Allows player to set the title of the currently held book |
| bookswithoutborders.setauthor | Allows player to set the author of the currently held book |
| bookswithoutborders.setlore | Allows player to set the lore of the currently held item |
| bookswithoutborders.bypassauthoronlycopy | Allows player to ignore Author_Only_Copy config setting |
| bookswithoutborders.bypassauthoronlyunsign | Allows player to ignore Author_Only_Unsign config setting |
| bookswithoutborders.bypassauthoronlysave | Allows player to ignore Author_Only_Save config setting |
| bookswithoutborders.bypassbookprice | Allows player to ignore Price_to_create_book config setting |
| bookswithoutborders.setbookprice | Allows player to set the cost of creating a book |
| bookswithoutborders.setgeneration | Allows player to change the generation of a book (Original, Copy, Copy of Copy) |
| bookswithoutborders.clear | Allows player to clear the contents of the held writable book |
| bookswithoutborders.peekbookshelf | Allows player to left-click a bookshelf to see the contents of the shelf |
| 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
This plugin supports several custom signs with special functionality. Each plugin sign must have [BwB] on its first This plugin supports several custom signs with special functionality. Each plugin sign must have \[BwB] on its first
line. line.
#### Give sign #### Give sign
The **_give_** sign must have **[Give]** on its second line. The third and fourth line contains the book to be loaded. The **_give_**-sign must have **\[Give]** on its second line. The third and fourth line contains the book to be loaded.
This can either be a numerical id pointing to a publicly saved book, or the full text identifier of the book (book name, This can either be a numerical id pointing to a publicly saved book, or the full text identifier of the book (book name,
author). author).
#### Encrypt sign #### Encrypt sign
The **_encrypt_** sign must have **[Encrypt]** on its second line. The third line must contain the encryption key The The **_encrypt_**-sign must have **\[Encrypt]** on its second line. The third line must contain the encryption key The
fourth line can be empty or contain "dna" for dna-based encryption. fourth line can be empty or contain "dna" for dna-based encryption.
#### Decrypt sign #### Decrypt sign
The **_decrypt_** sign must have **[Decrypt]** on its second line. The third line must contain the decryption key The **_decrypt_** sign must have **\[Decrypt]** on its second line. The third line must contain the decryption key
### Configuration options:
| Option | Description |
|----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Max_Number_of_Duplicates | The maximum number of duplicates of a saved book allowed |
| Author_Separator | The separator used to separate the book title and the book author |
| Lore_line_separator | The separator used to denote a new line in the book/item lore |
| Books_for_new_players | A list of books given to new players the first time they join the server |
| Message_for_new_players | An optional message displayed to new players the first time they join the server |
| Price_to_create_book.Item_type | The item type used as currency for copying books. Use "Economy" to use money instead of items |
| Price_to_create_book.Required_quantity | The quantity of currency required to pay for each book produced |
| Admin_Auto_Decrypt | Whether any admin can decrypt any book regardless of the group it was encrypted for |
| Author_Only_Copy | Whether to only allow the author of a book to create copies |
| Author_Only_Unsign | Whether to only allow the author of a book to unsign it |
| Author_Only_Save | Whether to only allow saving a player's own books with /savebook |
| Format_Book_After_Signing | Whether to automatically format every book when it's signed |
| Change_Generation_On_Copy | Whether to display "COPY" or "COPY_OF_COPY" instead of "ORIGINAL" when a book is copied. This also uses the vanilla behavior where a copy of a copy or tattered book cannot be copied further. |
| Enable_Book_Peeking | Whether to enable hitting a chiseled bookshelf while sneaking to see the shelf's contents. |
| Use_Real_Encryption | Enables true AES encryption instead of the very fake legacy encryption. The encryption key is stored in the book file to allow admin decryption, but looking at the encrypted book in the file system, only reveals the encrypted pages. |

66
pom.xml
View File

@@ -6,7 +6,7 @@
<groupId>net.knarcraft</groupId> <groupId>net.knarcraft</groupId>
<artifactId>BooksWithoutBorders</artifactId> <artifactId>BooksWithoutBorders</artifactId>
<version>1.0</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>
@@ -38,7 +38,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId> <artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version> <version>3.3.0</version>
<executions> <executions>
<execution> <execution>
<phase>package</phase> <phase>package</phase>
@@ -47,6 +47,30 @@
</goals> </goals>
<configuration> <configuration>
<createDependencyReducedPom>false</createDependencyReducedPom> <createDependencyReducedPom>false</createDependencyReducedPom>
<relocations>
<relocation>
<pattern>net.knarcraft.knarlib</pattern>
<shadedPattern>net.knarcraft.bookswithoutborders.lib.knarlib</shadedPattern>
</relocation>
<relocation>
<pattern>org.jetbrains.annotations</pattern>
<shadedPattern>net.knarcraft.bookswithoutborders.lib.annotations</shadedPattern>
</relocation>
</relocations>
<filters>
<filter>
<artifact>net.knarcraft:knarlib</artifact>
<includes>
<include>net/knarcraft/knarlib/**</include>
</includes>
</filter>
<filter>
<artifact>org.jetbrains:annotations</artifact>
<includes>
<include>org/jetbrains/annotations/**</include>
</includes>
</filter>
</filters>
</configuration> </configuration>
</execution> </execution>
</executions> </executions>
@@ -61,6 +85,10 @@
</build> </build>
<repositories> <repositories>
<repository>
<id>knarcraft-repo</id>
<url>https://git.knarcraft.net/api/packages/EpicKnarvik97/maven</url>
</repository>
<repository> <repository>
<id>spigotmc-repo</id> <id>spigotmc-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url> <url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
@@ -70,19 +98,47 @@
<url>https://jitpack.io</url> <url>https://jitpack.io</url>
</repository> </repository>
</repositories> </repositories>
<distributionManagement>
<repository>
<id>knarcraft-repo</id>
<url>https://git.knarcraft.net/api/packages/EpicKnarvik97/maven</url>
</repository>
<snapshotRepository>
<id>knarcraft-repo</id>
<url>https://git.knarcraft.net/api/packages/EpicKnarvik97/maven</url>
</snapshotRepository>
</distributionManagement>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>org.spigotmc</groupId> <groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId> <artifactId>spigot-api</artifactId>
<version>1.17.1-R0.1-SNAPSHOT</version> <version>1.21.8-R0.1-SNAPSHOT</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.github.MilkBowl</groupId> <groupId>com.github.MilkBowl</groupId>
<artifactId>VaultAPI</artifactId> <artifactId>VaultAPI</artifactId>
<version>1.7</version> <version>1.7.1</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>net.knarcraft</groupId>
<artifactId>knarlib</artifactId>
<version>1.2.7</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>24.0.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -1,81 +1,122 @@
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.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.CommandGive; import net.knarcraft.bookswithoutborders.command.CommandGive;
import net.knarcraft.bookswithoutborders.command.CommandGivePublic; 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.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.BwBCommand;
import net.knarcraft.bookswithoutborders.config.StaticMessage;
import net.knarcraft.bookswithoutborders.config.Translatable;
import net.knarcraft.bookswithoutborders.container.MigrationRequest;
import net.knarcraft.bookswithoutborders.handler.BookshelfHandler;
import net.knarcraft.bookswithoutborders.listener.BookEventListener;
import net.knarcraft.bookswithoutborders.listener.BookshelfListener;
import net.knarcraft.bookswithoutborders.listener.PlayerEventListener; import net.knarcraft.bookswithoutborders.listener.PlayerEventListener;
import net.knarcraft.bookswithoutborders.listener.SignEventListener; import net.knarcraft.bookswithoutborders.listener.SignEventListener;
import net.knarcraft.bookswithoutborders.state.BookDirectory; import net.knarcraft.bookswithoutborders.utility.BookFileHelper;
import net.knarcraft.bookswithoutborders.utility.BookToFromTextHelper; import net.knarcraft.knarlib.formatting.StringFormatter;
import net.knarcraft.bookswithoutborders.utility.EconomyHelper; import net.knarcraft.knarlib.formatting.Translator;
import net.knarcraft.bookswithoutborders.utility.FileHelper; import net.knarcraft.knarlib.property.ColorConversion;
import org.bukkit.Material; 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.Configuration; import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFactory; import org.bukkit.inventory.ItemFactory;
import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.inventory.meta.BookMeta;
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.logging.Level;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getBookFolder; /**
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getErrorColor; * The main Books Without Borders class
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getSlash; */
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getSuccessColor;
import static net.knarcraft.bookswithoutborders.utility.InputCleaningHelper.cleanString;
public class BooksWithoutBorders extends JavaPlugin { public class BooksWithoutBorders extends JavaPlugin {
private static int bookDuplicateLimit;
private static String titleAuthorSeparator;
private static String loreSeparator;
private static List<String> firstBooks;
private static String welcomeMessage;
private static Material bookPriceType = null;
private static double bookPriceQuantity;
private static boolean authorOnlyCopy;
private static boolean useYml;
private static boolean adminDecrypt;
private static ItemFactory itemFactory;
private static Map<String, List<String>> playerBooksList;
private static List<String> publicBooksList;
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;
} }
/** /**
@@ -85,16 +126,43 @@ 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 { } else if (sender instanceof Player player) {
String senderName = sender.getName(); UUID playerUUID = player.getUniqueId();
if (!playerBooksList.containsKey(senderName)) { if (!getInstance().playerBooksList.containsKey(playerUUID)) {
List<String> newFiles = FileHelper.listFiles(sender, false); List<String> newFiles = BookFileHelper.listFiles(sender, false);
playerBooksList.put(senderName, newFiles); if (newFiles != null) {
getInstance().playerBooksList.put(playerUUID, newFiles);
getInstance().playerLetterIndex.put(playerUUID, BookFileHelper.populateLetterIndices(newFiles));
}
} }
return playerBooksList.get(senderName); List<String> books = getInstance().playerBooksList.get(playerUUID);
if (books != null) {
return new ArrayList<>(books);
} else {
return new ArrayList<>();
}
} else {
return new ArrayList<>();
}
}
/**
* Gets the letter index map for public books, or a specific player's books
*
* @param playerIndex <p>The player to get the index for, or null for the public index</p>
* @return <p>An index mapping between a character and the first index containing that character</p>
*/
@NotNull
public static Map<Character, Integer> getLetterIndex(@Nullable UUID playerIndex) {
if (playerIndex == null) {
return getInstance().publicLetterIndex;
} else {
Map<Character, Integer> letterIndex = getInstance().playerLetterIndex.get(playerIndex);
return Objects.requireNonNullElseGet(letterIndex, HashMap::new);
} }
} }
@@ -104,148 +172,73 @@ public class BooksWithoutBorders extends JavaPlugin {
* @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 = FileHelper.listFiles(sender, updatePublic); List<String> newFiles = BookFileHelper.listFiles(sender, updatePublic);
if (newFiles == null) {
return;
}
if (updatePublic) { if (updatePublic) {
publicBooksList = newFiles; getInstance().publicBooksList = newFiles;
} else { getInstance().publicLetterIndex = BookFileHelper.populateLetterIndices(newFiles);
playerBooksList.put(sender.getName(), newFiles); } else if (sender instanceof Player player) {
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();
config.options().copyDefaults(true);
this.saveDefaultConfig();
//Get plugin info
PluginDescriptionFile pluginDescriptionFile = this.getDescription();
String pluginVersion = pluginDescriptionFile.getVersion();
Translator translator = new Translator();
translator.registerMessageCategory(Translatable.SUCCESS_COPY);
stringFormatter = new StringFormatter(this.getDescription().getName(), translator);
stringFormatter.setColorConversion(ColorConversion.RGB);
stringFormatter.setSuccessColor(ChatColor.of("#A9FF84"));
stringFormatter.setErrorColor(ChatColor.of("#FF84A9"));
booksWithoutBorders = this; booksWithoutBorders = this;
consoleSender = this.getServer().getConsoleSender();
playerBooksList = new HashMap<>(); playerBooksList = new HashMap<>();
firstBooks = new ArrayList<>(); playerLetterIndex = new HashMap<>();
BooksWithoutBordersSettings.initialize(this); booksWithoutBordersConfig = new BooksWithoutBordersConfig(this, translator);
publicBooksList = FileHelper.listFiles(consoleSender, true); @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 BookshelfListener(), this);
} else { } else {
this.getPluginLoader().disablePlugin(this); this.getPluginLoader().disablePlugin(this);
} }
registerCommands(); registerCommands();
}
/** UpdateChecker.checkForUpdate(this, "https://api.spigotmc.org/legacy/update.php?resource=96069",
* Gets whether only the author of a book should be able to copy it () -> pluginVersion, null);
*
* @return <p>Whether only the book author can copy it</p>
*/
public static boolean getAuthorOnlyCopy() {
return authorOnlyCopy;
}
/**
* Gets whether to use YML, not TXT, for saving books
*
* @return <p>Whether to use YML for saving books</p>
*/
public static boolean getUseYml() {
return useYml;
}
/**
* Gets whether admins should be able to decrypt books without a password, and decrypt all group encrypted books
*
* @return <p>Whether admins can bypass the encryption password</p>
*/
public static boolean getAdminDecrypt() {
return adminDecrypt;
}
/**
* Sets the quantity of items/currency necessary for copying books
*
* @param newQuantity <p>The new quantity necessary for payment</p>
*/
public static void setBookPriceQuantity(double newQuantity) {
bookPriceQuantity = newQuantity;
}
/**
* Gets the quantity of items/currency necessary for copying books
*
* @return <p>The quantity necessary for payment</p>
*/
public static double getBookPriceQuantity() {
return bookPriceQuantity;
}
/**
* Sets the item type used for book pricing
*
* <p>This item is the one a player has to pay for copying books. AIR is used to denote economy. null is used if
* payment is disabled. Otherwise, any item can be used.</p>
*
* @param newType <p>The new item type to use for book pricing</p>
*/
public static void setBookPriceType(Material newType) {
bookPriceType = newType;
}
/**
* Gets the item type used for book pricing
*
* <p>This item is the one a player has to pay for copying books. AIR is used to denote economy. null is used if
* payment is disabled. Otherwise, any item can be used.</p>
*
* @return <p>The item type used for book pricing</p>
*/
public static Material getBookPriceType() {
return bookPriceType;
}
/**
* Gets the welcome message to show to new players
*
* @return <p>The welcome message to show new players</p>
*/
public static String getWelcomeMessage() {
return welcomeMessage;
}
/**
* Gets the limit of duplicates for each book
*
* @return <p>The book duplicate limit</p>
*/
public static int getBookDuplicateLimit() {
return bookDuplicateLimit;
}
/**
* Gets the separator used to split book title from book author
*
* @return <p>The separator between title and author</p>
*/
public static String getTitleAuthorSeparator() {
return titleAuthorSeparator;
}
/**
* Gets the separator used to denote a newline in a lore string
*
* @return <p>The separator used to denote lore newline</p>
*/
public static String getLoreSeparator() {
return loreSeparator;
}
/**
* Gets a copy of the list of books to give new players
*
* @return <p>The books to give new players</p>
*/
public static List<String> getFirstBooks() {
return new ArrayList<>(firstBooks);
} }
/** /**
@@ -253,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;
} }
@@ -261,25 +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(BwBCommand.FORMAT_BOOK.toString(), new CommandFormat());
registerCommand(BwBCommand.SET_BOOK_GENERATION.toString(), new CommandSetGeneration());
registerCommand(BwBCommand.CLEAR_BOOK.toString(), new CommandClear());
registerCommand(BwBCommand.SET_BOOKSHELF_DATA.toString(), new CommandSetBookshelfData());
registerCommand(BwBCommand.ADD_TITLE_PAGE.toString(), new CommandAddTitlePage());
registerCommand(BwBCommand.DELETE_PAGE.toString(), new CommandDeletePage());
registerCommand(BwBCommand.MIGRATE.toString(), new CommandMigrate());
} }
/** /**
@@ -288,291 +289,98 @@ 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);
} }
} }
/** /**
* Initializes the plugin, loading and fixing the config file * Initializes the plugin, loading and fixing the config file
*
* @return <p>True if successful</p> * @return <p>True if successful</p>
*/ */
private boolean initialize() { private boolean initialize() {
//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 (!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
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
*
* @return <p>The server's item factory</p>
*/
@NotNull
public static ItemFactory getItemFactory() {
return getInstance().itemFactory;
}
/** /**
* Makes sure necessary folders exist * Makes sure necessary folders exist
* *
* @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 e) { } catch (Exception exception) {
e.printStackTrace(); 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 e) { } catch (Exception exception) {
e.printStackTrace(); getLogger().log(Level.SEVERE, StaticMessage.BOOK_FOLDER_CREATE_FAILED.toString());
return false; return false;
} }
} }
return true; return true;
} }
/**
* Saves the config
*/
public void saveConfigValues() {
Configuration config = this.getConfig();
config.set("Options.Save_Books_in_Yaml_Format", useYml);
config.set("Options.Max_Number_of_Duplicates", bookDuplicateLimit);
config.set("Options.Title-Author_Separator", titleAuthorSeparator);
config.set("Options.Lore_line_separator", loreSeparator);
config.set("Options.Books_for_new_players", firstBooks);
config.set("Options.Message_for_new_players", welcomeMessage);
if (bookPriceType != null) {
if (bookPriceType != Material.AIR) {
config.set("Options.Price_to_create_book.Item_type", bookPriceType.toString());
} else {
config.set("Options.Price_to_create_book.Item_type", "Economy");
}
} else {
config.set("Options.Price_to_create_book.Item_type", "Item type name");
}
config.set("Options.Price_to_create_book.Required_quantity", bookPriceQuantity);
config.set("Options.Admin_Auto_Decrypt", adminDecrypt);
config.set("Options.Author_Only_Copy", authorOnlyCopy);
//Handles old book and quill settings
if (config.contains("Options.Require_book_and_quill_to_create_book")) {
sendSuccessMessage(consoleSender, "[BooksWithoutBorders] Found old config setting \"Require_book_and_quill_to_create_book\"");
sendSuccessMessage(consoleSender, "Updating to \"Price_to_create_book\" settings");
if (config.getBoolean("Options.Require_book_and_quill_to_create_book")) {
bookPriceType = Material.WRITABLE_BOOK;
bookPriceQuantity = 1;
config.set("Options.Price_to_create_book.Item_type", bookPriceType.toString());
config.set("Options.Price_to_create_book.Required_quantity", bookPriceQuantity);
}
config.set("Options.Require_book_and_quill_to_create_book", null);
}
this.saveConfig();
}
/**
* Loads the config
* @return <p>True if the config was loaded successfully</p>
*/
public boolean loadConfig() {
this.reloadConfig();
Configuration config = this.getConfig();
try {
useYml = config.getBoolean("Options.Save_Books_in_Yaml_Format", true);
bookDuplicateLimit = config.getInt("Options.Max_Number_of_Duplicates", 5);
titleAuthorSeparator = config.getString("Options.Title-Author_Separator", ",");
loreSeparator = config.getString("Options.Lore_line_separator", "~");
adminDecrypt = config.getBoolean("Options.Admin_Auto_Decrypt", false);
authorOnlyCopy = config.getBoolean("Options.Author_Only_Copy", false);
//Set books to give new players
firstBooks = config.getStringList("Options.Books_for_new_players");
if (config.contains("Options.Book_for_new_players")) {
firstBooks.add(config.getString("Options.Book_for_new_players"));
}
if (firstBooks.isEmpty()) {
firstBooks.add(" ");
}
welcomeMessage = config.getString("Options.Message_for_new_players", " ");
//Convert string into material
String paymentMaterial = config.getString("Options.Price_to_create_book.Item_type", " ");
if (paymentMaterial.equalsIgnoreCase("Economy")) {
if (EconomyHelper.setupEconomy()) {
bookPriceType = Material.AIR;
} else {
sendErrorMessage(consoleSender, "BooksWithoutBorders failed to hook into Vault! Book price not set!");
bookPriceType = null;
}
} else if (!paymentMaterial.equalsIgnoreCase(" ")) {
Material material = Material.matchMaterial(paymentMaterial);
if (material != null) {
bookPriceType = material;
}
}
bookPriceQuantity = config.getDouble("Options.Price_to_create_book.Required_quantity", 0);
//Make sure titleAuthorSeparator is a valid value
titleAuthorSeparator = cleanString(titleAuthorSeparator);
if (titleAuthorSeparator.length() != 1) {
sendErrorMessage(consoleSender, "Title-Author_Separator is set to an invalid value!");
sendErrorMessage(consoleSender, "Reverting to default value of \",\"");
titleAuthorSeparator = ",";
config.set("Options.Title-Author_Separator", titleAuthorSeparator);
}
} catch (Exception e) {
sendErrorMessage(consoleSender, "Warning! Config.yml failed to load!");
sendErrorMessage(consoleSender, "Try Looking for settings that are missing values!");
return false;
}
return true;
}
/**
* Loads the given book
*
* @param sender <p>The command sender trying to load the book</p>
* @param fileName <p>The index or file name of the book to load</p>
* @param isSigned <p>Whether to load the book as signed, and not unsigned</p>
* @param directory <p>The directory to save the book in</p>
* @return <p>The loaded book</p>
*/
public ItemStack loadBook(CommandSender sender, String fileName, String isSigned, String directory) {
return loadBook(sender, fileName, isSigned, directory, 1);
}
/**
* Loads the given book
*
* @param sender <p>The command sender trying to load the book</p>
* @param fileName <p>The index or file name of the book to load</p>
* @param isSigned <p>Whether to load the book as signed, and not unsigned</p>
* @param directory <p>The directory to save the book in</p>
* @param numCopies <p>The number of copies to load</p>
* @return <p>The loaded book</p>
*/
public ItemStack loadBook(CommandSender sender, String fileName, String isSigned, String directory, int numCopies) {
BookDirectory bookDirectory = BookDirectory.getFromString(directory);
//Find the filename if a book index is given
try {
int bookIndex = Integer.parseInt(fileName);
List<String> availableFiles = getAvailableBooks(sender, bookDirectory == BookDirectory.PUBLIC);
if (bookIndex <= availableFiles.size()) {
fileName = availableFiles.get(Integer.parseInt(fileName) - 1);
}
} catch (NumberFormatException ignored) {
}
//Get the full path of the book to load
File file = getFullPath(sender, fileName, bookDirectory, directory);
if (file == null) {
return null;
}
//Make sure the player can pay for the book
if (booksHavePrice() && !sender.hasPermission("bookswithoutborders.bypassBookPrice") &&
(bookDirectory == BookDirectory.PUBLIC || bookDirectory == BookDirectory.PLAYER) &&
EconomyHelper.cannotPayForBookPrinting((Player) sender, numCopies)) {
return null;
}
//Generate a new empty book
ItemStack book;
BookMeta bookMetadata = (BookMeta) itemFactory.getItemMeta(Material.WRITTEN_BOOK);
if (isSigned.equalsIgnoreCase("true")) {
book = new ItemStack(Material.WRITTEN_BOOK);
} else {
book = new ItemStack(Material.WRITABLE_BOOK);
}
//Load the book from the given file
BookToFromTextHelper.bookFromFile(file, bookMetadata);
if (bookMetadata == null) {
sendErrorMessage(sender, "File was blank!!");
return null;
}
//Remove "encrypted" from the book lore
if (bookDirectory == BookDirectory.ENCRYPTED && bookMetadata.hasLore()) {
List<String> oldLore = bookMetadata.getLore();
if (oldLore != null) {
List<String> newLore = new ArrayList<>(oldLore);
newLore.remove(0);
bookMetadata.setLore(newLore);
}
}
//Set the metadata and amount to the new book
book.setItemMeta(bookMetadata);
book.setAmount(numCopies);
return book;
}
/**
* Gets a File pointing to the wanted book
*
* @param sender <p>The sender to send errors to</p>
* @param fileName <p>The name of the book file</p>
* @param bookDirectory <p>The book directory the file resides in</p>
* @param directory <p>The relative directory given</p>
* @return <p>A file or null if it does not exist</p>
*/
private File getFullPath(CommandSender sender, String fileName, BookDirectory bookDirectory, String directory) {
File file = null;
if (bookDirectory == BookDirectory.PUBLIC) {
file = FileHelper.getBookFile(getBookFolder() + fileName);
} else if (bookDirectory == BookDirectory.PLAYER) {
file = FileHelper.getBookFile(getBookFolder() + cleanString(sender.getName()) + getSlash() + fileName);
} else if (bookDirectory == BookDirectory.ENCRYPTED) {
file = FileHelper.getBookFile(getBookFolder() + "Encrypted" + getSlash() + directory + getSlash() + fileName);
}
if (file == null || !file.isFile()) {
sendErrorMessage(sender, "Incorrect file name!");
return null;
} else {
return file;
}
}
/** /**
* Sends a success message to a command sender (player or a console) * Sends a success message to a command sender (player or a console)
* *
* @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);
} }
/** /**
@@ -581,17 +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);
}
/**
* Checks whether books have a price for printing them
*
* @return <p>True if players need to pay for printing books</p>
*/
public boolean booksHavePrice() {
return (bookPriceType != null && bookPriceQuantity > 0);
} }
} }

View File

@@ -1,77 +0,0 @@
package net.knarcraft.bookswithoutborders;
import org.bukkit.ChatColor;
/**
* Class for getting various settings
*/
public class BooksWithoutBordersSettings {
//Static settings
private static final ChatColor errorColor = ChatColor.RED;
private static final ChatColor successColor = ChatColor.GREEN;
private static final ChatColor commandColor = ChatColor.YELLOW;
private static final String SLASH = System.getProperty("file.separator");
private static boolean isInitialized;
public static String bookFolder;
/**
* Initializes the books without borders settings class
*
* @param booksWithoutBorders <p>The books without borders object used for getting required data</p>
*/
public static void initialize(BooksWithoutBorders booksWithoutBorders) {
if (isInitialized) {
throw new IllegalArgumentException("Settings class initialized twice. This should not happen!");
}
isInitialized = true;
bookFolder = booksWithoutBorders.getDataFolder().getAbsolutePath() + getSlash() + "Books" + getSlash();
}
/**
* Gets the folder used for storing books
*
* @return <p>The folder used for storing books</p>
*/
public static String getBookFolder() {
return bookFolder;
}
/**
* Gets the color to use for error messages
*
* @return <p>The color to use for error messages</p>
*/
public static ChatColor getErrorColor() {
return errorColor;
}
/**
* Gets the color to use for success messages
*
* @return <p>The color to use for success messages</p>
*/
public static ChatColor getSuccessColor() {
return successColor;
}
/**
* Gets the color used to color commands
*
* @return <p>The color used to color commands</p>
*/
public static ChatColor getCommandColor() {
return commandColor;
}
/**
* Gets the correct slash to use for the used OS
*
* @return <p>The slash to use for file separators</p>
*/
public static String getSlash() {
return SLASH;
}
}

View File

@@ -0,0 +1,170 @@
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.BookHelper;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import net.knarcraft.knarlib.formatting.StringFormatter;
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,
stringFormatter.replacePlaceholder(Translatable.ERROR_NOT_HOLDING_WRITTEN_BOOK, "{action}",
stringFormatter.getUnFormattedColoredMessage(Translatable.ACTION_ADD_TITLE_AUTHOR_PAGE)),
stringFormatter.replacePlaceholder(Translatable.ERROR_ONLY_ONE_BOOK, "{action}",
stringFormatter.getUnFormattedColoredMessage(Translatable.ACTION_ADD_TITLE_AUTHOR_PAGE)))) {
return false;
}
index = 0;
} else {
try {
index = Integer.parseInt(arguments[0]) - 1;
} catch (NumberFormatException exception) {
stringFormatter.displayErrorMessage(sender, Translatable.ERROR_INVALID_BOOK_PAGE);
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) {
stringFormatter.displayErrorMessage(sender, Translatable.ERROR_METADATA_MISSING);
return false;
}
List<String> pages = new ArrayList<>(bookMeta.getPages());
if (index < 0) {
stringFormatter.displayErrorMessage(sender, Translatable.ERROR_INVALID_BOOK_PAGE);
return false;
}
if (title == null && heldBook.getType() == Material.WRITTEN_BOOK) {
// Add a page with the book title and book author
String loreSeparator = BooksWithoutBorders.getConfiguration().getLoreSeparator();
String pageText = formatTitle(stringFormatter.replacePlaceholders(Translatable.NEUTRAL_TITLE_PAGE_TITLE_AUTHOR_FORMAT,
List.of("{title}", "{separator}", "{author}"), List.of(BookHelper.getBookTitle(bookMeta),
loreSeparator, BookHelper.getBookAuthor(bookMeta, null))));
if (index > pages.size()) {
pages.add(pageText);
} else {
pages.add(index, pageText);
}
} else if (title == null) {
// Add a blank page
if (index > pages.size()) {
pages.add("");
} else {
pages.add(index, "");
}
} else {
// Add a custom chapter page
if (index > pages.size()) {
pages.add(formatTitle(title));
} else {
pages.add(index, formatTitle(title));
}
}
bookMeta.setPages(pages);
heldBook.setItemMeta(bookMeta);
stringFormatter.displaySuccessMessage(sender, Translatable.SUCCESS_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) {
StringFormatter stringFormatter = BooksWithoutBorders.getStringFormatter();
String loreSeparator = BooksWithoutBorders.getConfiguration().getLoreSeparator();
if (input.contains(loreSeparator)) {
String[] parts = input.split(loreSeparator);
StringBuilder output = new StringBuilder();
output.append(stringFormatter.replacePlaceholder(Translatable.NEUTRAL_TITLE_PAGE_HEADER_FORMAT,
"{header}", BookFormatter.stripColor(parts[0])));
for (int i = 1; i < parts.length; i++) {
output.append(stringFormatter.replacePlaceholder(Translatable.NEUTRAL_TITLE_PAGE_TEXT_FORMAT,
"{text}", BookFormatter.stripColor(parts[i])));
}
return output.toString();
} else {
return 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

@@ -1,51 +1,100 @@
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.EconomyHelper; import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
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;
import org.bukkit.command.PluginCommand; import org.bukkit.command.PluginCommand;
import org.bukkit.command.TabExecutor; import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
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.BooksWithoutBordersSettings.getCommandColor;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getSuccessColor;
/** /**
* Command executor for the books without borders (bwb) command * Command executor for the books without borders (bwb) command
*/ */
public class CommandBooksWithoutBorders implements TabExecutor { public class CommandBooksWithoutBorders implements TabExecutor {
private final BooksWithoutBorders booksWithoutBorders = BooksWithoutBorders.getInstance();
@Override @Override
public boolean onCommand(CommandSender sender, Command command, String label, 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() + "In some cases, commands with required parameters can be called with no parameters"); String header = stringFormatter.replacePlaceholders(Translatable.NEUTRAL_COMMANDS_HEADER,
if (sender instanceof Player) { List.of("{bookPrice}", "{commands}"), List.of(getBookPrice(), getCommands(sender)));
showPlayerCommands(sender); sender.sendMessage(header);
} 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("reload", sender); StringBuilder builder = new StringBuilder();
showCommandInfo("givePublicBook", sender); for (BwBCommand command : BwBCommand.values()) {
showCommandInfo("deletePublicBook", sender); if (!command.requiresPlayer()) {
builder.append(showCommandInfo(command.toString(), sender));
}
}
return builder.toString();
} }
/** /**
@@ -53,39 +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 = BooksWithoutBorders.getBookPriceType(); StringBuilder builder = new StringBuilder();
double bookPriceQuantity = BooksWithoutBorders.getBookPriceQuantity(); for (BwBCommand command : BwBCommand.values()) {
if (booksWithoutBorders.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("loadBook", sender);
showCommandInfo("loadPublicBook", sender);
showCommandInfo("saveBook", sender);
showCommandInfo("savePublicBook", sender);
showCommandInfo("giveBook", sender);
showCommandInfo("givePublicBook", sender);
showCommandInfo("deleteBook", sender);
showCommandInfo("deletePublicBook", sender);
showCommandInfo("unsignBook", sender);
showCommandInfo("copyBook", sender);
showCommandInfo("encryptBook", sender);
showCommandInfo("groupEncryptBook", sender);
showCommandInfo("decryptBook", sender);
showCommandInfo("setTitle", sender);
showCommandInfo("setAuthor", sender);
showCommandInfo("setLore", sender);
showCommandInfo("setBookPrice", sender);
showCommandInfo("reload", sender);
} }
/** /**
@@ -94,20 +117,38 @@ 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));
sender.sendMessage("\n" + getCommandColor() + return "";
pluginCommand.getUsage().replace("<command>", pluginCommand.getName()) + ": " +
getSuccessColor() + pluginCommand.getDescription());
}
} }
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(CommandSender sender, Command command, String alias, 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

@@ -0,0 +1,65 @@
package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.Translatable;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import net.knarcraft.knarlib.formatting.StringFormatter;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
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;
/**
* Command executor for the clear command
*/
public class CommandClear implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
StringFormatter stringFormatter = BooksWithoutBorders.getStringFormatter();
if (!(sender instanceof Player player)) {
BooksWithoutBorders.getStringFormatter().displayErrorMessage(sender, Translatable.ERROR_PLAYER_ONLY);
return false;
}
if (InventoryHelper.notHoldingOneWritableBookCheck(player,
stringFormatter.replacePlaceholder(Translatable.ERROR_NOT_HOLDING_WRITABLE_BOOK, "{action}",
stringFormatter.getUnFormattedColoredMessage(Translatable.ACTION_CLEAR)),
stringFormatter.replacePlaceholder(Translatable.ERROR_ONLY_ONE_BOOK, "{action}",
stringFormatter.getUnFormattedColoredMessage(Translatable.ACTION_CLEAR)))) {
return false;
}
//Clear the player's held book
ItemStack heldBook = InventoryHelper.getHeldBook(player, false);
BookMeta bookMeta = (BookMeta) heldBook.getItemMeta();
if (bookMeta == null) {
stringFormatter.displayErrorMessage(sender, Translatable.ERROR_METADATA_MISSING);
return false;
}
bookMeta.setPages("");
bookMeta.setAuthor(null);
bookMeta.setGeneration(null);
bookMeta.setTitle(null);
heldBook.setItemMeta(bookMeta);
stringFormatter.displaySuccessMessage(sender, Translatable.SUCCESS_CLEARED);
return true;
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] arguments) {
return new ArrayList<>();
}
}

View File

@@ -1,16 +1,22 @@
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.EconomyHelper; import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper; import net.knarcraft.bookswithoutborders.config.Permission;
import net.knarcraft.bookswithoutborders.config.Translatable;
import net.knarcraft.bookswithoutborders.utility.BookHelper;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper; import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import net.knarcraft.bookswithoutborders.utility.TabCompletionHelper; import net.knarcraft.bookswithoutborders.utility.TabCompletionTypeHelper;
import net.knarcraft.knarlib.formatting.StringFormatter;
import net.knarcraft.knarlib.util.TabCompletionHelper;
import org.bukkit.Material;
import org.bukkit.command.Command; 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.jetbrains.annotations.NotNull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -21,76 +27,135 @@ import java.util.Objects;
*/ */
public class CommandCopy implements TabExecutor { public class CommandCopy implements TabExecutor {
private final BooksWithoutBorders booksWithoutBorders = BooksWithoutBorders.getInstance();
@Override @Override
public boolean onCommand(CommandSender sender, Command command, String label, 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;
} }
ItemStack heldBook = InventoryHelper.getHeldBook(player, true);
try { try {
int copies = Integer.parseInt(args[0]); ItemStack heldBook = InventoryHelper.getHeldBook(player, true);
if (copies > 0) { int copies = Integer.parseInt(arguments[0]);
if (BooksWithoutBorders.getAuthorOnlyCopy() && !player.hasPermission("bookswithoutborders.bypassAuthorOnlyCopy")) { if (copies <= 0) {
if (!isAuthor(player, (BookMeta) Objects.requireNonNull(heldBook.getItemMeta()))) stringFormatter.displayErrorMessage(player, Translatable.ERROR_COPY_NEGATIVE_AMOUNT);
return false; return false;
}
if (booksWithoutBorders.booksHavePrice() &&
!player.hasPermission("bookswithoutborders.bypassBookPrice") &&
EconomyHelper.cannotPayForBookPrinting(player, copies)) {
return false;
}
heldBook.setAmount(heldBook.getAmount() + copies);
BooksWithoutBorders.sendSuccessMessage(player, "Book copied!");
return true;
} }
return performCopy(copies, player, heldBook);
} catch (NumberFormatException ignored) { } catch (NumberFormatException ignored) {
stringFormatter.displayErrorMessage(player, Translatable.ERROR_COPY_INVALID_AMOUNT);
return false;
} }
BooksWithoutBorders.sendErrorMessage(player, "Book not copied!");
BooksWithoutBorders.sendErrorMessage(player, "Number specified was invalid!");
return false;
} }
/** /**
* Checks whether the given player is the author of a given book * Performs the actual copying
* *
* @param player <p>The player to check</p> * @param copies <p>The number of copies to be made</p>
* @param book <p>The book to check</p> * @param player <p>The player requesting the copies</p>
* @return <p>True if the player is the book's author</p> * @param heldBook <p>The book to be copied</p>
* @return <p>True if the copying was successful</p>
*/ */
private boolean isAuthor(Player player, BookMeta book) { private boolean performCopy(int copies, @NotNull Player player, @NotNull ItemStack heldBook) {
String author = book.getAuthor(); BooksWithoutBordersConfig config = BooksWithoutBorders.getConfiguration();
String playerName = InputCleaningHelper.cleanString(player.getName());
if (author != null && playerName.equalsIgnoreCase(InputCleaningHelper.cleanString(author))) { //Make sure the player owns the book if authorOnlyCopy is enabled
return true; if (config.getAuthorOnlyCopy() &&
!player.hasPermission(Permission.BYPASS_AUTHOR_ONLY_COPY.toString())) {
if (BookHelper.isNotAuthor(player, (BookMeta) Objects.requireNonNull(heldBook.getItemMeta()))) {
return false;
}
} }
BooksWithoutBorders.sendErrorMessage(player, "You must be the author of this book to use this command!"); BookMeta bookMeta = (BookMeta) heldBook.getItemMeta();
return false; if (config.changeGenerationOnCopy() && bookMeta != null) {
return copyNextGenerationBook(bookMeta, player, copies);
} else {
//Make sure the player can pay for the copying
if (paymentUnSuccessful(player, copies)) {
return false;
}
heldBook.setAmount(heldBook.getAmount() + copies);
BooksWithoutBorders.getStringFormatter().displaySuccessMessage(player, Translatable.SUCCESS_COPY);
return true;
}
}
/**
* Tries to take payment from a player
*
* @param player <p>The player to take the payment from</p>
* @param copies <p>The number of copies to create for the player</p>
* @return <p>True if the payment failed</p>
*/
private boolean paymentUnSuccessful(@NotNull Player player, int copies) {
BooksWithoutBordersConfig config = BooksWithoutBorders.getConfiguration();
return (config.booksHavePrice() &&
!player.hasPermission(Permission.BYPASS_BOOK_PRICE.toString()) &&
config.getEconomyManager().cannotPayForBookPrinting(player, copies));
}
/**
* Copies a book with the next generation relative to the input book
*
* @param bookMeta <p>The book to copy</p>
* @param player <p>The player copying the book</p>
* @param copies <p>The number of copies requested</p>
* @return <p>True if the book was successfully copied</p>
*/
private boolean copyNextGenerationBook(@NotNull BookMeta bookMeta, @NotNull Player player, int copies) {
//Copy the vanilla behavior of refusing copying any further
if (bookMeta.getGeneration() == BookMeta.Generation.COPY_OF_COPY ||
bookMeta.getGeneration() == BookMeta.Generation.TATTERED) {
BooksWithoutBorders.getStringFormatter().displayErrorMessage(player, Translatable.ERROR_BOOK_COPIED_TOO_FAR);
return false;
}
//Make sure the player can fit the book in their inventory
int nextAvailableSlot = player.getInventory().firstEmpty();
if (nextAvailableSlot == -1) {
BooksWithoutBorders.getStringFormatter().displayErrorMessage(player, Translatable.ERROR_INVENTORY_FULL);
return false;
}
//Make sure the player can pay for the copying
if (paymentUnSuccessful(player, copies)) {
return false;
}
ItemStack itemStack = new ItemStack(Material.WRITTEN_BOOK);
itemStack.setItemMeta(bookMeta);
//Increase the generation of the book
BookHelper.increaseGeneration(itemStack);
itemStack.setAmount(copies);
player.getInventory().addItem(itemStack);
return true;
} }
@Override @Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, 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) {
return TabCompletionHelper.getNumbers(1, 20); return TabCompletionHelper.filterMatchingStartsWith(TabCompletionTypeHelper.getNumbers(1, 20), arguments[0]);
} }
return new ArrayList<>(); return new ArrayList<>();
} }
} }

View File

@@ -1,114 +1,131 @@
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.Permission;
import net.knarcraft.bookswithoutborders.config.Translatable;
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;
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 java.io.File; 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.BooksWithoutBordersSettings.getBookFolder;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.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(CommandSender sender, Command command, String label, 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 && BooksWithoutBorders.getAdminDecrypt() && player.hasPermission("bookswithoutborders.admin")) { if (arguments.length == 0 && BooksWithoutBorders.getConfiguration().getAdminDecrypt() &&
String path = getBookFolder() + "Encrypted" + getSlash(); player.hasPermission(Permission.ADMIN.toString())) {
String fileName; return adminDecrypt(player, bookMetadata);
if (bookMetadata.hasTitle()) { } else if (arguments.length == 0) {
fileName = bookMetadata.getTitle() + BooksWithoutBorders.getTitleAuthorSeparator() + bookMetadata.getAuthor(); stringFormatter.displayErrorMessage(player, Translatable.ERROR_DECRYPT_MISSING_KEY);
} else {
fileName = "Untitled," + player.getName();
}
File encryptedDirectory = new File(path);
String[] encryptedFiles = encryptedDirectory.list();
if (encryptedFiles == null) {
BooksWithoutBorders.sendErrorMessage(player, "Could not find any encrypted files!");
return false;
}
//Get the "encryption key" from the filename
String key = "";
for (String encryptedFile : encryptedFiles) {
if (encryptedFile.contains(fileName)) {
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(CommandSender sender, Command command, String alias, 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,64 +1,67 @@
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.FileHelper; import net.knarcraft.bookswithoutborders.config.BwBCommand;
import net.knarcraft.bookswithoutborders.config.Translatable;
import net.knarcraft.bookswithoutborders.gui.PagedBookIndex;
import net.knarcraft.bookswithoutborders.state.BookDirectory;
import net.knarcraft.bookswithoutborders.utility.BookFileHelper;
import net.knarcraft.bookswithoutborders.utility.BookHelper;
import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper; import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper;
import net.knarcraft.bookswithoutborders.utility.TabCompletionTypeHelper;
import net.knarcraft.knarlib.formatting.StringFormatter;
import net.knarcraft.knarlib.util.TabCompletionHelper;
import org.bukkit.command.Command; import org.bukkit.command.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.jetbrains.annotations.NotNull;
import java.io.File; import java.io.File;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getBookFolder;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getSlash;
/** /**
* Command executor for the delete command * Command executor for the delete command
*/ */
public class CommandDelete implements TabExecutor { public class CommandDelete implements TabExecutor {
@Override @Override
public boolean onCommand(CommandSender sender, Command command, String label, 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();
FileHelper.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);
@@ -80,50 +84,54 @@ public class CommandDelete implements TabExecutor {
} }
//Get the file to be deleted //Get the file to be deleted
File file; String bookDirectory = BookHelper.getBookDirectoryPathString(
if (isPublic) { isPublic ? BookDirectory.PUBLIC : BookDirectory.PLAYER, sender);
file = FileHelper.getBookFile(getBookFolder() + fileName); File file = BookFileHelper.getBookFile(bookDirectory + fileName);
} else {
file = FileHelper.getBookFile(getBookFolder() +
InputCleaningHelper.cleanString(sender.getName()) + getSlash() + fileName);
}
//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(CommandSender sender, Command command, String alias, 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 args <p>The arguments given</p> * @param sender <p>The sender of the command</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 BooksWithoutBorders.getAvailableBooks(sender, deletePublic); BooksWithoutBorders.getAvailableBooks(sender, deletePublic),
InputCleaningHelper.mergeArguments(arguments, 0));
if (arguments.length > 1) {
return TabCompletionTypeHelper.getCleanedTabCompletions(arguments, filtered);
} else {
return filtered;
} }
return new ArrayList<>();
} }
} }

View File

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

View File

@@ -3,6 +3,7 @@ package net.knarcraft.bookswithoutborders.command;
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.jetbrains.annotations.NotNull;
import java.util.List; import java.util.List;
@@ -12,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(CommandSender sender, Command command, 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(CommandSender sender, Command command, 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,16 +1,21 @@
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 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.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -21,54 +26,68 @@ import java.util.List;
public class CommandEncrypt implements TabExecutor { public class CommandEncrypt implements TabExecutor {
@Override @Override
public boolean onCommand(CommandSender sender, Command command, String label, 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;
@@ -83,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);
@@ -96,39 +116,48 @@ public class CommandEncrypt implements TabExecutor {
} }
@Override @Override
public List<String> onTabComplete(CommandSender sender, Command command, 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);
} }
/** /**
* Gets a list of string for tab completions * Gets a list of string for tab completions
* @param args <p>The arguments given</p> *
* @param args <p>The arguments given</p>
* @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) {
List<String> info = new ArrayList<>();
info.add("<password>");
return info;
} else if (argumentsCount == 2) {
if (groupEncrypt) {
List<String> info = new ArrayList<>();
info.add("<group>");
return info;
} else {
return encryptionStyles;
}
} else if (argumentsCount == 3 && groupEncrypt) {
return encryptionStyles;
} }
return new ArrayList<>();
if (groupEncrypt) {
if (argumentsCount == 1) {
return List.of("<group>");
} else if (argumentsCount == 2) {
return List.of("<password>");
} else if (argumentsCount == 3) {
return TabCompletionHelper.filterMatchingStartsWith(encryptionStyles, args[2]);
}
} else {
if (argumentsCount == 1) {
return List.of("<password>");
} else if (argumentsCount == 2) {
if (BooksWithoutBorders.getConfiguration().useRealEncryption()) {
return List.of();
}
return TabCompletionHelper.filterMatchingStartsWith(encryptionStyles, args[1]);
}
}
return List.of();
} }
} }

View File

@@ -0,0 +1,59 @@
package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.Translatable;
import net.knarcraft.bookswithoutborders.utility.BookFormatter;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import net.knarcraft.knarlib.formatting.StringFormatter;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
/**
* A command for converting color codes to color formatting
*/
public class CommandFormat implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@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;
}
BookMeta meta = (BookMeta) heldBook.getItemMeta();
if (meta == null) {
stringFormatter.displayErrorMessage(player, Translatable.ERROR_METADATA_MISSING);
return false;
}
heldBook.setItemMeta(BookFormatter.formatPages(meta));
stringFormatter.displaySuccessMessage(sender, Translatable.SUCCESS_FORMATTED);
return true;
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] arguments) {
return new ArrayList<>();
}
}

View File

@@ -1,15 +1,22 @@
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.FileHelper; import net.knarcraft.bookswithoutborders.config.Translatable;
import net.knarcraft.bookswithoutborders.gui.PagedBookIndex;
import net.knarcraft.bookswithoutborders.utility.BookLoader;
import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper; import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper;
import net.knarcraft.bookswithoutborders.utility.TabCompletionHelper; import net.knarcraft.bookswithoutborders.utility.TabCompletionTypeHelper;
import net.knarcraft.knarlib.formatting.StringFormatter;
import net.knarcraft.knarlib.util.TabCompletionHelper;
import org.bukkit.Bukkit;
import org.bukkit.Server; import org.bukkit.Server;
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.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -22,49 +29,76 @@ public class CommandGive implements TabExecutor {
private final BooksWithoutBorders booksWithoutBorders = BooksWithoutBorders.getInstance(); private final BooksWithoutBorders booksWithoutBorders = BooksWithoutBorders.getInstance();
@Override @Override
public boolean onCommand(CommandSender sender, Command command, String label, 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) {
FileHelper.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
Player receivingPlayer = booksWithoutBorders.getServer().getPlayerExact(receivingPlayerName);
if (receivingPlayer == null) {
stringFormatter.displayErrorMessage(sender, Translatable.ERROR_GIVE_RECIPIENT_UNKNOWN);
return false;
}
//Make sure the receiver is able to fit the book
if (receivingPlayer.getInventory().firstEmpty() == -1) {
stringFormatter.displayErrorMessage(sender, Translatable.ERROR_GIVE_RECIPIENT_FULL);
return false;
} }
//Load books available to the player //Load books available to the player
@@ -74,77 +108,96 @@ public class CommandGive implements TabExecutor {
} catch (NumberFormatException ignored) { } catch (NumberFormatException ignored) {
} }
Player receivingPlayer = booksWithoutBorders.getServer().getPlayer(receivingPlayerName);
if (receivingPlayer == null) {
BooksWithoutBorders.sendErrorMessage(sender, "Player not found!");
return false;
}
if (receivingPlayer.getInventory().firstEmpty() == -1) {
BooksWithoutBorders.sendErrorMessage(sender, "Receiving player must have space in their inventory to receive books!");
return false;
}
String bookToLoad = InputCleaningHelper.cleanString(bookIdentifier);
try { try {
ItemStack newBook = booksWithoutBorders.loadBook(sender, bookToLoad, isSigned, folder, Integer.parseInt(copies)); return loadAndGiveBook(bookIdentifier, sender, receivingPlayer, isSigned, folder, copies);
if (newBook != null) {
receivingPlayer.getInventory().addItem(newBook);
BooksWithoutBorders.sendSuccessMessage(sender, "Book sent!");
BooksWithoutBorders.sendSuccessMessage(receivingPlayer, "Book received!");
return true;
} else {
BooksWithoutBorders.sendErrorMessage(sender, "Book failed to load!");
return false;
}
} 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(CommandSender sender, Command command, String alias, 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 args <p>The arguments given</p> * @param sender <p>The sender of the command</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 BooksWithoutBorders.getAvailableBooks(sender, listPublic); }
} else if (argumentCount == 2) {
//Return online players List<String> output = new ArrayList<>();
return null; List<String> books = BooksWithoutBorders.getAvailableBooks(sender, listPublic);
} else if (argumentCount == 3) { List<String> filtered = TabCompletionHelper.filterMatchingContains(books,
//Number of copies InputCleaningHelper.mergeArguments(arguments, 0));
return TabCompletionHelper.getBooleansAndNumbers(1, 3);
} else if (argumentCount == 4) { if (!filtered.isEmpty()) {
//Signed List<String> cleaned = TabCompletionTypeHelper.getCleanedTabCompletions(arguments, filtered);
try { if (!books.contains(InputCleaningHelper.mergeArguments(arguments, 1))) {
Integer.parseInt(args[2]); return cleaned;
return TabCompletionHelper.getBooleans(); } else {
} catch (NumberFormatException e) { output.addAll(cleaned);
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;
} }
/**
* Loads a book and gives it to the correct player
*
* @param bookIdentifier <p>The file name specified by the user</p>
* @param sender <p>The player trying to give the book</p>
* @param receivingPlayer <p>The player which is the receiver of the book</p>
* @param isSigned <p>The value given for if the given book should be signed or not</p>
* @param folder <p>The folder containing the book to load</p>
* @param copies <p>The number of copies the player wants to give</p>
* @return <p>True if the book was successfully given</p>
*/
private boolean loadAndGiveBook(@NotNull String bookIdentifier, @NotNull CommandSender sender,
@NotNull Player receivingPlayer, @NotNull String isSigned, @NotNull String folder,
@NotNull String copies) throws NumberFormatException {
StringFormatter stringFormatter = BooksWithoutBorders.getStringFormatter();
String bookToLoad = InputCleaningHelper.cleanString(bookIdentifier);
ItemStack newBook = BookLoader.loadBook(sender, bookToLoad, isSigned, folder, Integer.parseInt(copies));
if (newBook != null) {
//NOTE: As this method bypasses cost, it should also bypass the generation change
receivingPlayer.getInventory().addItem(newBook);
stringFormatter.displaySuccessMessage(sender, Translatable.SUCCESS_GIVE_SENT);
stringFormatter.displaySuccessMessage(receivingPlayer, Translatable.SUCCESS_GIVE_RECEIVED);
return true;
} else {
stringFormatter.displayErrorMessage(sender, Translatable.ERROR_GIVE_LOAD_FAILED);
return false;
}
}
} }

View File

@@ -3,6 +3,8 @@ package net.knarcraft.bookswithoutborders.command;
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.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List; import java.util.List;
@@ -12,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(CommandSender sender, Command command, 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(CommandSender sender, Command command, 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,12 +1,13 @@
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;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.inventory.meta.BookMeta; import org.bukkit.inventory.meta.BookMeta;
import org.jetbrains.annotations.NotNull;
import java.util.List; import java.util.List;
@@ -16,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(CommandSender sender, Command command, 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) {
@@ -31,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(CommandSender sender, Command command, 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,14 +1,17 @@
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.FileHelper; import net.knarcraft.bookswithoutborders.gui.PagedBookIndex;
import net.knarcraft.bookswithoutborders.utility.BookLoader;
import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper; import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper;
import net.knarcraft.bookswithoutborders.utility.TabCompletionHelper; import net.knarcraft.bookswithoutborders.utility.TabCompletionTypeHelper;
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;
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.jetbrains.annotations.NotNull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -18,23 +21,23 @@ import java.util.List;
*/ */
public class CommandLoad implements TabExecutor { public class CommandLoad implements TabExecutor {
private final BooksWithoutBorders booksWithoutBorders = BooksWithoutBorders.getInstance();
@Override @Override
public boolean onCommand(CommandSender sender, Command command, String label, 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;
@@ -45,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)) {
FileHelper.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);
} }
} }
@@ -78,7 +87,7 @@ public class CommandLoad implements TabExecutor {
String bookToLoad = InputCleaningHelper.cleanString(bookIdentifier); String bookToLoad = InputCleaningHelper.cleanString(bookIdentifier);
try { try {
//Give the new book if it can be loaded //Give the new book if it can be loaded
ItemStack newBook = booksWithoutBorders.loadBook(player, bookToLoad, isSigned, directory, Integer.parseInt(copies)); ItemStack newBook = BookLoader.loadBook(player, bookToLoad, isSigned, directory, Integer.parseInt(copies));
if (newBook != null) { if (newBook != null) {
player.getInventory().addItem(newBook); player.getInventory().addItem(newBook);
BooksWithoutBorders.sendSuccessMessage(player, "Book created!"); BooksWithoutBorders.sendSuccessMessage(player, "Book created!");
@@ -94,34 +103,48 @@ public class CommandLoad implements TabExecutor {
} }
@Override @Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, 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 args <p>The arguments given</p> * @param sender <p>The sender of the command</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 BooksWithoutBorders.getAvailableBooks(sender, loadPublic); }
} else if (argumentCount == 2) {
//Number of copies List<String> output = new ArrayList<>();
return TabCompletionHelper.getBooleansAndNumbers(1, 3); List<String> books = BooksWithoutBorders.getAvailableBooks(sender, loadPublic);
} else if (argumentCount == 3) { List<String> filtered = TabCompletionHelper.filterMatchingContains(books,
//Signed InputCleaningHelper.mergeArguments(arguments, 0));
try {
Integer.parseInt(args[1]); if (!filtered.isEmpty()) {
return TabCompletionHelper.getBooleans(); List<String> cleaned = TabCompletionTypeHelper.getCleanedTabCompletions(arguments, filtered);
} catch (NumberFormatException e) { if (!books.contains(InputCleaningHelper.mergeArguments(arguments, 1))) {
return new ArrayList<>(); return cleaned;
} else {
output.addAll(cleaned);
} }
} }
return new ArrayList<>();
if (InputCleaningHelper.isBoolean(arguments[argumentCount - 2])) {
return output;
} else if (InputCleaningHelper.isInt(arguments[argumentCount - 2])) {
output.addAll(TabCompletionHelper.filterMatchingStartsWith(TabCompletionTypeHelper.getBooleans(), arguments[argumentCount - 1]));
} else {
output.addAll(TabCompletionHelper.filterMatchingStartsWith(TabCompletionTypeHelper.getBooleansAndNumbers(1, 3), arguments[argumentCount - 1]));
}
return output;
} }
} }

View File

@@ -3,6 +3,7 @@ package net.knarcraft.bookswithoutborders.command;
import org.bukkit.command.Command; import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import java.util.List; import java.util.List;
@@ -12,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(CommandSender sender, Command command, 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(CommandSender sender, Command command, 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

@@ -4,6 +4,7 @@ import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
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.jetbrains.annotations.NotNull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -13,21 +14,26 @@ import java.util.List;
*/ */
public class CommandReload implements TabExecutor { public class CommandReload implements TabExecutor {
private final BooksWithoutBorders booksWithoutBorders = BooksWithoutBorders.getInstance();
@Override @Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
if (booksWithoutBorders.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(CommandSender sender, Command command, String alias, 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,29 +1,27 @@
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.BookDirectory;
import net.knarcraft.bookswithoutborders.state.ItemSlot; import net.knarcraft.bookswithoutborders.state.ItemSlot;
import net.knarcraft.bookswithoutborders.utility.BookFileHelper;
import net.knarcraft.bookswithoutborders.utility.BookHelper;
import net.knarcraft.bookswithoutborders.utility.BookToFromTextHelper; import net.knarcraft.bookswithoutborders.utility.BookToFromTextHelper;
import net.knarcraft.bookswithoutborders.utility.FileHelper;
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;
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 java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
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.getTitleAuthorSeparator;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getBookFolder;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getCommandColor;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getErrorColor;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getSlash;
import static net.knarcraft.bookswithoutborders.utility.InputCleaningHelper.cleanString;
import static net.knarcraft.bookswithoutborders.utility.InputCleaningHelper.fixName;
/** /**
* Command executor for the save command * Command executor for the save command
@@ -31,19 +29,19 @@ import static net.knarcraft.bookswithoutborders.utility.InputCleaningHelper.fixN
public class CommandSave implements TabExecutor { public class CommandSave implements TabExecutor {
@Override @Override
public boolean onCommand(CommandSender sender, Command command, 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;
@@ -52,7 +50,7 @@ public class CommandSave implements TabExecutor {
ItemSlot holdingSlot = InventoryHelper.getHeldSlotBook(player, false, false, false, false); ItemSlot holdingSlot = InventoryHelper.getHeldSlotBook(player, false, false, false, false);
if (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 {
@@ -69,86 +67,100 @@ 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;
} }
String savePath; BooksWithoutBordersConfig config = BooksWithoutBorders.getConfiguration();
if (saveToPublicFolder) {
savePath = getBookFolder(); //Only allow saving of own books if enabled
} else { if (config.getAuthorOnlySave() && !saveToPublicFolder &&
savePath = getBookFolder() + cleanString(player.getName()) + getSlash(); (!player.hasPermission("bookswithoutborders.bypassAuthorOnlySave") &&
BookHelper.isNotAuthor(player, book))) {
return;
}
String savePath = BookHelper.getBookDirectoryPathString(saveToPublicFolder ?
BookDirectory.PUBLIC : BookDirectory.PLAYER, player);
if (savePath == null) {
BooksWithoutBorders.sendErrorMessage(player, "Saving Failed! Unable to find the save path!");
return;
} }
//Generate book filename //Generate book filename
String fileName; String fileName;
if (!book.hasTitle()) { try {
fileName = "Untitled," + player.getName(); fileName = BookHelper.getBookFile(book, player, saveToPublicFolder);
} else { } catch (IllegalArgumentException exception) {
fileName = book.getTitle() + getTitleAuthorSeparator() + book.getAuthor(); BooksWithoutBorders.sendErrorMessage(player, exception.getMessage());
return;
} }
fileName = cleanString(fileName);
fileName = fixName(fileName, false);
//Make sure the used folders exist //Make sure the used folders exist
File file = new File(savePath); File file = new File(savePath);
if (!file.exists() && !file.mkdir()) { if (!file.exists() && !file.mkdir()) {
BooksWithoutBorders.sendErrorMessage(player, "Saving Failed! If this continues to happen, consult server admin!"); BooksWithoutBorders.sendErrorMessage(player, "Saving Failed! If this continues to happen, consult" +
" a server admin!");
return; return;
} }
File[] foundFiles = file.listFiles(); File[] foundFiles = file.listFiles();
if (foundFiles == null) { if (foundFiles == null) {
BooksWithoutBorders.sendErrorMessage(player, "Saving Failed! If this continues to happen, consult server admin!"); BooksWithoutBorders.sendErrorMessage(player, "Saving Failed! If this continues to happen, consult" +
" a server admin!");
return; return;
} }
//Find any duplicates of the book //Find any duplicates of the book
int foundDuplicates = FileHelper.findDuplicates(foundFiles, fileName); int foundDuplicates = BookFileHelper.findDuplicates(foundFiles, fileName);
//Deal with duplicates //Deal with duplicates
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") && !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 " + getErrorColor() + "to overwrite!"); BooksWithoutBorders.sendErrorMessage(player, "Use " + config.getCommandColor() + (saveToPublicFolder ?
"/savepublicbook" : "/savebook") + " true " + config.getErrorColor() + "to overwrite!");
return; return;
} }
//Skip if duplicate limit is reached //Skip if duplicate limit is reached
if (foundDuplicates > BooksWithoutBorders.getBookDuplicateLimit()) { if (foundDuplicates > config.getBookDuplicateLimit()) {
BooksWithoutBorders.sendErrorMessage(player, "Maximum amount of " + fileName + " duplicates reached!"); BooksWithoutBorders.sendErrorMessage(player, "Maximum amount of " + fileName +
BooksWithoutBorders.sendErrorMessage(player, "Use " + getCommandColor() + "/savebook true " + getErrorColor() + "to overwrite!"); " duplicates reached!");
BooksWithoutBorders.sendErrorMessage(player, "Use " + config.getCommandColor() + (saveToPublicFolder ?
"/savepublicbook" : "/savebook") + " true " +
config.getErrorColor() + "to overwrite!");
return; return;
} }
//Alter duplicated filename //Alter duplicated filename
if (fileName.contains("Untitled") && !overwrite) { if (fileName.contains("Untitled" + config.getTitleAuthorSeparator()) && !overwrite) {
fileName = "(" + foundDuplicates + ")" + fileName; fileName = "(" + foundDuplicates + ")" + fileName;
} }
} }
try { try {
if (BooksWithoutBorders.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 e) { } catch (IOException exception) {
e.printStackTrace(); BooksWithoutBorders.log(Level.SEVERE, "Unable to save book");
} }
} }
@Override @Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, 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<>();
} }
} }

View File

@@ -3,6 +3,7 @@ package net.knarcraft.bookswithoutborders.command;
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.jetbrains.annotations.NotNull;
/** /**
* Command executor for the save public command * Command executor for the save public command
@@ -10,8 +11,9 @@ import org.bukkit.command.TabExecutor;
public class CommandSavePublic extends CommandSave implements TabExecutor { public class CommandSavePublic extends CommandSave implements TabExecutor {
@Override @Override
public boolean onCommand(CommandSender sender, Command command, String label, 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,12 +3,16 @@ 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;
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.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -19,13 +23,14 @@ import java.util.List;
public class CommandSetAuthor implements TabExecutor { public class CommandSetAuthor implements TabExecutor {
@Override @Override
public boolean onCommand(CommandSender sender, Command command, String label, 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;
} }
@@ -40,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 + "!");
@@ -49,11 +58,14 @@ public class CommandSetAuthor implements TabExecutor {
} }
@Override @Override
public List<String> onTabComplete(CommandSender sender, Command command, 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

@@ -1,15 +1,19 @@
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.EconomyHelper; import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
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.TabCompletionHelper; import net.knarcraft.bookswithoutborders.utility.TabCompletionTypeHelper;
import net.knarcraft.knarlib.util.TabCompletionHelper;
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;
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.jetbrains.annotations.NotNull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -20,37 +24,39 @@ import java.util.List;
public class CommandSetBookPrice implements TabExecutor { public class CommandSetBookPrice implements TabExecutor {
private final BooksWithoutBorders booksWithoutBorders = BooksWithoutBorders.getInstance(); private final BooksWithoutBorders booksWithoutBorders = BooksWithoutBorders.getInstance();
private List<String> paymentTypes;
@Override @Override
public boolean onCommand(CommandSender sender, Command command, 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) {
@@ -64,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) {
BooksWithoutBorders.setBookPriceType(null); BooksWithoutBordersConfig config = BooksWithoutBorders.getConfiguration();
BooksWithoutBorders.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", BooksWithoutBorders.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!");
@@ -81,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;
@@ -93,10 +100,11 @@ public class CommandSetBookPrice implements TabExecutor {
return false; return false;
} }
BooksWithoutBorders.setBookPriceType(heldItem.getType()); BooksWithoutBordersConfig config = BooksWithoutBorders.getConfiguration();
BooksWithoutBorders.setBookPriceQuantity(price); config.setBookPriceType(heldItem.getType());
String newPriceType = BooksWithoutBorders.getBookPriceType().toString(); config.setBookPriceQuantity(price);
double newPriceQuantity = BooksWithoutBorders.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();
@@ -113,35 +121,49 @@ 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();
BooksWithoutBorders.setBookPriceQuantity(price); if (economyManager.getEconomy() != null) {
BooksWithoutBorders.setBookPriceType(Material.AIR); BooksWithoutBordersConfig config = BooksWithoutBorders.getConfiguration();
double newPriceQuantity = BooksWithoutBorders.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(CommandSender sender, Command command, String alias, String[] args) { public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
int argumentCount = args.length; @NotNull String[] arguments) {
if (paymentTypes == null) {
initializeTabCompleteLists();
}
int argumentCount = arguments.length;
if (argumentCount == 1) { if (argumentCount == 1) {
List<String> paymentTypes = new ArrayList<>(); return TabCompletionHelper.filterMatchingStartsWith(paymentTypes, arguments[0]);
paymentTypes.add("item");
paymentTypes.add("eco");
return paymentTypes;
} else if (argumentCount == 2) { } else if (argumentCount == 2) {
return TabCompletionHelper.getNumbers(1, 3); return TabCompletionHelper.filterMatchingStartsWith(TabCompletionTypeHelper.getNumbers(1, 3), arguments[1]);
} }
return new ArrayList<>(); return new ArrayList<>();
} }
/**
* Initializes the lists of tab complete values
*/
private void initializeTabCompleteLists() {
paymentTypes = new ArrayList<>();
paymentTypes.add("item");
paymentTypes.add("eco");
}
} }

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

@@ -0,0 +1,74 @@
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;
/**
* Command executor for the set generation command
*/
public class CommandSetGeneration implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] arguments) {
if (!(sender instanceof Player player)) {
BooksWithoutBorders.sendErrorMessage(sender, "This command can only be used by a player!");
return false;
}
if (InventoryHelper.notHoldingOneWrittenBookCheck(player, "You must be holding a written book to" +
" change its generation!", "You cannot change the generation of two books at once!")) {
return false;
}
if (arguments.length < 1) {
BooksWithoutBorders.sendErrorMessage(player, "You must specify the new generation for your book!");
return false;
}
BookMeta.Generation generation;
try {
generation = BookMeta.Generation.valueOf(arguments[0]);
} catch (IllegalArgumentException exception) {
BooksWithoutBorders.sendErrorMessage(player, "Invalid book generation specified!");
return false;
}
ItemStack heldBook = InventoryHelper.getHeldBook(player, true);
BookMeta bookMeta = (BookMeta) heldBook.getItemMeta();
if (bookMeta != null) {
bookMeta.setGeneration(generation);
heldBook.setItemMeta(bookMeta);
return true;
} else {
BooksWithoutBorders.sendErrorMessage(player, "Unable to get book metadata!");
return false;
}
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] arguments) {
if (arguments.length == 1) {
List<String> generations = new ArrayList<>();
for (BookMeta.Generation generation : BookMeta.Generation.values()) {
generations.add(generation.name());
}
return generations;
}
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.utility.InventoryHelper; import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import org.bukkit.ChatColor; import net.knarcraft.knarlib.property.ColorConversion;
import net.knarcraft.knarlib.util.ColorHelper;
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;
@@ -10,6 +11,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.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.Arrays; import java.util.Arrays;
@@ -21,13 +23,14 @@ import java.util.List;
public class CommandSetLore implements TabExecutor { public class CommandSetLore implements TabExecutor {
@Override @Override
public boolean onCommand(CommandSender sender, Command command, String label, 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;
} }
@@ -39,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 = ChatColor.translateAlternateColorCodes('&', rawLore); rawLore = ColorHelper.translateColorCodes(rawLore, ColorConversion.RGB);
String[] loreParts = rawLore.split(BooksWithoutBorders.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();
@@ -59,10 +61,12 @@ public class CommandSetLore implements TabExecutor {
} }
@Override @Override
public List<String> onTabComplete(CommandSender sender, Command command, 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>");
return options; return options;
} }
} }

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.utility.InventoryHelper; import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import org.bukkit.ChatColor; import net.knarcraft.knarlib.property.ColorConversion;
import net.knarcraft.knarlib.util.ColorHelper;
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;
@@ -11,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.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;
@@ -21,13 +23,14 @@ import java.util.List;
public class CommandSetTitle implements TabExecutor { public class CommandSetTitle implements TabExecutor {
@Override @Override
public boolean onCommand(CommandSender sender, Command command, String label, 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;
} }
@@ -38,8 +41,8 @@ public class CommandSetTitle implements TabExecutor {
return false; return false;
} }
String title = String.join(" ", args); String title = String.join(" ", arguments).strip();
title = ChatColor.translateAlternateColorCodes('&', title); title = ColorHelper.translateColorCodes(title, ColorConversion.RGB);
ItemMeta itemMetadata = heldItem.getItemMeta(); ItemMeta itemMetadata = heldItem.getItemMeta();
if (itemMetadata == null) { if (itemMetadata == null) {
@@ -50,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;
@@ -65,9 +72,11 @@ public class CommandSetTitle implements TabExecutor {
} }
@Override @Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) { public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@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

@@ -2,17 +2,19 @@ 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.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.jetbrains.annotations.NotNull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
/** /**
* Command executor for the unsign command * Command executor for the unsign command
@@ -20,7 +22,8 @@ import java.util.List;
public class CommandUnSign implements TabExecutor { public class CommandUnSign implements TabExecutor {
@Override @Override
public boolean onCommand(CommandSender sender, Command command, String label, 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;
@@ -43,19 +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 oldBook = 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);
//UnSign the book //Only allow the owner to un-sign the book
ItemStack newBook = new ItemStack(Material.WRITABLE_BOOK); if (BooksWithoutBorders.getConfiguration().getAuthorOnlyUnsign() &&
newBook.setItemMeta(oldBook); !player.hasPermission("bookswithoutborders.bypassAuthorOnlyUnsign")) {
if (BookHelper.isNotAuthor(player, Objects.requireNonNull(oldMetadata))) {
return;
}
}
InventoryHelper.replaceHeldItem(player, newBook, mainHand); // Give the player the new book
ItemStack book = BookHelper.unsignBook(oldMetadata, heldBook.getAmount());
if (book == null) {
return;
}
reverseColorCodes(book);
InventoryHelper.replaceHeldItem(player, book, mainHand);
} }
@Override @Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, 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

@@ -0,0 +1,433 @@
package net.knarcraft.bookswithoutborders.config;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.manager.EconomyManager;
import net.knarcraft.knarlib.formatting.Translator;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.Material;
import org.bukkit.configuration.Configuration;
import org.jetbrains.annotations.NotNull;
import java.nio.file.FileSystems;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import static net.knarcraft.bookswithoutborders.utility.InputCleaningHelper.cleanString;
/**
* A config class that keeps track of all config values
*/
public class BooksWithoutBordersConfig {
private final ChatColor errorColor = ChatColor.RED;
private final ChatColor successColor = ChatColor.GREEN;
private final ChatColor commandColor = ChatColor.YELLOW;
private final String SLASH = FileSystems.getDefault().getSeparator();
private boolean isInitialized;
private final String bookFolder;
private int bookDuplicateLimit;
private String titleAuthorSeparator;
private String loreSeparator;
private List<String> firstBooks = new ArrayList<>();
private String welcomeMessage;
private Material bookPriceType = null;
private double bookPriceQuantity;
private boolean authorOnlyCopy;
private boolean authorOnlyUnsign;
private boolean authorOnlySave;
private boolean adminDecrypt;
private boolean formatBooks;
private boolean changeGenerationOnCopy;
private boolean enableBookshelfPeeking;
private boolean useRealEncryption;
private final Translator translator;
private EconomyManager economyManager;
/**
* Initializes the books without borders settings class
*
* @param booksWithoutBorders <p>The books without borders object used for getting required data</p>
*/
public BooksWithoutBordersConfig(@NotNull BooksWithoutBorders booksWithoutBorders, @NotNull Translator translator) {
if (isInitialized) {
throw new IllegalArgumentException("Settings class initialized twice. This should not happen!");
}
this.translator = translator;
isInitialized = true;
bookFolder = booksWithoutBorders.getDataFolder().getAbsolutePath() + getSlash() + "Books" + getSlash();
loadConfig();
}
/**
* Gets the economy manager
*
* @return <p>The economy manager</p>
*/
@NotNull
public EconomyManager getEconomyManager() {
return this.economyManager;
}
/**
* Gets the folder used for storing books
*
* @return <p>The folder used for storing books</p>
*/
public String getBookFolder() {
return this.bookFolder;
}
/**
* Gets the color to use for error messages
*
* @return <p>The color to use for error messages</p>
*/
public ChatColor getErrorColor() {
return this.errorColor;
}
/**
* Gets the color to use for success messages
*
* @return <p>The color to use for success messages</p>
*/
public ChatColor getSuccessColor() {
return this.successColor;
}
/**
* Gets the color used to color commands
*
* @return <p>The color used to color commands</p>
*/
public ChatColor getCommandColor() {
return this.commandColor;
}
/**
* Gets the correct slash to use for the used OS
*
* @return <p>The slash to use for file separators</p>
*/
public String getSlash() {
return this.SLASH;
}
/**
* Gets whether only the author of a book should be able to copy it
*
* @return <p>Whether only the book author can copy it</p>
*/
public boolean getAuthorOnlyCopy() {
return this.authorOnlyCopy;
}
/**
* Gets whether only the author of a book should be able to unsign it
*
* @return <p>Whether only the book author can unsign it</p>
*/
public boolean getAuthorOnlyUnsign() {
return this.authorOnlyUnsign;
}
/**
* Gets whether a player can only save their own books with /savebook
*
* @return <p>Whether a player can only save their own books</p>
*/
public boolean getAuthorOnlySave() {
return this.authorOnlySave;
}
/**
* Gets whether players can left-click a bookshelf to peek at the contained books
*
* @return <p>True if players can peek at the contained books</p>
*/
public boolean getEnableBookshelfPeeking() {
return this.enableBookshelfPeeking;
}
/**
* Gets whether admins should be able to decrypt books without a password, and decrypt all group encrypted books
*
* @return <p>Whether admins can bypass the encryption password</p>
*/
public boolean getAdminDecrypt() {
return this.adminDecrypt;
}
/**
* Sets the quantity of items/currency necessary for copying books
*
* @param newQuantity <p>The new quantity necessary for payment</p>
*/
public void setBookPriceQuantity(double newQuantity) {
this.bookPriceQuantity = newQuantity;
}
/**
* Gets the quantity of items/currency necessary for copying books
*
* @return <p>The quantity necessary for payment</p>
*/
public double getBookPriceQuantity() {
return this.bookPriceQuantity;
}
/**
* Sets the item type used for book pricing
*
* <p>This item is the one a player has to pay for copying books. AIR is used to denote economy. null is used if
* payment is disabled. Otherwise, any item can be used.</p>
*
* @param newType <p>The new item type to use for book pricing</p>
*/
public void setBookPriceType(Material newType) {
this.bookPriceType = newType;
}
/**
* Gets the item type used for book pricing
*
* <p>This item is the one a player has to pay for copying books. AIR is used to denote economy. null is used if
* payment is disabled. Otherwise, any item can be used.</p>
*
* @return <p>The item type used for book pricing</p>
*/
public Material getBookPriceType() {
return this.bookPriceType;
}
/**
* Gets the welcome message to show to new players
*
* @return <p>The welcome message to show new players</p>
*/
public String getWelcomeMessage() {
return this.welcomeMessage;
}
/**
* Gets the limit of duplicates for each book
*
* @return <p>The book duplicate limit</p>
*/
public int getBookDuplicateLimit() {
return this.bookDuplicateLimit;
}
/**
* Gets whether books should change their generation during copy
*
* @return <p>True if books should change their generation</p>
*/
public boolean changeGenerationOnCopy() {
return this.changeGenerationOnCopy;
}
/**
* Gets the separator used to split book title from book author
*
* @return <p>The separator between title and author</p>
*/
public String getTitleAuthorSeparator() {
return this.titleAuthorSeparator;
}
/**
* Gets the separator used to denote a newline in a lore string
*
* @return <p>The separator used to denote lore newline</p>
*/
public String getLoreSeparator() {
return this.loreSeparator;
}
/**
* Gets whether all books should be formatted when they are signed
*
* @return <p>Whether all books should be formatted</p>
*/
public boolean formatBooks() {
return this.formatBooks;
}
/**
* Gets a copy of the list of books to give new players
*
* @return <p>The books to give new players</p>
*/
public List<String> getFirstBooks() {
return new ArrayList<>(this.firstBooks);
}
/**
* Checks whether books have a price for printing them
*
* @return <p>True if players need to pay for printing books</p>
*/
public boolean booksHavePrice() {
return (this.bookPriceType != null && this.bookPriceQuantity > 0);
}
/**
* Checks whether to use real encryption for encrypted books
*
* @return <p>True if real encryption should be used</p>
*/
public boolean useRealEncryption() {
return this.useRealEncryption;
}
/**
* Gets the path used to store encrypted books
*
* @return <p>The encrypted book path</p>
*/
@NotNull
public String getEncryptedBookPath() {
return getBookFolder() + "Encrypted" + getSlash();
}
/**
* Saves the config
*/
public void saveConfigValues() {
Logger logger = BooksWithoutBorders.getInstance().getLogger();
Configuration config = BooksWithoutBorders.getInstance().getConfig();
config.set(ConfigOption.MAX_DUPLICATES.getConfigNode(), this.bookDuplicateLimit);
config.set(ConfigOption.TITLE_AUTHOR_SEPARATOR.getConfigNode(), this.titleAuthorSeparator);
config.set(ConfigOption.LORE_LINE_SEPARATOR.getConfigNode(), this.loreSeparator);
config.set(ConfigOption.BOOKS_FOR_NEW_PLAYERS.getConfigNode(), this.firstBooks);
config.set(ConfigOption.MESSAGE_FOR_NEW_PLAYERS.getConfigNode(), this.welcomeMessage);
config.set(ConfigOption.FORMAT_AFTER_SIGNING.getConfigNode(), this.formatBooks);
config.set(ConfigOption.ENABLE_BOOKSHELF_PEEKING.getConfigNode(), this.enableBookshelfPeeking);
config.set(ConfigOption.USE_REAL_ENCRYPTION.getConfigNode(), this.useRealEncryption);
String itemTypeNode = ConfigOption.PRICE_ITEM_TYPE.getConfigNode();
if (this.bookPriceType != null) {
if (this.bookPriceType != Material.AIR) {
config.set(itemTypeNode, this.bookPriceType.toString());
} else {
config.set(itemTypeNode, "Economy");
}
} else {
config.set(itemTypeNode, "Item type name");
}
config.set(ConfigOption.PRICE_QUANTITY.getConfigNode(), this.bookPriceQuantity);
config.set(ConfigOption.ADMIN_AUTO_DECRYPT.getConfigNode(), this.adminDecrypt);
config.set(ConfigOption.AUTHOR_ONLY_COPY.getConfigNode(), this.authorOnlyCopy);
config.set(ConfigOption.AUTHOR_ONLY_UNSIGN.getConfigNode(), this.authorOnlyUnsign);
config.set(ConfigOption.AUTHOR_ONLY_SAVE.getConfigNode(), this.authorOnlySave);
config.set(ConfigOption.CHANGE_GENERATION_ON_COPY.getConfigNode(), this.changeGenerationOnCopy);
//Handles old book and quill settings
if (config.contains("Options.Require_book_and_quill_to_create_book")) {
logger.log(Level.INFO, "[BooksWithoutBorders] Found old config setting " +
"\"Require_book_and_quill_to_create_book\"\nUpdating to \"Price_to_create_book\" settings");
if (config.getBoolean("Options.Require_book_and_quill_to_create_book")) {
this.bookPriceType = Material.WRITABLE_BOOK;
this.bookPriceQuantity = 1;
config.set("Options.Price_to_create_book.Item_type", this.bookPriceType.toString());
config.set("Options.Price_to_create_book.Required_quantity", this.bookPriceQuantity);
}
config.set("Options.Require_book_and_quill_to_create_book", null);
}
BooksWithoutBorders.getInstance().saveConfig();
}
/**
* Loads the config
*
* @return <p>True if the config was loaded successfully</p>
*/
public boolean loadConfig() {
Logger logger = BooksWithoutBorders.getInstance().getLogger();
BooksWithoutBorders.getInstance().reloadConfig();
Configuration config = BooksWithoutBorders.getInstance().getConfig();
try {
this.bookDuplicateLimit = config.getInt(ConfigOption.MAX_DUPLICATES.getConfigNode(),
(Integer) ConfigOption.MAX_DUPLICATES.getDefaultValue());
this.titleAuthorSeparator = getString(config, ConfigOption.TITLE_AUTHOR_SEPARATOR);
this.loreSeparator = getString(config, ConfigOption.LORE_LINE_SEPARATOR);
this.adminDecrypt = getBoolean(config, ConfigOption.ADMIN_AUTO_DECRYPT);
this.authorOnlyCopy = getBoolean(config, ConfigOption.AUTHOR_ONLY_COPY);
this.authorOnlyUnsign = getBoolean(config, ConfigOption.AUTHOR_ONLY_UNSIGN);
this.authorOnlySave = getBoolean(config, ConfigOption.AUTHOR_ONLY_SAVE);
this.firstBooks = config.getStringList(ConfigOption.BOOKS_FOR_NEW_PLAYERS.getConfigNode());
this.welcomeMessage = getString(config, ConfigOption.MESSAGE_FOR_NEW_PLAYERS);
this.formatBooks = getBoolean(config, ConfigOption.FORMAT_AFTER_SIGNING);
this.changeGenerationOnCopy = getBoolean(config, ConfigOption.CHANGE_GENERATION_ON_COPY);
this.enableBookshelfPeeking = getBoolean(config, ConfigOption.ENABLE_BOOKSHELF_PEEKING);
this.useRealEncryption = getBoolean(config, ConfigOption.USE_REAL_ENCRYPTION);
String language = config.getString("language", "en");
this.translator.loadLanguages(BooksWithoutBorders.getInstance().getDataFolder(), "en", language);
//Convert string into material
this.economyManager = new EconomyManager();
String paymentMaterial = getString(config, ConfigOption.PRICE_ITEM_TYPE);
if (paymentMaterial.equalsIgnoreCase("Economy")) {
if (this.economyManager.getEconomy() == null) {
logger.log(Level.SEVERE, StaticMessage.EXCEPTION_VAULT_NOT_AVAILABLE.toString());
this.bookPriceType = null;
} else {
this.bookPriceType = Material.AIR;
}
} else if (!paymentMaterial.trim().isEmpty()) {
Material material = Material.matchMaterial(paymentMaterial);
if (material != null) {
this.bookPriceType = material;
}
}
this.bookPriceQuantity = config.getDouble(ConfigOption.PRICE_QUANTITY.getConfigNode(),
(Double) ConfigOption.PRICE_QUANTITY.getDefaultValue());
//Make sure titleAuthorSeparator is a valid value
this.titleAuthorSeparator = cleanString(this.titleAuthorSeparator);
if (this.titleAuthorSeparator.length() != 1) {
logger.log(Level.SEVERE, "Title-Author_Separator is set to an invalid value!\n" +
"Reverting to default value of \",\"");
this.titleAuthorSeparator = ",";
config.set("Options.Title-Author_Separator", this.titleAuthorSeparator);
}
} catch (Exception exception) {
logger.log(Level.SEVERE, exception.getMessage());
logger.log(Level.SEVERE, "Warning! Config.yml failed to load!\n" +
"Try Looking for settings that are missing values!");
return false;
}
return true;
}
/**
* Gets the string value of the given config option
*
* @param config <p>The configuration to read from</p>
* @param configOption <p>The configuration option to get the value for</p>
* @return <p>The value of the option</p>
*/
private static String getString(@NotNull Configuration config, @NotNull ConfigOption configOption) {
return config.getString(configOption.getConfigNode(), (String) configOption.getDefaultValue());
}
/**
* Gets the boolean value of the given config option
*
* @param config <p>The configuration to read from</p>
* @param configOption <p>The configuration option to get the value for</p>
* @return <p>The value of the option</p>
*/
private static boolean getBoolean(@NotNull Configuration config, @NotNull ConfigOption configOption) {
return config.getBoolean(configOption.getConfigNode(), (Boolean) configOption.getDefaultValue());
}
}

View File

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

View File

@@ -0,0 +1,118 @@
package net.knarcraft.bookswithoutborders.config;
import org.jetbrains.annotations.NotNull;
/**
* A representation of the different available config options
*/
public enum ConfigOption {
/**
* The max duplicates of a book that can be saved
*/
MAX_DUPLICATES("Options.Max_Number_of_Duplicates", 5),
/**
* The separator used to separate book title and book author
*/
TITLE_AUTHOR_SEPARATOR("Options.Title-Author_Separator", ","),
/**
* The separator used to specify a new line in an item's lore
*/
LORE_LINE_SEPARATOR("Options.Lore_line_separator", "~"),
/**
* The books given to new players when they first join
*/
BOOKS_FOR_NEW_PLAYERS("Options.Books_for_new_players", "[]"),
/**
* The message to display to new players when they first join
*/
MESSAGE_FOR_NEW_PLAYERS("Options.Message_for_new_players", ""),
/**
* The item type used to pay for book copying
*/
PRICE_ITEM_TYPE("Options.Price_to_create_book.Item_type", ""),
/**
* The amount of items used to pay for book copying
*/
PRICE_QUANTITY("Options.Price_to_create_book.Required_quantity", 0.0),
/**
* Whether admins should be able to decrypt books for all groups
*/
ADMIN_AUTO_DECRYPT("Options.Admin_Auto_Decrypt", false),
/**
* Whether only the book author should be able to copy a book
*/
AUTHOR_ONLY_COPY("Options.Author_Only_Copy", false),
/**
* Whether only the book author should be able to unsign a book
*/
AUTHOR_ONLY_UNSIGN("Options.Author_Only_Unsign", false),
/**
* Whether a player can only save their own books with /savebook
*/
AUTHOR_ONLY_SAVE("Options.Author_Only_Save", false),
/**
* Whether to turn Original into Copy when copying books
*/
CHANGE_GENERATION_ON_COPY("Options.Change_Generation_On_Copy", false),
/**
* Whether to automatically format every signed book
*/
FORMAT_AFTER_SIGNING("Options.Format_Book_After_Signing", false),
/**
* Whether hitting a bookshelf should display information about the contained books
*/
ENABLE_BOOKSHELF_PEEKING("Options.Enable_Book_Peeking", 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 Object defaultValue;
/**
* Instantiates a new config option
*
* @param configNode <p>The config node in the config file this option represents</p>
* @param defaultValue <p>The default value for this config option</p>
*/
ConfigOption(@NotNull String configNode, @NotNull Object defaultValue) {
this.configNode = configNode;
this.defaultValue = defaultValue;
}
/**
* Gets the config node used for loading/saving this config value
*
* @return <p>The config node</p>
*/
public String getConfigNode() {
return this.configNode;
}
/**
* Gets the default value of this config option
*
* @return <p>The default value of this config option</p>
*/
public Object getDefaultValue() {
return this.defaultValue;
}
}

View File

@@ -0,0 +1,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,292 @@
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 success message displayed when a title page is successfully added
*/
SUCCESS_TITLE_PAGE_ADDED,
/**
* 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 error displayed when a book page is specified outside the allowed range.
*/
ERROR_INVALID_BOOK_PAGE,
/**
* 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 unknown title for a book
*/
NEUTRAL_UNKNOWN_TITLE,
/**
* The format used when generating a title + author title page
*/
NEUTRAL_TITLE_PAGE_TITLE_AUTHOR_FORMAT,
/**
* The formatting used when formatting the header on a title page
*/
NEUTRAL_TITLE_PAGE_HEADER_FORMAT,
/**
* The format used when formatting text on a title page
*/
NEUTRAL_TITLE_PAGE_TEXT_FORMAT,
/**
* 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,
/**
* The translation of the add title page action
*/
ACTION_ADD_TITLE_AUTHOR_PAGE,
;
@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,5 +1,9 @@
package net.knarcraft.bookswithoutborders.encryption; package net.knarcraft.bookswithoutborders.encryption;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.crypto.BadPaddingException; import javax.crypto.BadPaddingException;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException; import javax.crypto.IllegalBlockSizeException;
@@ -15,6 +19,7 @@ import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException; import java.security.spec.InvalidKeySpecException;
import java.util.Base64; import java.util.Base64;
import java.util.logging.Level;
/** /**
* This class represents and AES encryptor/decryptor * This class represents and AES encryptor/decryptor
@@ -24,32 +29,58 @@ import java.util.Base64;
* *
* @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
@@ -66,32 +97,23 @@ public class AES {
//Initialize cipher //Initialize cipher
try { try {
aes.init(mode, secretKeySpec, ivParameterSpec); aes.init(mode, secretKeySpec, ivParameterSpec);
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) { } catch (InvalidKeyException | InvalidAlgorithmParameterException exception) {
e.printStackTrace(); 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 e) { } catch (IllegalBlockSizeException exception) {
e.printStackTrace(); 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
* *
@@ -99,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 {
@@ -114,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);
@@ -127,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 e) { } catch (NoSuchAlgorithmException exception) {
e.printStackTrace(); 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;
@@ -144,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 e) { } catch (NoSuchAlgorithmException exception) {
e.printStackTrace(); 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 e) { } catch (InvalidKeySpecException exception) {
e.printStackTrace(); 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,13 +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 ArrayList<String> originalCodonList; private final String[] availableCharacters;
private final ArrayList<String> shuffledCodonList;
private final String[] charList;
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;
@@ -26,16 +27,16 @@ 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
originalCodonList = new ArrayList<>(); ArrayList<String> originalCodonList = new ArrayList<>();
bases = new String[]{"A", "T", "G", "C"}; bases = new String[]{"A", "T", "G", "C"};
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) { for (int j = 0; j < 4; j++) {
for (int k = 0; k < 4; k++) { for (int k = 0; k < 4; k++) {
for (int l = 0; l < 4; l++) { for (int l = 0; l < 4; l++) {
originalCodonList.add("" + bases[i] + bases[j] + bases[k] + bases[l]); originalCodonList.add(bases[i] + bases[j] + bases[k] + bases[l]);
} }
} }
} }
@@ -50,8 +51,8 @@ public class GenenCrypt {
ranGen = new java.util.Random(longKey); ranGen = new java.util.Random(longKey);
// use the random number generator and the originalCodonList to make a shuffled list // use the random number generator and the originalCodonList to make a shuffled list
shuffledCodonList = new ArrayList<>(); ArrayList<String> shuffledCodonList = new ArrayList<>();
while (originalCodonList.size() > 0) { while (!originalCodonList.isEmpty()) {
int index = ranGen.nextInt(originalCodonList.size()); int index = ranGen.nextInt(originalCodonList.size());
shuffledCodonList.add(originalCodonList.get(index)); shuffledCodonList.add(originalCodonList.get(index));
originalCodonList.remove(index); originalCodonList.remove(index);
@@ -62,50 +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
* Prints the shuffled codon list used for generating the codon table public @Nullable String encryptText(@NotNull String input) {
*/ return encrypt(input);
public void printShuffledList() {
for (String s : shuffledCodonList) {
System.out.println(s);
}
} }
/** @Override
* Prints the original codon list before it was shuffled public @Nullable String decryptText(@NotNull String input) {
*/ return decrypt(input);
public void printOriginalList() {
for (String s : originalCodonList) {
System.out.println(s);
}
} }
/** /**
@@ -114,12 +105,15 @@ 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" -> System.out.println(i + "\t" + "\\t" + "\t" + sa[0] + ", " + sa[1] + ", " + sa[2] + ", " + sa[3]); case "\t" ->
case "\n" -> System.out.println(i + "\t" + "\\n" + "\t" + sa[0] + ", " + sa[1] + ", " + sa[2] + ", " + sa[3]); System.out.println(i + "\t" + "\\t" + "\t" + sa[0] + ", " + sa[1] + ", " + sa[2] + ", " + sa[3]);
case " " -> System.out.println(i + "\t" + "\" \"" + "\t" + sa[0] + ", " + sa[1] + ", " + sa[2] + ", " + sa[3]); case "\n" ->
System.out.println(i + "\t" + "\\n" + "\t" + sa[0] + ", " + sa[1] + ", " + sa[2] + ", " + sa[3]);
case " " ->
System.out.println(i + "\t" + "\" \"" + "\t" + sa[0] + ", " + sa[1] + ", " + sa[2] + ", " + sa[3]);
default -> System.out.println(i + "\t" + s + "\t" + sa[0] + ", " + sa[1] + ", " + sa[2] + ", " + sa[3]); default -> System.out.println(i + "\t" + s + "\t" + sa[0] + ", " + sa[1] + ", " + sa[2] + ", " + sa[3]);
} }
} }
@@ -131,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
@@ -164,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);
@@ -181,4 +177,5 @@ public class GenenCrypt {
} }
return output.toString(); return output.toString();
} }
} }

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,87 +1,98 @@
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;
public class SubstitutionCipher { /**
* A simple substitution cipher
*/
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.length() > 0) {
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>
public String decrypt(String in, String key) { * @return <p>The offset array</p>
StringBuilder output = new StringBuilder(); */
if (key != null && key.length() > 0) { private int[] getOffsetArray(@NotNull String key) {
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++) {
offsetArray[i] = Integer.parseInt(tokenizer.nextToken()); String nextToken = tokenizer.nextToken();
}
int offsetPosition = 0; try {
for (int i = 0; i < in.length(); i++) { offsetArray[i] = Integer.parseInt(nextToken);
output.append((char) (in.charAt(i) - offsetArray[offsetPosition])); //encrypts the letter and adds to the output string } catch (NumberFormatException e) {
// uses the next offset in the key, goes back to first offset if at end of list BigInteger big = new BigInteger(nextToken);
if (offsetPosition < offsetArray.length - 1) { offsetArray[i] = Math.abs(big.intValue());
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.
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

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

View File

@@ -0,0 +1,202 @@
package net.knarcraft.bookswithoutborders.listener;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.container.Bookshelf;
import net.knarcraft.bookswithoutborders.handler.BookshelfHandler;
import net.knarcraft.bookswithoutborders.utility.BookHelper;
import net.knarcraft.bookswithoutborders.utility.IntegerToRomanConverter;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.ChiseledBookshelf;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.ChiseledBookshelfInventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
import org.bukkit.inventory.meta.EnchantmentStorageMeta;
import org.bukkit.inventory.meta.ItemMeta;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* A listener for bookshelf clicking
*/
public class BookshelfListener implements Listener {
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onBookshelfBreak(@NotNull BlockBreakEvent event) {
Block block = event.getBlock();
if (block.getType() != Material.CHISELED_BOOKSHELF) {
return;
}
BookshelfHandler bookshelfHandler = BooksWithoutBorders.getBookshelfHandler();
Bookshelf bookshelf = bookshelfHandler.getFromLocation(block.getLocation());
if (bookshelf != null) {
bookshelfHandler.unregisterBookshelf(bookshelf);
bookshelfHandler.save();
}
}
@EventHandler
public void onBookshelfClick(@NotNull PlayerInteractEvent event) {
Player player = event.getPlayer();
// If left-clicking a chiseled bookshelf and sneaking, display contents
if (!event.hasBlock() || event.getClickedBlock() == null ||
!(event.getClickedBlock().getState() instanceof ChiseledBookshelf chiseledBookshelf) ||
event.getAction() != Action.LEFT_CLICK_BLOCK || !player.isSneaking()) {
return;
}
// Check if bookshelf peeking is enabled, and the player can peek
if (!BooksWithoutBorders.getConfiguration().getEnableBookshelfPeeking() ||
!event.getPlayer().hasPermission("bookswithoutborders.peekbookshelf")) {
return;
}
event.setUseInteractedBlock(Event.Result.DENY);
event.setUseItemInHand(Event.Result.DENY);
ChiseledBookshelfInventory bookshelfInventory = chiseledBookshelf.getInventory();
player.sendMessage(getBookshelfDescription(bookshelfInventory, event.getClickedBlock().getLocation()));
}
/**
* Gets the description for a bookshelf's contents
*
* @param bookshelfInventory <p>The inventory of the bookshelf to describe</p>
* @param location <p>The location of the clicked bookshelf</p>
* @return <p>A textual description of the bookshelf's contents</p>
*/
@NotNull
private String getBookshelfDescription(@NotNull ChiseledBookshelfInventory bookshelfInventory, @NotNull Location location) {
StringBuilder builder = new StringBuilder();
Bookshelf bookshelf = BooksWithoutBorders.getBookshelfHandler().getFromLocation(location);
if (bookshelf != null) {
builder.append(ChatColor.of("#FF5700")).append("Books in ").append(bookshelf.getTitle())
.append(":").append(ChatColor.RESET);
for (String lore : bookshelf.getLore()) {
builder.append("\n ").append(ChatColor.LIGHT_PURPLE).append(lore);
}
} else {
builder.append(ChatColor.of("#FF5700")).append("Books in shelf:").append(ChatColor.RESET);
}
for (int i = 0; i < bookshelfInventory.getSize(); i++) {
int index = (i % 3) + 1;
if (i % 3 == 0) {
builder.append("\n ").append(ChatColor.of("#FF5700")).append(
i < 3 ? "Top Row:" : "Bottom Row:").append(ChatColor.RESET);
}
builder.append("\n ").append(ChatColor.of("#ffd700")).append(index).append(". ").append(ChatColor.RESET);
ItemStack itemStack = bookshelfInventory.getItem(i);
if (itemStack == null) {
builder.append(ChatColor.GRAY).append("<empty>");
continue;
}
ItemMeta meta = itemStack.getItemMeta();
if (meta instanceof BookMeta bookMeta) {
builder.append(getBookDescription(bookMeta));
} else if (meta instanceof EnchantmentStorageMeta enchantmentStorageMeta) {
builder.append(getEnchantedBookDescription(enchantmentStorageMeta));
} else if (meta != null) {
builder.append(ChatColor.of("#A5682A")).append("[P]").append(ChatColor.RESET).append(getPlainBookDescription(meta));
}
}
return builder.toString();
}
/**
* Gets the description of a plain (enchant-able) book
*
* @param itemMeta <p>The metadata for the book to describe</p>
* @return <p>The description of the book</p>
*/
@NotNull
private String getPlainBookDescription(@NotNull ItemMeta itemMeta) {
String name = itemMeta.getDisplayName();
if (name.isEmpty()) {
name = "Plain book";
}
return name;
}
/**
* Gets the description of a book
*
* @param bookMeta <p>The metadata for the book to describe</p>
* @return <p>The book's description</p>
*/
@NotNull
private String getBookDescription(@NotNull BookMeta bookMeta) {
String title = BookHelper.getBookTitle(bookMeta);
String author = BookHelper.getBookAuthor(bookMeta, null);
return ChatColor.of("#686868") + "[Q]" + ChatColor.RESET + title + ChatColor.RESET + " by " + author;
}
/**
* Gets the description of an enchanted book
*
* @param enchantmentStorageMeta <p>The metadata for the enchanted book to describe</p>
* @return <p>The enchanted book's description</p>
*/
@NotNull
private String getEnchantedBookDescription(@NotNull EnchantmentStorageMeta enchantmentStorageMeta) {
StringBuilder builder = new StringBuilder();
builder.append(ChatColor.of("#A64CFF")).append("[E]").append(ChatColor.RESET);
Map<Enchantment, Integer> enchantmentMap = enchantmentStorageMeta.getStoredEnchants();
List<String> enchantments = new ArrayList<>(enchantmentMap.size());
for (Map.Entry<Enchantment, Integer> enchantmentEntry : enchantmentMap.entrySet()) {
enchantments.add(getEnchantmentName(enchantmentEntry.getKey()) + " " +
IntegerToRomanConverter.getRomanNumber(enchantmentEntry.getValue()));
}
builder.append(String.join(", ", enchantments));
return builder.toString();
}
/**
* Gets a prettified name of an enchantment
*
* @param enchantment <p>The enchantment to get the name of</p>
* @return <p>The prettified enchantment name</p>
*/
@NotNull
private String getEnchantmentName(@NotNull Enchantment enchantment) {
// Note: While depreciated, changing this is incompatible with Paper
return uppercaseFirst(enchantment.getKey().getKey().replace("_", " "));
}
/**
* Turns the first character of each of a string's words into uppercase
*
* @param input <p>The input to uppercase</p>
* @return <p>The input string with more uppercase</p>
*/
@NotNull
private String uppercaseFirst(@NotNull String input) {
String[] parts = input.split(" ");
for (int i = 0; i < parts.length; i++) {
parts[i] = parts[i].substring(0, 1).toUpperCase() + parts[i].substring(1);
}
return String.join(" ", parts);
}
}

View File

@@ -1,79 +1,50 @@
package net.knarcraft.bookswithoutborders.listener; 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.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 static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getBookFolder; /**
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getSlash; * A listener for listening to player-related events such as joining or holding a book
*/
public class PlayerEventListener implements Listener { public class PlayerEventListener implements Listener {
private final String slash = getSlash();
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(); Player player = event.getPlayer();
int selectedSlot = event.getNewSlot(); BooksWithoutBordersConfig config = BooksWithoutBorders.getConfiguration();
PlayerInventory playerInventory = player.getInventory();
ItemStack selectedItem = playerInventory.getItem(selectedSlot);
//Ignore irrelevant items //If a book directory exists with this player's name, move it to this player's UUID
if (selectedItem == null || selectedItem.getType() != Material.WRITTEN_BOOK) { String bookFolder = config.getBookFolder();
return; File file = new File(bookFolder + InputCleaningHelper.cleanString(player.getName()));
if (file.exists()) {
if (!file.renameTo(new File(bookFolder + player.getUniqueId()))) {
BooksWithoutBorders.log(Level.WARNING, "Unable to migrate player book " +
"directory for player " + player.getName());
}
} }
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();
//Handle new players //Handle new players
if (!player.hasPlayedBefore()) { if (!player.hasPlayedBefore()) {
boolean sendMessage = true; boolean sendMessage = true;
//Gives new players necessary books //Gives new players necessary books
for (String bookName : BooksWithoutBorders.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);
}
} }
/** /**
@@ -84,17 +55,17 @@ 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
ItemStack newBook = booksWithoutBorders.loadBook(player, bookName, "true", "public"); ItemStack newBook = BookLoader.loadBook(player, bookName, "true", "public");
if (newBook != null) { if (newBook != null) {
player.getInventory().addItem(newBook); player.getInventory().addItem(newBook);
} }
//Send the player a welcome message if it exists //Send the player a welcome message if it exists
String welcomeMessage = BooksWithoutBorders.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,
@@ -104,67 +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() + BooksWithoutBorders.getTitleAuthorSeparator() + oldBook.getAuthor();
}
String cleanPlayerName = InputCleaningHelper.cleanString(player.getName());
String[] possiblePaths = new String[]{
getBookFolder() + fileName + ".yml",
getBookFolder() + fileName + ".txt",
getBookFolder() + cleanPlayerName + slash + fileName + ".yml",
getBookFolder() + cleanPlayerName + slash + fileName + ".txt"
};
for (String path : possiblePaths) {
File file = new File(path);
if (file.isFile()) {
return booksWithoutBorders.loadBook(player, fileName, "true", "player");
}
}
return null;
}
return null;
}
} }

View File

@@ -1,15 +1,23 @@
package net.knarcraft.bookswithoutborders.listener; package net.knarcraft.bookswithoutborders.listener;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders; import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.state.EncryptionStyle; import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.config.Permission;
import net.knarcraft.bookswithoutborders.encryption.EncryptionStyle;
import net.knarcraft.bookswithoutborders.state.BookDirectory;
import net.knarcraft.bookswithoutborders.utility.BookFileHelper;
import net.knarcraft.bookswithoutborders.utility.BookFormatter;
import net.knarcraft.bookswithoutborders.utility.BookLoader;
import net.knarcraft.bookswithoutborders.utility.EncryptionHelper; import net.knarcraft.bookswithoutborders.utility.EncryptionHelper;
import net.knarcraft.bookswithoutborders.utility.FileHelper;
import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper; import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper;
import org.bukkit.ChatColor; import net.md_5.bungee.api.ChatColor;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.Tag; import org.bukkit.Tag;
import org.bukkit.block.Sign; import org.bukkit.block.Sign;
import org.bukkit.block.sign.Side;
import org.bukkit.block.sign.SignSide;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.block.Action; import org.bukkit.event.block.Action;
@@ -19,19 +27,20 @@ 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.BooksWithoutBordersSettings.getBookFolder; import static net.knarcraft.bookswithoutborders.utility.BookFileHelper.isBookListIndex;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getSlash;
import static net.knarcraft.bookswithoutborders.utility.FileHelper.isBookListIndex;
/**
* A listener for relevant sign events such as clicking a decryption sign
*/
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;
} }
@@ -48,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().length() > 0)) { !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;
} }
@@ -71,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();
@@ -84,27 +89,39 @@ public class SignEventListener implements Listener {
return; return;
} }
ItemStack heldItem = playerInventory.getItem(hand); ItemStack heldItem = playerInventory.getItem(hand);
if (heldItem == null) {
return;
}
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()) ||
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)) {
if (signLineEquals(sign, 1, "[Encrypt]", ChatColor.DARK_BLUE)) { return;
encryptHeldBookUsingSign(sign, heldItemType, player, hand); }
} else if (signLineEquals(sign, 1, "[Decrypt]", ChatColor.DARK_BLUE)) {
decryptHeldBookUsingSign(sign, heldItemType, player, hand); event.setUseItemInHand(Event.Result.DENY);
} else if (signLineEquals(sign, 1, "[Give]", ChatColor.DARK_BLUE) && event.setCancelled(true);
getSignLine2Color(sign) == ChatColor.DARK_GREEN) {
giveBook(sign, player); if (signLineEquals(sign, 1, "[Encrypt]", ChatColor.DARK_BLUE)) {
} else { encryptHeldBookUsingSign(sign, heldItemType, player, hand);
player.sendMessage("Sign command " + sign.getLine(1) + " " + sign.getLine(2) + " invalid"); } else if (signLineEquals(sign, 1, "[Decrypt]", ChatColor.DARK_BLUE)) {
player.sendMessage(String.valueOf(getSignLine2Color(sign))); decryptHeldBookUsingSign(sign, heldItemType, player, hand);
} } else if (signLineEquals(sign, 1, "[Give]", ChatColor.DARK_BLUE) &&
getSignLine2Color(sign) == ChatColor.DARK_GREEN) {
giveBook(sign, player);
} else {
SignSide front = sign.getSide(Side.FRONT);
player.sendMessage(String.format("Sign command %s %s is invalid", front.getLine(1),
front.getLine(2)));
player.sendMessage(String.valueOf(getSignLine2Color(sign)));
} }
} else if (heldItemType == Material.WRITTEN_BOOK && (event.getAction() == Action.LEFT_CLICK_AIR } 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;
@@ -122,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.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!");
@@ -145,10 +167,11 @@ 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
String line = sign.getLine(2); private ChatColor getSignLine2Color(@NotNull Sign sign) {
if (!ChatColor.stripColor(line).equals(line)) { String line = sign.getSide(Side.FRONT).getLine(2);
return ChatColor.getByChar(sign.getLine(2).substring(1, 2)); if (!BookFormatter.stripColor(line).equals(line)) {
return ChatColor.getByChar(sign.getSide(Side.FRONT).getLine(2).substring(1, 2).charAt(0));
} else { } else {
return null; return null;
} }
@@ -163,8 +186,9 @@ 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,
String line = sign.getLine(lineNumber); @NotNull ChatColor color) {
String line = sign.getSide(Side.FRONT).getLine(lineNumber);
return line.equalsIgnoreCase(color + compareTo); return line.equalsIgnoreCase(color + compareTo);
} }
@@ -175,16 +199,18 @@ 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) {
player.sendMessage(ChatColor.RED + "[Give] signs' 3rd and 4th lines must be 13 characters or less!"); BooksWithoutBorders.sendErrorMessage(player,
"[Give] signs' 3rd and 4th lines must be 13 characters or less!");
markGiveSignValidity(event, false); markGiveSignValidity(event, false);
return; return;
} }
//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 (FileHelper.bookFileExists(signFile)) { if (BookFileHelper.bookFileExists(signFile)) {
markGiveSignValidity(event, true); markGiveSignValidity(event, true);
return; return;
} else { } else {
@@ -203,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]);
@@ -222,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
@@ -233,31 +262,38 @@ 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) &&
!(BooksWithoutBorders.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() + BooksWithoutBorders.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 = BooksWithoutBorders.getInstance().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;
} }
player.getInventory().setItem(hand, newBook); player.getInventory().setItem(hand, newBook);
player.closeInventory(); player.closeInventory();
player.sendMessage(ChatColor.GREEN + "Book auto-decrypted!"); BooksWithoutBorders.sendSuccessMessage(player, "Book auto-decrypted!");
} }
/** /**
@@ -268,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.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);
} }
@@ -288,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.getLine(2)); String fileName = BookFormatter.stripColor(sign.getSide(Side.FRONT).getLine(2));
boolean isLoadListNumber = false; boolean isLoadListNumber = false;
try { try {
@@ -299,18 +336,18 @@ 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.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 = BooksWithoutBorders.getInstance().loadBook(player, fileName, "true", "public"); ItemStack newBook = BookLoader.loadBook(player, fileName, "true", "public");
if (newBook != null) { if (newBook != null) {
player.getInventory().addItem(newBook); player.getInventory().addItem(newBook);
player.sendMessage(ChatColor.GREEN + "Received book!"); BooksWithoutBorders.sendSuccessMessage(player, "Received book!");
} else { } else {
player.sendMessage(ChatColor.RED + "Book failed to load!"); BooksWithoutBorders.sendErrorMessage(player, "Book failed to load!");
} }
} }

View File

@@ -0,0 +1,211 @@
package net.knarcraft.bookswithoutborders.manager;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.config.Translatable;
import net.milkbowl.vault.economy.Economy;
import org.bukkit.Material;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.bukkit.inventory.meta.BookMeta;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.RegisteredServiceProvider;
import org.bukkit.plugin.ServicesManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* A manager for managing Vault economy integration
*/
public class EconomyManager {
private @Nullable Economy economy = null;
/**
* 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();
}
}
/**
* Gets an economy instance for making transactions
*
* @return <p>An economy instance, or null if it's not initialized</p>
*/
@Nullable
public Economy getEconomy() {
return economy;
}
/**
* Makes the player pay for printing a given number of books
*
* @param player <p>The player printing the books</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>
*/
public boolean cannotPayForBookPrinting(@NotNull Player player, int numCopies) {
BooksWithoutBordersConfig config = BooksWithoutBorders.getConfiguration();
//BookPriceQuantity: How many items are required to pay for each book
//BookPriceType: Which item is used to pay for the books. AIR = use economy
Material bookCurrency = config.getBookPriceType();
double cost = config.getBookPriceQuantity() * numCopies;
int itemCost = (int) cost;
if (bookCurrency == Material.AIR) {
return !payForBookPrintingEconomy(player, cost, numCopies);
} else {
if (bookCurrency == Material.WRITABLE_BOOK) {
//Writable books are treated as a special case to prevent WIP books from being used
return !takeWritableBookPayment(player, itemCost);
} else {
if (player.getInventory().contains(bookCurrency, itemCost)) {
payForBookPrintingItem(player, itemCost);
return false;
} else {
BooksWithoutBorders.sendErrorMessage(player, itemCost + " " + bookCurrency +
"(s) are required for this command!");
return true;
}
}
}
}
/**
* Takes a writable book payment, ignoring any books containing text
*
* @param player <p>The player to take payment from</p>
* @param itemCost <p>The number of writable books to pay</p>
* @return <p>True if the payment was successful</p>
*/
private boolean takeWritableBookPayment(@NotNull Player player, int itemCost) {
List<ItemStack> books = getPlayersEmptyBooks(player);
if (countItems(books) < itemCost) {
BooksWithoutBorders.sendErrorMessage(player, itemCost + " empty " + Material.WRITABLE_BOOK +
"(s) are required for this command!");
return false;
} else {
int clearedAmount = 0;
for (ItemStack itemStack : books) {
if (itemStack.getAmount() > itemCost) {
//If encountering an item stack with more than enough books, remove the necessary books
itemStack.setAmount(itemStack.getAmount() - itemCost);
return true;
} else {
clearedAmount += itemStack.getAmount();
player.getInventory().removeItem(itemStack);
}
if (clearedAmount >= itemCost) {
return true;
}
}
return true;
}
}
/**
* Gets the total number of items contained in the given list of items
*
* @param items <p>The items to count</p>
* @return <p>The total number of items</p>
*/
private int countItems(@NotNull List<ItemStack> items) {
int totalItems = 0;
for (ItemStack itemStack : items) {
totalItems += itemStack.getAmount();
}
return totalItems;
}
/**
* Gets all empty books in a player's inventory
*
* @param player <p>The player to get books for</p>
* @return <p>The empty books in the player's inventory</p>
*/
@NotNull
private List<ItemStack> getPlayersEmptyBooks(@NotNull Player player) {
List<ItemStack> validBooks = new ArrayList<>();
for (ItemStack itemStack : player.getInventory().getContents()) {
if (itemStack == null || itemStack.getType() != Material.WRITABLE_BOOK) {
continue;
}
BookMeta book = (BookMeta) itemStack.getItemMeta();
//Only accept empty books
if (book != null && (!book.hasPages() || (book.getPageCount() == 1 && book.getPage(1).trim().isEmpty()))) {
validBooks.add(itemStack);
}
}
return validBooks;
}
/**
* Uses economy to take payment for printing a number of books
*
* @param player <p>The player which needs to pay</p>
* @param cost <p>The cost of the book 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>
*/
private boolean payForBookPrintingEconomy(@NotNull Player player, double cost, int numCopies) {
if (economy == null) {
BooksWithoutBorders.getStringFormatter().displayErrorMessage(player, Translatable.ERROR_VAULT_COST_BUT_UNAVAILABLE);
return false;
}
if ((economy.getBalance(player) - cost) >= 0) {
economy.withdrawPlayer(player, cost);
BooksWithoutBorders.sendSuccessMessage(player, economy.format(cost) + " withdrawn to create " +
numCopies + " book(s)");
BooksWithoutBorders.sendSuccessMessage(player, "New balance: " +
economy.format(economy.getBalance(player)));
return true;
} else {
BooksWithoutBorders.sendErrorMessage(player, economy.format(cost) + " is required for this command!");
return false;
}
}
/**
* Takes payment for printing a number of books by withdrawing the correct item
*
* @param player <p>The player which needs to pay</p>
* @param itemCost <p>The number of items to pay</p>
*/
private void payForBookPrintingItem(@NotNull Player player, int itemCost) {
PlayerInventory playerInventory = player.getInventory();
int clearedAmount = 0;
while (clearedAmount < itemCost) {
int firstItemIndex = playerInventory.first(BooksWithoutBorders.getConfiguration().getBookPriceType());
ItemStack firstItem = playerInventory.getItem(firstItemIndex);
if (Objects.requireNonNull(firstItem).getAmount() <= itemCost - clearedAmount) {
clearedAmount += firstItem.getAmount();
player.getInventory().clear(firstItemIndex);
} else {
clearedAmount = itemCost;
firstItem.setAmount(firstItem.getAmount() - (clearedAmount));
}
}
}
}

View File

@@ -1,11 +1,26 @@
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
*/ */
public enum BookDirectory { public enum BookDirectory {
/**
* The public directory
*/
PUBLIC, PUBLIC,
/**
* A player directory
*/
PLAYER, PLAYER,
/**
* The encrypted directory
*/
ENCRYPTED; ENCRYPTED;
/** /**
@@ -14,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")) {
@@ -25,4 +41,5 @@ public enum BookDirectory {
return null; return null;
} }
} }
} }

View File

@@ -26,22 +26,22 @@ public enum BookHoldingState {
UNSIGNED_MAIN_HAND, UNSIGNED_MAIN_HAND,
/** /**
* The player is holding one signed book in their off hand * The player is holding one signed book in their off-hand
*/ */
SIGNED_OFF_HAND, SIGNED_OFF_HAND,
/** /**
* The player is holding one unsigned book in their off hand * The player is holding one unsigned book in their off-hand
*/ */
UNSIGNED_OFF_HAND, UNSIGNED_OFF_HAND,
/** /**
* The player is holding one signed book in their main hand and one unsigned book in their off hand * The player is holding one signed book in their main hand and one unsigned book in their off-hand
*/ */
SIGNED_MAIN_HAND_UNSIGNED_OFF_HAND, SIGNED_MAIN_HAND_UNSIGNED_OFF_HAND,
/** /**
* The player is holding one unsigned book in their main hand and one signed book in their off hand * The player is holding one unsigned book in their main hand and one signed book in their off-hand
*/ */
UNSIGNED_MAIN_HAND_SIGNED_OFF_HAND, UNSIGNED_MAIN_HAND_SIGNED_OFF_HAND,

View File

@@ -1,30 +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

@@ -11,7 +11,7 @@ public enum ItemSlot {
MAIN_HAND, MAIN_HAND,
/** /**
* The item is in the player's off hand * The item is in the player's off-hand
*/ */
OFF_HAND, OFF_HAND,

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

@@ -0,0 +1,316 @@
package net.knarcraft.bookswithoutborders.utility;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.Translatable;
import net.knarcraft.bookswithoutborders.state.BookDirectory;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.meta.BookMeta;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;
/**
* Helper class for dealing with files
*/
public final class BookFileHelper {
private BookFileHelper() {
}
/**
* Check whether an integer points to a public book
*
* @param possibleIndex <p>The string which might be a book index</p>
* @return <p>True if the number is a book index</p>
*/
public static boolean isBookListIndex(@NotNull String possibleIndex) {
File bookDirectory = new File(BooksWithoutBorders.getConfiguration().getBookFolder().replaceAll("[\\\\/]$", ""));
try {
//Tests if a load list number has been supplied
int parsedLine = Integer.parseInt(possibleIndex);
File[] foundFiles = bookDirectory.listFiles(File::isFile);
return parsedLine >= 0 && parsedLine <= Objects.requireNonNull(foundFiles).length;
} catch (NumberFormatException ignored) {
}
return false;
}
/**
* Checks if a file path points to an actual book
*
* @param bookFile <p>The path to a book</p>
* @return <p>True if the file exists and points to a book file</p>
*/
public static boolean bookFileExists(@NotNull String bookFile) {
return ((new File(bookFile).isFile() && (bookFile.endsWith(".txt") ||
bookFile.endsWith(".yml"))) || new File(bookFile + ".txt").isFile() ||
new File(bookFile + ".yml").isFile()) && !bookFile.contains("../") && !bookFile.contains("..\\");
}
/**
* Gets a book file given its path
*
* <p>This function adds extensions to the path as necessary, returning the first match.</p>
*
* @param bookPath <p>The path of the book to get</p>
* @return <p>The file the path points to, or null otherwise</p>
*/
@Nullable
public static File getBookFile(@NotNull String bookPath) {
if (!bookFileExists(bookPath)) {
return null;
}
File bookFile = new File(bookPath);
if (bookFile.exists()) {
return bookFile;
}
File bookFileYml = new File(bookPath + ".yml");
if (bookFileYml.exists()) {
return bookFileYml;
}
File bookFileTxt = new File(bookPath + ".txt");
if (bookFileTxt.exists()) {
return bookFileTxt;
}
return null;
}
/**
* Lists available files
*
* @param sender <p>The command sender looking for files</p>
* @param listPublic <p>Whether to list public or personal files</p>
* @return <p>A list of available files</p>
*/
@Nullable
public static List<String> listFiles(@NotNull CommandSender sender, @NotNull Boolean listPublic) {
File file = BookHelper.getBookDirectoryPath(listPublic ? BookDirectory.PUBLIC : BookDirectory.PLAYER, sender);
if (file == null) {
return new ArrayList<>();
}
return BookFileHelper.listFiles(sender, file);
}
/**
* Gets a map between characters, and the first instance of a book's title starting with that character
*
* @param books <p>The books to look through</p>
* @return <p>The map of the first index containing each character</p>
*/
@NotNull
public static Map<Character, Integer> populateLetterIndices(@NotNull List<String> books) {
Map<Character, Integer> firstEncounter = new HashMap<>();
Character current = null;
for (int i = 0; i < books.size(); i++) {
char first = BookFormatter.stripColor(books.get(i)).toLowerCase().charAt(0);
if (current == null || current != first) {
current = first;
firstEncounter.put(first, i);
}
}
return firstEncounter;
}
/**
* Lists available files
*
* @param sender <p>The command sender looking for files</p>
* @param searchDirectory <p>The directory to search for files</p>
* @return <p>A list of available files</p>
*/
@Nullable
private static List<String> listFiles(@NotNull CommandSender sender, @NotNull File searchDirectory) {
List<String> fileList = new ArrayList<>();
File[] existingFiles = searchDirectory.listFiles();
if (!searchDirectory.exists() || existingFiles == null || existingFiles.length == 0) {
BooksWithoutBorders.sendErrorMessage(sender, "No books have been saved!");
return null;
}
for (File foundFile : existingFiles) {
// Filter out invalid files
if (!foundFile.isFile() || foundFile.getName().contains("§")) {
continue;
}
String fileName = foundFile.getName();
String separator = BooksWithoutBorders.getConfiguration().getTitleAuthorSeparator();
if (fileName.contains(separator)) {
//Convert the UUID into a username if necessary
String userName = getBookAuthorFromPath(fileName);
String title = getBookTitleFromPath(fileName);
fileList.add(title + separator + BookHelper.authorFromUUID(userName));
} else {
fileList.add(fileName);
}
}
// Sort the book list
Comparator<String> bookComparator = Comparator.naturalOrder();
fileList.sort((a, b) -> bookComparator.compare(BookFormatter.stripColor(a).toLowerCase(),
BookFormatter.stripColor(b).toLowerCase()));
return fileList;
}
/**
* Finds any duplicate book files
*
* @param foundFiles <p>The files to check for duplicates</p>
* @param fileName <p>The name of the file which might already exist</p>
* @return <p>The number of found duplicates</p>
*/
public static int findDuplicates(@NotNull File[] foundFiles, @NotNull String fileName) {
int foundDuplicates = 0;
for (File foundFile : foundFiles) {
if (foundFile.getName().matches("(\\([0-9]+\\))?" + Pattern.quote(fileName) + "(\\.yml|\\.txt)?")) {
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

@@ -1,6 +1,13 @@
package net.knarcraft.bookswithoutborders.utility; package net.knarcraft.bookswithoutborders.utility;
import net.knarcraft.knarlib.property.ColorConversion;
import net.knarcraft.knarlib.util.ColorHelper;
import org.bukkit.inventory.meta.BookMeta;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
/** /**
* A class for formatting text to fit books * A class for formatting text to fit books
@@ -8,7 +15,6 @@ import java.util.List;
public final class BookFormatter { public final class BookFormatter {
private BookFormatter() { private BookFormatter() {
} }
/** /**
@@ -19,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;
@@ -44,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);
@@ -72,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) {
@@ -87,11 +93,41 @@ 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");
} }
} }
/**
* Formats every page in the given book meta by converting color and formatting codes
*
* @param bookMeta <p>The book meta to change</p>
* @return <p>The changed book meta</p>
*/
@NotNull
public static BookMeta formatPages(@NotNull BookMeta bookMeta) {
List<String> formattedPages = new ArrayList<>(Objects.requireNonNull(bookMeta).getPageCount());
for (String page : bookMeta.getPages()) {
formattedPages.add(ColorHelper.translateColorCodes(page, ColorConversion.RGB));
}
bookMeta.setPages(formattedPages);
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

@@ -0,0 +1,248 @@
package net.knarcraft.bookswithoutborders.utility;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.config.Translatable;
import net.knarcraft.bookswithoutborders.state.BookDirectory;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
import org.bukkit.inventory.meta.WritableBookMeta;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;
/**
* Helper class for getting abstract book information
*/
public final class BookHelper {
private BookHelper() {
}
/**
* Un-signs a book
*
* @param oldMetadata <p>The signed book's metadata</p>
* @param amount <p>The amount of unsigned books to produce</p>
* @return <p>The unsigned books, or null if unable to generate metadata</p>
*/
@Nullable
public static ItemStack unsignBook(@NotNull BookMeta oldMetadata, int amount) {
WritableBookMeta newMetadata = (BookMeta) BooksWithoutBorders.getItemFactory().getItemMeta(Material.WRITABLE_BOOK);
if (newMetadata == null) {
BooksWithoutBorders.log(Level.SEVERE, "Unable to create writable book metadata");
return null;
}
//Create a new unsigned book with the same data
ItemStack newBook = new ItemStack(Material.WRITABLE_BOOK, amount);
if (oldMetadata.hasLore()) {
List<String> oldLore = oldMetadata.getLore();
if (oldLore != null) {
List<String> newLore = new ArrayList<>(oldLore);
newLore.remove(0);
newMetadata.setLore(newLore);
}
}
newMetadata.setPages(oldMetadata.getPages());
newBook.setItemMeta(newMetadata);
// Give the player the new book
return newBook;
}
/**
* Converts the author of a book from UUID if necessary
*
* @param author <p>The author string</p>
* @return <p>The author string, converted if it was a UUID</p>
*/
@NotNull
public static String authorFromUUID(@NotNull String author) {
try {
UUID authorID = UUID.fromString(BookFormatter.stripColor(author));
Player player = Bukkit.getPlayer(authorID);
if (player != null) {
author = player.getName();
}
} catch (IllegalArgumentException ignored) {
}
return author;
}
/**
* Gets the file path of the selected book directory
*
* @param bookDirectory <p>The book directory to get (ENCRYPTED is not supported here)</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>
*/
@Nullable
public static File getBookDirectoryPath(@NotNull BookDirectory bookDirectory, @NotNull CommandSender sender) {
String bookFolderString = getBookDirectoryPathString(bookDirectory, sender);
if (bookFolderString == null) {
return null;
}
return new File(bookFolderString);
}
/**
* Gets the string path of the selected book directory
*
* @param bookDirectory <p>The book directory to get (ENCRYPTED is not supported here)</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>
*/
@Nullable
public static String getBookDirectoryPathString(@NotNull BookDirectory bookDirectory, @NotNull CommandSender sender) {
BooksWithoutBordersConfig config = BooksWithoutBorders.getConfiguration();
String folder = null;
String bookFolder = config.getBookFolder();
if (bookDirectory == BookDirectory.PUBLIC) {
folder = bookFolder;
} else if (bookDirectory == BookDirectory.PLAYER && sender instanceof Player player) {
folder = bookFolder + player.getUniqueId() + config.getSlash();
}
return folder;
}
/**
* Increases the generation of the given book, if necessary
*
* @param bookItem <p>The book item to increase the generation of</p>
*/
public static void increaseGeneration(@NotNull ItemStack bookItem) {
BookMeta bookMeta = (BookMeta) bookItem.getItemMeta();
if (BooksWithoutBorders.getConfiguration().changeGenerationOnCopy() && bookMeta != null) {
bookMeta.setGeneration(BookHelper.getNextGeneration(bookMeta.getGeneration()));
bookItem.setItemMeta(bookMeta);
}
}
/**
* Gets the next generation of the given book
*
* <p>If an original book is given, this will yield a copy of the original. If a copy of original is given, this
* will yield a copy of a copy. In all other cases, the generation will stay the same</p>
*
* @param currentGeneration <p>The current generation of the book</p>
* @return <p>The next generation of the book</p>
*/
@NotNull
public static BookMeta.Generation getNextGeneration(@Nullable BookMeta.Generation currentGeneration) {
if (currentGeneration == null) {
return BookMeta.Generation.COPY_OF_ORIGINAL;
}
return switch (currentGeneration) {
case ORIGINAL -> BookMeta.Generation.COPY_OF_ORIGINAL;
case COPY_OF_ORIGINAL -> BookMeta.Generation.COPY_OF_COPY;
default -> currentGeneration;
};
}
/**
* Gets the file name of the given book
*
* @param book <p>The book to get the file of</p>
* @param player <p>The player trying to do something with the book</p>
* @return <p>The book file</p>
* @throws IllegalArgumentException <p>If the book title or author contains the title author separator</p>
*/
@NotNull
public static String getBookFile(@NotNull BookMeta book, @NotNull OfflinePlayer player, boolean isPublic) throws IllegalArgumentException {
String separator = BooksWithoutBorders.getConfiguration().getTitleAuthorSeparator();
String bookName = getBookTitle(book);
String authorName = getBookAuthor(book, isPublic ? null : player);
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);
}
/**
* Gets the author of a book, with fallback for unknown author
*
* @param book <p>The book to get the author of</p>
* @param player <p>A player that might be the author (only used for private book folders), or null if dealing with public books</p>
* @return <p>The book author, or an appropriate fallback</p>
*/
public static String getBookAuthor(@NotNull BookMeta book, @Nullable OfflinePlayer player) {
String playerName = player == null ? "?" : (player.getName() == null ? player.getUniqueId().toString() : player.getName());
String authorName;
if ((!book.hasAuthor() || isAuthor(playerName, book.getAuthor())) && player != null) {
//Store as unique id to account for name changes
authorName = player.getUniqueId().toString();
} else if (!book.hasAuthor() && player != null) {
authorName = player.getName();
} else {
authorName = book.getAuthor();
if (authorName == null) {
authorName = BooksWithoutBorders.getStringFormatter().getUnFormattedColoredMessage(Translatable.NEUTRAL_UNKNOWN_AUTHOR);
}
}
return authorName;
}
/**
* Gets a book's name, defaulting to "Untitled" if not named
*
* @param book <p>The book to get the name of</p>
* @return <p>The book's name or "Untitled"</p>
*/
public static String getBookTitle(@NotNull BookMeta book) {
String bookName = null;
if (book.hasTitle()) {
bookName = book.getTitle();
}
if (bookName == null) {
bookName = BooksWithoutBorders.getStringFormatter().getUnFormattedColoredMessage(Translatable.NEUTRAL_UNKNOWN_TITLE);
}
return bookName;
}
/**
* Checks whether the given player is the author of a given book
*
* @param player <p>The player to check</p>
* @param book <p>The book to check</p>
* @return <p>True if the player is not the book's author</p>
*/
public static boolean isNotAuthor(@NotNull Player player, @NotNull BookMeta book) {
if (isAuthor(player.getName(), book.getAuthor())) {
return false;
} else {
BooksWithoutBorders.sendErrorMessage(player,
"You must be the author of this book to use this command!");
return true;
}
}
/**
* Gets whether the given player name is equal to the given book author
*
* @param playerName <p>The player name to check</p>
* @param author <p>The author to check</p>
* @return <p>True if the player is the author</p>
*/
private static boolean isAuthor(@NotNull String playerName, @Nullable String author) {
playerName = InputCleaningHelper.cleanString(playerName);
return author != null && playerName.equalsIgnoreCase(InputCleaningHelper.cleanString(author));
}
}

View File

@@ -0,0 +1,181 @@
package net.knarcraft.bookswithoutborders.utility;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.state.BookDirectory;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
/**
* A helper class for loading books from files
*/
public final class BookLoader {
private BookLoader() {
}
/**
* Loads the given book
*
* @param sender <p>The command sender trying to load the book</p>
* @param fileName <p>The index or file name of the book to load</p>
* @param isSigned <p>Whether to load the book as signed, and not unsigned</p>
* @param directory <p>The directory to save the book in</p>
* @return <p>The loaded book</p>
*/
@Nullable
public static ItemStack loadBook(@NotNull CommandSender sender, @NotNull String fileName, @NotNull String isSigned,
@NotNull String directory) {
return loadBook(sender, fileName, isSigned, directory, 1);
}
/**
* Loads the given book
*
* @param sender <p>The command sender trying to load the book</p>
* @param fileName <p>The index or file name of the book to load</p>
* @param isSigned <p>Whether to load the book as signed, and not unsigned</p>
* @param directory <p>The directory to save the book in</p>
* @param numCopies <p>The number of copies to load</p>
* @return <p>The loaded book</p>
*/
@Nullable
public static ItemStack loadBook(@NotNull CommandSender sender, @NotNull String fileName, @NotNull String isSigned,
@NotNull String directory, int numCopies) {
BookDirectory bookDirectory = BookDirectory.getFromString(directory);
if (bookDirectory == null) {
BooksWithoutBorders.sendErrorMessage(sender, "Unrecognized book directory!");
return null;
}
return loadBook(sender, fileName, isSigned, bookDirectory, directory, numCopies);
}
/**
* Loads the given book
*
* @param sender <p>The command sender trying to load the book</p>
* @param fileName <p>The index or file name of the book to load</p>
* @param isSigned <p>Whether to load the book as signed, and not unsigned</p>
* @param bookDirectory <p>The type of directory to save in</p>
* @param directory <p>The directory to save the book in</p>
* @param numCopies <p>The number of copies to load</p>
* @return <p>The loaded book</p>
*/
@Nullable
public static ItemStack loadBook(@NotNull CommandSender sender, @NotNull String fileName, @NotNull String isSigned,
@NotNull BookDirectory bookDirectory, @NotNull String directory, int numCopies) {
//Find the filename if a book index is given
try {
int bookIndex = Integer.parseInt(fileName);
List<String> availableFiles = BooksWithoutBorders.getAvailableBooks(sender,
bookDirectory == BookDirectory.PUBLIC);
if (bookIndex <= availableFiles.size()) {
fileName = availableFiles.get(Integer.parseInt(fileName) - 1);
}
} catch (NumberFormatException ignored) {
}
BooksWithoutBordersConfig config = BooksWithoutBorders.getConfiguration();
//Get the full path of the book to load
File file = getFullPath(sender, fileName, bookDirectory, directory);
if (file == null) {
//Try converting the username to UUID
String replaced = BookFileHelper.replaceAuthorWithUUID(fileName);
file = getFullPath(sender, replaced, bookDirectory, directory);
if (file == null) {
BooksWithoutBorders.sendErrorMessage(sender, "Incorrect file name!");
return null;
}
}
//Make sure the player can pay for the book
if (config.booksHavePrice() &&
!sender.hasPermission("bookswithoutborders.bypassBookPrice") &&
(bookDirectory == BookDirectory.PUBLIC || bookDirectory == BookDirectory.PLAYER) &&
config.getEconomyManager().cannotPayForBookPrinting((Player) sender, numCopies)) {
return null;
}
//Generate a new empty book
ItemStack book = new ItemStack(Material.WRITTEN_BOOK);
BookMeta bookMetadata = (BookMeta) BooksWithoutBorders.getItemFactory().getItemMeta(Material.WRITTEN_BOOK);
if (bookMetadata == null) {
BooksWithoutBorders.sendErrorMessage(sender, "Unable to create blank book metadata!");
return null;
}
//Load the book from the given file
bookMetadata = BookToFromTextHelper.bookFromFile(file, bookMetadata);
if (bookMetadata == null) {
BooksWithoutBorders.sendErrorMessage(sender, "File was blank!!");
return null;
}
//Remove "encrypted" from the book lore
if (bookDirectory == BookDirectory.ENCRYPTED && bookMetadata.hasLore()) {
List<String> oldLore = bookMetadata.getLore();
if (oldLore != null) {
List<String> newLore = new ArrayList<>(oldLore);
newLore.remove(0);
bookMetadata.setLore(newLore);
}
}
//Set the metadata and amount to the new book
book.setItemMeta(bookMetadata);
//Increase book generation if enabled
BookHelper.increaseGeneration(book);
book.setAmount(numCopies);
if (!isSigned.equalsIgnoreCase("true") && book.getItemMeta() != null) {
return BookHelper.unsignBook((BookMeta) book.getItemMeta(), book.getAmount());
}
return book;
}
/**
* Gets a File pointing to the wanted book
*
* @param sender <p>The sender to send errors to</p>
* @param fileName <p>The name of the book file</p>
* @param bookDirectory <p>The book directory the file resides in</p>
* @param directory <p>The relative directory given</p>
* @return <p>A file or null if it does not exist</p>
*/
@Nullable
private static File getFullPath(@NotNull CommandSender sender, @NotNull String fileName,
@NotNull BookDirectory bookDirectory, @NotNull String directory) {
BooksWithoutBordersConfig config = BooksWithoutBorders.getConfiguration();
File file = null;
String slash = config.getSlash();
if (bookDirectory == BookDirectory.ENCRYPTED) {
file = BookFileHelper.findBookFile(config.getEncryptedBookPath() + directory + slash, fileName);
} else {
String folder = BookHelper.getBookDirectoryPathString(bookDirectory, sender);
if (folder != null) {
file = BookFileHelper.findBookFile(folder, fileName);
} else {
BooksWithoutBorders.log(Level.WARNING, "Unknown directory " + bookDirectory);
}
}
if (file == null || !file.isFile()) {
return null;
} else {
return file;
}
}
}

View File

@@ -1,20 +1,26 @@
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.Translatable;
import net.knarcraft.bookswithoutborders.encryption.AESConfiguration;
import net.knarcraft.bookswithoutborders.encryption.EncryptionStyle;
import net.knarcraft.knarlib.formatting.StringFormatter;
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.FileReader; import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter;
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.utility.InputCleaningHelper.fixName; import static net.knarcraft.bookswithoutborders.utility.BookHelper.authorFromUUID;
/** /**
* Helper class for converting books to and from text/yml files * Helper class for converting books to and from text/yml files
@@ -32,8 +38,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());
@@ -41,6 +95,11 @@ public final class BookToFromTextHelper {
if (bookMetadata.hasAuthor()) { if (bookMetadata.hasAuthor()) {
bookYml.set("Author", bookMetadata.getAuthor()); bookYml.set("Author", bookMetadata.getAuthor());
} }
BookMeta.Generation generation = bookMetadata.getGeneration();
if (generation == null) {
generation = BookMeta.Generation.ORIGINAL;
}
bookYml.set("Generation", generation.name());
if (bookMetadata.hasPages()) { if (bookMetadata.hasPages()) {
bookYml.set("Pages", bookMetadata.getPages()); bookYml.set("Pages", bookMetadata.getPages());
} }
@@ -48,7 +107,7 @@ public final class BookToFromTextHelper {
bookYml.set("Lore", bookMetadata.getLore()); bookYml.set("Lore", bookMetadata.getLore());
} }
bookYml.save(path + fileName + ".yml"); return bookYml;
} }
/** /**
@@ -58,7 +117,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")) {
@@ -69,24 +129,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"); 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();
//Save each page of the book as a text line try {
printWriter.println("[Book]"); meta = bookFromYml(file, bookMetadata);
for (String page : pages) { if (meta == null) {
printWriter.println(page); return null;
}
} catch (IllegalArgumentException exception) {
return null;
} }
printWriter.close();
// If the plaintext is stored in the file, don't bother with real decryption
if (!meta.getPages().isEmpty()) {
return meta;
}
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;
} }
/** /**
@@ -96,12 +199,17 @@ 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) {
StringFormatter stringFormatter = BooksWithoutBorders.getStringFormatter();
try { try {
FileConfiguration bookYml = YamlConfiguration.loadConfiguration(file); FileConfiguration bookYml = YamlConfiguration.loadConfiguration(file);
bookMetadata.setTitle(bookYml.getString("Title", "Untitled")); bookMetadata.setGeneration(BookMeta.Generation.valueOf(bookYml.getString("Generation", "ORIGINAL")));
bookMetadata.setAuthor(bookYml.getString("Author", "Unknown")); bookMetadata.setTitle(bookYml.getString("Title",
stringFormatter.getUnFormattedColoredMessage(Translatable.NEUTRAL_UNKNOWN_TITLE)));
bookMetadata.setAuthor(authorFromUUID(bookYml.getString("Author",
stringFormatter.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) {
@@ -118,38 +226,37 @@ 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 = BooksWithoutBorders.getTitleAuthorSeparator();
//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);
author = fileName.substring(fileName.indexOf(titleAuthorSeparator) + 1, fileName.length() - 4); String author = BookFileHelper.getBookAuthorFromPath(fileName);
title = fileName.substring(0, fileName.indexOf(titleAuthorSeparator));
} else {
author = "Unknown";
title = fileName.substring(0, fileName.length() - 4);
}
//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);
} catch (IOException e) { if (rawPages == null) {
e.printStackTrace(); BooksWithoutBorders.log(Level.SEVERE, "Text file's first line was null");
return null;
}
} catch (IOException exception) {
BooksWithoutBorders.log(Level.SEVERE, "Unable to read text file");
return null; return null;
} }
//Parse the generation from the book data
if (!rawPages.isEmpty() && rawPages.get(0).startsWith("Generation:")) {
bookMetadata.setGeneration(BookMeta.Generation.valueOf(rawPages.get(0).split(":")[1]));
rawPages.remove(0);
}
//Remove any empty pages //Remove any empty pages
List<String> pages = new ArrayList<>(InputCleaningHelper.cleanList(rawPages)); List<String> pages = new ArrayList<>(InputCleaningHelper.cleanList(rawPages));
//Update the metadata of the book with its new values //Update the metadata of the book with its new values
bookMetadata.setAuthor(author); bookMetadata.setAuthor(authorFromUUID(author));
bookMetadata.setTitle(title); bookMetadata.setTitle(title.substring(0, 32));
bookMetadata.setPages(pages); bookMetadata.setPages(pages);
return bookMetadata; return bookMetadata;
@@ -162,9 +269,10 @@ 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 = new BufferedReader(new FileReader(file)); BufferedReader bufferedReader = FileHelper.getBufferedReaderFromInputStream(new FileInputStream(file));
//Use the first line to decide if we are loading a book, or creating a new book //Use the first line to decide if we are loading a book, or creating a new book
String firstLine = bufferedReader.readLine(); String firstLine = bufferedReader.readLine();
@@ -172,8 +280,11 @@ public final class BookToFromTextHelper {
bufferedReader.close(); bufferedReader.close();
return null; return null;
} }
if (firstLine.equalsIgnoreCase("[Book]")) { if (firstLine.toLowerCase().startsWith("[book]")) {
//Read every line directly as a page, as this is a saved book //Read every line directly as a page, as this is a saved book
if (firstLine.contains(":")) {
rawPages.add("Generation:" + firstLine.split(":")[1]);
}
String readLine; String readLine;
do { do {
readLine = bufferedReader.readLine(); readLine = bufferedReader.readLine();

View File

@@ -1,132 +0,0 @@
package net.knarcraft.bookswithoutborders.utility;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.milkbowl.vault.economy.Economy;
import org.bukkit.Material;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.RegisteredServiceProvider;
import org.bukkit.plugin.ServicesManager;
import java.util.Objects;
/**
* Helper class for economy related functions
*/
public final class EconomyHelper {
private static Economy economy;
private EconomyHelper() {
}
/**
* Gets an economy instance for making transactions
*
* @return <p>An economy instance, or null if it's not initialized</p>
*/
public static Economy getEconomy() {
return economy;
}
/**
* Tries to set up economy
*
* @return <p>True if economy is set up and enabled</p>
*/
public static boolean setupEconomy() {
if (economy != null) {
return true;
}
Server server = BooksWithoutBorders.getInstance().getServer();
Plugin plugin = server.getPluginManager().getPlugin("Vault");
ServicesManager servicesManager = server.getServicesManager();
if (plugin != null) {
RegisteredServiceProvider<Economy> economyProvider = servicesManager.getRegistration(Economy.class);
if (economyProvider != null) {
economy = economyProvider.getProvider();
}
}
return (economy != null);
}
/**
* Makes the player pay for printing a given number of books
*
* @param player <p>The player printing the books</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>
*/
public static boolean cannotPayForBookPrinting(Player player, int numCopies) {
//BookPriceQuantity: How many items are required to pay for each book
//BookPriceType: Which item is used to pay for the books. AIR = use economy
Material bookCurrency = BooksWithoutBorders.getBookPriceType();
double cost = BooksWithoutBorders.getBookPriceQuantity() * numCopies;
int itemCost = (int) cost;
if (bookCurrency == Material.AIR) {
return !EconomyHelper.payForBookPrintingEconomy(player, cost, numCopies);
} else {
if (player.getInventory().contains(bookCurrency, itemCost)) {
payForBookPrintingItem(player, itemCost);
return false;
} else {
BooksWithoutBorders.sendErrorMessage(player, itemCost + " " + bookCurrency +
"(s) are required for this command!");
return true;
}
}
}
/**
* Uses economy to take payment for printing a number of books
*
* @param player <p>The player which needs to pay</p>
* @param cost <p>The cost of the book 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>
*/
private static boolean payForBookPrintingEconomy(Player player, double cost, int numCopies) {
if ((economy.getBalance(player) - cost) >= 0) {
economy.withdrawPlayer(player, cost);
BooksWithoutBorders.sendSuccessMessage(player, economy.format(cost) + " withdrawn to create " + numCopies + " book(s)");
BooksWithoutBorders.sendSuccessMessage(player, "New balance: " + economy.format(economy.getBalance(player)));
return true;
} else {
BooksWithoutBorders.sendErrorMessage(player, economy.format(cost) + " is required for this command!");
return false;
}
}
/**
* Takes payment for printing a number of books by withdrawing the correct item
*
* @param player <p>The player which needs to pay</p>
* @param itemCost <p>The number of items to pay</p>
*/
private static void payForBookPrintingItem(Player player, int itemCost) {
PlayerInventory playerInventory = player.getInventory();
int clearedAmount = 0;
while (clearedAmount < itemCost) {
int firstItemIndex = playerInventory.first(BooksWithoutBorders.getBookPriceType());
ItemStack firstItem = playerInventory.getItem(firstItemIndex);
if (Objects.requireNonNull(firstItem).getAmount() <= itemCost - clearedAmount) {
clearedAmount += firstItem.getAmount();
player.getInventory().clear(firstItemIndex);
} else {
clearedAmount = itemCost;
firstItem.setAmount(firstItem.getAmount() - (clearedAmount));
}
}
}
}

View File

@@ -1,79 +1,123 @@
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.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 org.bukkit.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.Logger;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getBookFolder;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.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 EncryptionHelper() { private static final byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII);
private 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;
} }
/** /**
@@ -85,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, "");
} }
@@ -99,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;
} }
@@ -142,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);
@@ -159,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;
@@ -187,22 +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 = (!bookMetadata.hasTitle()) ? "Untitled," + player.getName() : bookMetadata.getTitle() + String fileName = BookHelper.getBookFile(bookMetadata, player, true);
BooksWithoutBorders.getTitleAuthorSeparator() + bookMetadata.getAuthor();
fileName = "[" + key + "]" + fileName;
fileName = cleanString(fileName); fileName = cleanString(fileName);
fileName = fixName(fileName, false);
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()) {
@@ -215,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;
@@ -222,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
* *
@@ -248,27 +409,26 @@ 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 e) { } catch (Exception exception) {
e.printStackTrace(); BooksWithoutBorders.log(Level.SEVERE, "Unable to save group encrypted book");
return null; return null;
} }
} }
//Creates file //Generate file name
String fileName = (!bookMetadata.hasTitle()) ? "Untitled," + player.getName() : String fileName = BookHelper.getBookFile(bookMetadata, player, true);
bookMetadata.getTitle() + BooksWithoutBorders.getTitleAuthorSeparator() + bookMetadata.getAuthor();
fileName = cleanString(fileName);
fileName = fixName(fileName, false);
List<String> newLore = new ArrayList<>(); List<String> newLore = new ArrayList<>();
newLore.add(ChatColor.GRAY + "[" + groupName + " encrypted]"); newLore.add(ChatColor.GRAY + "[" + groupName + " encrypted]");
@@ -281,12 +441,11 @@ public final class EncryptionHelper {
bookMetadata.setLore(newLore); bookMetadata.setLore(newLore);
//Save file //Save file
File file = (BooksWithoutBorders.getUseYml()) ? new File(path + fileName + ".yml") : new File(path + fileName + ".txt"); File file = new File(path + fileName + ".yml");
if (!file.isFile()) { if (!file.isFile()) {
try { try {
BookToFromTextHelper.bookToYml(path, fileName, bookMetadata); BookToFromTextHelper.bookToYml(path, fileName, bookMetadata);
} catch (IOException e) { } catch (IOException exception) {
e.printStackTrace();
BooksWithoutBorders.sendErrorMessage(player, "Group encrypted failed!"); BooksWithoutBorders.sendErrorMessage(player, "Group encrypted failed!");
return null; return null;
} }
@@ -298,30 +457,31 @@ 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,
String fileName = (!bookMetaData.hasTitle()) ? "Untitled," + player.getName() : @NotNull EncryptionStyle encryptionStyle, @NotNull String key,
bookMetaData.getTitle() + BooksWithoutBorders.getTitleAuthorSeparator() + bookMetaData.getAuthor(); @Nullable AESConfiguration aesConfiguration) {
String path = BooksWithoutBorders.getConfiguration().getEncryptedBookPath();
fileName = "[" + key + "]" + fileName; String fileName = BookHelper.getBookFile(bookMetaData, player, true);
fileName = cleanString(fileName); fileName = cleanString(fileName);
fileName = fixName(fileName, false);
//cancels saving if file is already encrypted //cancels saving if file is already encrypted
File file = (BooksWithoutBorders.getUseYml()) ? new File(path + fileName + ".yml") : new File(path + fileName + ".txt"); File file = new File(path + fileName + ".yml");
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 e) { } catch (IOException exception) {
e.printStackTrace();
BooksWithoutBorders.sendErrorMessage(player, "Encryption failed!"); BooksWithoutBorders.sendErrorMessage(player, "Encryption failed!");
return false; return false;
} }

View File

@@ -1,174 +0,0 @@
package net.knarcraft.bookswithoutborders.utility;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getBookFolder;
import static net.knarcraft.bookswithoutborders.BooksWithoutBordersSettings.getSlash;
import static net.knarcraft.bookswithoutborders.utility.InputCleaningHelper.cleanString;
/**
* Helper class for dealing with files
*/
public final class FileHelper {
private FileHelper() {
}
/**
* Check whether an integer points to a public book
*
* @param possibleIndex <p>The string which might be a book index</p>
* @return <p>True if the number is a book index</p>
*/
public static boolean isBookListIndex(String possibleIndex) {
File bookDirectory = new File(getBookFolder().replaceAll("[\\\\/]$", ""));
try {
//Tests if a load list number has been supplied
int parsedLine = Integer.parseInt(possibleIndex);
File[] foundFiles = bookDirectory.listFiles(File::isFile);
return parsedLine >= 0 && parsedLine <= Objects.requireNonNull(foundFiles).length;
} catch (NumberFormatException ignored) {
}
return false;
}
/**
* Checks if a file path points to an actual book
*
* @param bookFile <p>The path to a book</p>
* @return <p>True if the file exists and points to a book file</p>
*/
public static boolean bookFileExists(String bookFile) {
return ((new File(bookFile).isFile() && (bookFile.endsWith(".txt") ||
bookFile.endsWith(".yml"))) || new File(bookFile + ".txt").isFile() ||
new File(bookFile + ".yml").isFile()) && !bookFile.contains("../") && !bookFile.contains("..\\");
}
/**
* Gets a book file given its path
*
* <p>This function adds extensions to the path as necessary, returning the first match.</p>
*
* @param bookPath <p>The path of the book to get</p>
* @return <p>The file the path points to, or null otherwise</p>
*/
public static File getBookFile(String bookPath) {
if (!bookFileExists(bookPath)) {
return null;
}
File bookFile = new File(bookPath);
if (bookFile.exists()) {
return bookFile;
}
File bookFileYml = new File(bookPath + ".yml");
if (bookFileYml.exists()) {
return bookFileYml;
}
File bookFileTxt = new File(bookPath + ".txt");
if (bookFileTxt.exists()) {
return bookFileTxt;
}
return null;
}
/**
* Lists available files
*
* @param sender <p>The command sender looking for files</p>
* @param listPublic <p>Whether to list public or personal files</p>
* @return <p>A list of available files</p>
*/
public static List<String> listFiles(CommandSender sender, Boolean listPublic) {
File file;
if (listPublic) {
file = new File(getBookFolder());
} else {
file = new File(getBookFolder() + cleanString(sender.getName()) + getSlash());
}
return FileHelper.listFiles(sender, file);
}
/**
* Prints the available books
*
* @param sender <p>The sender to display the books to</p>
* @param listPublic <p>Whether to display public books</p>
*/
public static void printBooks(CommandSender sender, boolean listPublic) {
List<String> availableBooks = BooksWithoutBorders.getAvailableBooks(sender, listPublic);
FileHelper.printFiles(sender, availableBooks);
}
/**
* Prints a list of files
*
* @param sender <p>The command sender to show the list to</p>
* @param fileList <p>The files to list</p>
*/
public static void printFiles(CommandSender sender, List<String> fileList) {
BooksWithoutBorders.sendSuccessMessage(sender, "Available Books:");
int listSize = fileList.size();
for (int fileIndex = 0; fileIndex < listSize; fileIndex++) {
sender.sendMessage(ChatColor.GRAY + "[" + (fileIndex + 1) + "] " + fileList.get(fileIndex));
}
}
/**
* Lists available files
*
* @param sender <p>The command sender looking for files</p>
* @param searchDirectory <p>The directory to search for files</p>
* @return <p>A list of available files</p>
*/
private static List<String> listFiles(CommandSender sender, File searchDirectory) {
List<String> fileList = new ArrayList<>();
File[] existingFiles = searchDirectory.listFiles();
if (!searchDirectory.exists() || existingFiles == null || existingFiles.length == 0) {
BooksWithoutBorders.sendErrorMessage(sender, "No books have been saved!");
return null;
}
for (File foundFile : existingFiles) {
if (foundFile.isFile()) {
fileList.add(foundFile.getName());
}
}
return fileList;
}
/**
* Finds any duplicate book files
*
* @param foundFiles <p>The files to check for duplicates</p>
* @param fileName <p>The name of the file which might already exist</p>
* @return <p>The number of found duplicates</p>
*/
public static int findDuplicates(File[] foundFiles, String fileName) {
int foundDuplicates = 0;
for (File foundFile : foundFiles) {
if (foundFile.getName().matches("(\\([0-9]+\\))?" + Pattern.quote(fileName) + "(\\.yml|\\.txt)?")) {
foundDuplicates++;
}
}
return foundDuplicates;
}
}

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
@@ -9,7 +14,6 @@ import java.util.List;
public final class InputCleaningHelper { public final class InputCleaningHelper {
private InputCleaningHelper() { private InputCleaningHelper() {
} }
/** /**
@@ -18,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;
@@ -30,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("*", "");
@@ -40,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

@@ -0,0 +1,94 @@
package net.knarcraft.bookswithoutborders.utility;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
/**
* A converter from an integer to a roman numeral
*/
public final class IntegerToRomanConverter {
private final static List<Integer> romanValues = new ArrayList<>();
private final static List<Character> romanCharacters = new ArrayList<>();
static {
// Initialize the roman numbers
romanValues.add(1000);
romanValues.add(500);
romanValues.add(100);
romanValues.add(50);
romanValues.add(10);
romanValues.add(5);
romanValues.add(1);
romanCharacters.add('M');
romanCharacters.add('D');
romanCharacters.add('C');
romanCharacters.add('L');
romanCharacters.add('X');
romanCharacters.add('V');
romanCharacters.add('I');
}
private IntegerToRomanConverter() {
}
/**
* Gets the given number as a roman number string
*
* @param number <p>The number to convert</p>
* @return <p>The roman representation of the number</p>
*/
@NotNull
public static String getRomanNumber(int number) {
StringBuilder output = new StringBuilder();
int remainder = number;
for (int i = 0; i < romanCharacters.size(); i++) {
int romanValue = romanValues.get(i);
char romanCharacter = romanCharacters.get(i);
// Repeat the roman character, and calculate the new remainder
if (remainder >= romanValue) {
output.append(repeat(romanCharacter, remainder / romanValue));
remainder = remainder % romanValue;
}
// Exit early to prevent unexpected trailing characters
if (remainder == 0) {
return output.toString();
}
// Generate the special case IV and similar
for (int j = i; j < romanCharacters.size(); j++) {
int value = romanValues.get(j);
int difference = Math.max(romanValue - value, 0);
/* If the remainder is "one" less than the current roman value, we hit the IV/IX/XL case.
Note that 5 triggers the special case when 10 is tested, as 5 = 10 - 5, which requires a test, so it
can be filtered out. */
if (remainder == difference && value != romanValue / 2) {
output.append(romanCharacters.get(j)).append(romanCharacter);
remainder = 0;
}
}
}
return output.toString();
}
/**
* Repeats the given character
*
* @param character <p>The character to repeat</p>
* @param times <p>The number of times to repeat the character</p>
* @return <p>The repeated string</p>
*/
@NotNull
private static String repeat(char character, int times) {
return String.valueOf(character).repeat(Math.max(0, times));
}
}

View File

@@ -7,6 +7,8 @@ import org.bukkit.Material;
import org.bukkit.entity.Player; import org.bukkit.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
@@ -14,7 +16,25 @@ import org.bukkit.inventory.meta.BookMeta;
public final class InventoryHelper { 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;
}
} }
/** /**
@@ -24,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);
@@ -41,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);
@@ -52,6 +73,32 @@ public final class InventoryHelper {
} }
} }
/**
* Performs checks to validate that a player contains exactly one unwritten book
*
* @param player <p>The player to validate</p>
* @param noBookMessage <p>The message to display if the player is not holding a book</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>
*/
public static boolean notHoldingOneWritableBookCheck(@NotNull Player player, @NotNull String noBookMessage,
@NotNull String twoBooksMessage) {
BookHoldingState holdingState = getBookHoldingState(player);
if (holdingState == BookHoldingState.NONE || holdingState == BookHoldingState.SIGNED_BOTH_HANDS ||
holdingState == BookHoldingState.SIGNED_MAIN_HAND || holdingState == BookHoldingState.SIGNED_OFF_HAND) {
BooksWithoutBorders.sendErrorMessage(player, noBookMessage);
return true;
}
if (holdingState == BookHoldingState.UNSIGNED_BOTH_HANDS) {
BooksWithoutBorders.sendErrorMessage(player, twoBooksMessage);
return true;
}
return false;
}
/** /**
* Performs checks to validate that a player contains exactly one written book * Performs checks to validate that a player contains exactly one written book
* *
@@ -60,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 ||
@@ -87,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);
@@ -97,7 +146,7 @@ public final class InventoryHelper {
if (state == BookHoldingState.SIGNED_BOTH_HANDS || if (state == BookHoldingState.SIGNED_BOTH_HANDS ||
state == BookHoldingState.UNSIGNED_BOTH_HANDS || state == BookHoldingState.UNSIGNED_BOTH_HANDS ||
state == BookHoldingState.NONE) { state == BookHoldingState.NONE) {
return null; return ItemSlot.NONE;
} }
if (handMatters && typeMatters) { if (handMatters && typeMatters) {
if (mainHand && mainHandItem.getType() == requiredMaterial) { if (mainHand && mainHandItem.getType() == requiredMaterial) {
@@ -144,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);
@@ -181,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();
} }
@@ -192,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 {
@@ -207,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,15 +1,16 @@
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;
/** /**
* Helper class for getting string lists required for auto-completion * Helper class for getting string lists required for auto-completion
*/ */
public final class TabCompletionHelper { public final class TabCompletionTypeHelper {
private TabCompletionHelper() {
private TabCompletionTypeHelper() {
} }
/** /**
@@ -17,6 +18,7 @@ public final class TabCompletionHelper {
* *
* @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");
@@ -31,6 +33,7 @@ public final class TabCompletionHelper {
* @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++) {
@@ -46,6 +49,7 @@ public final class TabCompletionHelper {
* @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();
@@ -55,4 +59,27 @@ public final class TabCompletionHelper {
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

@@ -0,0 +1,39 @@
Options:
# The language to use. Only "en" is built-in, but custom languages can be added
Language: "en"
# The maximum number of duplicates of a saved book allowed
Max_Number_of_Duplicates: 5
# The separator used to separate the book title and the book author. While this is a ',' by default for backwards
# compatibility, a rarely used character like '¤' is better to prevent restrictions on book titles.
Title-Author_Separator: ","
# The separator used to denote a new line in the book/item lore
Lore_line_separator: "~"
# A list of books given to new players the first time they join the server
Books_for_new_players: [ ]
# An optional message displayed to new players the first time they join the server
Message_for_new_players: ""
# Price settings for book copying
Price_to_create_book:
# The item type used as currency for copying books. Use "Economy" to use money instead of items
Item_type: ""
# The quantity of currency required to pay for each book produced
Required_quantity: 0
# Whether any admin can decrypt any book regardless of the group it was encrypted for
Admin_Auto_Decrypt: false
# Whether to only allow the author of a book to create copies
Author_Only_Copy: false
# Whether to only allow the author of a book to unsign it
Author_Only_Unsign: false
# Whether to only allow saving a player's own books with /savebook
Author_Only_Save: false
# Whether to automatically format every book when it's signed
Format_Book_After_Signing: false
# Whether to display "COPY" or "COPY_OF_COPY" instead of "ORIGINAL" when a book is copied. This also uses the
# vanilla behavior where a copy of a copy cannot be copied further.
Change_Generation_On_Copy: false
# Whether to enable hitting a chiseled bookshelf while sneaking to see the shelf's contents.
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

@@ -1,18 +1,26 @@
name: BooksWithoutBorders name: BooksWithoutBorders
version: '${project.version}' version: '${project.version}'
main: net.knarcraft.bookswithoutborders.BooksWithoutBorders main: net.knarcraft.bookswithoutborders.BooksWithoutBorders
api-version: 1.17 api-version: '1.20'
prefix: Books Without Borders prefix: 'Books Without Borders'
authors: [ EpicKnarvik97, AkiraAkiba ] authors: [ EpicKnarvik97, AkiraAkiba ]
description: A continuation of the original Books Without Borders description: 'A continuation of the original Books Without Borders'
softdepend: [ Vault ] softdepend: [ Vault ]
website: ???? website: https://www.spigotmc.org/resources/books-without-borders-updated.96069/
dev-url: https://git.knarcraft.net/EpicKnarvik97/Books-Without-Borders dev-url: https://git.knarcraft.net/EpicKnarvik97/Books-Without-Borders
commands: commands:
bookswithoutborders: bookswithoutborders:
description: Lists Books Without Borders's commands and uses. description: Lists Books Without Borders's commands and uses.
aliases: [ bwb ] aliases: [ bwb ]
usage: /<command> usage: /<command>
decryptbook:
description: Decrypts the book the player is holding. "key" is required and MUST be IDENTICAL to the key used to encrypt held book
usage: /<command> <key>
permission: bookswithoutborders.decrypt
formatbook:
description: Replaces color/formatting codes in a written book with formatted text
usage: /<command>
permission: bookswithoutborders.format
givebook: givebook:
description: Gives the selected player a book from your personal directory description: Gives the selected player a book from your personal directory
usage: /<command> <file name or number> <playername> [# of copies (num)] [signed (true/false)] usage: /<command> <file name or number> <playername> [# of copies (num)] [signed (true/false)]
@@ -21,10 +29,6 @@ commands:
description: Same as givebook, but uses books from the public directory description: Same as givebook, but uses books from the public directory
usage: /<command> <file name or number> <playername> [# of copies (num)] [signed (true/false)] usage: /<command> <file name or number> <playername> [# of copies (num)] [signed (true/false)]
permission: bookswithoutborders.givepublic permission: bookswithoutborders.givepublic
decryptbook:
description: Decrypts the book the player is holding. "key" is required and MUST be IDENTICAL to the key used to encrypt held book
usage: /<command> <key>
permission: bookswithoutborders.decrypt
groupencryptbook: groupencryptbook:
description: Encrypts book so that only players with the bookswithoutborders.decrypt.<group name> permission may decrypt the book by holding and left clicking the book description: Encrypts book so that only players with the bookswithoutborders.decrypt.<group name> permission may decrypt the book by holding and left clicking the book
usage: /<command> <group name> <key> [encryption style] usage: /<command> <group name> <key> [encryption style]
@@ -41,6 +45,10 @@ commands:
description: Copies the book the player is holding description: Copies the book the player is holding
usage: /<command> <# of copies> usage: /<command> <# of copies>
permission: bookswithoutborders.copy permission: bookswithoutborders.copy
clearbook:
description: Removes all text from the book the player is holding
usage: /<command>
permission: bookswithoutborders.clear
unsignbook: unsignbook:
description: Un-signs the book the player is holding description: Un-signs the book the player is holding
usage: /<command> usage: /<command>
@@ -49,12 +57,16 @@ commands:
description: 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 "" description: 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 ""
usage: /<command> <key> [encryption style] usage: /<command> <key> [encryption style]
permission: bookswithoutborders.encrypt permission: bookswithoutborders.encrypt
setbookgeneration:
description: Sets the generation of the held book
usage: /<command> <generation>
permission: bookswithoutborders.setgeneration
setbookprice: setbookprice:
description: 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> or <quantity> are specified the current price to create books will be removed. description: 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> or <quantity> are specified, the current price to create books will be removed.
usage: /<command> <item/eco> <quantity> usage: /<command> <item/eco> <quantity>
permission: bookswithoutborders.setbookprice permission: bookswithoutborders.setbookprice
setlore: setlore:
description: Sets the lore of the item the player is holding. Insert the lore_line_separator character to force a new line ("~" by default). description: Sets the lore of the item the player is holding. Insert the lore_line_separator character to force a new line ("~" by default)
usage: /<command> <new lore> usage: /<command> <new lore>
permission: bookswithoutborders.setlore permission: bookswithoutborders.setlore
savepublicbook: savepublicbook:
@@ -74,7 +86,7 @@ commands:
usage: /<command> <title> usage: /<command> <title>
permission: bookswithoutborders.settitle permission: bookswithoutborders.settitle
loadbook: loadbook:
description: 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 description: 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
usage: /<command> <file name or number> [# of copies] [signed (true/false)] usage: /<command> <file name or number> [# of copies] [signed (true/false)]
permission: bookswithoutborders.load permission: bookswithoutborders.load
loadpublicbook: loadpublicbook:
@@ -84,6 +96,22 @@ commands:
reload: reload:
description: Reloads BwB's configuration file description: Reloads BwB's configuration file
usage: /<command> usage: /<command>
permission: bookswithoutborders.reload
setBookshelfData:
description: Sets custom data for a chiseled bookshelf used when peeking at the bookshelf
usage: /<command> <delete/name/lore> <text> [more text]
permission: bookswithoutborders.editbookshelf
addBookTitlePage:
description: Adds a blank page, title page or chapter page depending on input and whether the book is signed
usage: /<command> [page index] [title~description]
permission: bookswithoutborders.addtitlepage
deleteBookPage:
description: Deletes one page from a book
usage: /<command> <page>
permission: bookswithoutborders.deletepage
migrateBooks:
description: Migrates all txt books to yml, and fixes any incorrect filenames
usage: /<command>
permission: bookswithoutborders.admin permission: bookswithoutborders.admin
permissions: permissions:
bookswithoutborders.*: bookswithoutborders.*:
@@ -96,28 +124,45 @@ permissions:
default: op default: op
children: children:
bookswithoutborders.use: true bookswithoutborders.use: true
bookswithoutborders.unsign: true bookswithoutborders.alterbooks: true
bookswithoutborders.copy: true bookswithoutborders.copy: true
bookswithoutborders.loadpublic: true bookswithoutborders.loadpublic: true
bookswithoutborders.savepublic: true bookswithoutborders.savepublic: true
bookswithoutborders.encrypt: true bookswithoutborders.encrypt: true
bookswithoutborders.decrypt: true bookswithoutborders.decrypt: true
bookswithoutborders.groupencrypt: true
bookswithoutborders.signs: true bookswithoutborders.signs: true
bookswithoutborders.give: true bookswithoutborders.give: true
bookswithoutborders.givepublic: true bookswithoutborders.givepublic: true
bookswithoutborders.settitle: true
bookswithoutborders.setauthor: true
bookswithoutborders.setlore: true
bookswithoutborders.bypassauthoronlycopy: true bookswithoutborders.bypassauthoronlycopy: true
bookswithoutborders.bypassauthoronlyunsign: true
bookswithoutborders.bypassauthoronlysave: true
bookswithoutborders.bypassbookprice: true bookswithoutborders.bypassbookprice: true
bookswithoutborders.groupencrypt: true
bookswithoutborders.setbookprice: true bookswithoutborders.setbookprice: true
bookswithoutborders.reload: true
bookswithoutborders.setgeneration: true
bookswithoutborders.editbookshelf: true
bookswithoutborders.use: bookswithoutborders.use:
description: Allows player to use commands and to save/load/delete in their personal directory description: Allows player to use commands to save/load/delete in their personal directory, and peeking at bookshelves if enabled
children: children:
bookswithoutborders.save: true bookswithoutborders.save: true
bookswithoutborders.load: true bookswithoutborders.load: true
bookswithoutborders.delete: true bookswithoutborders.delete: true
bookswithoutborders.peekbookshelf: true
bookswithoutborders.alterbooks:
description: Allows player to change books' data such as lore/title/author/generation/formatting and unsigning books
children:
bookswithoutborders.clear: true
bookswithoutborders.unsign: true
bookswithoutborders.settitle: true
bookswithoutborders.setauthor: true
bookswithoutborders.setlore: true
bookswithoutborders.format: true
bookswithoutborders.setgeneration: true
bookswithoutborders.addtitlepage: true
bookswithoutborders.deletepage: true
bookswithoutborders.format:
description: Allows a player to format a book
bookswithoutborders.save: bookswithoutborders.save:
description: Allows player to save books to their personal directory description: Allows player to save books to their personal directory
bookswithoutborders.load: bookswithoutborders.load:
@@ -125,17 +170,19 @@ permissions:
bookswithoutborders.delete: bookswithoutborders.delete:
description: Allows player to delete books from their personal directory description: Allows player to delete books from their personal directory
bookswithoutborders.unsign: bookswithoutborders.unsign:
description: Allows player to use unsign command description: Allows player to use the unsign command
bookswithoutborders.copy: bookswithoutborders.copy:
description: Allows player to use copy command description: Allows player to use the copy command
bookswithoutborders.clear:
description: Allows player to use the clear command
bookswithoutborders.loadpublic: bookswithoutborders.loadpublic:
description: Allows player to load in the public directory description: Allows player to load from the public directory
bookswithoutborders.savepublic: bookswithoutborders.savepublic:
description: Allows player to save in the public directory description: Allows player to save to the public directory
bookswithoutborders.encrypt: bookswithoutborders.encrypt:
description: Allows player to encrypt books description: Allows player to encrypt books
bookswithoutborders.groupencrypt: bookswithoutborders.groupencrypt:
description: Allows player to set group based encryption description: Allows player to use group-based encryption
bookswithoutborders.decrypt: bookswithoutborders.decrypt:
description: Allows player to decrypt books description: Allows player to decrypt books
bookswithoutborders.signs: bookswithoutborders.signs:
@@ -152,7 +199,23 @@ permissions:
description: Allows player to set the lore of the currently held item description: Allows player to set the lore of the currently held item
bookswithoutborders.bypassauthoronlycopy: bookswithoutborders.bypassauthoronlycopy:
description: Allows player to ignore Author_Only_Copy config setting description: Allows player to ignore Author_Only_Copy config setting
bookswithoutborders.bypassauthoronlyunsign:
description: Allows player to ignore Author_Only_Unsign config setting
bookswithoutborders.bypassauthoronlysave:
description: Allows player to ignore Author_Only_Save config setting
bookswithoutborders.bypassbookprice: bookswithoutborders.bypassbookprice:
description: Allows player to ignore Price_to_create_book config setting description: Allows player to ignore Price_to_create_book config setting
bookswithoutborders.setbookprice: bookswithoutborders.setbookprice:
description: Allows player to set the cost of creating a book description: Allows player to set the cost of creating a book
bookswithoutborders.reload:
description: Allows player to reload this plugin
bookswithoutborders.setgeneration:
description: Allows player to change the generation of a book (Original, Copy, Copy of Copy)
bookswithoutborders.peekbookshelf:
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,65 @@
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!"
SUCCESS_TITLE_PAGE_ADDED: "Title page added!"
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"
ACTION_ADD_TITLE_AUTHOR_PAGE: "add an author title page to"
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!"
ERROR_INVALID_BOOK_PAGE: "Invalid page index given!"
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"
NEUTRAL_UNKNOWN_TITLE: "Untitled"
NEUTRAL_TITLE_PAGE_TITLE_AUTHOR_FORMAT: "{title}{separator}By: {author}"
NEUTRAL_TITLE_PAGE_HEADER_FORMAT: "\n&n&l{header}&r"
NEUTRAL_TITLE_PAGE_TEXT_FORMAT: "\n\n&o{text}&r"

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.assertFalse; 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},
assertFalse(encrypted.equals(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

@@ -1,13 +1,13 @@
package net.knarcraft.bookswithoutborders.util; package net.knarcraft.bookswithoutborders.util;
import net.knarcraft.bookswithoutborders.utility.FileHelper; import net.knarcraft.bookswithoutborders.utility.BookFileHelper;
import org.junit.Test; import org.junit.Test;
import java.io.File; import java.io.File;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
public class FileHelperTest { public class BookFileHelperTest {
@Test @Test
public void findDuplicatesTest() { public void findDuplicatesTest() {
@@ -17,9 +17,9 @@ public class FileHelperTest {
files[2] = new File("test/asd/(3)book+)Crab.yml"); files[2] = new File("test/asd/(3)book+)Crab.yml");
files[3] = new File("test/asd/(2)book+)Crab.yml"); files[3] = new File("test/asd/(2)book+)Crab.yml");
files[4] = new File("test/asd/(1)book+)Crab.yml"); files[4] = new File("test/asd/(1)book+)Crab.yml");
assertEquals(4, FileHelper.findDuplicates(files, "book+)Crab.yml")); assertEquals(4, BookFileHelper.findDuplicates(files, "book+)Crab.yml"));
assertEquals(1, FileHelper.findDuplicates(files, "book+)Fish.yml")); assertEquals(1, BookFileHelper.findDuplicates(files, "book+)Fish.yml"));
assertEquals(0, FileHelper.findDuplicates(files, "book+)Horse.yml")); assertEquals(0, BookFileHelper.findDuplicates(files, "book+)Horse.yml"));
} }
} }

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

View File

@@ -0,0 +1,40 @@
package net.knarcraft.bookswithoutborders.util;
import org.junit.Test;
import static net.knarcraft.bookswithoutborders.utility.IntegerToRomanConverter.getRomanNumber;
import static org.junit.Assert.assertEquals;
/**
* A test class for IntegerToRomanConverter
*/
public class IntegerToRomanConverterTest {
@Test
public void basicNumbersTest() {
assertEquals("I", getRomanNumber(1));
assertEquals("II", getRomanNumber(2));
assertEquals("III", getRomanNumber(3));
assertEquals("IV", getRomanNumber(4));
assertEquals("V", getRomanNumber(5));
assertEquals("X", getRomanNumber(10));
assertEquals("XV", getRomanNumber(15));
assertEquals("XX", getRomanNumber(20));
assertEquals("L", getRomanNumber(50));
assertEquals("C", getRomanNumber(100));
assertEquals("D", getRomanNumber(500));
assertEquals("M", getRomanNumber(1000));
}
@Test
public void nineFourTest() {
assertEquals("IV", getRomanNumber(4));
assertEquals("IX", getRomanNumber(9));
assertEquals("XIV", getRomanNumber(14));
assertEquals("XIX", getRomanNumber(19));
assertEquals("XXIV", getRomanNumber(24));
assertEquals("XL", getRomanNumber(40));
assertEquals("IL", getRomanNumber(49));
}
}