Compare commits

...

64 Commits

Author SHA1 Message Date
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
44 changed files with 1521 additions and 568 deletions

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

181
README.md
View File

@ -1,10 +1,9 @@
# Books Without Borders
This is an attempt at a rewrite of the Books Without Borders plugin. This rewrite uses 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 this time. The only goal is to make it 1.17.1 compliant, but I'll make the code more maintainable along the way.
While the original version still works, it's using a lot of depreciated function calls which will most likely break in
the future.
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). While the old plugin still worked
the last time I checked, this plugin does not use any depreciated function calls, making sure it works for the
foreseeable future.
## Books without Borders!
@ -13,90 +12,94 @@ Books without Borders has got your back!
### Features
* Export written books and book and quills to .txt or .yml files
* Import books from files as written books or unsigned books
* Text files can be any length, and the import process fits the content to the correct page length
* Books can be saved privately, or to a directory visible server wide
* Encrypt books to prevent other players from reading them
* Give, encrypt, or decrypt held books with signs
* Give players books via command blocks
* Unsign or copy held books with a simple command
* 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
* Add lore to any item with a simple command
* Supports adding and saving color to title, lore, and book contents
* Color and formatting codes can be manually turned into formatting using /formatbook
* Formatting and color codes can be turned into formatting once any book is signed. This is enabled through a config
- Export written books and book and quills to .txt or .yml files
- Import books from files as written books or unsigned books
- Text files can be any length, and the import process fits the content to the correct page length
- Books can be saved privately, or to a directory visible server wide
- Encrypt books to prevent other players from reading them
- Give, encrypt, or decrypt held books with signs
- Give players books via command blocks
- Unsign or copy held books with a simple command
- 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
- Add lore to any item with a simple command
- Supports adding and saving color to title, lore, and book contents
- Color and formatting codes can be manually turned into formatting using /formatbook
- Clear a book, and start anew with /clearbook
- 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
#### 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.
### Commands:
An in-game description of available commands is available through the /bwb command.
* /bookswithoutborders - Displays information about commands (and permissions if the user has bookswithoutborders.admin)
* /copybook <# of copies> - Copies the book the player is holding
* /decryptbook <key> - Decrypts the book the player is holding. "key" is required and MUST be IDENTICAL to the key used
to encrypt the held book
* /deletebook <file name or number> - Deletes the specified file in the player's directory
* /deletepublicbook <file name or number> - Same as deletebook, but deletes files in the public directory
* encryptbook <key> \[encryption style] - Encrypts the book the player is holding. "key" is required and can be any
phrase or number excluding spaces. "style" is not required. Possible values are "DNA" or ""
* /formatbook - Formats the held written book (converts color and formatting codes to the corresponding formatted text)
* /givebook <file name or number> <playername> \[# of copies (num)] \[signed (true/false)] - Gives the selected player a
book from your personal directory
* /givepublicbook <file name or number> <playername> \[# of copies (num)] \[signed (true/false)] - Same as givebook, but
uses books from the public directory
* /loadbook <file name or number> \[# of copies] \[signed (true/false)] - Creates a book from the specified file and
gives it to the player. If no file is specified, a list of available files is returned. If true is specified, the book
will be signed, if false it will be unsigned
* /loadpublicbook <file name or number> \[# of copies] \[signed (true/false)] - Same as loadbook, but views files in the
public directory
* /reload - Reloads BwB's configuration file
* /savebook \[overwrite (true/false)] - Saves the book the player is holding to a text file in a private directory. If
true is specified, a book of the same name by the same author will be overwritten by the new book
* /savepublicbook \[overwrite (true/false)] - Same as savebook, but saves files in the public directory
* /setbookauthor <author> - Sets the author of the book the player is holding
* /setbookprice <item/eco> <quantity> - Sets the per-book price to create a book via commands. If "Item", the item in
the player's hand in the amount of <quantity> will be the price. If "Eco", a Vault based economy will be used for
price. If neither <Item/Eco> nor <quantity> are specified, the current price to create books will be removed.
* /setlore <new lore> - Sets the lore of the item the player is holding. Insert the lore_line_separator character to
force a new line ("~" by default)
* /settitle <title> - Sets the title of the book/item the player is holding
* /unsignbook - Un-signs the book the player is holding
| Command | Arguments | Description |
|----------------------|----------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| /bookswithoutborders | None | Displays information about commands (and permissions if the user has bookswithoutborders.admin) |
| /copybook | \<# of copies> | Copies the book the player is holding |
| /decryptbook | \<key> | Decrypts the book the player is holding. "key" is required and MUST be IDENTICAL to the key used to encrypt the held book |
| /deletebook | \<file name or number> | Deletes the specified file in the player's directory |
| /deletepublicbook | \<file name or number> | Same as deletebook, but deletes files in the public directory |
| /encryptbook | \<key> \[encryption style] | Encrypts the book the player is holding. "key" is required and can be any phrase or number excluding spaces. "style" is not required. Possible values are "DNA" or "" |
| /formatbook | None | Formats the held written book (converts color and formatting codes to the corresponding formatted text) |
| /givebook | \<file name or number> \<playername> \[# of copies (num)] \[signed (true/false)] | Gives the selected player a book from your personal directory |
| /givepublicbook | \<file name or number> \<playername> \[# of copies (num)] \[signed (true/false)] | Same as givebook, but uses books from the public directory |
| /loadbook | \<file name or number> \[# of copies] \[signed (true/false)] | Creates a book from the specified file and gives it to the player. If no file is specified, a list of available files is returned. If true is specified, the book will be signed, if false it will be unsigned |
| /loadpublicbook | \<file name or number> \[# of copies] \[signed (true/false)] | Same as loadbook, but views files in the public directory |
| /reload | None | Reloads BwB's configuration file |
| /savebook | \[overwrite (true/false)] | Saves the book the player is holding to a text file in a private directory. If true is specified, a book of the same name by the same author will be overwritten by the new book |
| /savepublicbook | \[overwrite (true/false)] | Same as savebook, but saves files in the public directory |
| /setbookauthor | \<author> | Sets the author of the book the player is holding |
| /setbookgeneration | \<generation> | Sets the generation of the held book (ORIGINAL, COPY_OF_ORIGINAL, COPY_OF_COPY, TATTERED) |
| /setbookprice | \<item/eco> \<quantity> | Sets the per-book price to create a book via commands. If "Item", the item in the player's hand in the amount of \<quantity> will be the price. If "Eco", a Vault based economy will be used for price. If neither \<Item/Eco> nor \<quantity> are specified, the current price to create books will be removed. |
| /setlore | \<new lore> | Sets the lore of the item the player is holding. Insert the lore_line_separator character to force a new line ("~" by default) |
| /settitle | \<title> | Sets the title of the book/item the player is holding |
| /unsignbook | None | Un-signs the book the player is holding |
| /clearbook | None | Removes all text from the held un-signed book |
### Permissions:
* bookswithoutborders.* - Grants all permissions
* bookswithoutborders.admin - Grants all permissions
* bookswithoutborders.use - Allows player to use commands to save/load/delete in their personal directory
* bookswithoutborders.alterbooks - Allows player to change books' data such as lore/title/author/formatting and
unsigning books
* 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.bypassbookprice - Allows player to ignore Price_to_create_book config setting
* bookswithoutborders-setbookprice - Allows player to set the cost of creating a book
| 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 |
| 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 |
### Signs
@ -105,15 +108,35 @@ line.
#### Give sign
The **_give_** sign must have **\[Give]** on its second line. The third and fourth line contains the book to be loaded.
The **_give_**-sign must have **\[Give]** on its second line. The third and fourth line contains the book to be loaded.
This can either be a numerical id pointing to a publicly saved book, or the full text identifier of the book (book name,
author).
#### Encrypt sign
The **_encrypt_** sign must have **\[Encrypt]** on its second line. The third line must contain the encryption key The
The **_encrypt_**-sign must have **\[Encrypt]** on its second line. The third line must contain the encryption key The
fourth line can be empty or contain "dna" for dna-based encryption.
#### Decrypt sign
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 |
|----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Save_Books_in_Yaml_Format | Whether to use YAML for saved books instead of just storing them as text |
| Max_Number_of_Duplicates | The maximum number of duplicates of a saved book allowed |
| Author_Separator | The separator used to separate the book title and the book author |
| Lore_line_separator | The separator used to denote a new line in the book/item lore |
| Books_for_new_players | A list of books given to new players the first time they join the server |
| Message_for_new_players | An optional message displayed to new players the first time they join the server |
| Price_to_create_book.Item_type | The item type used as currency for copying books. Use "Economy" to use money instead of items |
| Price_to_create_book.Required_quantity | The quantity of currency required to pay for each book produced |
| Admin_Auto_Decrypt | Whether any admin can decrypt any book regardless of the group it was encrypted for |
| Author_Only_Copy | Whether to only allow the author of a book to create copies |
| Author_Only_Unsign | Whether to only allow the author of a book to unsign it |
| Author_Only_Save | Whether to only allow saving a player's own books with /savebook |
| Format_Book_After_Signing | Whether to automatically format every book when it's signed |
| Change_Generation_On_Copy | Whether to display "COPY" or "COPY_OF_COPY" instead of "ORIGINAL" when a book is copied. This also uses the vanilla behavior where a copy of a copy or tattered book cannot be copied further. |
| Enable_Book_Peeking | Whether to enable hitting a chiseled bookshelf while sneaking to see the shelf's contents. |

66
pom.xml
View File

@ -6,7 +6,7 @@
<groupId>net.knarcraft</groupId>
<artifactId>BooksWithoutBorders</artifactId>
<version>1.2.2</version>
<version>1.3.6-SNAPSHOT</version>
<packaging>jar</packaging>
<licenses>
@ -20,7 +20,7 @@
<description>A continuation of the original Books Without Borders</description>
<properties>
<java.version>17</java.version>
<java.version>16</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
@ -31,14 +31,14 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<version>3.3.0</version>
<executions>
<execution>
<phase>package</phase>
@ -47,6 +47,30 @@
</goals>
<configuration>
<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>
</execution>
</executions>
@ -61,6 +85,10 @@
</build>
<repositories>
<repository>
<id>knarcraft-repo</id>
<url>https://git.knarcraft.net/api/packages/EpicKnarvik97/maven</url>
</repository>
<repository>
<id>spigotmc-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
@ -70,25 +98,47 @@
<url>https://jitpack.io</url>
</repository>
</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>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.18.1-R0.1-SNAPSHOT</version>
<version>1.21-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.github.MilkBowl</groupId>
<artifactId>VaultAPI</artifactId>
<version>1.7</version>
<version>1.7.1</version>
<scope>provided</scope>
</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>23.0.0</version>
<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>
</project>

View File

@ -1,6 +1,7 @@
package net.knarcraft.bookswithoutborders;
import net.knarcraft.bookswithoutborders.command.CommandBooksWithoutBorders;
import net.knarcraft.bookswithoutborders.command.CommandClear;
import net.knarcraft.bookswithoutborders.command.CommandCopy;
import net.knarcraft.bookswithoutborders.command.CommandDecrypt;
import net.knarcraft.bookswithoutborders.command.CommandDelete;
@ -17,20 +18,23 @@ import net.knarcraft.bookswithoutborders.command.CommandSave;
import net.knarcraft.bookswithoutborders.command.CommandSavePublic;
import net.knarcraft.bookswithoutborders.command.CommandSetAuthor;
import net.knarcraft.bookswithoutborders.command.CommandSetBookPrice;
import net.knarcraft.bookswithoutborders.command.CommandSetGeneration;
import net.knarcraft.bookswithoutborders.command.CommandSetLore;
import net.knarcraft.bookswithoutborders.command.CommandSetTitle;
import net.knarcraft.bookswithoutborders.command.CommandUnSign;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.listener.BookEventListener;
import net.knarcraft.bookswithoutborders.listener.BookshelfListener;
import net.knarcraft.bookswithoutborders.listener.PlayerEventListener;
import net.knarcraft.bookswithoutborders.listener.SignEventListener;
import net.knarcraft.bookswithoutborders.utility.FileHelper;
import net.knarcraft.bookswithoutborders.utility.UpdateChecker;
import net.knarcraft.bookswithoutborders.utility.BookFileHelper;
import net.knarcraft.knarlib.util.UpdateChecker;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.command.PluginCommand;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemFactory;
import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.PluginManager;
@ -41,6 +45,8 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getBookFolder;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getErrorColor;
@ -53,7 +59,7 @@ import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig
public class BooksWithoutBorders extends JavaPlugin {
private static ItemFactory itemFactory;
private static Map<String, List<String>> playerBooksList;
private static Map<UUID, List<String>> playerBooksList;
private static List<String> publicBooksList;
private static BooksWithoutBorders booksWithoutBorders;
private static ConsoleCommandSender consoleSender;
@ -77,13 +83,15 @@ public class BooksWithoutBorders extends JavaPlugin {
public static List<String> getAvailableBooks(CommandSender sender, boolean getPublic) {
if (getPublic) {
return new ArrayList<>(publicBooksList);
} else {
String senderName = sender.getName();
if (!playerBooksList.containsKey(senderName)) {
List<String> newFiles = FileHelper.listFiles(sender, false);
playerBooksList.put(senderName, newFiles);
} else if (sender instanceof Player player) {
UUID playerUUID = player.getUniqueId();
if (!playerBooksList.containsKey(playerUUID)) {
List<String> newFiles = BookFileHelper.listFiles(sender, false);
playerBooksList.put(playerUUID, newFiles);
}
return playerBooksList.get(senderName);
return playerBooksList.get(playerUUID);
} else {
return new ArrayList<>();
}
}
@ -94,11 +102,11 @@ public class BooksWithoutBorders extends JavaPlugin {
* @param updatePublic <p>Whether to update public books</p>
*/
public static void updateBooks(CommandSender sender, boolean updatePublic) {
List<String> newFiles = FileHelper.listFiles(sender, updatePublic);
List<String> newFiles = BookFileHelper.listFiles(sender, updatePublic);
if (updatePublic) {
publicBooksList = newFiles;
} else {
playerBooksList.put(sender.getName(), newFiles);
} else if (sender instanceof Player player) {
playerBooksList.put(player.getUniqueId(), newFiles);
}
}
@ -116,7 +124,7 @@ public class BooksWithoutBorders extends JavaPlugin {
consoleSender = this.getServer().getConsoleSender();
playerBooksList = new HashMap<>();
BooksWithoutBordersConfig.initialize(this);
publicBooksList = FileHelper.listFiles(consoleSender, true);
publicBooksList = BookFileHelper.listFiles(consoleSender, true);
PluginManager pluginManager = this.getServer().getPluginManager();
@ -124,6 +132,7 @@ public class BooksWithoutBorders extends JavaPlugin {
pluginManager.registerEvents(new PlayerEventListener(), this);
pluginManager.registerEvents(new SignEventListener(), this);
pluginManager.registerEvents(new BookEventListener(), this);
pluginManager.registerEvents(new BookshelfListener(), this);
} else {
this.getPluginLoader().disablePlugin(this);
}
@ -167,6 +176,8 @@ public class BooksWithoutBorders extends JavaPlugin {
registerCommand("booksWithoutBorders", new CommandBooksWithoutBorders());
registerCommand("reload", new CommandReload());
registerCommand("formatBook", new CommandFormat());
registerCommand("setBookGeneration", new CommandSetGeneration());
registerCommand("clearBook", new CommandClear());
}
/**
@ -234,8 +245,8 @@ public class BooksWithoutBorders extends JavaPlugin {
sendErrorMessage(consoleSender, "Saving failed! Aborting...");
return false;
}
} catch (Exception e) {
e.printStackTrace();
} catch (Exception exception) {
BooksWithoutBorders.getInstance().getLogger().log(Level.SEVERE, "Unable to create necessary folders");
return false;
}
}
@ -245,8 +256,8 @@ public class BooksWithoutBorders extends JavaPlugin {
sendErrorMessage(consoleSender, "Saving failed! Aborting...");
return false;
}
} catch (Exception e) {
e.printStackTrace();
} catch (Exception exception) {
BooksWithoutBorders.getInstance().getLogger().log(Level.SEVERE, "Unable to create necessary folders");
return false;
}
}

View File

@ -84,6 +84,7 @@ public class CommandBooksWithoutBorders implements TabExecutor {
showCommandInfo("saveBook", sender);
showCommandInfo("savePublicBook", sender);
showCommandInfo("setAuthor", sender);
showCommandInfo("setBookGeneration", sender);
showCommandInfo("setBookPrice", sender);
showCommandInfo("setLore", sender);
showCommandInfo("setTitle", sender);
@ -104,6 +105,9 @@ public class CommandBooksWithoutBorders implements TabExecutor {
String commandInfo = "\n" + getCommandColor() + pluginCommand.getUsage().replace("<command>",
pluginCommand.getName()) + ": " + getSuccessColor() + pluginCommand.getDescription();
if (sender.hasPermission("bookswithoutborders.admin")) {
if (permission == null) {
permission = "None";
}
commandInfo += getCommandColor() + " {" + permission + "}";
}
sender.sendMessage(commandInfo);

View File

@ -0,0 +1,58 @@
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 clear command
*/
public class CommandClear implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
if (!(sender instanceof Player player)) {
BooksWithoutBorders.sendErrorMessage(sender, "This command can only be used by a player!");
return false;
}
if (InventoryHelper.notHoldingOneWritableBookCheck(player, "You must be holding a writable book to " +
"clear it!", "You cannot clear two books at once!")) {
return false;
}
//Clear the player's held book
ItemStack heldBook = InventoryHelper.getHeldBook(player, false);
BookMeta bookMeta = (BookMeta) heldBook.getItemMeta();
if (bookMeta == null) {
BooksWithoutBorders.sendErrorMessage(sender, "Unable to get metadata for the held book!");
return false;
}
bookMeta.setPages("");
bookMeta.setAuthor(null);
bookMeta.setGeneration(null);
bookMeta.setTitle(null);
heldBook.setItemMeta(bookMeta);
BooksWithoutBorders.sendSuccessMessage(sender, "Book cleared!");
return true;
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
return new ArrayList<>();
}
}

View File

@ -2,10 +2,12 @@ package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.utility.BookHelper;
import net.knarcraft.bookswithoutborders.utility.EconomyHelper;
import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper;
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.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
@ -40,57 +42,108 @@ public class CommandCopy implements TabExecutor {
return false;
}
ItemStack heldBook = InventoryHelper.getHeldBook(player, true);
try {
ItemStack heldBook = InventoryHelper.getHeldBook(player, true);
int copies = Integer.parseInt(args[0]);
if (copies > 0) {
if (BooksWithoutBordersConfig.getAuthorOnlyCopy() && !player.hasPermission("bookswithoutborders.bypassAuthorOnlyCopy")) {
if (!isAuthor(player, (BookMeta) Objects.requireNonNull(heldBook.getItemMeta()))) {
return false;
}
}
if (BooksWithoutBordersConfig.booksHavePrice() &&
!player.hasPermission("bookswithoutborders.bypassBookPrice") &&
EconomyHelper.cannotPayForBookPrinting(player, copies)) {
return false;
}
heldBook.setAmount(heldBook.getAmount() + copies);
BooksWithoutBorders.sendSuccessMessage(player, "Book copied!");
return true;
if (copies <= 0) {
throw new NumberFormatException("Number of copies must be larger than 0");
}
return performCopy(copies, player, heldBook);
} catch (NumberFormatException ignored) {
BooksWithoutBorders.sendErrorMessage(player, "Book not copied!");
BooksWithoutBorders.sendErrorMessage(player, "Number specified was invalid!");
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 book <p>The book to check</p>
* @return <p>True if the player is the book's author</p>
* @param copies <p>The number of copies to be made</p>
* @param player <p>The player requesting the copies</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) {
String author = book.getAuthor();
String playerName = InputCleaningHelper.cleanString(player.getName());
if (author != null && playerName.equalsIgnoreCase(InputCleaningHelper.cleanString(author))) {
return true;
private boolean performCopy(int copies, Player player, ItemStack heldBook) {
//Make sure the player owns the book if authorOnlyCopy is enabled
if (BooksWithoutBordersConfig.getAuthorOnlyCopy() &&
!player.hasPermission("bookswithoutborders.bypassAuthorOnlyCopy")) {
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!");
return false;
BookMeta bookMeta = (BookMeta) heldBook.getItemMeta();
if (BooksWithoutBordersConfig.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.sendSuccessMessage(player, "Book copied!");
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(Player player, int copies) {
return BooksWithoutBordersConfig.booksHavePrice() &&
!player.hasPermission("bookswithoutborders.bypassBookPrice") &&
EconomyHelper.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(BookMeta bookMeta, Player player, int copies) {
//Copy the vanilla behavior of refusing copying any further
if (bookMeta.getGeneration() == BookMeta.Generation.COPY_OF_COPY ||
bookMeta.getGeneration() == BookMeta.Generation.TATTERED) {
BooksWithoutBorders.sendErrorMessage(player, "You cannot copy this book any further. " +
"You must have the original or a direct copy.");
return false;
}
//Make sure the player can fit the book in their inventory
int nextAvailableSlot = player.getInventory().firstEmpty();
if (nextAvailableSlot == -1) {
BooksWithoutBorders.sendErrorMessage(player, "You need an available slot in your inventory.");
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
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] args) {
int argumentCount = args.length;
if (argumentCount == 1) {
return TabCompletionHelper.filterMatchingStartsWith(TabCompletionHelper.getNumbers(1, 20), args[0]);
return TabCompletionHelper.filterMatchingStartsWith(TabCompletionTypeHelper.getNumbers(1, 20), args[0]);
}
return new ArrayList<>();
}

View File

@ -2,6 +2,7 @@ package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.utility.BookHelper;
import net.knarcraft.bookswithoutborders.utility.EncryptionHelper;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import org.bukkit.command.Command;
@ -47,12 +48,6 @@ public class CommandDecrypt implements TabExecutor {
//Warning: admin decrypt only allows decrypting files created by the same player. Not sure if intended
if (args.length == 0 && BooksWithoutBordersConfig.getAdminDecrypt() && player.hasPermission("bookswithoutborders.admin")) {
String path = getBookFolder() + "Encrypted" + getSlash();
String fileName;
if (bookMetadata.hasTitle()) {
fileName = bookMetadata.getTitle() + BooksWithoutBordersConfig.getTitleAuthorSeparator() + bookMetadata.getAuthor();
} else {
fileName = "Untitled," + player.getName();
}
File encryptedDirectory = new File(path);
String[] encryptedFiles = encryptedDirectory.list();
@ -64,7 +59,7 @@ public class CommandDecrypt implements TabExecutor {
//Get the "encryption key" from the filename
String key = "";
for (String encryptedFile : encryptedFiles) {
if (encryptedFile.contains(fileName)) {
if (encryptedFile.contains(BookHelper.getBookFile(bookMetadata, player, true))) {
key = encryptedFile.substring(encryptedFile.indexOf("[") + 1, encryptedFile.indexOf("]"));
break;
}

View File

@ -1,9 +1,10 @@
package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.utility.FileHelper;
import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper;
import net.knarcraft.bookswithoutborders.utility.TabCompletionHelper;
import net.knarcraft.bookswithoutborders.state.BookDirectory;
import net.knarcraft.bookswithoutborders.utility.BookFileHelper;
import net.knarcraft.bookswithoutborders.utility.BookHelper;
import net.knarcraft.knarlib.util.TabCompletionHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
@ -14,9 +15,6 @@ import java.io.File;
import java.util.ArrayList;
import java.util.List;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getBookFolder;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getSlash;
/**
* Command executor for the delete command
*/
@ -43,7 +41,7 @@ public class CommandDelete implements TabExecutor {
boolean deleteBook(CommandSender sender, String[] args, boolean deletePublic) {
//List deletable files
if (args.length == 0) {
FileHelper.printBooks(sender, deletePublic);
BookFileHelper.printBooks(sender, deletePublic);
return true;
}
//Delete the file
@ -82,13 +80,9 @@ public class CommandDelete implements TabExecutor {
}
//Get the file to be deleted
File file;
if (isPublic) {
file = FileHelper.getBookFile(getBookFolder() + fileName);
} else {
file = FileHelper.getBookFile(getBookFolder() +
InputCleaningHelper.cleanString(sender.getName()) + getSlash() + fileName);
}
String bookDirectory = BookHelper.getBookDirectoryPathString(
isPublic ? BookDirectory.PUBLIC : BookDirectory.PLAYER, sender);
File file = BookFileHelper.getBookFile(bookDirectory + fileName);
//Send message if no such file could be found
if (file == null) {

View File

@ -5,7 +5,7 @@ import net.knarcraft.bookswithoutborders.state.EncryptionStyle;
import net.knarcraft.bookswithoutborders.state.ItemSlot;
import net.knarcraft.bookswithoutborders.utility.EncryptionHelper;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import net.knarcraft.bookswithoutborders.utility.TabCompletionHelper;
import net.knarcraft.knarlib.util.TabCompletionHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;

View File

@ -1,10 +1,11 @@
package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.utility.BookFileHelper;
import net.knarcraft.bookswithoutborders.utility.BookLoader;
import net.knarcraft.bookswithoutborders.utility.FileHelper;
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.Server;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
@ -49,7 +50,7 @@ public class CommandGive implements TabExecutor {
}
if (args.length == 0) {
FileHelper.printBooks(sender, givePublic);
BookFileHelper.printBooks(sender, givePublic);
return true;
}
@ -69,6 +70,20 @@ public class CommandGive implements TabExecutor {
}
}
//Try and find the target player
Player receivingPlayer = booksWithoutBorders.getServer().getPlayer(receivingPlayerName);
if (receivingPlayer == null) {
BooksWithoutBorders.sendErrorMessage(sender, "Player not found!");
return false;
}
//Make sure the receiver is able to fit the book
if (receivingPlayer.getInventory().firstEmpty() == -1) {
BooksWithoutBorders.sendErrorMessage(sender, "Receiving player must have space in their inventory" +
" to receive books!");
return false;
}
//Load books available to the player
try {
Integer.parseInt(bookIdentifier);
@ -76,29 +91,8 @@ public class CommandGive implements TabExecutor {
} 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 {
ItemStack newBook = BookLoader.loadBook(sender, bookToLoad, isSigned, folder, Integer.parseInt(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;
}
return loadAndGiveBook(bookIdentifier, sender, receivingPlayer, isSigned, folder, copies);
} catch (NumberFormatException e) {
BooksWithoutBorders.sendErrorMessage(sender, "Invalid number of book copies specified!");
return false;
@ -139,12 +133,12 @@ public class CommandGive implements TabExecutor {
return null;
} else if (argumentCount == 3) {
//Number of copies
return TabCompletionHelper.filterMatchingStartsWith(TabCompletionHelper.getBooleansAndNumbers(1, 3), args[2]);
return TabCompletionHelper.filterMatchingStartsWith(TabCompletionTypeHelper.getBooleansAndNumbers(1, 3), args[2]);
} else if (argumentCount == 4) {
//Signed
try {
Integer.parseInt(args[2]);
return TabCompletionHelper.filterMatchingStartsWith(TabCompletionHelper.getBooleans(), args[3]);
return TabCompletionHelper.filterMatchingStartsWith(TabCompletionTypeHelper.getBooleans(), args[3]);
} catch (NumberFormatException e) {
return new ArrayList<>();
}
@ -152,4 +146,31 @@ public class CommandGive implements TabExecutor {
return new ArrayList<>();
}
/**
* 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(String bookIdentifier, CommandSender sender, Player receivingPlayer,
String isSigned, String folder, String copies) throws NumberFormatException {
String bookToLoad = InputCleaningHelper.cleanString(bookIdentifier);
ItemStack newBook = BookLoader.loadBook(sender, bookToLoad, isSigned, folder, Integer.parseInt(copies));
if (newBook != null) {
//NOTE: As this method bypasses cost, it should also bypass the generation change
receivingPlayer.getInventory().addItem(newBook);
BooksWithoutBorders.sendSuccessMessage(sender, "Book sent!");
BooksWithoutBorders.sendSuccessMessage(receivingPlayer, "Book received!");
return true;
} else {
BooksWithoutBorders.sendErrorMessage(sender, "Book failed to load!");
return false;
}
}
}

View File

@ -1,10 +1,11 @@
package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.utility.BookFileHelper;
import net.knarcraft.bookswithoutborders.utility.BookLoader;
import net.knarcraft.bookswithoutborders.utility.FileHelper;
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.CommandSender;
import org.bukkit.command.TabExecutor;
@ -49,7 +50,7 @@ public class CommandLoad implements TabExecutor {
//Show books available to the player
if (argumentCount == 0) {
FileHelper.printBooks(sender, loadPublic);
BookFileHelper.printBooks(sender, loadPublic);
return true;
}
@ -114,12 +115,12 @@ public class CommandLoad implements TabExecutor {
args[0]);
} else if (argumentCount == 2) {
//Number of copies
return TabCompletionHelper.filterMatchingStartsWith(TabCompletionHelper.getBooleansAndNumbers(1, 3), args[1]);
return TabCompletionHelper.filterMatchingStartsWith(TabCompletionTypeHelper.getBooleansAndNumbers(1, 3), args[1]);
} else if (argumentCount == 3) {
//Signed
try {
Integer.parseInt(args[1]);
return TabCompletionHelper.filterMatchingStartsWith(TabCompletionHelper.getBooleans(), args[2]);
return TabCompletionHelper.filterMatchingStartsWith(TabCompletionTypeHelper.getBooleans(), args[2]);
} catch (NumberFormatException e) {
return new ArrayList<>();
}

View File

@ -2,9 +2,11 @@ package net.knarcraft.bookswithoutborders.command;
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.utility.BookFileHelper;
import net.knarcraft.bookswithoutborders.utility.BookHelper;
import net.knarcraft.bookswithoutborders.utility.BookToFromTextHelper;
import net.knarcraft.bookswithoutborders.utility.FileHelper;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
@ -18,13 +20,11 @@ import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getBookFolder;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getCommandColor;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getErrorColor;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getSlash;
import static net.knarcraft.bookswithoutborders.utility.InputCleaningHelper.cleanString;
import static net.knarcraft.bookswithoutborders.utility.InputCleaningHelper.fixName;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getTitleAuthorSeparator;
/**
* Command executor for the save command
@ -51,7 +51,7 @@ public class CommandSave implements TabExecutor {
}
ItemSlot holdingSlot = InventoryHelper.getHeldSlotBook(player, false, false, false, false);
if (holdingSlot != ItemSlot.NONE) {
if (holdingSlot != null && holdingSlot != ItemSlot.NONE) {
ItemStack holdingItem = InventoryHelper.getHeldItem(player, holdingSlot == ItemSlot.MAIN_HAND);
boolean duplicate = args.length == 1 && Boolean.parseBoolean(args[0]);
saveBook(player, holdingItem, duplicate, savePublic);
@ -77,57 +77,58 @@ public class CommandSave implements TabExecutor {
return;
}
String savePath;
if (saveToPublicFolder) {
savePath = getBookFolder();
} else {
savePath = getBookFolder() + cleanString(player.getName()) + getSlash();
//Only allow saving of own books if enabled
if (BooksWithoutBordersConfig.getAuthorOnlySave() && !saveToPublicFolder &&
(!player.hasPermission("bookswithoutborders.bypassAuthorOnlySave") &&
BookHelper.isNotAuthor(player, book))) {
return;
}
String savePath = BookHelper.getBookDirectoryPathString(
saveToPublicFolder ? BookDirectory.PUBLIC : BookDirectory.PLAYER, player);
//Generate book filename
String fileName;
if (!book.hasTitle()) {
fileName = "Untitled," + player.getName();
} else {
fileName = book.getTitle() + BooksWithoutBordersConfig.getTitleAuthorSeparator() + book.getAuthor();
}
fileName = cleanString(fileName);
fileName = fixName(fileName, false);
String fileName = BookHelper.getBookFile(book, player, saveToPublicFolder);
//Make sure the used folders exist
File file = new File(savePath);
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;
}
File[] foundFiles = file.listFiles();
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;
}
//Find any duplicates of the book
int foundDuplicates = FileHelper.findDuplicates(foundFiles, fileName);
int foundDuplicates = BookFileHelper.findDuplicates(foundFiles, fileName);
//Deal with duplicates
if (foundDuplicates > 0) {
//TODO: Decide if this makes sense or needs to be changed
//Skip duplicate book
if (!fileName.contains("Untitled") && !overwrite) {
if (!fileName.contains("Untitled" + getTitleAuthorSeparator()) && !overwrite) {
BooksWithoutBorders.sendErrorMessage(player, "Book is already saved!");
BooksWithoutBorders.sendErrorMessage(player, "Use " + getCommandColor() + "/savebook true " + getErrorColor() + "to overwrite!");
BooksWithoutBorders.sendErrorMessage(player, "Use " + getCommandColor() + "/savebook true " +
getErrorColor() + "to overwrite!");
return;
}
//Skip if duplicate limit is reached
if (foundDuplicates > BooksWithoutBordersConfig.getBookDuplicateLimit()) {
BooksWithoutBorders.sendErrorMessage(player, "Maximum amount of " + fileName + " duplicates reached!");
BooksWithoutBorders.sendErrorMessage(player, "Use " + getCommandColor() + "/savebook true " + getErrorColor() + "to overwrite!");
BooksWithoutBorders.sendErrorMessage(player, "Maximum amount of " + fileName +
" duplicates reached!");
BooksWithoutBorders.sendErrorMessage(player, "Use " + getCommandColor() + "/savebook true " +
getErrorColor() + "to overwrite!");
return;
}
//Alter duplicated filename
if (fileName.contains("Untitled") && !overwrite) {
if (fileName.contains("Untitled" + getTitleAuthorSeparator()) && !overwrite) {
fileName = "(" + foundDuplicates + ")" + fileName;
}
}
@ -142,14 +143,15 @@ public class CommandSave implements TabExecutor {
//Update the relevant book list
BooksWithoutBorders.updateBooks(player, saveToPublicFolder);
BooksWithoutBorders.sendSuccessMessage(player, "Book Saved as \"" + fileName + "\"");
} catch (IOException e) {
e.printStackTrace();
} catch (IOException exception) {
BooksWithoutBorders.getInstance().getLogger().log(Level.SEVERE, "Unable to save book");
}
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] args) {
return new ArrayList<>();
}

View File

@ -4,7 +4,8 @@ import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.utility.EconomyHelper;
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.command.Command;
import org.bukkit.command.CommandSender;
@ -144,7 +145,7 @@ public class CommandSetBookPrice implements TabExecutor {
if (argumentCount == 1) {
return TabCompletionHelper.filterMatchingStartsWith(paymentTypes, args[0]);
} else if (argumentCount == 2) {
return TabCompletionHelper.filterMatchingStartsWith(TabCompletionHelper.getNumbers(1, 3), args[1]);
return TabCompletionHelper.filterMatchingStartsWith(TabCompletionTypeHelper.getNumbers(1, 3), args[1]);
}
return new ArrayList<>();
}

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[] args) {
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 (args.length < 1) {
BooksWithoutBorders.sendErrorMessage(player, "You must specify the new generation for your book!");
return false;
}
BookMeta.Generation generation;
try {
generation = BookMeta.Generation.valueOf(args[0]);
} 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[] args) {
if (args.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

@ -3,7 +3,8 @@ package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
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.command.Command;
import org.bukkit.command.CommandSender;
@ -44,7 +45,7 @@ public class CommandSetLore implements TabExecutor {
String rawLore = String.join(" ", args);
//Format lore
rawLore = ChatColor.translateAlternateColorCodes('&', rawLore);
rawLore = ColorHelper.translateColorCodes(rawLore, ColorConversion.RGB);
String[] loreParts = rawLore.split(BooksWithoutBordersConfig.getLoreSeparator());
List<String> newLore = new ArrayList<>(Arrays.asList(loreParts));

View File

@ -2,7 +2,8 @@ package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
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.command.Command;
import org.bukkit.command.CommandSender;
@ -22,7 +23,8 @@ import java.util.List;
public class CommandSetTitle implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
if (!(sender instanceof Player player)) {
BooksWithoutBorders.sendErrorMessage(sender, "This command can only be used by a player!");
return false;
@ -40,7 +42,7 @@ public class CommandSetTitle implements TabExecutor {
}
String title = String.join(" ", args);
title = ChatColor.translateAlternateColorCodes('&', title);
title = ColorHelper.translateColorCodes(title, ColorConversion.RGB);
ItemMeta itemMetadata = heldItem.getItemMeta();
if (itemMetadata == null) {
@ -66,7 +68,8 @@ public class CommandSetTitle implements TabExecutor {
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] args) {
List<String> options = new ArrayList<>();
options.add("<new title>");
return options;

View File

@ -1,7 +1,9 @@
package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.state.ItemSlot;
import net.knarcraft.bookswithoutborders.utility.BookHelper;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
import org.bukkit.Material;
import org.bukkit.command.Command;
@ -10,10 +12,13 @@ import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
import org.bukkit.inventory.meta.WritableBookMeta;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.logging.Level;
/**
* Command executor for the unsign command
@ -46,12 +51,36 @@ public class CommandUnSign implements TabExecutor {
*/
public void unSignHeldBook(Player player, boolean mainHand) {
//Get the old book
BookMeta oldBook = InventoryHelper.getHeldBookMetadata(player, mainHand);
BookMeta oldMetadata = InventoryHelper.getHeldBookMetadata(player, mainHand);
ItemStack heldBook = InventoryHelper.getHeldBook(player, mainHand);
//UnSign the book
ItemStack newBook = new ItemStack(Material.WRITABLE_BOOK);
newBook.setItemMeta(oldBook);
//Only allow the owner to un-sign the book
if (BooksWithoutBordersConfig.getAuthorOnlyUnsign() && !player.hasPermission("bookswithoutborders.bypassAuthorOnlyUnsign")) {
if (BookHelper.isNotAuthor(player, Objects.requireNonNull(oldMetadata))) {
return;
}
}
WritableBookMeta newMetadata = (BookMeta) BooksWithoutBorders.getItemFactory().getItemMeta(Material.WRITABLE_BOOK);
if (newMetadata == null) {
BooksWithoutBorders.getInstance().getLogger().log(Level.SEVERE, "Unable to create writable book metadata");
return;
}
//Create a new unsigned book with the same data
ItemStack newBook = new ItemStack(Material.WRITABLE_BOOK, heldBook.getAmount());
if (oldMetadata.hasLore()) {
List<String> oldLore = oldMetadata.getLore();
if (oldLore != null) {
List<String> newLore = new ArrayList<>(oldLore);
newLore.remove(0);
newMetadata.setLore(newLore);
}
}
newMetadata.setPages(oldMetadata.getPages());
newBook.setItemMeta(newMetadata);
// Give the player the new book
InventoryHelper.replaceHeldItem(player, newBook, mainHand);
}

View File

@ -2,11 +2,12 @@ package net.knarcraft.bookswithoutborders.config;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.utility.EconomyHelper;
import org.bukkit.ChatColor;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.Material;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.configuration.Configuration;
import java.nio.file.FileSystems;
import java.util.ArrayList;
import java.util.List;
@ -22,7 +23,7 @@ public class BooksWithoutBordersConfig {
private static final ChatColor errorColor = ChatColor.RED;
private static final ChatColor successColor = ChatColor.GREEN;
private static final ChatColor commandColor = ChatColor.YELLOW;
private static final String SLASH = System.getProperty("file.separator");
private static final String SLASH = FileSystems.getDefault().getSeparator();
private static boolean isInitialized;
public static String bookFolder;
@ -34,9 +35,13 @@ public class BooksWithoutBordersConfig {
private static Material bookPriceType = null;
private static double bookPriceQuantity;
private static boolean authorOnlyCopy;
private static boolean authorOnlyUnsign;
private static boolean authorOnlySave;
private static boolean useYml;
private static boolean adminDecrypt;
private static boolean formatBooks;
private static boolean changeGenerationOnCopy;
private static boolean enableBookshelfPeeking;
/**
* Initializes the books without borders settings class
@ -106,6 +111,33 @@ public class BooksWithoutBordersConfig {
return 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 static boolean getAuthorOnlyUnsign() {
return 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 static boolean getAuthorOnlySave() {
return 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 static boolean getEnableBookshelfPeeking() {
return enableBookshelfPeeking;
}
/**
* Gets whether to use YML, not TXT, for saving books
*
@ -184,6 +216,15 @@ public class BooksWithoutBordersConfig {
return bookDuplicateLimit;
}
/**
* Gets whether books should change their generation during copy
*
* @return <p>True if books should change their generation</p>
*/
public static boolean changeGenerationOnCopy() {
return changeGenerationOnCopy;
}
/**
* Gets the separator used to split book title from book author
*
@ -242,6 +283,7 @@ public class BooksWithoutBordersConfig {
config.set(ConfigOption.BOOKS_FOR_NEW_PLAYERS.getConfigNode(), firstBooks);
config.set(ConfigOption.MESSAGE_FOR_NEW_PLAYERS.getConfigNode(), welcomeMessage);
config.set(ConfigOption.FORMAT_AFTER_SIGNING.getConfigNode(), formatBooks);
config.set(ConfigOption.ENABLE_BOOKSHELF_PEEKING.getConfigNode(), enableBookshelfPeeking);
String itemTypeNode = ConfigOption.PRICE_ITEM_TYPE.getConfigNode();
if (bookPriceType != null) {
@ -257,10 +299,14 @@ public class BooksWithoutBordersConfig {
config.set(ConfigOption.PRICE_QUANTITY.getConfigNode(), bookPriceQuantity);
config.set(ConfigOption.ADMIN_AUTO_DECRYPT.getConfigNode(), adminDecrypt);
config.set(ConfigOption.AUTHOR_ONLY_COPY.getConfigNode(), authorOnlyCopy);
config.set(ConfigOption.AUTHOR_ONLY_UNSIGN.getConfigNode(), authorOnlyUnsign);
config.set(ConfigOption.AUTHOR_ONLY_SAVE.getConfigNode(), authorOnlySave);
config.set(ConfigOption.CHANGE_GENERATION_ON_COPY.getConfigNode(), changeGenerationOnCopy);
//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, "[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")) {
@ -284,32 +330,29 @@ public class BooksWithoutBordersConfig {
BooksWithoutBorders.getInstance().reloadConfig();
Configuration config = BooksWithoutBorders.getInstance().getConfig();
try {
useYml = config.getBoolean(ConfigOption.USE_YAML.getConfigNode(),
(Boolean) ConfigOption.USE_YAML.getDefaultValue());
useYml = getBoolean(config, ConfigOption.USE_YAML);
bookDuplicateLimit = config.getInt(ConfigOption.MAX_DUPLICATES.getConfigNode(),
(Integer) ConfigOption.MAX_DUPLICATES.getDefaultValue());
titleAuthorSeparator = config.getString(ConfigOption.TITLE_AUTHOR_SEPARATOR.getConfigNode(),
(String) ConfigOption.TITLE_AUTHOR_SEPARATOR.getDefaultValue());
loreSeparator = config.getString(ConfigOption.LORE_LINE_SEPARATOR.getConfigNode(),
(String) ConfigOption.LORE_LINE_SEPARATOR.getDefaultValue());
adminDecrypt = config.getBoolean(ConfigOption.ADMIN_AUTO_DECRYPT.getConfigNode(),
(Boolean) ConfigOption.ADMIN_AUTO_DECRYPT.getDefaultValue());
authorOnlyCopy = config.getBoolean(ConfigOption.AUTHOR_ONLY_COPY.getConfigNode(),
(Boolean) ConfigOption.AUTHOR_ONLY_COPY.getDefaultValue());
titleAuthorSeparator = getString(config, ConfigOption.TITLE_AUTHOR_SEPARATOR);
loreSeparator = getString(config, ConfigOption.LORE_LINE_SEPARATOR);
adminDecrypt = getBoolean(config, ConfigOption.ADMIN_AUTO_DECRYPT);
authorOnlyCopy = getBoolean(config, ConfigOption.AUTHOR_ONLY_COPY);
authorOnlyUnsign = getBoolean(config, ConfigOption.AUTHOR_ONLY_UNSIGN);
authorOnlySave = getBoolean(config, ConfigOption.AUTHOR_ONLY_SAVE);
firstBooks = config.getStringList(ConfigOption.BOOKS_FOR_NEW_PLAYERS.getConfigNode());
welcomeMessage = config.getString(ConfigOption.MESSAGE_FOR_NEW_PLAYERS.getConfigNode(),
(String) ConfigOption.MESSAGE_FOR_NEW_PLAYERS.getDefaultValue());
formatBooks = config.getBoolean(ConfigOption.FORMAT_AFTER_SIGNING.getConfigNode(),
(Boolean) ConfigOption.FORMAT_AFTER_SIGNING.getDefaultValue());
welcomeMessage = getString(config, ConfigOption.MESSAGE_FOR_NEW_PLAYERS);
formatBooks = getBoolean(config, ConfigOption.FORMAT_AFTER_SIGNING);
changeGenerationOnCopy = getBoolean(config, ConfigOption.CHANGE_GENERATION_ON_COPY);
enableBookshelfPeeking = getBoolean(config, ConfigOption.ENABLE_BOOKSHELF_PEEKING);
//Convert string into material
String paymentMaterial = config.getString(ConfigOption.PRICE_ITEM_TYPE.getConfigNode(),
(String) ConfigOption.PRICE_ITEM_TYPE.getDefaultValue());
String paymentMaterial = getString(config, ConfigOption.PRICE_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!");
sendErrorMessage(consoleSender,
"BooksWithoutBorders failed to hook into Vault! Book price not set!");
bookPriceType = null;
}
} else if (!paymentMaterial.trim().isEmpty()) {
@ -338,4 +381,26 @@ public class BooksWithoutBordersConfig {
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(Configuration config, 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(Configuration config, ConfigOption configOption) {
return config.getBoolean(configOption.getConfigNode(), (Boolean) configOption.getDefaultValue());
}
}

View File

@ -55,10 +55,31 @@ public enum ConfigOption {
*/
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);
FORMAT_AFTER_SIGNING("Options.Format_Book_After_Signing", false),
/**
* Whether hitting a bookshelf should display information about the contained books
*/
ENABLE_BOOKSHELF_PEEKING("Options.Enable_Book_Peeking", false),
;
private final String configNode;
private final Object defaultValue;

View File

@ -1,5 +1,7 @@
package net.knarcraft.bookswithoutborders.encryption;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
@ -15,6 +17,7 @@ import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.util.Base64;
import java.util.logging.Level;
/**
* This class represents and AES encryptor/decryptor
@ -66,16 +69,16 @@ public class AES {
//Initialize cipher
try {
aes.init(mode, secretKeySpec, ivParameterSpec);
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
e.printStackTrace();
} catch (InvalidKeyException | InvalidAlgorithmParameterException exception) {
BooksWithoutBorders.getInstance().getLogger().log(Level.SEVERE, "Invalid AES input given!");
return null;
}
//Perform encryption/decryption and output result
try {
byte[] output = aes.doFinal(getInputBytes(input, encrypt));
return createResult(output, encrypt);
} catch (IllegalBlockSizeException | BadPaddingException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException | BadPaddingException exception) {
BooksWithoutBorders.getInstance().getLogger().log(Level.SEVERE, "Invalid AES block size or padding");
return null;
}
}
@ -131,8 +134,8 @@ public class AES {
Cipher aes;
try {
aes = Cipher.getInstance("AES/CBC/PKCS5Padding");
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException | NoSuchPaddingException exception) {
BooksWithoutBorders.getInstance().getLogger().log(Level.SEVERE, "Invalid AES algorithm or padding");
return null;
}
return aes;
@ -149,15 +152,15 @@ public class AES {
SecretKeyFactory keyFactory;
try {
keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException exception) {
BooksWithoutBorders.getInstance().getLogger().log(Level.SEVERE, "Invalid AES algorithm");
return null;
}
SecretKey tmp;
try {
tmp = keyFactory.generateSecret(spec);
} catch (InvalidKeySpecException e) {
e.printStackTrace();
} catch (InvalidKeySpecException exception) {
BooksWithoutBorders.getInstance().getLogger().log(Level.SEVERE, "Invalid AES key specification");
return null;
}
return new SecretKeySpec(tmp.getEncoded(), "AES");

View File

@ -33,7 +33,7 @@ public class GenenCrypt {
for (int j = 0; j < 4; j++) {
for (int k = 0; k < 4; k++) {
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]);
}
}
}
@ -49,7 +49,7 @@ public class GenenCrypt {
// use the random number generator and the originalCodonList to make a shuffled list
ArrayList<String> shuffledCodonList = new ArrayList<>();
while (originalCodonList.size() > 0) {
while (!originalCodonList.isEmpty()) {
int index = ranGen.nextInt(originalCodonList.size());
shuffledCodonList.add(originalCodonList.get(index));
originalCodonList.remove(index);
@ -97,9 +97,12 @@ public class GenenCrypt {
String s = charList[i];
String[] sa = codonTable.get(s);
switch (s) {
case "\t" -> System.out.println(i + "\t" + "\\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]);
case "\t" ->
System.out.println(i + "\t" + "\\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]);
}
}

View File

@ -19,7 +19,7 @@ public class SubstitutionCipher {
// original message is offset by
public String encrypt(String in, String key) {
StringBuilder output = new StringBuilder();
if (key != null && key.length() > 0) {
if (key != null && !key.isEmpty()) {
StringTokenizer tokenizer = new StringTokenizer(key, ", "); // tokenizes the key
// converts each number in the key to an integer and adds to an array
int[] offsetArray = new int[tokenizer.countTokens()];
@ -52,9 +52,10 @@ public class SubstitutionCipher {
// but in reverse. Could probably be combined into one
// method with a flag for encryption / decryption, but
// I'm lazy.
@SuppressWarnings("unused")
public String decrypt(String in, String key) {
StringBuilder output = new StringBuilder();
if (key != null && key.length() > 0) {
if (key != null && !key.isEmpty()) {
StringTokenizer tokenizer = new StringTokenizer(key, ", "); // tokenizes the key
// converts each number in the key to an integer and adds to an array
int[] offsetArray = new int[tokenizer.countTokens()];
@ -79,6 +80,7 @@ public class SubstitutionCipher {
// the one time pad encryption is very secure, and
// encryption works just like decryption, but is
// vulnerable if the same key is used more than once.
@SuppressWarnings("unused")
public String oneTimePad(String in, String key) {
StringBuilder output = new StringBuilder();
for (int i = 0; i < in.length(); i++) {

View File

@ -0,0 +1,160 @@
package net.knarcraft.bookswithoutborders.listener;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.utility.IntegerToRomanConverter;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.block.ChiseledBookshelf;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.ChiseledBookshelfInventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
import org.bukkit.inventory.meta.EnchantmentStorageMeta;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getSuccessColor;
/**
* A listener for bookshelf clicking
*/
public class BookshelfListener implements Listener {
@EventHandler
public void onBookshelfClick(PlayerInteractEvent event) {
Player player = event.getPlayer();
// If left-clicking a chiseled bookshelf and 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 (!BooksWithoutBordersConfig.getEnableBookshelfPeeking() ||
!event.getPlayer().hasPermission("bookswithoutborders.peekbookshelf")) {
return;
}
event.setUseInteractedBlock(Event.Result.DENY);
event.setUseItemInHand(Event.Result.DENY);
ChiseledBookshelfInventory bookshelfInventory = chiseledBookshelf.getInventory();
player.sendMessage(getBookshelfDescription(bookshelfInventory));
}
/**
* Gets the description for a bookshelf's contents
*
* @param bookshelfInventory <p>The inventory of the bookshelf to describe</p>
* @return <p>A textual description of the bookshelf's contents</p>
*/
private String getBookshelfDescription(ChiseledBookshelfInventory bookshelfInventory) {
StringBuilder builder = new StringBuilder(getSuccessColor() + "Books in shelf:");
for (ItemStack itemStack : bookshelfInventory.getStorageContents()) {
if (itemStack == null) {
continue;
}
ItemMeta meta = itemStack.getItemMeta();
builder.append("\n ").append(ChatColor.GRAY).append(" - ");
if (meta instanceof BookMeta bookMeta) {
builder.append(getBookDescription(bookMeta));
} else if (meta instanceof EnchantmentStorageMeta enchantmentStorageMeta) {
builder.append(getEnchantedBookDescription(enchantmentStorageMeta));
} else if (meta != null) {
builder.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>
*/
private String getPlainBookDescription(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>
*/
private String getBookDescription(BookMeta bookMeta) {
String title;
String author;
if (!bookMeta.hasTitle() || bookMeta.getTitle() == null) {
title = "Untitled";
} else {
title = bookMeta.getTitle();
}
if (!bookMeta.hasAuthor() || bookMeta.getAuthor() == null) {
author = "Unknown";
} else {
author = bookMeta.getAuthor();
}
return title + " 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>
*/
private String getEnchantedBookDescription(EnchantmentStorageMeta enchantmentStorageMeta) {
StringBuilder builder = new StringBuilder();
builder.append("Enchanted book (");
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));
builder.append(")");
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>
*/
private String getEnchantmentName(Enchantment enchantment) {
return uppercaseFirst(enchantment.getKey().getKey().replace("_", " "));
}
/**
* Turns the first character of each of a string's words into uppercase
*
* @param input <p>The input to uppercase</p>
* @return <p>The input string with more uppercase</p>
*/
private String uppercaseFirst(String input) {
String[] parts = input.split(" ");
for (int i = 0; i < parts.length; i++) {
parts[i] = parts[i].substring(0, 1).toUpperCase() + parts[i].substring(1);
}
return String.join(" ", parts);
}
}

View File

@ -2,6 +2,8 @@ package net.knarcraft.bookswithoutborders.listener;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.state.BookDirectory;
import net.knarcraft.bookswithoutborders.utility.BookHelper;
import net.knarcraft.bookswithoutborders.utility.BookLoader;
import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper;
import net.knarcraft.bookswithoutborders.utility.InventoryHelper;
@ -17,16 +19,13 @@ import org.bukkit.inventory.meta.BookMeta;
import org.bukkit.inventory.meta.ItemMeta;
import java.io.File;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getBookFolder;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getSlash;
import java.util.logging.Level;
/**
* A listener for listening to player-related events such as joining or holding a book
*/
public class PlayerEventListener implements Listener {
private final String slash = getSlash();
private final BooksWithoutBorders booksWithoutBorders = BooksWithoutBorders.getInstance();
@EventHandler
@ -58,6 +57,16 @@ public class PlayerEventListener implements Listener {
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
//If a book directory exists with this player's name, move it to this player's UUID
String bookFolder = BooksWithoutBordersConfig.getBookFolder();
File file = new File(bookFolder + InputCleaningHelper.cleanString(player.getName()));
if (file.exists()) {
if (!file.renameTo(new File(bookFolder + player.getUniqueId()))) {
BooksWithoutBorders.getInstance().getLogger().log(Level.WARNING, "Unable to migrate player book " +
"directory for player " + player.getName());
}
}
//Handle new players
if (!player.hasPlayedBefore()) {
boolean sendMessage = true;
@ -151,13 +160,14 @@ public class PlayerEventListener implements Listener {
fileName = oldBook.getTitle() + BooksWithoutBordersConfig.getTitleAuthorSeparator() + oldBook.getAuthor();
}
String cleanPlayerName = InputCleaningHelper.cleanString(player.getName());
String playerFolderPath = BookHelper.getBookDirectoryPathString(BookDirectory.PLAYER, player);
String publicFolderPath = BookHelper.getBookDirectoryPathString(BookDirectory.PUBLIC, player);
String[] possiblePaths = new String[]{
getBookFolder() + fileName + ".yml",
getBookFolder() + fileName + ".txt",
getBookFolder() + cleanPlayerName + slash + fileName + ".yml",
getBookFolder() + cleanPlayerName + slash + fileName + ".txt"
publicFolderPath + fileName + ".yml",
publicFolderPath + fileName + ".txt",
playerFolderPath + fileName + ".yml",
playerFolderPath + fileName + ".txt"
};
for (String path : possiblePaths) {

View File

@ -3,15 +3,18 @@ package net.knarcraft.bookswithoutborders.listener;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.state.EncryptionStyle;
import net.knarcraft.bookswithoutborders.utility.BookFileHelper;
import net.knarcraft.bookswithoutborders.utility.BookLoader;
import net.knarcraft.bookswithoutborders.utility.EncryptionHelper;
import net.knarcraft.bookswithoutborders.utility.FileHelper;
import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper;
import org.bukkit.ChatColor;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.Material;
import org.bukkit.Tag;
import org.bukkit.block.Sign;
import org.bukkit.block.sign.Side;
import org.bukkit.block.sign.SignSide;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
@ -26,7 +29,7 @@ import java.io.File;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getBookFolder;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getSlash;
import static net.knarcraft.bookswithoutborders.utility.FileHelper.isBookListIndex;
import static net.knarcraft.bookswithoutborders.utility.BookFileHelper.isBookListIndex;
/**
* A listener for relevant sign events such as clicking a decryption sign
@ -54,7 +57,7 @@ public class SignEventListener implements Listener {
//Check if the sign is of a valid type
if (!((lines[1].equalsIgnoreCase("[Encrypt]") || lines[1].equalsIgnoreCase("[Decrypt]") ||
lines[1].equalsIgnoreCase("[Give]")) && lines[2].trim().length() > 0)) {
lines[1].equalsIgnoreCase("[Give]")) && !lines[2].trim().isEmpty())) {
//Mark the second line as invalid
event.setLine(1, ChatColor.DARK_RED + lines[1]);
return;
@ -89,24 +92,32 @@ public class SignEventListener implements Listener {
return;
}
ItemStack heldItem = playerInventory.getItem(hand);
if (heldItem == null) {
return;
}
Material heldItemType = heldItem.getType();
if (event.getAction() == Action.RIGHT_CLICK_BLOCK && (Tag.SIGNS.isTagged(clickedBlockType) ||
Tag.WALL_SIGNS.isTagged(clickedBlockType))) {
event.setUseItemInHand(Event.Result.DENY);
//The player right-clicked a sign
Sign sign = (Sign) event.getClickedBlock().getState();
if (signLineEquals(sign, 0, "[BwB]", ChatColor.DARK_GREEN)) {
if (signLineEquals(sign, 1, "[Encrypt]", ChatColor.DARK_BLUE)) {
encryptHeldBookUsingSign(sign, heldItemType, player, hand);
} else if (signLineEquals(sign, 1, "[Decrypt]", ChatColor.DARK_BLUE)) {
decryptHeldBookUsingSign(sign, heldItemType, player, hand);
} else if (signLineEquals(sign, 1, "[Give]", ChatColor.DARK_BLUE) &&
getSignLine2Color(sign) == ChatColor.DARK_GREEN) {
giveBook(sign, player);
} else {
player.sendMessage("Sign command " + sign.getLine(1) + " " + sign.getLine(2) + " invalid");
player.sendMessage(String.valueOf(getSignLine2Color(sign)));
}
if (!signLineEquals(sign, 0, "[BwB]", ChatColor.DARK_GREEN)) {
return;
}
if (signLineEquals(sign, 1, "[Encrypt]", ChatColor.DARK_BLUE)) {
encryptHeldBookUsingSign(sign, heldItemType, player, hand);
} else if (signLineEquals(sign, 1, "[Decrypt]", ChatColor.DARK_BLUE)) {
decryptHeldBookUsingSign(sign, heldItemType, player, hand);
} else if (signLineEquals(sign, 1, "[Give]", ChatColor.DARK_BLUE) &&
getSignLine2Color(sign) == ChatColor.DARK_GREEN) {
giveBook(sign, player);
} else {
SignSide front = sign.getSide(Side.FRONT);
player.sendMessage(String.format("Sign command %s %s is invalid", front.getLine(1),
front.getLine(2)));
player.sendMessage(String.valueOf(getSignLine2Color(sign)));
}
} else if (heldItemType == Material.WRITTEN_BOOK && (event.getAction() == Action.LEFT_CLICK_AIR
|| event.getAction() == Action.LEFT_CLICK_BLOCK)) {
@ -133,7 +144,7 @@ public class SignEventListener implements Listener {
player.closeInventory();
//Converts user supplied key into integer form
String lineText = ChatColor.stripColor(sign.getLine(2));
String lineText = ChatColor.stripColor(sign.getSide(Side.FRONT).getLine(2));
String key = EncryptionHelper.getNumberKeyFromStringKey(lineText);
ItemStack book = EncryptionHelper.loadEncryptedBook(player, key, false);
@ -151,9 +162,9 @@ public class SignEventListener implements Listener {
* @return <p>The color of the sign</p>
*/
private ChatColor getSignLine2Color(Sign sign) {
String line = sign.getLine(2);
String line = sign.getSide(Side.FRONT).getLine(2);
if (!ChatColor.stripColor(line).equals(line)) {
return ChatColor.getByChar(sign.getLine(2).substring(1, 2));
return ChatColor.getByChar(sign.getSide(Side.FRONT).getLine(2).substring(1, 2).charAt(0));
} else {
return null;
}
@ -169,7 +180,7 @@ public class SignEventListener implements Listener {
* @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) {
String line = sign.getLine(lineNumber);
String line = sign.getSide(Side.FRONT).getLine(lineNumber);
return line.equalsIgnoreCase(color + compareTo);
}
@ -182,14 +193,15 @@ public class SignEventListener implements Listener {
*/
private void generateGiveSign(SignChangeEvent event, String[] lines, Player player) {
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);
return;
}
//Tests if a full file name has been supplied and points to an actual file
String signFile = getBookFolder() + lines[2] + lines[3];
if (FileHelper.bookFileExists(signFile)) {
if (BookFileHelper.bookFileExists(signFile)) {
markGiveSignValidity(event, true);
return;
} else {
@ -262,7 +274,7 @@ public class SignEventListener implements Listener {
player.getInventory().setItem(hand, newBook);
player.closeInventory();
player.sendMessage(ChatColor.GREEN + "Book auto-decrypted!");
BooksWithoutBorders.sendSuccessMessage(player, "Book auto-decrypted!");
}
/**
@ -275,7 +287,7 @@ public class SignEventListener implements Listener {
*/
private void encryptHeldBookUsingSign(Sign sign, Material heldItemType, Player player, EquipmentSlot hand) {
ItemStack eBook;
String[] lines = sign.getLines();
String[] lines = sign.getSide(Side.FRONT).getLines();
boolean mainHand = hand == EquipmentSlot.HAND;
if (heldItemType == Material.WRITTEN_BOOK) {
player.closeInventory();
@ -294,7 +306,7 @@ public class SignEventListener implements Listener {
* @param player <p>The player which clicked the sign</p>
*/
private void giveBook(Sign sign, Player player) {
String fileName = ChatColor.stripColor(sign.getLine(2));
String fileName = ChatColor.stripColor(sign.getSide(Side.FRONT).getLine(2));
boolean isLoadListNumber = false;
try {
@ -304,7 +316,7 @@ public class SignEventListener implements Listener {
}
//Add the third line to the second line for the full filename
String thirdLine = sign.getLine(3);
String thirdLine = sign.getSide(Side.FRONT).getLine(3);
if (!isLoadListNumber && thirdLine.length() >= 2) {
fileName += ChatColor.stripColor(thirdLine);
}
@ -313,9 +325,9 @@ public class SignEventListener implements Listener {
if (newBook != null) {
player.getInventory().addItem(newBook);
player.sendMessage(ChatColor.GREEN + "Received book!");
BooksWithoutBorders.sendSuccessMessage(player, "Received book!");
} else {
player.sendMessage(ChatColor.RED + "Book failed to load!");
BooksWithoutBorders.sendErrorMessage(player, "Book failed to load!");
}
}

View File

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

View File

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

View File

@ -1,29 +1,25 @@
package net.knarcraft.bookswithoutborders.utility;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import org.bukkit.ChatColor;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.state.BookDirectory;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.command.CommandSender;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getBookFolder;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getSlash;
import static net.knarcraft.bookswithoutborders.utility.InputCleaningHelper.cleanString;
/**
* Helper class for dealing with files
*/
public final class FileHelper {
public final class BookFileHelper {
private FileHelper() {
private BookFileHelper() {
}
/**
@ -98,13 +94,11 @@ public final class FileHelper {
* @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());
File file = BookHelper.getBookDirectoryPath(listPublic ? BookDirectory.PUBLIC : BookDirectory.PLAYER, sender);
if (file == null) {
return new ArrayList<>();
}
return FileHelper.listFiles(sender, file);
return BookFileHelper.listFiles(sender, file);
}
/**
@ -115,7 +109,7 @@ public final class FileHelper {
*/
public static void printBooks(CommandSender sender, boolean listPublic) {
List<String> availableBooks = BooksWithoutBorders.getAvailableBooks(sender, listPublic);
FileHelper.printFiles(sender, availableBooks);
BookFileHelper.printFiles(sender, availableBooks);
}
/**
@ -152,9 +146,20 @@ public final class FileHelper {
}
for (File foundFile : existingFiles) {
if (foundFile.isFile()) {
fileList.add(foundFile.getName());
if (!foundFile.isFile()) {
continue;
}
String fileName = foundFile.getName();
String separator = BooksWithoutBordersConfig.getTitleAuthorSeparator();
if (fileName.contains(separator)) {
//Convert the UUID into a username if necessary
String[] data = fileName.split(separator);
String extension = data[1].substring(data[1].length() - 4);
String userName = data[1].substring(0, data[1].length() - 4);
data[1] = BookHelper.authorFromUUID(userName) + extension;
fileList.add(String.join(separator, data));
}
fileList.add(fileName);
}
return fileList;
@ -177,15 +182,4 @@ public final class FileHelper {
return foundDuplicates;
}
/**
* Gets a buffered reader given an input stream
*
* @param inputStream <p>The input stream to read</p>
* @return <p>A buffered reader reading the input stream</p>
*/
public static BufferedReader getBufferedReaderFromInputStream(InputStream inputStream) {
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
return new BufferedReader(inputStreamReader);
}
}

View File

@ -1,13 +1,12 @@
package net.knarcraft.bookswithoutborders.utility;
import net.md_5.bungee.api.ChatColor;
import net.knarcraft.knarlib.property.ColorConversion;
import net.knarcraft.knarlib.util.ColorHelper;
import org.bukkit.inventory.meta.BookMeta;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A class for formatting text to fit books
@ -109,26 +108,10 @@ public final class BookFormatter {
public static BookMeta formatPages(BookMeta bookMeta) {
List<String> formattedPages = new ArrayList<>(Objects.requireNonNull(bookMeta).getPageCount());
for (String page : bookMeta.getPages()) {
formattedPages.add(BookFormatter.translateAllColorCodes(page));
formattedPages.add(ColorHelper.translateColorCodes(page, ColorConversion.RGB));
}
bookMeta.setPages(formattedPages);
return bookMeta;
}
/**
* Translates all found color codes to formatting in a string
*
* @param message <p>The string to search for color codes</p>
* @return <p>The message with color codes translated</p>
*/
private static String translateAllColorCodes(String message) {
message = ChatColor.translateAlternateColorCodes('&', message);
Pattern pattern = Pattern.compile("(#[a-fA-F0-9]{6})");
Matcher matcher = pattern.matcher(message);
while (matcher.find()) {
message = message.replace(matcher.group(), "" + ChatColor.of(matcher.group()));
}
return message;
}
}

View File

@ -0,0 +1,170 @@
package net.knarcraft.bookswithoutborders.utility;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.state.BookDirectory;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
import java.io.File;
import java.util.UUID;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getSlash;
import static net.knarcraft.bookswithoutborders.utility.InputCleaningHelper.cleanString;
import static net.knarcraft.bookswithoutborders.utility.InputCleaningHelper.fixName;
/**
* Helper class for getting abstract book information
*/
public final class BookHelper {
private BookHelper() {
}
/**
* Converts the author of a book from UUID if necessary
*
* @param author <p>The author string</p>
* @return <p>The author string, converted if it was a UUID</p>
*/
public static String authorFromUUID(String author) {
try {
UUID authorID = UUID.fromString(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>
*/
public static File getBookDirectoryPath(BookDirectory bookDirectory, 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>
*/
public static String getBookDirectoryPathString(BookDirectory bookDirectory, CommandSender sender) {
String folder = null;
String bookFolder = BooksWithoutBordersConfig.getBookFolder();
if (bookDirectory == BookDirectory.PUBLIC) {
folder = bookFolder;
} else if (bookDirectory == BookDirectory.PLAYER && sender instanceof Player player) {
folder = bookFolder + player.getUniqueId() + 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(ItemStack bookItem) {
BookMeta bookMeta = (BookMeta) bookItem.getItemMeta();
if (BooksWithoutBordersConfig.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>
*/
public static BookMeta.Generation getNextGeneration(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>
*/
public static String getBookFile(BookMeta book, Player player, boolean isPublic) {
String titleAuthorSeparator = BooksWithoutBordersConfig.getTitleAuthorSeparator();
String bookName;
if (book.hasTitle()) {
bookName = book.getTitle();
} else {
bookName = "Untitled";
}
String authorName;
if ((!book.hasAuthor() || isAuthor(player.getName(), book.getAuthor())) && !isPublic) {
//Store as unique id to account for name changes
authorName = player.getUniqueId().toString();
} else if (!book.hasAuthor()) {
authorName = player.getName();
} else {
authorName = book.getAuthor();
}
return fixName(cleanString(bookName + titleAuthorSeparator + authorName), false);
}
/**
* 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(Player player, 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(String playerName, String author) {
playerName = InputCleaningHelper.cleanString(playerName);
return author != null && playerName.equalsIgnoreCase(InputCleaningHelper.cleanString(author));
}
}

View File

@ -3,6 +3,7 @@ package net.knarcraft.bookswithoutborders.utility;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.state.BookDirectory;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
@ -60,11 +61,27 @@ public final class BookLoader {
//Get the full path of the book to load
File file = getFullPath(sender, fileName, bookDirectory, directory);
if (file == null) {
return null;
//Try converting the username to UUID
String titleAuthorSeparator = BooksWithoutBordersConfig.getTitleAuthorSeparator();
String[] data = fileName.split(titleAuthorSeparator);
String extension = data[1].substring(data[1].length() - 4);
String userName = data[1].substring(0, data[1].length() - 4);
Player player = Bukkit.getPlayer(userName);
if (player != null) {
data[1] = player.getUniqueId() + extension;
file = getFullPath(sender, String.join(titleAuthorSeparator, data), bookDirectory, directory);
if (file == null) {
BooksWithoutBorders.sendErrorMessage(sender, "Incorrect file name!");
return null;
}
} else {
return null;
}
}
//Make sure the player can pay for the book
if (BooksWithoutBordersConfig.booksHavePrice() && !sender.hasPermission("bookswithoutborders.bypassBookPrice") &&
if (BooksWithoutBordersConfig.booksHavePrice() &&
!sender.hasPermission("bookswithoutborders.bypassBookPrice") &&
(bookDirectory == BookDirectory.PUBLIC || bookDirectory == BookDirectory.PLAYER) &&
EconomyHelper.cannotPayForBookPrinting((Player) sender, numCopies)) {
return null;
@ -98,6 +115,8 @@ public final class BookLoader {
//Set the metadata and amount to the new book
book.setItemMeta(bookMetadata);
//Increase book generation if enabled
BookHelper.increaseGeneration(book);
book.setAmount(numCopies);
return book;
@ -113,18 +132,15 @@ public final class BookLoader {
* @return <p>A file or null if it does not exist</p>
*/
private static File getFullPath(CommandSender sender, String fileName, BookDirectory bookDirectory, String directory) {
File file = null;
File file;
String slash = BooksWithoutBordersConfig.getSlash();
String bookFolder = BooksWithoutBordersConfig.getBookFolder();
if (bookDirectory == BookDirectory.PUBLIC) {
file = FileHelper.getBookFile(bookFolder + fileName);
} else if (bookDirectory == BookDirectory.PLAYER) {
file = FileHelper.getBookFile(bookFolder + InputCleaningHelper.cleanString(sender.getName()) + slash + fileName);
} else if (bookDirectory == BookDirectory.ENCRYPTED) {
file = FileHelper.getBookFile(bookFolder + "Encrypted" + slash + directory + slash + fileName);
if (bookDirectory == BookDirectory.ENCRYPTED) {
file = BookFileHelper.getBookFile(bookFolder + "Encrypted" + slash + directory + slash + fileName);
} else {
file = BookFileHelper.getBookFile(BookHelper.getBookDirectoryPathString(bookDirectory, sender) + fileName);
}
if (file == null || !file.isFile()) {
BooksWithoutBorders.sendErrorMessage(sender, "Incorrect file name!");
return null;
} else {
return file;

View File

@ -1,19 +1,24 @@
package net.knarcraft.bookswithoutborders.utility;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.knarlib.util.FileHelper;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.inventory.meta.BookMeta;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import static net.knarcraft.bookswithoutborders.utility.BookHelper.authorFromUUID;
import static net.knarcraft.bookswithoutborders.utility.InputCleaningHelper.fixName;
/**
@ -41,6 +46,11 @@ public final class BookToFromTextHelper {
if (bookMetadata.hasAuthor()) {
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()) {
bookYml.set("Pages", bookMetadata.getPages());
}
@ -77,12 +87,18 @@ public final class BookToFromTextHelper {
* @throws IOException <p>If unable to save the book</p>
*/
public static void bookToTXT(String folderPath, String fileName, BookMeta bookMetadata) throws IOException {
FileWriter fileWriter = new FileWriter(folderPath + fileName + ".txt");
FileWriter fileWriter = new FileWriter(folderPath + fileName + ".txt", StandardCharsets.UTF_8);
PrintWriter printWriter = new PrintWriter(fileWriter);
List<String> pages = bookMetadata.getPages();
BookMeta.Generation generation = bookMetadata.getGeneration();
if (generation == null) {
generation = BookMeta.Generation.ORIGINAL;
}
String generationString = ":" + generation.name();
//Save each page of the book as a text line
printWriter.println("[Book]");
printWriter.println("[Book]" + generationString);
for (String page : pages) {
printWriter.println(page);
}
@ -100,8 +116,9 @@ public final class BookToFromTextHelper {
try {
FileConfiguration bookYml = YamlConfiguration.loadConfiguration(file);
bookMetadata.setGeneration(BookMeta.Generation.valueOf(bookYml.getString("Generation", "ORIGINAL")));
bookMetadata.setTitle(bookYml.getString("Title", "Untitled"));
bookMetadata.setAuthor(bookYml.getString("Author", "Unknown"));
bookMetadata.setAuthor(authorFromUUID(bookYml.getString("Author", "Unknown")));
bookMetadata.setPages(bookYml.getStringList("Pages"));
bookMetadata.setLore(bookYml.getStringList("Lore"));
} catch (IllegalArgumentException e) {
@ -123,13 +140,16 @@ public final class BookToFromTextHelper {
String title;
String titleAuthorSeparator = BooksWithoutBordersConfig.getTitleAuthorSeparator();
//Remove .txt extension
fileName = fileName.substring(0, fileName.length() - 4);
//Get title and author from the file name
if (fileName.contains(titleAuthorSeparator)) {
author = fileName.substring(fileName.indexOf(titleAuthorSeparator) + 1, fileName.length() - 4);
title = fileName.substring(0, fileName.indexOf(titleAuthorSeparator));
String[] titleAuthor = fileName.split(titleAuthorSeparator);
title = titleAuthor[0];
author = titleAuthor[1];
} else {
author = "Unknown";
title = fileName.substring(0, fileName.length() - 4);
title = fileName;
}
//Replace underscores with spaces
@ -139,16 +159,22 @@ public final class BookToFromTextHelper {
List<String> rawPages;
try {
rawPages = readTextFile(file);
} catch (IOException e) {
e.printStackTrace();
} catch (IOException exception) {
BooksWithoutBorders.getInstance().getLogger().log(Level.SEVERE, "Unable to read text file");
return null;
}
//Parse the generation from the book data
if (rawPages != null && !rawPages.isEmpty() && rawPages.get(0).startsWith("Generation:")) {
bookMetadata.setGeneration(BookMeta.Generation.valueOf(rawPages.get(0).split(":")[1]));
rawPages.remove(0);
}
//Remove any empty pages
List<String> pages = new ArrayList<>(InputCleaningHelper.cleanList(rawPages));
//Update the metadata of the book with its new values
bookMetadata.setAuthor(author);
bookMetadata.setAuthor(authorFromUUID(author));
bookMetadata.setTitle(title);
bookMetadata.setPages(pages);
@ -164,7 +190,7 @@ public final class BookToFromTextHelper {
*/
private static List<String> readTextFile(File file) throws IOException {
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
String firstLine = bufferedReader.readLine();
@ -172,8 +198,11 @@ public final class BookToFromTextHelper {
bufferedReader.close();
return null;
}
if (firstLine.equalsIgnoreCase("[Book]")) {
if (firstLine.toLowerCase().startsWith("[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;
do {
readLine = bufferedReader.readLine();

View File

@ -8,10 +8,13 @@ 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 java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
@ -74,17 +77,89 @@ public final class EconomyHelper {
if (bookCurrency == Material.AIR) {
return !EconomyHelper.payForBookPrintingEconomy(player, cost, numCopies);
} else {
if (player.getInventory().contains(bookCurrency, itemCost)) {
payForBookPrintingItem(player, itemCost);
return false;
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 {
BooksWithoutBorders.sendErrorMessage(player, itemCost + " " + bookCurrency +
"(s) are required for this command!");
return true;
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 static boolean takeWritableBookPayment(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 static int countItems(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>
*/
private static List<ItemStack> getPlayersEmptyBooks(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
*
@ -96,8 +171,10 @@ public final class EconomyHelper {
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)));
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!");

View File

@ -5,7 +5,7 @@ import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig;
import net.knarcraft.bookswithoutborders.encryption.GenenCrypt;
import net.knarcraft.bookswithoutborders.encryption.SubstitutionCipher;
import net.knarcraft.bookswithoutborders.state.EncryptionStyle;
import org.bukkit.ChatColor;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.Material;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.entity.Player;
@ -16,6 +16,7 @@ import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getBookFolder;
import static net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig.getSlash;
@ -198,11 +199,8 @@ public final class EncryptionHelper {
return null;
}
String fileName = (!bookMetadata.hasTitle()) ? "Untitled," + player.getName() : bookMetadata.getTitle() +
BooksWithoutBordersConfig.getTitleAuthorSeparator() + bookMetadata.getAuthor();
fileName = "[" + key + "]" + fileName;
fileName = cleanString(fileName);
fileName = fixName(fileName, false);
String fileName = "[" + key + "]" + BookHelper.getBookFile(bookMetadata, player, true);
fileName = fixName(cleanString(fileName), false);
File file = new File(path + fileName + ".yml");
if (!file.isFile()) {
@ -258,17 +256,13 @@ public final class EncryptionHelper {
BooksWithoutBorders.sendErrorMessage(BooksWithoutBorders.getConsoleSender(), "Unable to create encryption group folder!");
return null;
}
} catch (Exception e) {
e.printStackTrace();
} catch (Exception exception) {
BooksWithoutBorders.getInstance().getLogger().log(Level.SEVERE, "Unable to save group encrypted book");
return null;
}
}
//Creates file
String fileName = (!bookMetadata.hasTitle()) ? "Untitled," + player.getName() :
bookMetadata.getTitle() + BooksWithoutBordersConfig.getTitleAuthorSeparator() + bookMetadata.getAuthor();
fileName = cleanString(fileName);
fileName = fixName(fileName, false);
//Generate file name
String fileName = BookHelper.getBookFile(bookMetadata, player, true);
List<String> newLore = new ArrayList<>();
newLore.add(ChatColor.GRAY + "[" + groupName + " encrypted]");
@ -281,12 +275,12 @@ public final class EncryptionHelper {
bookMetadata.setLore(newLore);
//Save file
File file = (BooksWithoutBordersConfig.getUseYml()) ? new File(path + fileName + ".yml") : new File(path + fileName + ".txt");
File file = (BooksWithoutBordersConfig.getUseYml()) ? new File(path + fileName + ".yml") :
new File(path + fileName + ".txt");
if (!file.isFile()) {
try {
BookToFromTextHelper.bookToYml(path, fileName, bookMetadata);
} catch (IOException e) {
e.printStackTrace();
} catch (IOException exception) {
BooksWithoutBorders.sendErrorMessage(player, "Group encrypted failed!");
return null;
}
@ -305,23 +299,20 @@ public final class EncryptionHelper {
*/
private static Boolean saveEncryptedBook(Player player, BookMeta bookMetaData, String key) {
String path = getBookFolder() + "Encrypted" + getSlash();
String fileName = (!bookMetaData.hasTitle()) ? "Untitled," + player.getName() :
bookMetaData.getTitle() + BooksWithoutBordersConfig.getTitleAuthorSeparator() + bookMetaData.getAuthor();
fileName = "[" + key + "]" + fileName;
fileName = cleanString(fileName);
fileName = fixName(fileName, false);
String fileName = "[" + key + "]" + BookHelper.getBookFile(bookMetaData, player, true);
fileName = fixName(cleanString(fileName), false);
//cancels saving if file is already encrypted
File file = (BooksWithoutBordersConfig.getUseYml()) ? new File(path + fileName + ".yml") : new File(path + fileName + ".txt");
File file = (BooksWithoutBordersConfig.getUseYml()) ? new File(path + fileName + ".yml") :
new File(path + fileName + ".txt");
if (file.isFile()) {
return true;
}
try {
BookToFromTextHelper.bookToYml(path, fileName, bookMetaData);
} catch (IOException e) {
e.printStackTrace();
} catch (IOException exception) {
BooksWithoutBorders.sendErrorMessage(player, "Encryption failed!");
return false;
}

View File

@ -0,0 +1,90 @@
package net.knarcraft.bookswithoutborders.utility;
import java.util.ArrayList;
import java.util.List;
/**
* A converter from an integer to a roman numeral
*/
public final class IntegerToRomanConverter {
private final static List<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>
*/
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>
*/
private static String repeat(char character, int times) {
return String.valueOf(character).repeat(Math.max(0, times));
}
}

View File

@ -51,6 +51,31 @@ 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(Player player, String noBookMessage, 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
*
@ -96,7 +121,7 @@ public final class InventoryHelper {
if (state == BookHoldingState.SIGNED_BOTH_HANDS ||
state == BookHoldingState.UNSIGNED_BOTH_HANDS ||
state == BookHoldingState.NONE) {
return null;
return ItemSlot.NONE;
}
if (handMatters && typeMatters) {
if (mainHand && mainHandItem.getType() == requiredMaterial) {

View File

@ -6,43 +6,9 @@ import java.util.List;
/**
* Helper class for getting string lists required for auto-completion
*/
public final class TabCompletionHelper {
public final class TabCompletionTypeHelper {
private TabCompletionHelper() {
}
/**
* Finds tab complete values that contain the typed text
*
* @param values <p>The values to filter</p>
* @param typedText <p>The text the player has started typing</p>
* @return <p>The given string values that contain the player's typed text</p>
*/
public static List<String> filterMatchingContains(List<String> values, String typedText) {
List<String> configValues = new ArrayList<>();
for (String value : values) {
if (value.toLowerCase().contains(typedText.toLowerCase())) {
configValues.add(value);
}
}
return configValues;
}
/**
* Finds tab complete values that match the start of the typed text
*
* @param values <p>The values to filter</p>
* @param typedText <p>The text the player has started typing</p>
* @return <p>The given string values that start with the player's typed text</p>
*/
public static List<String> filterMatchingStartsWith(List<String> values, String typedText) {
List<String> configValues = new ArrayList<>();
for (String value : values) {
if (value.toLowerCase().startsWith(typedText.toLowerCase())) {
configValues.add(value);
}
}
return configValues;
private TabCompletionTypeHelper() {
}
/**

View File

@ -1,92 +0,0 @@
package net.knarcraft.bookswithoutborders.utility;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitScheduler;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.logging.Level;
/**
* The update checker is responsible for looking for new updates
*/
public final class UpdateChecker {
private final static String updateNotice = "A new update is available: %s (You are still on %s)";
private UpdateChecker() {
}
/**
* Checks if there's a new update available, and alerts the user if necessary
*/
public static void checkForUpdate(Plugin plugin, String apiResourceURL, Supplier<String> getVersionMethod,
Consumer<String> setVersionMethod) {
BukkitScheduler scheduler = plugin.getServer().getScheduler();
scheduler.runTaskAsynchronously(plugin, () -> UpdateChecker.queryAPI(plugin, apiResourceURL, getVersionMethod,
setVersionMethod));
}
/**
* Queries the spigot API to check for a newer version, and informs the user
*/
private static void queryAPI(Plugin plugin, String APIResourceURL, Supplier<String> getVersionMethod,
Consumer<String> setVersionMethod) {
try {
InputStream inputStream = new URL(APIResourceURL).openStream();
BufferedReader reader = FileHelper.getBufferedReaderFromInputStream(inputStream);
//There should only be one line of output
String newVersion = reader.readLine();
reader.close();
String oldVersion = getVersionMethod.get();
//If there is a newer version, notify the user
if (isVersionHigher(oldVersion, newVersion)) {
plugin.getLogger().log(Level.INFO, getUpdateAvailableString(newVersion, oldVersion));
if (setVersionMethod != null) {
setVersionMethod.accept(newVersion);
}
}
} catch (IOException e) {
plugin.getLogger().log(Level.WARNING, "Unable to get newest version.");
}
}
/**
* Gets the string to display to a user to alert about a new update
*
* @param newVersion <p>The new available plugin version</p>
* @param oldVersion <p>The old (current) plugin version</p>
* @return <p>The string to display</p>
*/
public static String getUpdateAvailableString(String newVersion, String oldVersion) {
return String.format(updateNotice, newVersion, oldVersion);
}
/**
* Decides whether one version number is higher than another
*
* @param oldVersion <p>The old version to check</p>
* @param newVersion <p>The new version to check</p>
* @return <p>True if the new version is higher than the old one</p>
*/
public static boolean isVersionHigher(String oldVersion, String newVersion) {
String[] oldVersionParts = oldVersion.split("\\.");
String[] newVersionParts = newVersion.split("\\.");
int versionLength = Math.max(oldVersionParts.length, newVersionParts.length);
for (int i = 0; i < versionLength; i++) {
int oldVersionNumber = oldVersionParts.length > i ? Integer.parseInt(oldVersionParts[i]) : 0;
int newVersionNumber = newVersionParts.length > i ? Integer.parseInt(newVersionParts[i]) : 0;
if (newVersionNumber != oldVersionNumber) {
return newVersionNumber > oldVersionNumber;
}
}
return false;
}
}

View File

@ -5,7 +5,7 @@ Options:
Max_Number_of_Duplicates: 5
# The separator used to separate the book title and the book author
Title-Author_Separator: ","
# The separator used to denote a new line in the book lore
# 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: [ ]
@ -21,6 +21,14 @@ Options:
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: false

View File

@ -1,10 +1,10 @@
name: BooksWithoutBorders
version: '${project.version}'
main: net.knarcraft.bookswithoutborders.BooksWithoutBorders
api-version: 1.18
prefix: Books Without Borders
api-version: '1.20'
prefix: 'Books Without Borders'
authors: [ EpicKnarvik97, AkiraAkiba ]
description: A continuation of the original Books Without Borders
description: 'A continuation of the original Books Without Borders'
softdepend: [ Vault ]
website: https://www.spigotmc.org/resources/books-without-borders-updated.96069/
dev-url: https://git.knarcraft.net/EpicKnarvik97/Books-Without-Borders
@ -45,6 +45,10 @@ commands:
description: Copies the book the player is holding
usage: /<command> <# of copies>
permission: bookswithoutborders.copy
clearbook:
description: Removes all text from the book the player is holding
usage: /<command>
permission: bookswithoutborders.clear
unsignbook:
description: Un-signs the book the player is holding
usage: /<command>
@ -53,6 +57,10 @@ 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 ""
usage: /<command> <key> [encryption style]
permission: bookswithoutborders.encrypt
setbookgeneration:
description: Sets the generation of the held book
usage: /<command> <generation>
permission: bookswithoutborders.setgeneration
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.
usage: /<command> <item/eco> <quantity>
@ -88,7 +96,7 @@ commands:
reload:
description: Reloads BwB's configuration file
usage: /<command>
permission: bookswithoutborders.admin
permission: bookswithoutborders.reload
permissions:
bookswithoutborders.*:
description: Grants all permissions
@ -111,22 +119,29 @@ permissions:
bookswithoutborders.give: true
bookswithoutborders.givepublic: true
bookswithoutborders.bypassauthoronlycopy: true
bookswithoutborders.bypassauthoronlyunsign: true
bookswithoutborders.bypassauthoronlysave: true
bookswithoutborders.bypassbookprice: true
bookswithoutborders.setbookprice: true
bookswithoutborders.reload: true
bookswithoutborders.setgeneration: true
bookswithoutborders.use:
description: Allows player to use commands 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:
bookswithoutborders.save: true
bookswithoutborders.load: true
bookswithoutborders.delete: true
bookswithoutborders.peekbookshelf: true
bookswithoutborders.alterbooks:
description: Allows player to change books' data such as lore/title/author/formatting and unsigning books
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.format:
description: Allows a player to format a book
bookswithoutborders.save:
@ -136,9 +151,11 @@ permissions:
bookswithoutborders.delete:
description: Allows player to delete books from their personal directory
bookswithoutborders.unsign:
description: Allows player to use unsign command
description: Allows player to use the unsign command
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:
description: Allows player to load from the public directory
bookswithoutborders.savepublic:
@ -163,7 +180,17 @@ permissions:
description: Allows player to set the lore of the currently held item
bookswithoutborders.bypassauthoronlycopy:
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:
description: Allows player to ignore Price_to_create_book config setting
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

View File

@ -1,9 +1,9 @@
package net.knarcraft.bookswithoutborders.encryption;
import junit.framework.Assert;
import org.junit.Test;
import static junit.framework.Assert.assertEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
public class AESTest {
@ -15,7 +15,7 @@ public class AESTest {
AES aes = new AES(AES.generateIV(), AES.generateIV());
String encrypted = aes.encryptDecryptText(plainText, password, true);
Assert.assertNotSame(encrypted, plainText);
assertNotSame(encrypted, plainText);
String decrypted = aes.encryptDecryptText(encrypted, password, false);
assertEquals(plainText, decrypted);
}

View File

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

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