Compare commits

...

198 Commits

Author SHA1 Message Date
27c444f3b4 test: adjusted BadCommandUsage javadoc 2025-01-11 23:52:18 +01:00
c589da2a14 mvn: updated papermc repo url 2025-01-11 23:27:38 +01:00
3fe4a1b244 Big Javadoc update + cleaned/refactored some code 2025-01-11 00:17:44 +01:00
1925dd9b36 Removed utility method to remove stacked entities (as Paper API now provides a way to teleport entities riding other entities) 2025-01-01 17:59:04 +01:00
d637b92f6c Fix deprecation in 1.21.3 API 2025-01-01 13:47:38 +01:00
af4bab0d12 1.21.3 API 2024-12-30 00:00:41 +01:00
44dc690736 Few javadoc update 2024-12-27 23:15:55 +01:00
c9af5ad308 New module pandalib-config 2024-12-27 23:15:37 +01:00
27403a6e20 Backup : ignore error when a source file has been deleted during the backup precess 2024-12-26 19:51:25 +01:00
38a42dcca0 Fix reflection again 2024-12-26 00:30:09 +01:00
5782046b7a Reduce verbosity on some reflection errors 2024-12-26 00:24:17 +01:00
2b407d7f27 Fix various reflection issues for Paper 1.21.1 2024-12-26 00:23:53 +01:00
8f5f880754 More complete Javadoc 2024-12-22 23:45:10 +01:00
3d92c3afb6 Update Paper API to 1.21.1 2024-12-22 19:48:21 +01:00
5e1f98ab70 Update MC version file with 1.21.4 2024-12-11 21:56:30 +01:00
276f5b2dc1 Added temporary workaround due to Paper/Brigadier API not keeping modifier and forks properties of command nodes 2024-11-24 16:49:22 +01:00
9c72b8cda4 Properly handle setting final fields in reflection API 2024-11-24 16:18:14 +01:00
ee023b5d2c Trying again to do some shady reflection stuff to fix a Paper issue.
Paper plugin should be able to register Brigadier commands that redirects to the root command node (similar to vanilla /execute run)
2024-11-23 00:11:16 +01:00
974347cbde Paper commands should be built right before registration into command dispatcher (so the root command node exists in case we need it) 2024-11-22 22:40:18 +01:00
e6b77bcad6 Updated JDBC connection string for Database connection
- Updated SSL setting
- Due to a weard bug with MySQL in Docker in WSL (Windows), added allowPublicKeyRetrieval=true
2024-10-02 00:05:35 +02:00
36eb1227cf Do not use bungeecord-chat as a dependency for pandalib-chat anymore 2024-07-28 01:04:34 +02:00
4be37945cb Replace google guava with lighter library for a specific map implementation. 2024-07-27 17:41:21 +02:00
3e6cf96040 GameWorld loading ony runs /mvm command when Multiverse plugin is present 2024-07-19 23:12:56 +02:00
d1a04a7a66 Proper exception handling when not able to load player data file 2024-07-19 00:23:12 +02:00
fcac9af7d1 Fix reflection in PlayerDataStorage + new wrapOptional method in reflection library 2024-07-17 23:24:13 +02:00
e16487431d Removed some debug messages 2024-07-10 23:15:33 +02:00
50f5ab671a Debug better the alias registration forcing 2024-07-10 22:28:47 +02:00
5a04052f8e Fix (again) command registering 2024-07-10 21:34:03 +02:00
c86855ac16 Fix (again) command registering 2024-07-10 21:13:32 +02:00
001239fe57 Fix command identity 2024-07-10 16:08:42 +02:00
0c7fb9b370 Some other fixes to command registration 2024-07-10 15:31:00 +02:00
f416e30d45 Better log info with commands 2024-07-10 14:04:05 +02:00
e271ac2964 Trying to fix the command registration process
Interact only with the brigadier command dispatcher, since the bukkit command map is backed by the brigadier dispatcher.
2024-07-10 13:23:48 +02:00
2bb09ad42b Trying to fix the post command registration process 2024-07-10 11:30:30 +02:00
4f55890092 Fix reflection in PaperBrigadier 2024-07-10 01:15:01 +02:00
76470b963e Trying to register our Brigadier commands by force if necessary 2024-07-10 01:04:05 +02:00
76fc673e98 New reflected class ShadowBrigNode 2024-07-09 21:55:31 +02:00
307b5132df Command should also register in place of vanilla (e.g. /list) 2024-07-07 15:50:16 +02:00
ac52e024f3 Fix potential StackOverflowException 2024-06-29 00:30:18 +02:00
bb6d221ced MC client 1.21 support 2024-06-27 21:19:00 +02:00
2acfa53b63 Use $ to reflect on inner classes 2024-06-26 23:59:07 +02:00
640b255e1d Bypass PaperReflectionHolder that try to understand our Mojang mapped reflection call as Spigot mapped 2024-06-26 23:10:13 +02:00
ed0db5391d Added missing @ConcreteWrapper 2024-06-15 13:29:41 +02:00
cef4af80f0 Update reflection in NMS/OBC 2024-06-15 13:15:50 +02:00
7f56645ba5 new FunctionException type 2024-06-12 23:32:13 +02:00
827c13887c Removed all NMS mapping stuff since paper jar is now mojang mapped 2024-06-12 23:31:54 +02:00
0ff2ae1296 Fix Brigadier command stuff in pandalib-paper 2024-06-09 22:48:54 +02:00
e7b528718c Update paper plugin to MC 1.20.6
- Convert Brigadier command to use the new Brigadier/Paper API
- EquipmentSlot now has BODY value for horses and wolves armor
- ItemMeta’ canPlaceOn and canDestroy does not work anymore. Removed the related methods from ItemStackBuilder
- Enchantment DURABILITY now has the proper vanilla name UNBREAKING
- Pandalib-chat now uses Adventure’s TranslationArgument
2024-06-07 00:05:07 +02:00
d411618e63 Fix Javadoc warnings due to Java 21 update (+ some other warnings)
The default implicit constructor must also have a doc comment, so I have to make it explicit and either properly restrict the visibility of the constructor, or actually document it.
2024-06-06 23:59:32 +02:00
decf302851 Update to Java 21 2024-06-01 00:35:49 +02:00
d3097781bc Added 1.20.5 and 1.20.6 2024-05-08 10:07:13 +02:00
2942a030a6 pandalib-db do not relocate commons-logging classes 2024-04-17 22:55:18 +02:00
69af006001 Updated some dependencies 2024-03-18 16:55:23 +01:00
c60fb613d4 Add NMS access to raw display name of items 2024-03-05 18:09:18 +01:00
33e4e053cf Better error handling when loading offline player data 2024-03-05 16:29:11 +01:00
e02ccc2b60 Small improvements to offline player data handling. 2024-03-05 16:06:07 +01:00
1c22518dd9 Lag bar now shows target TPS from /tick command 2024-03-03 18:32:29 +01:00
5d294ea172 DB: some improvements
- Refactor auto-conversion of custom types
- Ability to create a WHERE ... IN ... expression with a custom left operand
- Typos in Javadoc
2024-03-02 23:23:45 +01:00
56632867ec DB: forgot static 2024-03-02 22:17:11 +01:00
0c074b9354 DB: custom WHERE expression 2024-03-02 22:14:53 +01:00
bdf60785e8 Fix some missing javadoc. 2024-03-01 20:21:29 +01:00
ca7a51af2c Permissions can now pre-cache all known players, and API users can get all cached players 2024-03-01 20:17:30 +01:00
d7705d8904 Rollback removal usage of teams prefix to handle colored scoreboard sidebar (was not working on Bedrock clients and on some Java clients) 2024-02-24 15:44:24 +01:00
649e1a56c8 Fix 1.20.4 reflection wrappers 2024-02-21 11:56:11 +01:00
7d89f0c376 Update to use new 1.20.4 API
- hide score number in autogenerated scoreboard sidebar and use score entries custom name
- remove now useless reflection wrapper for DamageSource since there is a proper API for that
2024-02-20 14:21:58 +01:00
f494c3bdb3 Prepare for 1.20.4 2024-02-19 23:33:30 +01:00
23a7b940b7 Adjust TPS Bar color: don't change the color between 19 and 21 inclusive. 2024-02-19 23:18:29 +01:00
bd0e0484cd Fix Reflection wrappers for 1.20.2 2024-02-19 13:08:05 +01:00
92a9afa22c Prepare 1.20.2 full support
- Update some POM files
- OBC reflection should not try to parse relocation package version
- NMS reflection dependency update
2024-02-18 14:28:42 +01:00
2393352770 Better display of BadCommandUsage message on paper server 2024-02-17 20:02:16 +01:00
7795d94dfb Update Bungee API 2024-02-17 13:45:59 +01:00
3c6d77f0bb Update Paper API 2024-02-17 13:00:12 +01:00
e4eb6dc9c9 Utility classes to handle IPv4 addresses 2024-02-16 13:00:57 +01:00
2d6d905b54 CLI application now correctly handles uncaught Exceptions 2024-02-15 10:46:03 +01:00
5a3831ba74 Use a lighter gray color for data 2024-02-14 20:29:49 +01:00
07f3841ee6 Fi TimeUtil.fullDateFr() compact date format 2024-02-14 20:18:01 +01:00
d84e4c87dc Use ComponentLike instead of Component or Chat in some places 2024-02-14 19:55:56 +01:00
49a32421c0 Fixing reflection 2024-02-11 13:48:24 +01:00
177733950d New reflected static method in CraftPlayer 2024-02-11 11:44:57 +01:00
8149d8fb54 New reflected field in CraftPlayer 2024-02-11 11:21:44 +01:00
eb72633dd8 TimeUtil.fullDateFr() method updated 2024-02-10 23:28:25 +01:00
ece1bc70bf PaperBackupManager: ignore CancellationException in async task 2024-01-21 13:53:16 +01:00
90009b8703 Fixing WorldUtil (wrong constant usage) 2024-01-21 00:45:19 +01:00
a39f3d8143 More precise error WorldUtil 2024-01-21 00:32:08 +01:00
ecc9932f5e New methods in ChunkCoord 2024-01-20 19:50:49 +01:00
e9b401f43d Fiw WorldUtil 2024-01-20 18:50:33 +01:00
77b0a0c73c Improved World utility classes 2024-01-20 18:46:50 +01:00
93960b83c2 New method Tick.toDuration(long) 2023-12-28 19:15:47 +01:00
075468854d Fix AABBBlockGroup 2023-12-27 18:56:44 +01:00
b82b59d57b Fix potential NPE in BrigadierCommand.wrapSuggestions() 2023-12-10 16:28:24 +01:00
b85c5acb21 MC version 1.20.3-4 2023-12-09 14:17:37 +01:00
ba496b0968 Public method in PaperBrigadierCommand 2023-10-29 15:42:08 +01:00
ecd8b3b0c9 Some docs 2023-10-28 23:46:47 +02:00
2f0b59a032 Improved Tick utility class 2023-10-28 23:16:54 +02:00
8f31ea54d1 Add chat MiniMessage support + add support for color downsampling 2023-10-21 20:12:41 +02:00
e2506d5d53 Fixed ordering of MC versions 2023-10-21 18:00:40 +02:00
0e016881d7 ChatFilledLine can now add spaces on the right, if enabled in the builder. 2023-10-20 23:07:25 +02:00
c7b33132a9 Proper deserialization of MinecraftVersionList to keep elements sorted 2023-10-18 23:22:57 +02:00
a24eab67b6 Gson now deserializes numbers to the appropriate Number subclass 2023-10-08 23:57:46 +02:00
db06ab1be9 CLI: Delay shutdown of logging system until the end of the stop() method 2023-10-08 01:38:52 +02:00
728961d19f Don't call System.exit() from shutdown hook thread 2023-10-08 01:02:14 +02:00
8b6fe63df1 Proper serialization of ItemStack and other Serializable stuff in Bukkit API 2023-10-08 00:30:56 +02:00
da1ee9d882 Trying to fix ItemStack Gson adapter 2023-09-24 16:17:09 +02:00
455226b762 1.20.2 2023-09-22 00:34:57 +02:00
3ee806c1ea Ability to clean up unused loaded temporary game worlds 2023-09-03 00:10:05 +02:00
69a4f2fe6f Clear player permission cache when changing permissions through Vault API 2023-08-29 13:59:27 +02:00
62949948e1 Some more javadoc 2023-08-27 17:28:12 +02:00
bd3bea8381 Some refactoring in pandalib-util 2023-08-27 13:37:17 +02:00
463a4d7e78 New method #isSet() in Lazy and LazyOrException class 2023-08-27 01:48:09 +02:00
84298b08b3 Added TriConsumerException 2023-08-25 17:09:21 +02:00
9ac7a98257 SQLElement: better error handling of the get method + A nullable field that is not set will return null instead of throwing an exception. 2023-08-24 12:40:38 +02:00
f16389d33d Upgrade GUIHotBar: add ability to unregister the event listeners of the hotbar + optional cleanup of inventory on player removal 2023-08-24 01:21:27 +02:00
a49061eb9f Removed the whole stop method synchronization block in CLIApplication. Not necessary due to the other one at the beginning of the method. 2023-08-17 18:04:07 +02:00
378e79b8ad Use newer version of Gson for chat API.
The Gson version should expand to all dependant modules and applications
2023-08-16 23:01:38 +02:00
ae634ab560 Better handling of IOException on client websocket when trying to send a payload. 2023-08-15 00:57:49 +02:00
45ab550d06 Ability to get min and max of AABBBlock 2023-08-14 02:10:59 +02:00
0fcd02c80d Permission system now provides a default player
The default player has a fixed name and uuid that should never collide with existing players.
It will never have self permission data, and it will always be part of the default groups.

It is useful for anonymous permission test (for instance, listing only the servers that are publicly visible for the ping server list)
2023-08-14 01:49:56 +02:00
2d950117d3 New BlockSet super-interface for AABBBlock and AABBBlockGroup + reorganized classes related to geometry to a new package 2023-08-14 00:43:01 +02:00
2f476ce8f2 Relative teleport API in PaperOnlinePlayer 2023-08-14 00:38:05 +02:00
75e292b1b8 Fixing big mistake in SchedulerUtil.runOnServerThread(). Provided Runnable was run twice if the method was called from Server Thread. 2023-08-03 23:32:29 +02:00
2969d51f72 Don't use random UUID for custom player heads 2023-07-29 13:39:13 +02:00
c0e0097b7b Some static values in PaperOnlinePlayer about default player movement speed 2023-07-28 22:56:25 +02:00
d047be35d9 Custom Bukkit event ServerStopEvent 2023-07-15 16:25:26 +02:00
5fb17be4c7 Add a method in BackupManager to check if a backup is currently running. 2023-07-14 19:07:02 +02:00
d7bb56e0b2 Fix CLIApplication shutdown hook 2023-07-14 16:26:30 +02:00
9e7d89cf70 Reimplement ChatColorGradient.pickColorAt() to make it less error-prone 2023-07-04 23:15:06 +02:00
79e4bb90f7 Inject permissions system into Vault on plugin load 2023-06-25 16:32:46 +02:00
736e0f0c23 PermissionsInjectorVault registers Vault services on highest priority and checks services access after server starts. 2023-06-25 14:52:02 +02:00
7481b12111 Fix FileUtils.copy not accepting regular file as source. Also made it handle directory merging. 2023-06-25 13:17:46 +02:00
7c4fd78680 Update some dependencies + Gson now natively supports record 2023-06-24 16:28:53 +02:00
8c25fb0dd1 Build against paper API 1.20 2023-06-24 15:53:26 +02:00
d5a2aa1c30 Completed implementation of PaperClientOptions 2023-06-23 23:43:16 +02:00
7d5060d09b Improved ItemStackBuilder + removed GUIInventory unused methods 2023-06-22 21:52:02 +02:00
a46e066669 also new ChatUtil.join() method 2023-06-20 21:32:52 +02:00
a4b33a1af7 new ChatUtil.joinGrammatically() method, using components 2023-06-20 18:38:07 +02:00
5edd8cdfec Various code simplification/fixes and a lot of typo/grammar fixes (may brake some stuff) 2023-06-20 00:15:46 +02:00
c984b63cee Removed never used NET library 2023-06-18 00:36:57 +02:00
69b72ef90d made ProtocolVersion comparable 2023-06-17 13:16:22 +02:00
555f5ab65c Added ProtocolVersion equals and hashcode 2023-06-17 11:53:34 +02:00
9f9fb55726 Added ProtocolVersion.allKnownProtocolVersions() method 2023-06-17 11:43:37 +02:00
98d1a21aab sendChatMessage() for online players 2023-06-17 10:56:58 +02:00
d59ae22970 Deprecation, deprecated, ... 2023-06-16 19:14:22 +02:00
e6fc31e5ca Improved ProtocolVersion + mcversion.json file automatically updated at build time from Pandacube API 2023-06-16 17:28:25 +02:00
70c4d59fdc Fix CronScheduler skipping 1 scheduled time after application restart 2023-06-15 23:30:08 +02:00
edd5b06a46 Deprecate old MinecraftVersion enum and create new ProtocolVersion class which automatically update the version list on startup 2023-06-15 15:08:18 +02:00
436c9b9381 MC 1.20.1 (same protocol version as 1.20) 2023-06-12 16:38:50 +02:00
f3f616cdca Better detection of closed client side WS connection (to reconnect if necessary) 2023-06-08 12:07:49 +02:00
20643fec62 Use Bungeecord version 1.20 2023-06-08 11:07:29 +02:00
c79d9b8006 MC 1.20 2023-06-08 10:19:38 +02:00
61fb7b3142 Added getServerPermissionName() for OnlinePlayer 2023-05-13 13:35:14 +02:00
f0a9fca952 Create backup directory if needed 2023-05-10 12:47:47 +02:00
913d5d91dd Reduce code duplication of DailyLogRotateFileHandler + generalize logs compression 2023-05-10 10:26:02 +02:00
1cd3749d7d Update ItemStackAdapter to try to handle ItemStack from newer MC version 2023-05-09 15:43:01 +02:00
e640cbb1a3 Fix MCDataConverter with new reflect wrapper stuff 2023-05-09 14:12:04 +02:00
edd4e89a6e Update WorldVersion/DataVersion reflect wrappers for new paper version 2023-05-09 12:30:50 +02:00
9b83f9699c Reflect wrapper initialization does not crash anymore on the first exception. It accumulates all the exceptions and shows everything at the end. 2023-05-09 11:57:05 +02:00
3e0297c8af Fix bamboo block reflect wrapper 2023-05-08 22:47:11 +02:00
df8dbe1e61 Fix reflect wrappers due to changes in NMS about DamageSources 2023-05-08 22:32:40 +02:00
d023bcb706 Bump paper version to 1.19.4.
Seems to compile, but reflection API may not work as is.
2023-05-08 18:31:27 +02:00
448ee6c62a CLI console command sender instance is now public 2023-05-04 16:34:17 +02:00
efcb155b3d Improved pandalib-cli
- Custom class to represent command sender
- Abstract class to use as a base for CLI application main class
- /stop and /admin command that are common to every CLI apps
2023-04-23 01:27:25 +02:00
d5c9876734 Plugins using Vault to interact with Pandalib permission system can now add and remove permission to specific players on server and world
+ added warning messages when plugin tries to manipulate player's groups and group's permissions
2023-04-16 23:49:41 +02:00
f036c22a56 Fix WS log message 2023-04-11 19:17:54 +02:00
15982cb837 Weird stuff with CLI due to invisible \r character 2023-04-10 19:54:11 +02:00
0453a72587 Supports null return values from AbstractWS#getRemoteIdentifier() in WS log messages 2023-04-10 19:39:07 +02:00
ff954a3903 Various code cleanup. 2023-04-10 19:17:18 +02:00
ba896e689a Fix deprecation in ItemStackBuilder 2023-04-10 19:16:42 +02:00
4259e5eccd Handle reception of partial data in WS client. 2023-04-10 19:15:58 +02:00
5b43aed0b4 Stop using deprecated Bungee ChatColor in BukkitChatColorUtil 2023-03-23 19:00:51 +01:00
cafb220768 Wait 1s before reconnect websocket. 2023-03-23 18:53:32 +01:00
544abd218c Un chouilla of logs 2023-03-21 23:37:27 +01:00
55556b0714 Handle CompletionException in websocket client 2023-03-21 19:20:29 +01:00
87b9ffcc37 Another websocket client fix: the socket reference is not set soon enough + add some more error handling 2023-03-20 23:10:52 +01:00
95fa33a488 Fix missing return statement. Was logging an error even if connection succeed. 2023-03-20 22:49:57 +01:00
46653f06ff Implements websocket client auto-reconnect 2023-03-19 23:30:36 +01:00
2bc60df11c Handle reception of ErrorPayload in WS. 2023-03-19 17:25:14 +01:00
e4a5bf0eac Don't print ClosedChannelException when we actually asked to close the connection. 2023-03-18 21:45:39 +01:00
5b40c4aabb Try a cleaner stdout/stderr -> logger redirector 2023-03-17 15:41:39 +01:00
ff5d776aa5 Better handling of Throwable in Gson 2023-03-17 10:55:58 +01:00
872746b46f Fix websocket timeout 2023-03-16 23:30:37 +01:00
ced9b0eaca Better handling of Json Exceptions in websockets 2023-03-16 22:36:58 +01:00
4ec47b5e4b Fix Gson unable to (de)serialize Throwable instance 2023-03-16 22:34:52 +01:00
2fb4775ca7 Try to fix some stuff with websocket 2023-03-16 12:29:43 +01:00
fdfb67757f Fix check Gson record support. May not work if Gson internal classes are not accessible. 2023-03-15 14:47:16 +01:00
d4ff95534f MC 1.19.4 2023-03-14 22:56:28 +01:00
fd828d600e WebSocket API 2023-03-14 16:22:50 +01:00
b6dba62fa4 Fix Gson native record support check 2023-03-12 14:28:31 +01:00
b2f5770461 Improved Json record support (Gson 2.10 natively supports it) + Added ItemStack Json support
- Extract RecordTypeAdapter to its own file + only use it if Gson library does not support it (it does since 2.10, but we are unsure of which version is actually used in paper/bungee/other)
- new ItemStackAdapter to support Json (de)serializing of Bukkit ItemStack.
2023-03-12 14:14:17 +01:00
f1ef4e1927 Makes exception types generic in ThrowableUtil 2023-03-11 12:16:09 +01:00
df46026457 Improves Json util class: ability to add TypeAdapterFactory on the fly. 2023-03-09 18:57:09 +01:00
a6bde9e191 Javadoc 2023-02-22 16:40:08 +01:00
6f310de32e Fix PlayerDataWrapper (don't store empty stacks as air stack) 2023-02-22 10:07:56 +01:00
add5d3bcd7 Fix reflect wrapper MCDataConverter 2023-02-19 19:31:02 +01:00
73d96d0bb7 Improved offline playerdata manipulation
- Ability to change player experience and score
- Handle upgrade of player data (from older Mc version)
2023-02-19 16:11:04 +01:00
bf59617e19 Refactor offline player data access
- Class that handle all Bukkit/NBT conversion of player data
- Ability to read and save the player inventory (more to come later)
2023-02-18 21:32:12 +01:00
fb4c62a0bc Ability to save offline player data 2023-02-16 12:17:59 +01:00
dd2b4467ed new StringUtil.asPatternInSentense method 2023-02-11 23:57:56 +01:00
6577367c27 Javadoc + some small refactoring 2023-02-11 23:40:36 +01:00
302 changed files with 11124 additions and 6884 deletions

4
.gitignore vendored
View File

@@ -1,3 +1,5 @@
/.idea
/*/target
dependency-reduced-pom.xml
dependency-reduced-pom.xml
*.iml

View File

@@ -3,7 +3,7 @@
### Development library for Minecraft server applications and plugins
This repository contains a collection of maven modules that are used for the development of our Minecraft server. Those
modules are made open source so they can be used by other developpers. Each of them provides different functionalities
modules are made open source, so they can be used by other developers. Each of them provides different functionalities
that are detailed in their respective Readme file (if any).
- `pandalib-util` General purpose utility and helper classes;
@@ -18,10 +18,10 @@ that are detailed in their respective Readme file (if any).
- `pandalib-players` A library to handle classes representing online or offline players;
- `pandalib-players-permissible` An extension of `pandalib-players` with support for the permission system `pandalib-permissions`;
- `pandalib-netapi` A poorly designed, but working TCP network library;
- `pandalib-net` A better-designed, packet-based TCP network library (_still in development_);
- `pandalib-config` Utility and helper classes to handle configuration related files and folders;
- `pandalib-commands` An abstract command manager working on top of [Brigadier](https://github.com/Mojang/brigadier);
- `pandalib-cli` Utility and helper classes for a standalone CLI Java application.
- `pandalib-core` A catch-all module for some helper classes that didnt have their own module yet;
- `pandalib-cli` Utility and helper classes for a standalone CLI Java application;
- `pandalib-core` A catch-all module for some helper classes that didn't have their own module yet;
### Use in your projects

1
pandalib-bungee-chat/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target/

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>pandalib-parent</artifactId>
<groupId>fr.pandacube.lib</groupId>
<version>1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>pandalib-bungee-chat</artifactId>
<packaging>jar</packaging>
<repositories>
<repository>
<id>bungeecord-repo</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>fr.pandacube.lib</groupId>
<artifactId>pandalib-util</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>fr.pandacube.lib</groupId>
<artifactId>pandalib-chat</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-chat</artifactId>
<version>${bungeecord.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-platform-bungeecord</artifactId>
<version>4.3.0</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,77 @@
package fr.pandacube.lib.bungee.chat;
import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.chat.Chat.FormatableChat;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer;
import net.md_5.bungee.api.chat.BaseComponent;
/**
* Utility class to ease conversion between our Adventure backed Chat API and BungeeCord chat API.
*/
public class ChatBungee {
/**
* Creates a {@link FormatableChat} from the provided Bungee {@link BaseComponent}.
* @param c the {@link BaseComponent}.
* @return a new {@link FormatableChat}.
*/
public static FormatableChat chatComponent(BaseComponent c) {
return Chat.chatComponent(toAdventure(c));
}
/**
* Creates a {@link FormatableChat} from the provided Bungee {@link BaseComponent BaseComponent[]}.
* @param c the array of {@link BaseComponent}.
* @return a new {@link FormatableChat}.
*/
public static FormatableChat chatComponent(BaseComponent[] c) {
return Chat.chatComponent(toAdventure(c));
}
/**
* Converts the Bungee {@link BaseComponent} array into Adventure {@link Component}.
* @param components the Bungee {@link BaseComponent} array.
* @return a {@link Component}.
*/
public static Component toAdventure(BaseComponent[] components) {
return BungeeComponentSerializer.get().deserialize(components);
}
/**
* Converts the Bungee {@link BaseComponent} into Adventure {@link Component}.
* @param component the Bungee {@link BaseComponent}.
* @return a {@link Component}.
*/
public static Component toAdventure(BaseComponent component) {
return toAdventure(new BaseComponent[] { component });
}
/**
* Converts the Adventure {@link Component} into Bungee {@link BaseComponent} array.
* @param component the Adventure {@link Component}.
* @return a {@link BaseComponent} array.
*/
public static BaseComponent[] toBungeeArray(ComponentLike component) {
return BungeeComponentSerializer.get().serialize(component.asComponent());
}
/**
* Converts the Adventure {@link Component} into Bungee {@link BaseComponent}.
* @param component the Adventure {@link Component}.
* @return a {@link BaseComponent}.
*/
public static BaseComponent toBungee(ComponentLike component) {
BaseComponent[] arr = toBungeeArray(component);
return arr.length == 1 ? arr[0] : new net.md_5.bungee.api.chat.TextComponent(arr);
}
private ChatBungee() {}
}

View File

@@ -25,7 +25,6 @@ import java.util.function.Function;
*/
public class PandalibBungeePermissions implements Listener {
/**
* Registers event listener to redirect permission checks to {@code pandalib-permissions}.
* @param bungeePlugin a BungeeCord plugin.
@@ -35,6 +34,8 @@ public class PandalibBungeePermissions implements Listener {
}
private PandalibBungeePermissions() {}
/**
* Event handler called when a plugin asks if a player has a permission.
* @param event the permission check event.

View File

@@ -33,7 +33,7 @@
</dependency>
<dependency>
<groupId>fr.pandacube.lib</groupId>
<artifactId>pandalib-chat</artifactId>
<artifactId>pandalib-bungee-chat</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>

View File

@@ -1,29 +1,49 @@
package fr.pandacube.lib.bungee;
import fr.pandacube.lib.bungee.util.DailyLogRotateFileHandler;
import fr.pandacube.lib.bungee.util.BungeeDailyLogRotateFileHandler;
import fr.pandacube.lib.bungee.util.PluginMessagePassthrough;
import net.md_5.bungee.api.plugin.Plugin;
/**
* General class used to initialize some tools of pandalib-bungee, following the bungee plugin's lifecycle.
*/
public class PandaLibBungee {
private static Plugin plugin;
/**
* Method to be called in {@link Plugin#onLoad()} method.
* @param plugin the plugin instance.
*/
public static void onLoad(Plugin plugin) {
PandaLibBungee.plugin = plugin;
}
/**
* Method to be called in {@link Plugin#onEnable()} method.
*/
public static void onEnable() {
PluginMessagePassthrough.init(plugin);
DailyLogRotateFileHandler.init(true);
BungeeDailyLogRotateFileHandler.init(true);
}
/**
* Method to be called in {@link Plugin#onDisable()} method.
*/
public static void disable() {
}
/**
* Returns the plugin instance.
* @return the plugin instance.
*/
public static Plugin getPlugin() {
return plugin;
}
private PandaLibBungee() {}
}

View File

@@ -6,11 +6,40 @@ import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* Class that holds the configuration varables for {@link BungeeBackupManager}.
*/
@SuppressWarnings("CanBeFinal")
public class BungeeBackupConfig {
/**
* Tells if the working directory of the current bungee instance should be backed up.
*/
public boolean workdirBackupEnabled = true;
/**
* Tells if the old logs of the current bungee instance should be backed up.
*/
public boolean logsBackupEnabled = true;
public String scheduling = "0 2 * * *"; // cron format, here is everyday at 2am
/**
* The cron scheduling of when the workdir backup occurs.
*/
public String scheduling = "0 2 * * *"; // cron format, here is every day at 2am
/**
* The destination directory for the backups.
*/
public File backupDirectory = null;
/**
* The configuration handling the cleaning of the backup directory.
*/
public BackupCleaner workdirBackupCleaner = BackupCleaner.KEEPING_1_EVERY_N_MONTH(3).merge(BackupCleaner.KEEPING_N_LAST(5));
/**
* A list of ignored files or directory in the workdir to exclude from the backup.
*/
public List<String> workdirIgnoreList = new ArrayList<>();
/**
* Creates a new {@link BungeeBackupConfig}.
*/
public BungeeBackupConfig() {
}
}

View File

@@ -6,10 +6,17 @@ import fr.pandacube.lib.core.backup.RotatedLogsBackupProcess;
import java.io.File;
/**
* Handles the backup processes for a Bungeecord instance.
*/
public class BungeeBackupManager extends BackupManager {
BungeeBackupConfig config;
/**
* Instanciate a new {@link BungeeBackupManager}.
* @param config the configuration.
*/
public BungeeBackupManager(BungeeBackupConfig config) {
super(config.backupDirectory);
setConfig(config);
@@ -24,12 +31,19 @@ public class BungeeBackupManager extends BackupManager {
super.addProcess(process);
}
/**
* Sets a new configuration for this backup manager.
* @param config the new configuration.
*/
public void setConfig(BungeeBackupConfig config) {
this.config = config;
backupQueue.forEach(this::updateProcessConfig);
}
/**
* Deploys the new configuration to the provided backup process.
* @param process the process on which to apply the new config.
*/
public void updateProcessConfig(BackupProcess process) {
if (process instanceof BungeeWorkdirProcess) {
process.setEnabled(config.workdirBackupEnabled);

View File

@@ -1,16 +1,19 @@
package fr.pandacube.lib.bungee.backup;
import fr.pandacube.lib.core.backup.BackupProcess;
import fr.pandacube.lib.util.Log;
import net.md_5.bungee.api.ChatColor;
import java.io.File;
import java.text.DateFormat;
import java.util.Date;
import java.util.function.BiPredicate;
/**
* The backup process responsible for the working directory of the current Bungeecord instance.
*/
public class BungeeWorkdirProcess extends BackupProcess {
/**
* Instantiates this backup process.
* @param bm the backup manager.
*/
protected BungeeWorkdirProcess(BungeeBackupManager bm) {
super(bm, "workdir");
}
@@ -22,15 +25,12 @@ public class BungeeWorkdirProcess extends BackupProcess {
public BiPredicate<File, String> getFilenameFilter() {
return new BiPredicate<File, String>() {
@Override
public boolean test(File file, String path) {
if (new File(getSourceDir(), "logs").equals(file))
return false;
if (file.isFile() && file.getName().endsWith(".lck"))
return false;
return BungeeWorkdirProcess.super.getFilenameFilter().test(file, path);
}
return (file, path) -> {
if (new File(getSourceDir(), "logs").equals(file))
return false;
if (file.isFile() && file.getName().endsWith(".lck"))
return false;
return BungeeWorkdirProcess.super.getFilenameFilter().test(file, path);
};
}
@@ -60,10 +60,4 @@ public class BungeeWorkdirProcess extends BackupProcess {
return "workdir";
}
public void displayNextSchedule() {
Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " next backup on "
+ DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(new Date(getNext())));
}
}

View File

@@ -7,7 +7,7 @@ import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.tree.LiteralCommandNode;
import fr.pandacube.lib.commands.BrigadierCommand;
import fr.pandacube.lib.commands.SuggestionsSupplier;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.connection.ProxiedPlayer;
@@ -28,10 +28,10 @@ public abstract class BungeeBrigadierCommand extends BrigadierCommand<CommandSen
/**
* The command dispatcher.
*/
protected BungeeBrigadierDispatcher dispatcher = BungeeBrigadierDispatcher.getInstance();
protected final BungeeBrigadierDispatcher dispatcher = BungeeBrigadierDispatcher.getInstance();
/**
* Instanciate this command instance.
* Instantiate this command instance.
*/
public BungeeBrigadierCommand() {
LiteralCommandNode<CommandSender> commandNode;

View File

@@ -1,7 +1,6 @@
package fr.pandacube.lib.bungee.commands;
import fr.pandacube.lib.bungee.PandaLibBungee;
import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.bungee.chat.ChatBungee;
import fr.pandacube.lib.commands.BrigadierDispatcher;
import net.kyori.adventure.text.ComponentLike;
import net.md_5.bungee.api.CommandSender;
@@ -21,6 +20,10 @@ public class BungeeBrigadierDispatcher extends BrigadierDispatcher<CommandSender
private static BungeeBrigadierDispatcher instance = null;
/**
* Gets the instance of {@link BungeeBrigadierDispatcher}.
* @return the instance of {@link BungeeBrigadierDispatcher}.
*/
public static synchronized BungeeBrigadierDispatcher getInstance() {
return instance;
}
@@ -36,7 +39,7 @@ public class BungeeBrigadierDispatcher extends BrigadierDispatcher<CommandSender
*/
public BungeeBrigadierDispatcher(Plugin pl) {
if (instance != null)
throw new IllegalStateException("Cannot instanciante more than one BungeeBrigadierDispatcher");
throw new IllegalStateException("Cannot instantiate more than one BungeeBrigadierDispatcher");
instance = this;
plugin = pl;
ProxyServer.getInstance().getPluginManager().registerListener(plugin, this);
@@ -44,7 +47,7 @@ public class BungeeBrigadierDispatcher extends BrigadierDispatcher<CommandSender
/**
* Called when a player sends a chat message. Used to gets the typed command and execute it.
* Called when a player sends a chat message. Used to get the typed command and execute it.
* @param event the event.
*/
@EventHandler
@@ -68,6 +71,6 @@ public class BungeeBrigadierDispatcher extends BrigadierDispatcher<CommandSender
@Override
protected void sendSenderMessage(CommandSender sender, ComponentLike message) {
sender.sendMessage(Chat.toBungee(message.asComponent()));
sender.sendMessage(ChatBungee.toBungee(message.asComponent()));
}
}

View File

@@ -1,13 +1,12 @@
package fr.pandacube.lib.bungee.players;
import java.util.Locale;
import java.util.UUID;
import fr.pandacube.lib.bungee.chat.ChatBungee;
import fr.pandacube.lib.core.mc_version.ProtocolVersion;
import fr.pandacube.lib.players.standalone.AbstractOnlinePlayer;
import fr.pandacube.lib.reflect.Reflect;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import net.kyori.adventure.identity.Identified;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.SkinConfiguration;
import net.md_5.bungee.api.connection.ProxiedPlayer;
@@ -18,10 +17,7 @@ import net.md_5.bungee.protocol.DefinedPacket;
import net.md_5.bungee.protocol.packet.ClientSettings;
import net.md_5.bungee.protocol.packet.PluginMessage;
import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.players.standalone.AbstractOnlinePlayer;
import fr.pandacube.lib.reflect.Reflect;
import fr.pandacube.lib.util.MinecraftVersion;
import java.util.Locale;
/**
* Represents any online player on a Bungeecord proxy.
@@ -53,11 +49,11 @@ public interface BungeeOnlinePlayer extends BungeeOffPlayer, AbstractOnlinePlaye
}
/**
* Gets the minecraft version of this players client.
* @return the minecraft version of this players client.
* Gets the protocol version of this players client.
* @return the protocol version of this players client.
*/
default MinecraftVersion getMinecraftVersion() {
return MinecraftVersion.getVersion(getBungeeProxiedPlayer().getPendingConnection().getVersion());
default ProtocolVersion getProtocolVersion() {
return ProtocolVersion.ofProtocol(getBungeeProxiedPlayer().getPendingConnection().getVersion());
}
@@ -88,33 +84,13 @@ public interface BungeeOnlinePlayer extends BungeeOffPlayer, AbstractOnlinePlaye
@Override
default void sendMessage(Component message) {
getBungeeProxiedPlayer().sendMessage(Chat.toBungee(message));
}
@Override
default void sendMessage(ComponentLike message, UUID sender) {
getBungeeProxiedPlayer().sendMessage(sender, Chat.toBungee(message.asComponent()));
}
@Override
default void sendMessage(Component message, Identified sender) {
getBungeeProxiedPlayer().sendMessage(sender == null ? null : sender.identity().uuid(), Chat.toBungee(message));
}
/**
* Display the provided message in the players chat, if they allows to display CHAT messages.
* @param message the message to send.
* @param sender the player causing the send of this message. Client side filtering may occur. May be null if we
* dont want client filtering, but still consider the message as CHAT message.
*/
default void sendMessage(ComponentLike message, ProxiedPlayer sender) {
getBungeeProxiedPlayer().sendMessage(sender == null ? null : sender.getUniqueId(), Chat.toBungee(message.asComponent()));
getBungeeProxiedPlayer().sendMessage(ChatBungee.toBungee(message));
}
@Override
default void sendTitle(Component title, Component subtitle, int fadeIn, int stay, int fadeOut) {
ProxyServer.getInstance().createTitle()
.title(Chat.toBungee(title)).subTitle(Chat.toBungee(subtitle))
.title(ChatBungee.toBungee(title)).subTitle(ChatBungee.toBungee(subtitle))
.fadeIn(fadeIn).stay(stay).fadeOut(fadeOut)
.send(getBungeeProxiedPlayer());
}

View File

@@ -0,0 +1,45 @@
package fr.pandacube.lib.bungee.util;
import fr.pandacube.lib.util.log.DailyLogRotateFileHandler;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.log.ConciseFormatter;
import java.util.logging.Filter;
import java.util.logging.Level;
import java.util.logging.LogRecord;
/**
* A log rotate that extends the functionalities of {@link DailyLogRotateFileHandler}
* to adapt with bungee specificities.
*/
public class BungeeDailyLogRotateFileHandler extends DailyLogRotateFileHandler {
/**
* Initialize this file handler.
* @param hideInitialHandlerLogEntries true if we want to hide some InitialHandler log entries
*/
public static void init(boolean hideInitialHandlerLogEntries) {
ProxyServer.getInstance().getLogger().addHandler(new BungeeDailyLogRotateFileHandler(hideInitialHandlerLogEntries));
}
private BungeeDailyLogRotateFileHandler(boolean hideInitialHandlerLogEntries) {
if (hideInitialHandlerLogEntries)
setFilter(new InitialHandlerLogRemover());
setFormatter(new ConciseFormatter(false));
setLevel(Level.parse(System.getProperty("net.md_5.bungee.file-log-level", "INFO")));
}
private class InitialHandlerLogRemover implements Filter {
@Override
public boolean isLoggable(LogRecord record) {
String formattedRecord = getFormatter().format(record);
return !(
formattedRecord.contains("<-> InitialHandler has connected")
|| formattedRecord.contains("<-> InitialHandler has pinged")
);
}
}
}

View File

@@ -1,147 +0,0 @@
package fr.pandacube.lib.bungee.util;
import com.google.common.io.Files;
import fr.pandacube.lib.bungee.PandaLibBungee;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.log.ConciseFormatter;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.logging.ErrorManager;
import java.util.logging.Filter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.zip.GZIPOutputStream;
public class DailyLogRotateFileHandler extends Handler {
private static final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
public static void init(boolean hideInitialHandlerLogEntries) {
ProxyServer.getInstance().getLogger().addHandler(new DailyLogRotateFileHandler(hideInitialHandlerLogEntries));
}
private BufferedWriter currentFile = null;
private String currentFileDate = getCurrentDay();
private boolean closed = false;
private DailyLogRotateFileHandler(boolean hideInitialHandlerLogEntries) {
if (hideInitialHandlerLogEntries)
setFilter(new InitialHandlerLogRemover());
setFormatter(new ConciseFormatter(false));
setLevel(Level.parse(System.getProperty("net.md_5.bungee.file-log-level", "INFO")));
}
@Override
public synchronized void close() throws SecurityException {
closed = true;
if (currentFile != null) try {
currentFile.close();
} catch (IOException ignored) {
}
}
@Override
public synchronized void flush() {
if (closed) return;
if (currentFile == null) return;
try {
currentFile.flush();
} catch (IOException ignored) {
}
}
@Override
public synchronized void publish(LogRecord record) {
if (closed || !isLoggable(record))
return;
if (currentFile == null || !currentFileDate.equals(getCurrentDay()))
changeFile();
if (currentFile == null)
return;
String formattedMessage;
try {
formattedMessage = getFormatter().format(record);
} catch (Exception ex) {
reportError(null, ex, ErrorManager.FORMAT_FAILURE);
return;
}
try {
currentFile.write(formattedMessage);
currentFile.flush();
} catch (Exception ex) {
reportError(null, ex, ErrorManager.WRITE_FAILURE);
}
}
private void changeFile() {
if (currentFile != null) {
try {
currentFile.flush();
currentFile.close();
} catch (IOException ignored) {
}
File fileNewName = new File("logs/" + currentFileDate + ".log");
new File("logs/latest.log").renameTo(fileNewName);
ProxyServer.getInstance().getScheduler().runAsync(PandaLibBungee.getPlugin(), () -> compress(fileNewName));
}
currentFileDate = getCurrentDay();
try {
File logDir = new File("logs");
logDir.mkdir();
currentFile = new BufferedWriter(new FileWriter("logs/latest.log", true));
} catch (SecurityException | IOException e) {
reportError("Erreur lors de l'initialisation d'un fichier log", e, ErrorManager.OPEN_FAILURE);
currentFile = null;
}
}
private String getCurrentDay() {
return dateFormat.format(new Date());
}
private void compress(File sourceFile) {
File destFile = new File(sourceFile.getParentFile(), sourceFile.getName() + ".gz");
if (destFile.exists())
destFile.delete();
try (GZIPOutputStream os = new GZIPOutputStream(new FileOutputStream(destFile))) {
Files.copy(sourceFile, os);
} catch (IOException e) {
if (destFile.exists())
destFile.delete();
throw new RuntimeException(e);
}
sourceFile.delete();
}
private class InitialHandlerLogRemover implements Filter {
@Override
public boolean isLoggable(LogRecord record) {
String formattedRecord = getFormatter().format(record);
if (formattedRecord.contains("<-> InitialHandler has connected")) return false;
if (formattedRecord.contains("<-> InitialHandler has pinged")) return false;
return true;
}
}
}

View File

@@ -26,26 +26,35 @@
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>4.11.0</version>
<version>4.15.0</version>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-platform-bungeecord</artifactId>
<version>4.1.1</version>
<artifactId>adventure-text-serializer-gson</artifactId>
<version>4.13.0</version>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-legacy</artifactId>
<version>4.13.0</version>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-serializer-plain</artifactId>
<version>4.11.0</version>
<version>4.15.0</version>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-minimessage</artifactId>
<version>4.15.0</version>
</dependency>
<dependency>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-chat</artifactId>
<version>${bungeecord.version}</version>
<scope>compile</scope>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
</dependencies>

View File

@@ -1,15 +1,12 @@
package fr.pandacube.lib.chat;
import java.awt.Color;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.UnaryOperator;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentBuilder;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.TranslationArgument;
import net.kyori.adventure.text.TranslationArgumentLike;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.event.HoverEventSource;
@@ -18,11 +15,17 @@ import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.format.TextDecoration.State;
import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
import org.jetbrains.annotations.NotNull;
import java.awt.*;
import java.util.Locale;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.UnaryOperator;
/**
* A builder for chat components.
@@ -30,10 +33,10 @@ import net.md_5.bungee.api.chat.BaseComponent;
* Use one of the provided static methods to create a new instance.
* <p>
* This class implements {@link ComponentLike} and {@link HoverEventSource} so they can be used directly in
* Adventure API and its implentation without using the final methods of this builder.
* Adventure API and its implementation without using the final methods of this builder.
* <p>
* The unique possible concrete subclass of this class, {@link FormatableChat}, takes care of the formating of the
* builded component. The rationale for this design is explained in the documentation of {@link FormatableChat}.
* The unique possible concrete subclass of this class, {@link FormatableChat}, takes care of the formatting of the
* built component. The rationale for this design is explained in the documentation of {@link FormatableChat}.
*/
public abstract sealed class Chat extends ChatStatic implements HoverEventSource<Component>, ComponentLike {
@@ -60,61 +63,70 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
/**
* Builds the component into Adventure Component instance.
* @return the {@link Component} builded from this {@link Chat} component.
* @return the {@link Component} built from this {@link Chat} component.
*/
public Component getAdv() {
public Component get() {
return builder.build();
}
/**
* Builds the component into BungeeCord {@link BaseComponent} instance.
* @return the {@link BaseComponent} builded from this {@link Chat} component.
*/
public BaseComponent get() {
return toBungee(getAdv());
}
/**
* Builds the component into BungeeCord {@link BaseComponent} array.
* @return the {@link BaseComponent} array builded from this {@link Chat} component.
*/
public BaseComponent[] getAsArray() {
return toBungeeArray(getAdv());
}
private static final LegacyComponentSerializer LEGACY_SERIALIZER_BUNGEE_FIENDLY = LegacyComponentSerializer.builder()
private static final LegacyComponentSerializer LEGACY_SERIALIZER_BUNGEE_FRIENDLY = LegacyComponentSerializer.builder()
.hexColors()
.useUnusualXRepeatedCharacterHexFormat()
.build();
/**
* Converts the builded component into legacy text.
* Converts the built component into legacy text.
* @return the legacy text. RGB colors are in BungeeCord format.
*/
public String getLegacyText() {
return LEGACY_SERIALIZER_BUNGEE_FIENDLY.serialize(getAdv());
return LEGACY_SERIALIZER_BUNGEE_FRIENDLY.serialize(get());
}
/**
* Converts the builded component into plain text.
* Converts the built component into plain text.
* @return the plain text of this component.
*/
public String getPlainText() {
return PlainTextComponentSerializer.plainText().serializeOr(getAdv(), "");
return PlainTextComponentSerializer.plainText().serializeOr(get(), "");
}
@Override
public HoverEvent<Component> asHoverEvent(UnaryOperator<Component> op) {
return HoverEvent.showText(op.apply(getAdv()));
public @NotNull HoverEvent<Component> asHoverEvent(@NotNull UnaryOperator<Component> op) {
return HoverEvent.showText(op.apply(get()));
}
/**
* Builds the component into Adventure Component instance.
* @return the {@link Component} builded from this {@link Chat} component.
* @return the {@link Component} built from this {@link Chat} component.
*/
@Override
public Component asComponent() {
return getAdv();
public @NotNull Component asComponent() {
return get();
}
/**
* Builds the component into Adventure Component instance, also down sampling the RGB colors to named colors.
* @return the {@link Component} built from this {@link Chat} component, with down-sampled colors.
*/
public Component getAsDownSampledColorsComponent() {
String json = GsonComponentSerializer.colorDownsamplingGson().serialize(get());
return GsonComponentSerializer.gson().deserialize(json);
}
/**
* Returns a new {@link Chat} consisting of this {@link Chat} instance, with the RGB colors down-sampled to named colors.
* @return a new {@link Chat} instance, with down-sampled colors.
*/
public Chat getAsDownSampledColors() {
return chatComponent(getAsDownSampledColorsComponent());
}
/**
* Returns a MiniMessage representation of this {@link Chat} component.
* @return the MiniMessage representation if this {@link Chat} component.
*/
public String getMiniMessage() {
return MiniMessage.miniMessage().serialize(get());
}
@@ -154,15 +166,6 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
return this;
}
/**
* Appends a BungeeCord {@link BaseComponent} to this component.
* @param comp the component to append.
* @return this.
*/
public Chat then(BaseComponent comp) {
return then(toAdventure(comp));
}
/**
* Appends a component to this component.
* @param comp the component to append.
@@ -177,15 +180,6 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
return then(comp.asComponent());
}
/**
* Appends a BungeeCord {@link BaseComponent} array to this component.
* @param comp the components to append.
* @return this.
*/
public Chat then(BaseComponent[] comp) {
return then(toAdventure(comp));
}
@@ -260,7 +254,7 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
* @param comp the component.
* @return this.
*/
public Chat thenPlayerName(Component comp) { return then(playerNameComponent(comp)); }
public Chat thenPlayerName(ComponentLike comp) { return then(playerNameComponent(comp)); }
/**
* Appends a component consisting of a new line.
@@ -269,12 +263,26 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
public Chat thenNewLine() { return then(Component.newline()); }
/**
* Appends a component with the provided legacy text as its content.
* @param legacyText the legacy text.
* Appends a component with the provided legacy text as its content, using the section {@code "§"} character.
* @param legacyText the legacy text that uses the {@code "§"} character.
* @return this.
*/
public Chat thenLegacyText(Object legacyText) { return then(legacyText(legacyText)); }
/**
* Appends a component with the provided legacy text as its content, using the ampersand {@code "&"} character.
* @param legacyText the legacy text that uses the {@code "&"} character.
* @return this.
*/
public Chat thenLegacyAmpersandText(Object legacyText) { return then(legacyAmpersandText(legacyText)); }
/**
* Appends a component with the provided MiniMessage text as its content.
* @param miniMessageText the MiniMessage text.
* @return this.
*/
public Chat thenMiniMessage(String miniMessageText) { return then(miniMessageText(miniMessageText)); }
/**
* Appends a component with the provided translation key and parameters.
* @param key the translation key.
@@ -284,8 +292,8 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
public Chat thenTranslation(String key, Object... with) { return then(translation(key, with)); }
/**
* Appends a component with the provided keybind.
* @param key the keybind to display.
* Appends a component with the provided keybinding.
* @param key the keybinding to display.
* @return this.
*/
public Chat thenKeyBind(String key) { return then(keybind(key)); }
@@ -443,50 +451,28 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
/**
* Appends a component filling a line of chat (or console) with the configured decoration character and
* Appends a component filling a chat line with the configured decoration character and
* color and a left-aligned text.
* @param leftText the text aligned to the left.
* @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character
* @return a new {@link FormatableChat} filling a chat line with the configured decoration character
* and color and a left-aligned text.
*/
public Chat thenLeftText(ComponentLike leftText) { return then(leftText(leftText, console)); }
/**
* Appends a component filling a line of chat (or console) with the configured decoration character and
* color and a left-aligned text.
* @param leftText the text aligned to the left.
* @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character
* and color and a left-aligned text.
* @deprecated uses Bungeecord chat API.
*/
@Deprecated
public Chat thenLeftText(BaseComponent leftText) { return thenLeftText(chatComponent(leftText)); }
/**
* Appends a component filling a line of chat (or console) with the configured decoration character and
* Appends a component filling a chat line with the configured decoration character and
* color and a right-aligned text.
* @param rightText the text aligned to the right.
* @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character
* @return a new {@link FormatableChat} filling a chat line with the configured decoration character
* and color and a right-aligned text.
*/
public Chat thenRightText(ComponentLike rightText) { return then(rightText(rightText, console)); }
/**
* Appends a component filling a line of chat (or console) with the configured decoration character and
* color and a right-aligned text.
* @param rightText the text aligned to the right.
* @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character
* and color and a right-aligned text.
* @deprecated uses Bungeecord chat API.
*/
@Deprecated
public Chat thenRightText(BaseComponent rightText) { return thenRightText(chatComponent(rightText)); }
/**
* Appends a component filling a line of chat (or console) with the configured decoration character and
* Appends a component filling a chat line with the configured decoration character and
* color and a centered text.
* @param centerText the text aligned to the center.
* @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character
* @return a new {@link FormatableChat} filling a chat line with the configured decoration character
* and color and a centered text.
*/
public Chat thenCenterText(ComponentLike centerText) {
@@ -494,21 +480,8 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
}
/**
* Appends a component filling a line of chat (or console) with the configured decoration character and
* color and a centered text.
* @param centerText the text aligned to the center.
* @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character
* and color and a centered text.
* @deprecated uses Bungeecord chat API.
*/
@Deprecated
public Chat thenCenterText(BaseComponent centerText) {
return thenCenterText(chatComponent(centerText));
}
/**
* Appends a component filling a line of chat (or console) with the configured decoration character and color.
* @return a new {@link FormatableChat} filling a line of chat (or console) with a decoration character and color.
* Appends a component filling a chat line with the configured decoration character and color.
* @return a new {@link FormatableChat} filling a chat line with a decoration character and color.
*/
public Chat thenFilledLine() { return then(filledLine(console)); }
@@ -534,11 +507,11 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
* .append("!").color(ChatColor.RED)
* .create();
* }</pre>
* Here, when you call a formating method (like {@code bold(boolean)} or {@code color(ChatColor)}) after the
* {@code append(String)} method, the formating apply to the last sub-component appended.
* Here, when you call a formatting method (like {@code bold(boolean)} or {@code color(ChatColor)}) after the
* {@code append(String)} method, the formatting apply to the last subcomponent appended.
* <p>
* In our design, we want the formating to apply to the currently builded component, not the last appended one.
* The purpose is to make the component structure clearer and have better control of the formating over the
* In our design, we want the formatting to apply to the currently built component, not the last appended one.
* The purpose is to make the component structure clearer and have better control of the formatting over the
* component hierarchy.
* Here is the equivalent of the above code, with the {@link Chat} API:
* <pre>{@code
@@ -547,9 +520,9 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
* .thenText("!"); // short for .then(Chat.text("!"))
* // the red color for "!" is not needed because the parent component is already red.
* }</pre>
* When calling {@link #then(Component) #then(...)} on a {@link FormatableChat}, the method returns itself, casted
* to {@link Chat}, to prevent future formating (that the programmer would think it formats the previously appended
* sub-component). If the formatting of the currently builded component is needed, since {@link Chat} is a sealed
* When calling {@link #then(Component) #then(...)} on a {@link FormatableChat}, the method returns itself, cast
* to {@link Chat}, to prevent future formatting (that the programmer would think it formats the previously appended
* subcomponent). If the formatting of the currently built component is needed, since {@link Chat} is a sealed
* class which only subclass is {@link FormatableChat}, you can cast the builder, and use the format methods again.
* <pre>{@code
* Chat component = Chat.text("Hello ").red()
@@ -585,12 +558,6 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
* @return this.
*/
public FormatableChat color(TextColor c) { builder.color(c); return this; }
/**
* Sets the color of this component.
* @param c the color.
* @return this.
*/
public FormatableChat color(ChatColor c) { return color(c == null ? null : TextColor.color(c.getColor().getRGB())); }
/**
* Sets the color of this component.
* @param c the color.
@@ -602,7 +569,16 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
* @param c the color.
* @return this.
*/
public FormatableChat color(String c) { return color(c == null ? null : ChatColor.of(c)); }
public FormatableChat color(String c) {
if (c == null)
return color((TextColor) null);
TextColor tc = c.startsWith("#")
? TextColor.fromCSSHexString(c)
: NamedTextColor.NAMES.value(c.toLowerCase(Locale.ROOT));
if (tc == null)
throw new IllegalArgumentException("Invalid color string '" + c + "'.");
return color(tc);
}
/**
@@ -880,18 +856,6 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
* @return this.
*/
public FormatableChat hover(ComponentLike v) { return hover(v.asComponent()); }
/**
* Configure this component to show the provided component when hovered.
* @param v the component to show.
* @return this.
*/
public FormatableChat hover(BaseComponent v) { return hover(toAdventure(v)); }
/**
* Configure this component to show the provided component when hovered.
* @param v the component to show.
* @return this.
*/
public FormatableChat hover(BaseComponent[] v) { return hover(toAdventure(v)); }
/**
* Configure this component to show the provided legacy text when hovered.
* @param legacyText the legacy text to show.
@@ -919,7 +883,7 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
@Override
public int hashCode() {
return getAdv().hashCode();
return get().hashCode();
}
@Override
@@ -930,63 +894,44 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
/* package */ static ComponentLike filterObjToComponentLike(Object v) {
return switch (v) {
case ComponentLike componentLike -> componentLike;
case null, default -> Component.text(Objects.toString(v));
};
}
/* package */ static ComponentLike[] filterObjToComponentLike(Object[] values) {
if (values == null)
return null;
ComponentLike[] ret = new ComponentLike[values.length];
for (int i = 0; i < values.length; i++) {
Object v = values[i];
if (v instanceof BaseComponent[])
ret[i] = toAdventure((BaseComponent[]) v);
else if (v instanceof BaseComponent)
ret[i] = toAdventure((BaseComponent) v);
else if (v instanceof ComponentLike)
ret[i] = (ComponentLike) v;
else
ret[i] = Component.text(Objects.toString(v));
ret[i] = filterObjToComponentLike(values[i]);
}
return ret;
}
/**
* Converts the Bungee {@link BaseComponent} array into Adventure {@link Component}.
* @param components the Bungee {@link BaseComponent} array.
* @return a {@link Component}.
*/
public static Component toAdventure(BaseComponent[] components) {
return BungeeComponentSerializer.get().deserialize(components);
}
/**
* Converts the Bungee {@link BaseComponent} into Adventure {@link Component}.
* @param component the Bungee {@link BaseComponent}.
* @return a {@link Component}.
*/
public static Component toAdventure(BaseComponent component) {
return toAdventure(new BaseComponent[] { component });
/* package */ static TranslationArgumentLike[] filterObjToTranslationArgumentLike(Object[] values) {
if (values == null)
return null;
TranslationArgumentLike[] ret = new TranslationArgumentLike[values.length];
for (int i = 0; i < values.length; i++) {
Object v = values[i];
if (v instanceof Number n)
ret[i] = TranslationArgument.numeric(n);
else if (v instanceof Boolean b)
ret[i] = TranslationArgument.bool(b);
else
ret[i] = TranslationArgument.component(filterObjToComponentLike(values[i]));
}
return ret;
}
/**
* Converts the Adventure {@link Component} into Bungee {@link BaseComponent} array.
* @param component the Adventure {@link Component}.
* @return a {@link BaseComponent} array.
*/
public static BaseComponent[] toBungeeArray(Component component) {
return BungeeComponentSerializer.get().serialize(component);
}
/**
* Converts the Adventure {@link Component} into Bungee {@link BaseComponent}.
* @param component the Adventure {@link Component}.
* @return a {@link BaseComponent}.
*/
public static BaseComponent toBungee(Component component) {
BaseComponent[] arr = toBungeeArray(component);
return arr.length == 1 ? arr[0] : new net.md_5.bungee.api.chat.TextComponent(arr);
}
/**
* Force the italic formating to be set to false if it is not explicitely set in the component.
* Force the italic formatting to be set to false if it is not explicitly set in the component.
* This is useful for item lores that defaults to italic in the game UI.
* @param c the {@link Chat} in which to set the italic property if needed.
* @return the provided {@link Chat} instance.

View File

@@ -4,15 +4,30 @@ import java.util.ArrayList;
import java.util.List;
import net.kyori.adventure.text.format.TextColor;
import org.jetbrains.annotations.NotNull;
/**
* A custom gradient with a least 2 colors in it.
* A custom gradient with at least 2 colors in it.
*/
public class ChatColorGradient {
private record GradientColor(float location, TextColor color) { }
private record GradientColor(
float location,
TextColor color
) implements Comparable<GradientColor> {
@Override
public int compareTo(@NotNull ChatColorGradient.GradientColor o) {
return Float.compare(location(), o.location());
}
}
private final List<GradientColor> colors = new ArrayList<>();
/**
* Create the custom gradient.
*/
public ChatColorGradient() {}
/**
* Put a specific color at a specific location in the gradient.
* @param gradientLocation the location in the gradient.
@@ -21,6 +36,7 @@ public class ChatColorGradient {
*/
public synchronized ChatColorGradient add(float gradientLocation, TextColor gradientColor) {
colors.add(new GradientColor(gradientLocation, gradientColor));
colors.sort(null);
return this;
}
@@ -31,25 +47,26 @@ public class ChatColorGradient {
*/
public synchronized TextColor pickColorAt(float gradientLocation) {
if (colors.isEmpty())
throw new IllegalStateException("Must define at least one color in this ChatValueGradient instance.");
throw new IllegalStateException("Must define at least one color in this ChatColorGradient instance.");
if (colors.size() == 1)
return colors.get(0).color();
return colors.getFirst().color();
colors.sort((p1, p2) -> Float.compare(p1.location(), p2.location()));
if (gradientLocation <= colors.get(0).location())
return colors.get(0).color();
if (gradientLocation >= colors.get(colors.size() - 1).location())
return colors.get(colors.size() - 1).color();
int p1 = 1;
for (; p1 < colors.size(); p1++) {
if (colors.get(p1).location() >= gradientLocation)
int i = 0;
for (; i < colors.size(); i++) {
if (gradientLocation <= colors.get(i).location())
break;
}
int p0 = p1 - 1;
float v0 = colors.get(p0).location(), v1 = colors.get(p1).location();
TextColor cc0 = colors.get(p0).color(), cc1 = colors.get(p1).color();
return ChatColorUtil.interpolateColor(v0, v1, gradientLocation, cc0, cc1);
if (i == 0)
return colors.get(i).color();
if (i == colors.size())
return colors.getLast().color();
int p = i - 1;
float pLoc = colors.get(p).location();
float iLoc = colors.get(i).location();
TextColor pCol = colors.get(p).color();
TextColor iCol = colors.get(i).color();
return ChatColorUtil.interpolateColor(pLoc, iLoc, gradientLocation, pCol, iCol);
}
}

View File

@@ -1,20 +1,20 @@
package fr.pandacube.lib.chat;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.format.TextFormat;
import net.kyori.adventure.util.RGBLike;
import java.util.regex.Pattern;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.util.RGBLike;
import net.md_5.bungee.api.ChatColor;
/**
* Provides methods to manipulate legacy colors and {@link ChatColor} class.
* Provides methods to manipulate legacy colors.
*/
public class ChatColorUtil {
/**
* All characters that represent a colorcode.
* All characters that represent a color code.
*/
public static final String ALL_COLORS = "0123456789AaBbCcDdEeFf";
/**
@@ -30,7 +30,7 @@ public class ChatColorUtil {
* Returns the legacy format needed to reproduce the format at the end of the provided legacy text.
* Supports standard chat colors and formats, BungeeCord Chat rgb format and EssentialsX rgb format.
* The RGB value from EssentialsX format is converted to BungeeCord Chat when included in the returned value.
* @param legacyText the legacy formated text.
* @param legacyText the legacy formatted text.
* @return the active format at the end of the provided text.
*/
public static String getLastColors(String legacyText) {
@@ -38,12 +38,12 @@ public class ChatColorUtil {
int length = legacyText.length();
for (int index = length - 2; index >= 0; index--) {
if (legacyText.charAt(index) == ChatColor.COLOR_CHAR) {
if (legacyText.charAt(index) == LegacyChatFormat.COLOR_CHAR) {
// detection of rgb color §x§0§1§2§3§4§5
String rgb;
if (index > 11
&& legacyText.charAt(index - 12) == ChatColor.COLOR_CHAR
&& legacyText.charAt(index - 12) == LegacyChatFormat.COLOR_CHAR
&& (legacyText.charAt(index - 11) == 'x'
|| legacyText.charAt(index - 11) == 'X')
&& HEX_COLOR_PATTERN.matcher(rgb = legacyText.substring(index - 12, index + 2)).matches()) {
@@ -64,7 +64,7 @@ public class ChatColorUtil {
// try detect non-rgb format
char colorChar = legacyText.charAt(index + 1);
ChatColor legacyColor = getChatColorByChar(colorChar);
LegacyChatFormat legacyColor = LegacyChatFormat.of(colorChar);
if (legacyColor != null) {
result.insert(0, legacyColor);
@@ -83,15 +83,6 @@ public class ChatColorUtil {
return result.toString();
}
/**
* Returns the {@link ChatColor} associated with the provided char, case insensitive.
* @param code the case insensitive char code.
* @return the corresponding {@link ChatColor}.
*/
public static ChatColor getChatColorByChar(char code) {
return ChatColor.getByChar(Character.toLowerCase(code));
}
@@ -99,7 +90,7 @@ public class ChatColorUtil {
* Translate the color code of the provided string, that uses the alt color char, to the {@code §} color code
* format.
* <p>
* This method is the improved version of {@link ChatColor#translateAlternateColorCodes(char, String)},
* This method is the improved version of Bukkits {@code ChatColor.translateAlternateColorCodes(char, String)},
* because it takes into account essentials RGB color code, and {@code altColorChar} escaping (by doubling it).
* Essentials RGB color code are converted to Bungee chat RGB format, so the returned string can be converted
* to component (see {@link Chat#legacyText(Object)}).
@@ -112,7 +103,7 @@ public class ChatColorUtil {
*/
public static String translateAlternateColorCodes(char altColorChar, String textToTranslate)
{
char colorChar = ChatColor.COLOR_CHAR;
char colorChar = LegacyChatFormat.COLOR_CHAR;
StringBuilder acc = new StringBuilder();
char[] b = textToTranslate.toCharArray();
for ( int i = 0; i < b.length; i++ )
@@ -180,7 +171,7 @@ public class ChatColorUtil {
* @return the text fully italic.
*/
public static String forceItalic(String legacyText) {
return forceFormat(legacyText, ChatColor.ITALIC);
return forceFormat(legacyText, TextDecoration.ITALIC);
}
/**
@@ -190,7 +181,7 @@ public class ChatColorUtil {
* @return the text fully bold.
*/
public static String forceBold(String legacyText) {
return forceFormat(legacyText, ChatColor.BOLD);
return forceFormat(legacyText, TextDecoration.BOLD);
}
/**
@@ -200,7 +191,7 @@ public class ChatColorUtil {
* @return the text fully underlined.
*/
public static String forceUnderline(String legacyText) {
return forceFormat(legacyText, ChatColor.UNDERLINE);
return forceFormat(legacyText, TextDecoration.UNDERLINED);
}
/**
@@ -210,7 +201,7 @@ public class ChatColorUtil {
* @return the text fully stroked through.
*/
public static String forceStrikethrough(String legacyText) {
return forceFormat(legacyText, ChatColor.STRIKETHROUGH);
return forceFormat(legacyText, TextDecoration.STRIKETHROUGH);
}
/**
@@ -220,15 +211,16 @@ public class ChatColorUtil {
* @return the text fully obfuscated.
*/
public static String forceObfuscated(String legacyText) {
return forceFormat(legacyText, ChatColor.MAGIC);
return forceFormat(legacyText, TextDecoration.OBFUSCATED);
}
private static String forceFormat(String legacyText, ChatColor format) {
private static String forceFormat(String legacyText, TextFormat format) {
String formatStr = LegacyChatFormat.of(format).toString();
return format + legacyText
.replace(format.toString(), "") // remove previous tag to make the result cleaner
.replaceAll("§([a-frA-FR\\d])", "§$1" + format);
.replace(formatStr, "") // remove previous tag to make the result cleaner
.replaceAll("§([a-frA-FR\\d])", "§$1" + formatStr);
}
@@ -243,40 +235,12 @@ public class ChatColorUtil {
* @return the resulting text.
*/
public static String resetToColor(String legacyText, String color) {
return legacyText.replace(ChatColor.RESET.toString(), color);
return legacyText.replace(LegacyChatFormat.RESET.toString(), color);
}
/**
* Converts the provided {@link ChatColor} to its Adventure counterpart.
* @param bungee a BungeeCord {@link ChatColor} instance.
* @return the {@link TextColor} equivalent to the provided {@link ChatColor}.
*/
public static TextColor toAdventure(ChatColor bungee) {
if (bungee == null)
return null;
if (bungee.getColor() == null)
throw new IllegalArgumentException("The provided Bungee ChatColor must be an actual color (not format nor reset).");
return TextColor.color(bungee.getColor().getRGB());
}
/**
* Converts the provided {@link TextColor} to its BungeeCord counterpart.
* @param col a Adventure {@link TextColor} instance.
* @return the {@link ChatColor} equivalent to the provided {@link TextColor}.
*/
public static ChatColor toBungee(TextColor col) {
if (col == null)
return null;
if (col instanceof NamedTextColor) {
return ChatColor.of(((NamedTextColor) col).toString());
}
return ChatColor.of(col.asHexString());
}
/**
* Create a color, interpolating between 2 colors.
* @param v0 the value corresponding to color {@code cc0}.
@@ -293,4 +257,7 @@ public class ChatColorUtil {
}
private ChatColorUtil() {}
}

View File

@@ -8,6 +8,7 @@ import net.kyori.adventure.text.format.TextColor;
/**
* Class holding static configuration values for chat component rendering.
*/
@SuppressWarnings("CanBeFinal")
public class ChatConfig {
/**
@@ -29,7 +30,7 @@ public class ChatConfig {
/**
* The color used for successful messages.
*/
public static TextColor successColor = PandaTheme.CHAT_GREEN_SATMAX;
public static TextColor successColor = PandaTheme.CHAT_GREEN_MAX_SAT;
/**
* The color used for error/failure messages.
@@ -49,7 +50,7 @@ public class ChatConfig {
/**
* The color used to display data in a message.
*/
public static TextColor dataColor = PandaTheme.CHAT_GRAY_MID;
public static TextColor dataColor = NamedTextColor.GRAY;
/**
* The color used for displayed URLs and clickable URLs.
@@ -67,14 +68,14 @@ public class ChatConfig {
public static TextColor highlightedCommandColor = NamedTextColor.WHITE;
/**
* The color used for broadcasted messages.
* The color used for broadcast messages.
* It is often used in combination with {@link #prefix}.
*/
public static TextColor broadcastColor = NamedTextColor.YELLOW;
/**
* The prefix used for prefixed messages.
* It can be a sylized name of the server, like {@code "[Pandacube] "}.
* It can be a stylized name of the server, like {@code "[Pandacube] "}.
* It is often used in combination with {@link #broadcastColor}.
*/
public static Supplier<Chat> prefix = PandaTheme::CHAT_MESSAGE_PREFIX;
@@ -86,48 +87,69 @@ public class ChatConfig {
*/
public static int getPrefixWidth(boolean console) {
Chat c;
return prefix == null ? 0 : (c = prefix.get()) == null ? 0 : ChatUtil.componentWidth(c.getAdv(), console);
return prefix == null ? 0 : (c = prefix.get()) == null ? 0 : ChatUtil.componentWidth(c.get(), console);
}
/**
* A set of predefined colors.
*/
public static class PandaTheme {
/** Green 1 color. */
public static final TextColor CHAT_GREEN_1_NORMAL = TextColor.fromHexString("#3db849"); // h=126 s=50 l=48
/** Green 2 color. */
public static final TextColor CHAT_GREEN_2 = TextColor.fromHexString("#5ec969"); // h=126 s=50 l=58
/** Green 3 color. */
public static final TextColor CHAT_GREEN_3 = TextColor.fromHexString("#85d68d"); // h=126 s=50 l=68
/** Green 4 color. */
public static final TextColor CHAT_GREEN_4 = TextColor.fromHexString("#abe3b0"); // h=126 s=50 l=78
public static final TextColor CHAT_GREEN_SATMAX = TextColor.fromHexString("#00ff19"); // h=126 s=100 l=50
/** Green max saturation color. */
public static final TextColor CHAT_GREEN_MAX_SAT = TextColor.fromHexString("#00ff19"); // h=126 s=100 l=50
/** Green 1 saturated color. */
public static final TextColor CHAT_GREEN_1_SAT = TextColor.fromHexString("#20d532"); // h=126 s=50 l=48
/** Green 2 saturated color. */
public static final TextColor CHAT_GREEN_2_SAT = TextColor.fromHexString("#45e354"); // h=126 s=50 l=58
/** Green 3 saturated color. */
public static final TextColor CHAT_GREEN_3_SAT = TextColor.fromHexString("#71ea7d"); // h=126 s=50 l=68
/** Green 4 saturated color. */
public static final TextColor CHAT_GREEN_4_SAT = TextColor.fromHexString("#9df0a6"); // h=126 s=50 l=78
/** Brown 1 color. */
public static final TextColor CHAT_BROWN_1 = TextColor.fromHexString("#b26d3a"); // h=26 s=51 l=46
/** Brown 2 color. */
public static final TextColor CHAT_BROWN_2 = TextColor.fromHexString("#cd9265"); // h=26 s=51 l=60
/** Brown 3 color. */
public static final TextColor CHAT_BROWN_3 = TextColor.fromHexString("#e0bb9f"); // h=26 s=51 l=75
/** Brown 1 saturated color. */
public static final TextColor CHAT_BROWN_1_SAT = TextColor.fromHexString("#b35c19"); // h=26 s=75 l=40
/** Brown 2 saturated color. */
public static final TextColor CHAT_BROWN_2_SAT = TextColor.fromHexString("#e28136"); // h=26 s=51 l=55
/** Brown 3 saturated color. */
public static final TextColor CHAT_BROWN_3_SAT = TextColor.fromHexString("#ecab79"); // h=26 s=51 l=70
/** Gray medium color. */
public static final TextColor CHAT_GRAY_MID = TextColor.fromHexString("#888888");
/** Red failure color. */
public static final TextColor CHAT_RED_FAILURE = TextColor.fromHexString("#ff3333");
/** Color used for private message prefix decoration. */
public static final TextColor CHAT_PM_PREFIX_DECORATION = CHAT_BROWN_2_SAT;
/** Color used for sent message text. */
public static final TextColor CHAT_PM_SELF_MESSAGE = CHAT_GREEN_2;
/** Color used for received message text. */
public static final TextColor CHAT_PM_OTHER_MESSAGE = CHAT_GREEN_4;
/** Discord color. */
public static final TextColor CHAT_DISCORD_LINK_COLOR = TextColor.fromHexString("#00aff4");
/**
* Generate a prefix for broadcast message.
* @return a prefix for broadcast message.
*/
public static Chat CHAT_MESSAGE_PREFIX() {
return Chat.text("[")
.broadcastColor()
@@ -135,5 +157,9 @@ public class ChatConfig {
.thenText("] ");
}
private PandaTheme() {}
}
private ChatConfig() {}
}

View File

@@ -59,6 +59,7 @@ public class ChatFilledLine implements ComponentLike {
private boolean decorationBold = false;
private int nbSide = ChatConfig.nbCharMargin;
private boolean spacesAroundText = false;
private boolean spacesDecorationRightSide = false;
private boolean console = false;
private Integer maxWidth = null;
@@ -116,6 +117,16 @@ public class ChatFilledLine implements ComponentLike {
return this;
}
/**
* If the {@link #decoChar(char)} is set to space, also add spaces at the right of the text
* to reach the desired width.
* @return this.
*/
public ChatFilledLine spacesDecorationRightSide() {
spacesDecorationRightSide = true;
return this;
}
/**
* Configure if the line will be rendered on console or not.
* @param console true for console, false for game UI.
@@ -140,7 +151,7 @@ public class ChatFilledLine implements ComponentLike {
/**
* Renders this line to a {@link FormatableChat}.
* @return a new {@link FormatableChat} builded by this {@link ChatFilledLine}.
* @return a new {@link FormatableChat} built by this {@link ChatFilledLine}.
*/
public FormatableChat toChat() {
int maxWidth = (this.maxWidth != null)
@@ -184,7 +195,7 @@ public class ChatFilledLine implements ComponentLike {
Chat d = Chat.chat()
.then(Chat.text(ChatUtil.repeatedChar(decorationChar, nbCharLeft)).color(decorationColor).bold(decorationBold))
.then(text);
if (decorationChar != ' ')
if (decorationChar != ' ' || spacesDecorationRightSide)
d.then(Chat.text(ChatUtil.repeatedChar(decorationChar, nbCharRight)).color(decorationColor).bold(decorationBold));
return (FormatableChat) d;
}

View File

@@ -1,7 +1,6 @@
package fr.pandacube.lib.chat;
import java.util.Objects;
import fr.pandacube.lib.chat.Chat.FormatableChat;
import net.kyori.adventure.text.BlockNBTComponent;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentBuilder;
@@ -16,10 +15,10 @@ import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.event.HoverEventSource;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.md_5.bungee.api.chat.BaseComponent;
import fr.pandacube.lib.chat.Chat.FormatableChat;
import java.util.Objects;
/**
* Abstract class holding the publicly accessible methods to create an instance of {@link Chat} component.
@@ -32,15 +31,6 @@ public abstract class ChatStatic {
return new FormatableChat(componentToBuilder(c));
}
/**
* Creates a {@link FormatableChat} from the provided Bungee {@link BaseComponent}.
* @param c the {@link BaseComponent}.
* @return a new {@link FormatableChat}.
*/
public static FormatableChat chatComponent(BaseComponent c) {
return new FormatableChat(componentToBuilder(Chat.toAdventure(c)));
}
/**
* Creates a {@link FormatableChat} from the provided {@link ComponentLike}.
* If the provided component is an instance of {@link Chat}, its content will be duplicated, and the provided one
@@ -60,15 +50,6 @@ public abstract class ChatStatic {
return new FormatableChat(Component.text());
}
/**
* Creates a {@link FormatableChat} from the provided Bungee {@link BaseComponent BaseComponent[]}.
* @param c the array of {@link BaseComponent}.
* @return a new {@link FormatableChat}.
*/
public static FormatableChat chatComponent(BaseComponent[] c) {
return chatComponent(Chat.toAdventure(c));
}
@@ -91,18 +72,58 @@ public abstract class ChatStatic {
/**
* Creates a {@link FormatableChat} with the provided legacy text as its content.
* @param legacyText the legacy text to use as the content.
* Creates a {@link FormatableChat} with the provided legacy text as its content, using the section {@code "§"}
* character.
* @param legacyText the legacy text to use as the content, that uses the {@code "§"} character.
* @return a new {@link FormatableChat} with the provided text as its content.
* @throws IllegalArgumentException If the {@code plainText} parameter is instance of {@link Chat} or
* @throws IllegalArgumentException If the {@code legacyText} parameter is instance of {@link Chat} or
* {@link Component}. The caller should use {@link #chatComponent(ComponentLike)}
* instead.
*/
public static FormatableChat legacyText(Object legacyText) {
return legacyText(legacyText, LegacyComponentSerializer.SECTION_CHAR);
}
/**
* Creates a {@link FormatableChat} with the provided legacy text as its content, using the ampersand {@code "&"}
* character.
* @param legacyText the legacy text to use as the content, that uses the {@code "&"} character.
* @return a new {@link FormatableChat} with the provided text as its content.
* @throws IllegalArgumentException If the {@code legacyText} parameter is instance of {@link Chat} or
* {@link Component}. The caller should use {@link #chatComponent(ComponentLike)}
* instead.
*/
public static FormatableChat legacyAmpersandText(Object legacyText) {
return legacyText(legacyText, LegacyComponentSerializer.AMPERSAND_CHAR);
}
/**
* Creates a {@link FormatableChat} with the provided legacy text as its content, using the specified
* legacyCharacter.
* @param legacyText the legacy text to use as the content.
* @param legacyCharacter the character used in the provided text to prefix color and format code.
* @return a new {@link FormatableChat} with the provided text as its content.
* @throws IllegalArgumentException If the {@code legacyText} parameter is instance of {@link Chat} or
* {@link Component}. The caller should use {@link #chatComponent(ComponentLike)}
* instead.
*/
private static FormatableChat legacyText(Object legacyText, char legacyCharacter) {
if (legacyText instanceof ComponentLike) {
throw new IllegalArgumentException("Expected any object except instance of " + ComponentLike.class + ". Received " + legacyText + ". Please use ChatStatic.chatComponent(ComponentLike) instead.");
}
return chatComponent(LegacyComponentSerializer.legacySection().deserialize(Objects.toString(legacyText)));
return chatComponent(LegacyComponentSerializer.legacy(legacyCharacter).deserialize(Objects.toString(legacyText)));
}
/**
* Creates a {@link FormatableChat} with the provided MiniMessage text as its content.
* @param miniMessageText the MiniMessage text to use as the content.
* @return a new {@link FormatableChat} with the provided text as its content.
*/
public static FormatableChat miniMessageText(String miniMessageText) {
return chatComponent(MiniMessage.miniMessage().deserialize(miniMessageText));
}
@@ -207,7 +228,7 @@ public abstract class ChatStatic {
* @param c the {@link Component}.
* @return a new {@link FormatableChat}.
*/
public static FormatableChat playerNameComponent(Component c) {
public static FormatableChat playerNameComponent(ComponentLike c) {
FormatableChat fc = chatComponent(c);
fc.builder.colorIfAbsent(NamedTextColor.WHITE);
return fc;
@@ -223,13 +244,13 @@ public abstract class ChatStatic {
* @return a new {@link FormatableChat} with the provided translation key and parameters.
*/
public static FormatableChat translation(String key, Object... with) {
return new FormatableChat(Component.translatable().key(key).args(Chat.filterObjToComponentLike(with)));
return new FormatableChat(Component.translatable().key(key).arguments(Chat.filterObjToTranslationArgumentLike(with)));
}
/**
* Creates a {@link FormatableChat} with the provided keybind.
* @param key the keybind to display.
* @return a new {@link FormatableChat} with the provided keybind.
* Creates a {@link FormatableChat} with the provided keybinding.
* @param key the keybinding to display.
* @return a new {@link FormatableChat} with the provided keybinding.
*/
public static FormatableChat keybind(String key) {
return new FormatableChat(Component.keybind().keybind(key));
@@ -451,12 +472,12 @@ public abstract class ChatStatic {
/**
* Creates a {@link FormatableChat} filling a line of chat (or console) with decoration and a left-aligned text.
* Creates a {@link FormatableChat} filling a chat line with decoration and a left-aligned text.
* @param text the text aligned to the left.
* @param decorationChar the character used for decoration around the text.
* @param decorationColor the color used for the decoration characters.
* @param console if the line is rendered on console (true) or IG (false).
* @return a new {@link FormatableChat} filling a line of chat (or console) with decoration and a left-aligned text.
* @return a new {@link FormatableChat} filling a chat line with decoration and a left-aligned text.
* @see ChatFilledLine#leftText(ComponentLike)
*/
public static FormatableChat leftText(ComponentLike text, char decorationChar, TextColor decorationColor, boolean console) {
@@ -464,11 +485,11 @@ public abstract class ChatStatic {
}
/**
* Creates a {@link FormatableChat} filling a line of chat (or console) with the configured decoration character and
* Creates a {@link FormatableChat} filling a chat line with the configured decoration character and
* color and a left-aligned text.
* @param text the text aligned to the left.
* @param console if the line is rendered on console (true) or IG (false).
* @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character
* @return a new {@link FormatableChat} filling a chat line with the configured decoration character
* and color and a left-aligned text.
* @see ChatFilledLine#leftText(ComponentLike)
* @see ChatConfig#decorationChar
@@ -479,12 +500,12 @@ public abstract class ChatStatic {
}
/**
* Creates a {@link FormatableChat} filling a line of chat (or console) with decoration and a right-aligned text.
* Creates a {@link FormatableChat} filling a chat line with decoration and a right-aligned text.
* @param text the text aligned to the right.
* @param decorationChar the character used for decoration around the text.
* @param decorationColor the color used for the decoration characters.
* @param console if the line is rendered on console (true) or IG (false).
* @return a new {@link FormatableChat} filling a line of chat (or console) with decoration and a right-aligned
* @return a new {@link FormatableChat} filling a chat line with decoration and a right-aligned
* text.
* @see ChatFilledLine#rightText(ComponentLike)
*/
@@ -493,11 +514,11 @@ public abstract class ChatStatic {
}
/**
* Creates a {@link FormatableChat} filling a line of chat (or console) with the configured decoration character and
* Creates a {@link FormatableChat} filling a chat line with the configured decoration character and
* color and a right-aligned text.
* @param text the text aligned to the right.
* @param console if the line is rendered on console (true) or IG (false).
* @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character
* @return a new {@link FormatableChat} filling a chat line with the configured decoration character
* and color and a right-aligned text.
* @see ChatFilledLine#rightText(ComponentLike)
* @see ChatConfig#decorationChar
@@ -508,12 +529,12 @@ public abstract class ChatStatic {
}
/**
* Creates a {@link FormatableChat} filling a line of chat (or console) with decoration and a centered text.
* Creates a {@link FormatableChat} filling a chat line with decoration and a centered text.
* @param text the text aligned to the center.
* @param decorationChar the character used for decoration around the text.
* @param decorationColor the color used for the decoration characters.
* @param console if the line is rendered on console (true) or IG (false).
* @return a new {@link FormatableChat} filling a line of chat (or console) with decoration and a centered text.
* @return a new {@link FormatableChat} filling a chat line with decoration and a centered text.
* @see ChatFilledLine#centerText(ComponentLike)
*/
public static FormatableChat centerText(ComponentLike text, char decorationChar, TextColor decorationColor, boolean console) {
@@ -521,11 +542,11 @@ public abstract class ChatStatic {
}
/**
* Creates a {@link FormatableChat} filling a line of chat (or console) with the configured decoration character and
* Creates a {@link FormatableChat} filling a chat line with the configured decoration character and
* color and a centered text.
* @param text the text aligned to the center.
* @param console if the line is rendered on console (true) or IG (false).
* @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character
* @return a new {@link FormatableChat} filling a chat line with the configured decoration character
* and color and a centered text.
* @see ChatFilledLine#centerText(ComponentLike)
* @see ChatConfig#decorationChar
@@ -536,11 +557,11 @@ public abstract class ChatStatic {
}
/**
* Creates a {@link FormatableChat} filling a line of chat (or console) with a decoration character and color.
* Creates a {@link FormatableChat} filling a chat line with a decoration character and color.
* @param decorationChar the character used for decoration.
* @param decorationColor the color used for the decoration characters.
* @param console if the line is rendered on console (true) or IG (false).
* @return a new {@link FormatableChat} filling a line of chat (or console) with a decoration character and color.
* @return a new {@link FormatableChat} filling a chat line with a decoration character and color.
* @see ChatFilledLine#filled()
*/
public static FormatableChat filledLine(char decorationChar, TextColor decorationColor, boolean console) {
@@ -548,10 +569,10 @@ public abstract class ChatStatic {
}
/**
* Creates a {@link FormatableChat} filling a line of chat (or console) with the configured decoration character and
* Creates a {@link FormatableChat} filling a chat line with the configured decoration character and
* color.
* @param console if the line is rendered on console (true) or IG (false).
* @return a new {@link FormatableChat} filling a line of chat (or console) with a decoration character and color.
* @return a new {@link FormatableChat} filling a chat line with a decoration character and color.
* @see ChatFilledLine#filled()
* @see ChatConfig#decorationChar
* @see ChatConfig#decorationColor
@@ -591,52 +612,39 @@ public abstract class ChatStatic {
private static ComponentBuilder<?, ?> componentToBuilder(Component c) {
ComponentBuilder<?, ?> builder;
if (c instanceof TextComponent) {
builder = Component.text()
.content(((TextComponent) c).content());
}
else if (c instanceof TranslatableComponent) {
builder = Component.translatable()
.key(((TranslatableComponent) c).key())
.args(((TranslatableComponent) c).args());
}
else if (c instanceof SelectorComponent) {
builder = Component.selector()
.pattern(((SelectorComponent) c).pattern());
}
else if (c instanceof ScoreComponent) {
builder = Component.score()
.name(((ScoreComponent) c).name())
.objective(((ScoreComponent) c).objective());
}
else if (c instanceof KeybindComponent) {
builder = Component.keybind()
.keybind(((KeybindComponent) c).keybind());
}
else if (c instanceof BlockNBTComponent) {
builder = Component.blockNBT()
.interpret(((BlockNBTComponent) c).interpret())
.nbtPath(((BlockNBTComponent) c).nbtPath())
.pos(((BlockNBTComponent) c).pos());
}
else if (c instanceof EntityNBTComponent) {
builder = Component.entityNBT()
.interpret(((EntityNBTComponent) c).interpret())
.nbtPath(((EntityNBTComponent) c).nbtPath())
.selector(((EntityNBTComponent) c).selector());
}
else if (c instanceof StorageNBTComponent) {
builder = Component.storageNBT()
.interpret(((StorageNBTComponent) c).interpret())
.nbtPath(((StorageNBTComponent) c).nbtPath())
.storage(((StorageNBTComponent) c).storage());
}
else {
throw new IllegalArgumentException("Unknows component type " + c.getClass());
}
ComponentBuilder<?, ?> builder = switch (c) {
case TextComponent textComponent -> Component.text()
.content(textComponent.content());
case TranslatableComponent translatableComponent -> Component.translatable()
.key(translatableComponent.key()).arguments(translatableComponent.arguments());
case SelectorComponent selectorComponent -> Component.selector()
.pattern(selectorComponent.pattern());
case ScoreComponent scoreComponent -> Component.score()
.name(scoreComponent.name())
.objective(scoreComponent.objective());
case KeybindComponent keybindComponent -> Component.keybind()
.keybind(keybindComponent.keybind());
case BlockNBTComponent blockNBTComponent -> Component.blockNBT()
.interpret(blockNBTComponent.interpret())
.nbtPath(blockNBTComponent.nbtPath())
.pos(blockNBTComponent.pos());
case EntityNBTComponent entityNBTComponent -> Component.entityNBT()
.interpret(entityNBTComponent.interpret())
.nbtPath(entityNBTComponent.nbtPath())
.selector(entityNBTComponent.selector());
case StorageNBTComponent storageNBTComponent -> Component.storageNBT()
.interpret(storageNBTComponent.interpret())
.nbtPath(storageNBTComponent.nbtPath())
.storage(storageNBTComponent.storage());
case null, default -> throw new IllegalArgumentException("Unknown component type " + (c == null ? "null" : c.getClass()));
};
return builder.style(c.style()).append(c.children());
}
/**
* Creates a new {@link ChatStatic} instance.
*/
protected ChatStatic() {}
}

View File

@@ -1,10 +1,13 @@
package fr.pandacube.lib.chat;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import java.util.ArrayList;
import java.util.List;
/**
* A tree structure of {@link Chat} component intended to be rendered in chat using {@link #render(boolean)}.
* A tree structure of chat {@link Component} intended to be rendered in chat using {@link #render(boolean)}.
*/
public class ChatTreeNode {
@@ -19,7 +22,7 @@ public class ChatTreeNode {
/**
* The component for the current node.
*/
public final Chat component;
public final ComponentLike component;
/**
* Children nodes.
@@ -27,10 +30,10 @@ public class ChatTreeNode {
public final List<ChatTreeNode> children = new ArrayList<>();
/**
* Construct an new {@link ChatTreeNode}.
* Construct a new {@link ChatTreeNode}.
* @param cmp the component for the current node.
*/
public ChatTreeNode(Chat cmp) {
public ChatTreeNode(ComponentLike cmp) {
component = cmp;
}
@@ -48,9 +51,9 @@ public class ChatTreeNode {
* Generate a tree view based on this tree structure.
* <p>
* Each element in the returned list represent 1 line of this tree view.
* Thus, the caller may send each line separately or at once depending of the quantity of data.
* Thus, the caller may send each line separately or at once, depending on the quantity of data.
* @param console true to render for console, false otherwise.
* @return an array of component, each element being a single line.
* @return a list of component, each element being a single line.
*/
public List<Chat> render(boolean console) {
List<Chat> ret = new ArrayList<>();

View File

@@ -1,5 +1,17 @@
package fr.pandacube.lib.chat;
import fr.pandacube.lib.chat.Chat.FormatableChat;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.TranslationArgument;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.format.TextDecoration.State;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -10,19 +22,10 @@ import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.format.TextDecoration.State;
import net.md_5.bungee.api.ChatColor;
import fr.pandacube.lib.chat.Chat.FormatableChat;
import static fr.pandacube.lib.chat.ChatStatic.chat;
/**
* Provides various methods and properties to manipulate text displayed in chat an other parts of the game.
* Provides various methods and properties to manipulate text displayed in chat and other parts of the game.
*/
public class ChatUtil {
@@ -48,7 +51,7 @@ public class ChatUtil {
/**
* Mapping indicating the text pixel with for specific characters in the default Minecraft font.
* If a character doesnt have a mapping in this map, then its width is {@link #DEFAULT_CHAR_SIZE}.
* If a character doesn't have a mapping in this map, then its width is {@link #DEFAULT_CHAR_SIZE}.
*/
public static final Map<Character, Integer> CHAR_SIZES;
static {
@@ -112,7 +115,7 @@ public class ChatUtil {
* @param nbPages the number of pages.
* @param nbPagesToDisplay the number of pages to display around the first page, the last page and the
* {@code currentPage}.
* @return a {@link Chat} containging the created page navigator.
* @return a {@link Chat} containing the created page navigator.
*/
public static Chat createPagination(String prefix, String cmdFormat, int currentPage, int nbPages, int nbPagesToDisplay) {
Set<Integer> pagesToDisplay = new TreeSet<>();
@@ -127,7 +130,7 @@ public class ChatUtil {
pagesToDisplay.add(i);
}
Chat d = ChatStatic.chat().thenLegacyText(prefix);
Chat d = chat().thenLegacyText(prefix);
boolean first = true;
int previous = 0;
@@ -149,11 +152,11 @@ public class ChatUtil {
else
first = false;
FormatableChat pDisp = Chat.clickableCommand(Chat.text(page), String.format(cmdFormat, page), Chat.text("Aller à la page " + page));
FormatableChat pDisplay = Chat.clickableCommand(Chat.text(page), String.format(cmdFormat, page), Chat.text("Aller à la page " + page));
if (page == currentPage) {
pDisp.highlightedCommandColor();
pDisplay.highlightedCommandColor();
}
d.then(pDisp);
d.then(pDisplay);
previous = page;
}
@@ -167,6 +170,58 @@ public class ChatUtil {
/**
* Do like {@link String#join(CharSequence, Iterable)}, but for components, and the last separator is different from
* the others. It is useful when enumerating things in a sentence, for instance :
* <code>"a thing<u>, </u>a thing<u> and </u>a thing"</code>
* (the coma being the usual separator, and {@code " and "} being the final separator).
* @param regularSeparator the separator used everywhere except between the two last components to join.
* @param finalSeparator the separator used between the two last components to join.
* @param elements the components to join.
* @return a new {@link Chat} instance with all the provided {@code component} joined using the separators.
*/
public static FormatableChat joinGrammatically(ComponentLike regularSeparator, ComponentLike finalSeparator, List<? extends ComponentLike> elements) {
int size = elements == null ? 0 : elements.size();
int last = size - 1;
return switch (size) {
case 0, 1, 2 -> join(finalSeparator, elements);
default -> (FormatableChat) join(regularSeparator, elements.subList(0, last))
.then(finalSeparator)
.then(elements.get(last));
};
}
/**
* Do like {@link String#join(CharSequence, Iterable)}, but for components.
* @param separator the separator used everywhere except between the two last components to join.
* @param elements the components to join.
* @return a new {@link Chat} instance with all the provided {@code component} joined using the separators.
*/
public static FormatableChat join(ComponentLike separator, Iterable<? extends ComponentLike> elements) {
FormatableChat c = chat();
if (elements == null)
return c;
boolean first = true;
for (ComponentLike el : elements) {
if (!first) {
c.then(separator);
}
c.then(el);
first = false;
}
return c;
}
@@ -210,8 +265,8 @@ public class ChatUtil {
count += strWidth(((TextComponent)component).content(), console, actuallyBold);
}
else if (component instanceof TranslatableComponent) {
for (Component c : ((TranslatableComponent)component).args())
count += componentWidth(c, console, actuallyBold);
for (TranslationArgument c : ((TranslatableComponent)component).arguments())
count += componentWidth(c.asComponent(), console, actuallyBold);
}
for (Component c : component.children())
@@ -258,7 +313,7 @@ public class ChatUtil {
/**
* Wraps the provided text in multiple lines, taking into account the legacy formating.
* Wraps the provided text in multiple lines, taking into account the legacy formatting.
* <p>
* This method only takes into account IG text width. Use a regular text-wrapper for console instead.
* @param legacyText the text to wrap.
@@ -272,7 +327,7 @@ public class ChatUtil {
}
/**
* Wraps the provided text in multiple lines, taking into account the legacy formating.
* Wraps the provided text in multiple lines, taking into account the legacy formatting.
* <p>
* This method only takes into account IG text width. Use a regular text-wrapper for console instead.
* @param legacyText the text to wrap.
@@ -295,7 +350,7 @@ public class ChatUtil {
do {
char c = legacyText.charAt(index);
if (c == ChatColor.COLOR_CHAR && index < legacyText.length() - 1) {
if (c == LegacyComponentSerializer.SECTION_CHAR && index < legacyText.length() - 1) {
currentWord.append(c);
c = legacyText.charAt(++index);
currentWord.append(c);
@@ -369,7 +424,7 @@ public class ChatUtil {
/**
* Try to render a matrix of {@link Chat} components into a table in the chat or console.
* @param data the component, in the form of {@link List} of {@link List} of {@link Chat}. The englobing list holds
* @param data the component, in the form of {@link List} of {@link List} of {@link Chat}. The parent list holds
* the table lines (line 0 being the top line). Each sublist holds the cells content (element 0 is the
* leftText one). The row lengths can be different.
* @param space a spacer to put between columns.
@@ -377,12 +432,12 @@ public class ChatUtil {
* alignment, much harder).
* @return a List containing each rendered line of the table.
*/
public static List<Component> renderTable(List<List<Chat>> data, String space, boolean console) {
public static List<Component> renderTable(List<List<ComponentLike>> data, String space, boolean console) {
List<List<Component>> compRows = new ArrayList<>(data.size());
for (List<Chat> row : data) {
for (List<ComponentLike> row : data) {
List<Component> compRow = new ArrayList<>(row.size());
for (Chat c : row) {
compRow.add(c.getAdv());
for (ComponentLike c : row) {
compRow.add(c.asComponent());
}
compRows.add(compRow);
}
@@ -392,7 +447,7 @@ public class ChatUtil {
/**
* Try to render a matrix of {@link Component} components into a table in the chat or console.
* @param data the component, in the form of {@link List} of {@link List} of {@link Component}. The englobing list holds
* @param data the component, in the form of {@link List} of {@link List} of {@link Component}. The parent list holds
* the table lines (line 0 being the top line). Each sublist holds the cells content (element 0 is the
* leftText one). The row lengths can be different.
* @param space a spacer to put between columns.
@@ -416,7 +471,7 @@ public class ChatUtil {
// create the lines with appropriate spacing
List<Component> spacedRows = new ArrayList<>(data.size());
for (List<Component> row : data) {
Chat spacedRow = Chat.chat();
Chat spacedRow = chat();
for (int i = 0; i < row.size() - 1; i++) {
int w = componentWidth(row.get(i), console);
int padding = nbPixelPerColumn.get(i) - w;
@@ -425,8 +480,8 @@ public class ChatUtil {
spacedRow.thenText(space);
}
if (!row.isEmpty())
spacedRow.then(row.get(row.size() - 1));
spacedRows.add(spacedRow.getAdv());
spacedRow.then(row.getLast());
spacedRows.add(spacedRow.get());
}
return spacedRows;
@@ -448,14 +503,14 @@ public class ChatUtil {
*/
public static Component customWidthSpace(int width, boolean console) {
if (console)
return Chat.text(" ".repeat(width)).getAdv();
return Chat.text(" ".repeat(width)).get();
return switch (width) {
case 0, 1 -> Component.empty();
case 2 -> Chat.text(".").black().getAdv();
case 3 -> Chat.text("`").black().getAdv();
case 6 -> Chat.text(". ").black().getAdv();
case 7 -> Chat.text("` ").black().getAdv();
case 11 -> Chat.text("` ").black().getAdv();
case 2 -> Chat.text(".").black().get();
case 3 -> Chat.text("`").black().get();
case 6 -> Chat.text(". ").black().get();
case 7 -> Chat.text("` ").black().get();
case 11 -> Chat.text("` ").black().get();
default -> {
int nbSpace = width / 4;
int nbBold = width % 4;
@@ -464,13 +519,13 @@ public class ChatUtil {
if (nbBold > 0) {
yield Chat.text(" ".repeat(nbNotBold)).bold(false)
.then(Chat.text(" ".repeat(nbBold)).bold(true))
.getAdv();
.get();
}
else
yield Chat.text(" ".repeat(nbNotBold)).bold(false).getAdv();
yield Chat.text(" ".repeat(nbNotBold)).bold(false).get();
}
else if (nbBold > 0) {
yield Chat.text(" ".repeat(nbBold)).bold(true).getAdv();
yield Chat.text(" ".repeat(nbBold)).bold(true).get();
}
throw new IllegalStateException("Should not be here (width=" + width + "; nbSpace=" + nbSpace + "; nbBold=" + nbBold + "; nbNotBold=" + nbNotBold + ")");
}
@@ -505,9 +560,9 @@ public class ChatUtil {
private static final char PROGRESS_BAR_FULL_CHAR = '|';
/**
* Generate a (eventually multi-part) progress bar using text.
* Generate a (eventually multipart) progress bar using text.
* @param values the values to render in the progress bar.
* @param colors the colors attributed to each values.
* @param colors the colors attributed to each value.
* @param total the total value of the progress bar.
* @param width the width in which the progress bar should fit (in pixel for IG, in character count for console)
* @param console true if the progress bar is intended to be displayed on the console, false if its in game chat.
@@ -602,5 +657,6 @@ public class ChatUtil {
return str;
}
private ChatUtil() {}
}

View File

@@ -0,0 +1,230 @@
package fr.pandacube.lib.chat;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.format.TextFormat;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.kyori.adventure.text.serializer.legacy.LegacyFormat;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Convenient enum to uses legacy format while keeping compatibility with modern chat format and API (Adventure, ...)
*/
public enum LegacyChatFormat {
/**
* Black (0) color format code.
*/
BLACK('0'),
/**
* Dark blue (1) color format code.
*/
DARK_BLUE('1'),
/**
* Dark green (2) color format code.
*/
DARK_GREEN('2'),
/**
* Dark aqua (3) color format code.
*/
DARK_AQUA('3'),
/**
* Dark red (4) color format code.
*/
DARK_RED('4'),
/**
* Dark purple (5) color format code.
*/
DARK_PURPLE('5'),
/**
* Gold (6) color format code.
*/
GOLD('6'),
/**
* Gray (7) color format code.
*/
GRAY('7'),
/**
* Dark gray (8) color format code.
*/
DARK_GRAY('8'),
/**
* Blue (9) color format code.
*/
BLUE('9'),
/**
* Green (A) color format code.
*/
GREEN('a'),
/**
* Aqua (B) color format code.
*/
AQUA('b'),
/**
* Red (C) color format code.
*/
RED('c'),
/**
* Light purple (D) color format code.
*/
LIGHT_PURPLE('d'),
/**
* Yellow (E) color format code.
*/
YELLOW('e'),
/**
* White (F) color format code.
*/
WHITE('f'),
/**
* Obfuscated (K) decoration format code.
*/
OBFUSCATED('k'),
/**
* Bold (L) decoration format code.
*/
BOLD('l'),
/**
* Strikethrough (M) decoration format code.
*/
STRIKETHROUGH('m'),
/**
* Underlined (N) decoration format code.
*/
UNDERLINED('n'),
/**
* Italic (O) decoration format code.
*/
ITALIC('o'),
/**
* Reset (R) format code.
*/
RESET('r');
/**
* The character used by Minecraft for legacy chat format.
*/
public static final char COLOR_CHAR = LegacyComponentSerializer.SECTION_CHAR;
/** {@link #COLOR_CHAR} but as a String! */
public static final String COLOR_STR_PREFIX = Character.toString(COLOR_CHAR);
private static final Map<Character, LegacyChatFormat> BY_CHAR;
private static final Map<TextFormat, LegacyChatFormat> BY_FORMAT;
private static final Map<LegacyFormat, LegacyChatFormat> BY_LEGACY;
/**
* Gets the {@link LegacyChatFormat} from the provided chat color code.
* @param code the character code from [0-9A-Fa-fK-Ok-oRr].
* @return the {@link LegacyChatFormat} related to the provided code.
*/
public static LegacyChatFormat of(char code) {
return BY_CHAR.get(Character.toLowerCase(code));
}
/**
* Gets the {@link LegacyChatFormat} from the provided {@link TextFormat} instance.
* @param format the {@link TextFormat} instance.
* @return the {@link LegacyChatFormat} related to the provided format.
*/
public static LegacyChatFormat of(TextFormat format) {
LegacyChatFormat colorOrDecoration = BY_FORMAT.get(format);
if (colorOrDecoration != null)
return colorOrDecoration;
if (format.getClass().getSimpleName().equals("Reset")) // an internal class of legacy serializer library
return RESET;
throw new IllegalArgumentException("Unsupported format of type " + format.getClass());
}
/**
* Gets the {@link LegacyChatFormat} from the provided {@link LegacyFormat} instance.
* @param advLegacy the {@link LegacyFormat} instance.
* @return the {@link LegacyChatFormat} related to the provided format.
*/
public static LegacyChatFormat of(LegacyFormat advLegacy) {
return BY_LEGACY.get(advLegacy);
}
/**
* The format code of this chat format.
*/
public final char code;
/**
* The Adventure legacy format instance related to this chat format.
*/
public final LegacyFormat advLegacyFormat;
LegacyChatFormat(char code) {
this.code = code;
advLegacyFormat = LegacyComponentSerializer.parseChar(code);
}
/**
* Gets the related {@link TextColor}, or null if it's not a color.
* @return the related {@link TextColor}, or null if it's not a color.
*/
public TextColor getTextColor() {
return advLegacyFormat.color();
}
/**
* Tells if this format is a color.
* @return true if this format is a color, false otherwise.
*/
public boolean isColor() {
return getTextColor() != null;
}
/**
* Gets the related {@link TextDecoration}, or null if it's not a decoration.
* @return the related {@link TextDecoration}, or null if it's not a decoration.
*/
public TextDecoration getTextDecoration() {
return advLegacyFormat.decoration();
}
/**
* Tells if this format is a decoration (bold, italic, ...).
* @return true if this format is a decoration, false otherwise.
*/
public boolean isDecoration() {
return getTextDecoration() != null;
}
/**
* Tells if this format is the reset.
* @return true if this format is the reset, false otherwise.
*/
public boolean isReset() {
return this == RESET;
}
@Override
public String toString() {
return COLOR_STR_PREFIX + code;
}
static {
BY_CHAR = Arrays.stream(values()).sequential()
.collect(Collectors.toMap(e -> e.code, e -> e, (e1, e2) -> e1, LinkedHashMap::new));
BY_FORMAT = Arrays.stream(values()).sequential()
.filter(e -> e.isColor() || e.isDecoration())
.collect(Collectors.toMap(e -> {
if (e.isColor())
return e.getTextColor();
return e.getTextDecoration();
}, e -> e, (e1, e2) -> e1, LinkedHashMap::new));
BY_LEGACY = Arrays.stream(values()).sequential()
.collect(Collectors.toMap(e -> e.advLegacyFormat, e -> e, (e1, e2) -> e1, LinkedHashMap::new));
}
}

View File

@@ -15,42 +15,36 @@
<packaging>jar</packaging>
<repositories>
<repository>
<id>minecraft-libraries</id>
<name>Minecraft Libraries</name>
<url>https://libraries.minecraft.net</url>
</repository>
<repository>
<id>bungeecord-repo</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>fr.pandacube.lib</groupId>
<artifactId>pandalib-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>fr.pandacube.lib</groupId>
<artifactId>pandalib-reflect</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>fr.pandacube.lib</groupId>
<artifactId>pandalib-commands</artifactId>
<version>${project.version}</version>
</dependency>
<dependencies>
<dependency>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-log</artifactId>
<version>${bungeecord.version}</version>
<groupId>fr.pandacube.lib</groupId>
<artifactId>pandalib-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>fr.pandacube.lib</groupId>
<artifactId>pandalib-reflect</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>fr.pandacube.lib</groupId>
<artifactId>pandalib-commands</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>fr.pandacube.lib</groupId>
<artifactId>pandalib-config</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-config</artifactId>
<artifactId>bungeecord-log</artifactId>
<version>${bungeecord.version}</version>
</dependency>

View File

@@ -8,10 +8,10 @@ import fr.pandacube.lib.cli.log.CLILogger;
import jline.console.ConsoleReader;
import org.fusesource.jansi.AnsiConsole;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
/**
* Class to hangle general standard IO operation for a CLI application. It uses Jlines {@link ConsoleReader} for the
* Class to handle general standard IO operation for a CLI application. It uses Jlines {@link ConsoleReader} for the
* console rendering, a JUL {@link Logger} for logging, and Brigadier to handle commands.
*/
public class CLI extends Thread {
@@ -30,10 +30,10 @@ public class CLI extends Thread {
AnsiConsole.systemInstall();
reader = new ConsoleReader();
reader.setPrompt("\r>");
reader.setPrompt(">");
reader.addCompleter(CLIBrigadierDispatcher.instance);
// configuration du formatteur pour le logger
// configure logger's formatter
System.setProperty("net.md_5.bungee.log-date-format", "yyyy-MM-dd HH:mm:ss");
logger = CLILogger.getLogger(this);
}

View File

@@ -0,0 +1,137 @@
package fr.pandacube.lib.cli;
import fr.pandacube.lib.cli.commands.CommandAdmin;
import fr.pandacube.lib.cli.commands.CommandStop;
import fr.pandacube.lib.cli.log.CLILogger;
import fr.pandacube.lib.util.log.Log;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
/**
* Main class of a CLI application.
*/
public abstract class CLIApplication {
private static CLIApplication instance;
/**
* Returns the current application instance.
* @return the current application instance.
*/
public static CLIApplication getInstance() {
return instance;
}
/**
* The instance of {@link CLI} for this application.
*/
public final CLI cli;
/**
* Creates a new application instance.
*/
protected CLIApplication() {
instance = this;
CLI tmpCLI = null;
try {
tmpCLI = new CLI();
Log.setLogger(tmpCLI.getLogger());
} catch (Throwable t) {
System.err.println("Unable to start application " + getName() + " version " + getClass().getPackage().getImplementationVersion());
t.printStackTrace();
System.exit(1);
}
cli = tmpCLI;
try {
Log.info("Starting " + getName() + " version " + getClass().getPackage().getImplementationVersion());
start();
new CommandAdmin();
new CommandStop();
Runtime.getRuntime().addShutdownHook(shutdownThread);
cli.start(); // actually starts the CLI thread
Log.info("Application started.");
} catch (Throwable t) {
Log.severe("Unable to start application " + getName() + " version " + getClass().getPackage().getImplementationVersion(), t);
}
}
/**
* Returns the application's {@link Logger}.
* @return the application's {@link Logger}.
*/
public Logger getLogger() {
return cli.getLogger();
}
private final Thread shutdownThread = new Thread(this::stop);
private final AtomicBoolean stopping = new AtomicBoolean(false);
/**
* Stops this application.
*/
public final void stop() {
synchronized (stopping) {
if (stopping.get())
return;
stopping.set(true);
}
Log.info("Stopping " + getName() + " version " + getClass().getPackage().getImplementationVersion());
try {
end();
} catch (Throwable t) {
Log.severe("Error stopping application " + getName() + " version " + getClass().getPackage().getImplementationVersion(), t);
} finally {
Log.info("Bye bye.");
CLILogger.actuallyResetLogManager();
if (!Thread.currentThread().equals(shutdownThread))
System.exit(0);
}
}
/**
* Tells if this application is currently stopping, that is the {@link #stop()} method has been called.
* @return true if the application is stopping, false otherwise.
*/
public boolean isStopping() {
return stopping.get();
}
/**
* Gets the name of this application.
* @return the name of this application.
*/
public abstract String getName();
/**
* Method to override to initialize stuff in this application.
* This method is called on instanciation of this Application.
* @throws Exception If an exception is thrown, the application will not start.
*/
protected abstract void start() throws Exception;
/**
* Method to override to reload specific stuff in this application.
* This method is called by using the command {@code admin reload}.
*/
public abstract void reload();
/**
* Method to override to execute stuff when this application stops.
* This method is called once before this application terminates, possibly from a shutdown hook Thread.
*/
protected abstract void end();
}

View File

@@ -12,13 +12,13 @@ import java.util.function.Predicate;
/**
* Abstract class that holds the logic of a specific command to be registered in {@link CLIBrigadierDispatcher}.
*/
public abstract class CLIBrigadierCommand extends BrigadierCommand<Object> {
public abstract class CLIBrigadierCommand extends BrigadierCommand<CLICommandSender> {
/**
* Instanciate this command instance.
* Instantiate this command instance.
*/
public CLIBrigadierCommand() {
LiteralCommandNode<Object> commandNode = buildCommand().build();
LiteralCommandNode<CLICommandSender> commandNode = buildCommand().build();
postBuildCommand(commandNode);
String[] aliases = getAliases();
if (aliases == null)
@@ -37,7 +37,7 @@ public abstract class CLIBrigadierCommand extends BrigadierCommand<Object> {
}
}
protected abstract LiteralArgumentBuilder<Object> buildCommand();
protected abstract LiteralArgumentBuilder<CLICommandSender> buildCommand();
protected String[] getAliases() {
return new String[0];
@@ -47,16 +47,16 @@ public abstract class CLIBrigadierCommand extends BrigadierCommand<Object> {
public boolean isPlayer(Object sender) {
return false;
public boolean isPlayer(CLICommandSender sender) {
return sender.isPlayer();
}
public boolean isConsole(Object sender) {
return true;
public boolean isConsole(CLICommandSender sender) {
return sender.isConsole();
}
public Predicate<Object> hasPermission(String permission) {
return sender -> true;
public Predicate<CLICommandSender> hasPermission(String permission) {
return sender -> sender.hasPermission(permission);
}
@@ -68,7 +68,7 @@ public abstract class CLIBrigadierCommand extends BrigadierCommand<Object> {
* @param suggestions the suggestions to wrap.
* @return a {@link SuggestionProvider} generating the suggestions from the provided {@link SuggestionsSupplier}.
*/
protected SuggestionProvider<Object> wrapSuggestions(SuggestionsSupplier<Object> suggestions) {
protected SuggestionProvider<CLICommandSender> wrapSuggestions(SuggestionsSupplier<CLICommandSender> suggestions) {
return wrapSuggestions(suggestions, Function.identity());
}

View File

@@ -1,20 +1,17 @@
package fr.pandacube.lib.cli.commands;
import java.util.List;
import com.mojang.brigadier.suggestion.Suggestion;
import com.mojang.brigadier.suggestion.Suggestions;
import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.commands.BrigadierDispatcher;
import fr.pandacube.lib.util.Log;
import jline.console.completer.Completer;
import net.kyori.adventure.text.ComponentLike;
import java.util.List;
/**
* Implementation of {@link BrigadierDispatcher} that integrates the commands into the JLine CLI interface.
*/
public class CLIBrigadierDispatcher extends BrigadierDispatcher<Object> implements Completer {
public class CLIBrigadierDispatcher extends BrigadierDispatcher<CLICommandSender> implements Completer {
/**
* The instance of {@link CLIBrigadierDispatcher}.
@@ -22,16 +19,22 @@ public class CLIBrigadierDispatcher extends BrigadierDispatcher<Object> implemen
public static final CLIBrigadierDispatcher instance = new CLIBrigadierDispatcher();
private static final Object sender = new Object();
/**
* The sender for the console itself.
*/
public static final CLICommandSender CLI_CONSOLE_COMMAND_SENDER = new CLIConsoleCommandSender();
private CLIBrigadierDispatcher() {}
/**
* Executes the provided command.
* @param commandWithoutSlash the command, without the eventual slash at the begining.
* Executes the provided command as the console.
* @param commandWithoutSlash the command, without the eventual slash at the beginning.
* @return the value returned by the executed command.
*/
public int execute(String commandWithoutSlash) {
return execute(sender, commandWithoutSlash);
return execute(CLI_CONSOLE_COMMAND_SENDER, commandWithoutSlash);
}
@@ -50,17 +53,17 @@ public class CLIBrigadierDispatcher extends BrigadierDispatcher<Object> implemen
}
/**
* Gets the suggestions for the currently being typed command.
* Gets the suggestions for the currently being typed command, as the console.
* @param buffer the command that is being typed.
* @return the suggestions for the currently being typed command.
*/
public Suggestions getSuggestions(String buffer) {
return getSuggestions(sender, buffer);
return getSuggestions(CLI_CONSOLE_COMMAND_SENDER, buffer);
}
@Override
protected void sendSenderMessage(Object sender, ComponentLike message) {
Log.info(Chat.chatComponent(message).getLegacyText());
protected void sendSenderMessage(CLICommandSender sender, ComponentLike message) {
sender.sendMessage(message);
}
}

View File

@@ -0,0 +1,46 @@
package fr.pandacube.lib.cli.commands;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.audience.MessageType;
import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
/**
* A command sender.
*/
public interface CLICommandSender extends Audience {
/**
* Gets the name of the sender.
* @return The name of the sender.
*/
String getName();
/**
* Tells if the sender is a player.
* @return true if the sender is a player, false otherwise.
*/
boolean isPlayer();
/**
* Tells if the sender is on the console.
* @return true if the sender is on the console, false otherwise.
*/
boolean isConsole();
/**
* Tells if the sender has the specified permission.
* @param permission the permission to test on the sender.
* @return true if the sender has the specified permission.
*/
boolean hasPermission(String permission);
/**
* Sends the provided message to the sender.
* @param message the message to send.
*/
void sendMessage(String message);
@Override // force implementation of super-interface default method
void sendMessage(@NotNull Identity source, @NotNull Component message, @NotNull MessageType type);
}

View File

@@ -0,0 +1,44 @@
package fr.pandacube.lib.cli.commands;
import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.util.log.Log;
import net.kyori.adventure.audience.MessageType;
import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
/**
* The console command sender.
*/
public class CLIConsoleCommandSender implements CLICommandSender {
/**
* Creates a new console command sender.
*/
protected CLIConsoleCommandSender() {}
public String getName() {
return "Console";
}
public boolean isPlayer() {
return false;
}
public boolean isConsole() {
return true;
}
public boolean hasPermission(String permission) {
return true;
}
public void sendMessage(String message) {
Log.info(message);
}
@Override
public void sendMessage(@NotNull Identity source, @NotNull Component message, @NotNull MessageType type) {
sendMessage(Chat.chatComponent(message).getLegacyText());
}
}

View File

@@ -0,0 +1,281 @@
package fr.pandacube.lib.cli.commands;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.arguments.BoolArgumentType;
import com.mojang.brigadier.arguments.DoubleArgumentType;
import com.mojang.brigadier.arguments.FloatArgumentType;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.arguments.LongArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.tree.ArgumentCommandNode;
import com.mojang.brigadier.tree.CommandNode;
import com.mojang.brigadier.tree.LiteralCommandNode;
import com.mojang.brigadier.tree.RootCommandNode;
import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.chat.Chat.FormatableChat;
import fr.pandacube.lib.chat.ChatTreeNode;
import fr.pandacube.lib.cli.CLIApplication;
import fr.pandacube.lib.util.log.Log;
import net.kyori.adventure.text.Component;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static fr.pandacube.lib.chat.ChatStatic.chat;
import static fr.pandacube.lib.chat.ChatStatic.failureText;
import static fr.pandacube.lib.chat.ChatStatic.successText;
import static fr.pandacube.lib.chat.ChatStatic.text;
/**
* The {@code admin} command for a {@link CLIApplication}.
*/
public class CommandAdmin extends CLIBrigadierCommand {
/**
* Initializes the admin command.
*/
public CommandAdmin() {}
@Override
protected LiteralArgumentBuilder<CLICommandSender> buildCommand() {
return literal("admin")
.executes(this::version)
.then(literal("version")
.executes(this::version)
)
.then(literal("reload")
.executes(this::reload)
)
.then(literal("debug")
.executes(this::debug)
)
.then(literal("commandstruct")
.executes(this::commandStruct)
.then(argument("path", StringArgumentType.greedyString())
.executes(this::commandStruct)
)
);
}
private int version(CommandContext<CLICommandSender> context) {
Log.info(chat()
.console(context.getSource().isConsole())
.infoColor()
.thenCenterText(text(CLIApplication.getInstance().getName()))
.thenNewLine()
.thenText("- Implem. version: ")
.thenData(CLIApplication.getInstance().getClass().getPackage().getImplementationVersion())
.thenNewLine()
.thenText("- Spec. version: ")
.thenData(CLIApplication.getInstance().getClass().getPackage().getSpecificationVersion())
.getLegacyText());
return 1;
}
private int reload(CommandContext<CLICommandSender> context) {
CLIApplication.getInstance().reload();
return 1;
}
private int debug(CommandContext<CLICommandSender> context) {
Log.setDebug(!Log.isDebugEnabled());
Log.info(successText("Mode débug "
+ (Log.isDebugEnabled() ? "" : "dés") + "activé").getLegacyText());
return 1;
}
private int commandStruct(CommandContext<CLICommandSender> context) {
CLICommandSender sender = context.getSource();
String[] tokens = tryGetArgument(context, "path", String.class, s -> s.split(" "), new String[0]);
CommandNode<CLICommandSender> node = CLIBrigadierDispatcher.instance.getDispatcher().findNode(Arrays.asList(tokens));
if (node == null) {
Log.severe(failureText("La commande spécifiée na pas été trouvée.").getLegacyText());
return 0;
}
Set<CommandNode<CLICommandSender>> scannedNodes = new HashSet<>();
DisplayCommandNode displayNode = new DisplayCommandNode();
// find parent nodes of scanned node to avoid displaying them after redirection and stuff
for (int i = 1; i < tokens.length; i++) {
CommandNode<CLICommandSender> ignoredNode = CLIBrigadierDispatcher.instance.getDispatcher().findNode(Arrays.asList(Arrays.copyOf(tokens, i)));
if (ignoredNode != null) {
displayNode.addInline(ignoredNode);
scannedNodes.add(ignoredNode);
}
}
buildDisplayCommandTree(displayNode, scannedNodes, node);
ChatTreeNode displayTreeNode = buildDisplayTree(displayNode, sender);
for (Chat comp : displayTreeNode.render(true))
Log.info(comp.getLegacyText());
return 1;
}
private void buildDisplayCommandTree(DisplayCommandNode displayNode, Set<CommandNode<CLICommandSender>> scannedNodes, CommandNode<CLICommandSender> node) {
displayNode.addInline(node);
scannedNodes.add(node);
if (node.getRedirect() != null) {
if (scannedNodes.contains(node.getRedirect()) || node.getRedirect() instanceof RootCommandNode) {
displayNode.addInline(node.getRedirect());
}
else {
buildDisplayCommandTree(displayNode, scannedNodes, node.getRedirect());
}
}
else if (node.getChildren().size() == 1) {
buildDisplayCommandTree(displayNode, scannedNodes, node.getChildren().iterator().next());
}
else if (node.getChildren().size() >= 2) {
for (CommandNode<CLICommandSender> child : node.getChildren()) {
DisplayCommandNode dNode = new DisplayCommandNode();
buildDisplayCommandTree(dNode, scannedNodes, child);
displayNode.addChild(dNode);
}
}
}
private ChatTreeNode buildDisplayTree(DisplayCommandNode displayNode, CLICommandSender sender) {
Chat d = chat().then(displayCurrentNode(displayNode.nodes.get(0), false, sender));
CommandNode<CLICommandSender> prevNode = displayNode.nodes.get(0);
for (int i = 1; i < displayNode.nodes.size(); i++) {
CommandNode<CLICommandSender> currNode = displayNode.nodes.get(i);
if (currNode.equals(prevNode.getRedirect())) {
d.then(text("")
.hover("Redirects to path: " + CLIBrigadierDispatcher.instance.getDispatcher().getPath(currNode))
);
d.then(displayCurrentNode(currNode, true, sender));
}
else {
d.thenText(" ");
d.then(displayCurrentNode(currNode, false, sender));
}
prevNode = currNode;
}
ChatTreeNode displayTree = new ChatTreeNode(d);
for (DisplayCommandNode child : displayNode.children) {
displayTree.addChild(buildDisplayTree(child, sender));
}
return displayTree;
}
private Component displayCurrentNode(CommandNode<CLICommandSender> node, boolean redirectTarget, CLICommandSender sender) {
if (node == null)
throw new IllegalArgumentException("node must not be null");
FormatableChat d;
if (node instanceof RootCommandNode) {
d = text("(root)").italic()
.hover("Root command node");
}
else if (node instanceof ArgumentCommandNode) {
ArgumentType<?> type = ((ArgumentCommandNode<?, ?>) node).getType();
String typeStr = type.getClass().getSimpleName();
if (type instanceof IntegerArgumentType
|| type instanceof LongArgumentType
|| type instanceof FloatArgumentType
|| type instanceof DoubleArgumentType) {
typeStr = type.toString();
}
else if (type instanceof BoolArgumentType) {
typeStr = "bool()";
}
else if (type instanceof StringArgumentType) {
typeStr = "string(" + ((StringArgumentType) type).getType().name().toLowerCase() + ")";
}
String t = "<" + node.getName() + ">";
String h = "Argument command node"
+ "\nType: " + typeStr;
if (node.getCommand() != null) {
t += "®";
h += "\nThis node has a command";
}
d = text(t);
if (!node.canUse(sender)) {
d.gray();
h += "\nPermission not granted for you";
}
d.hover(h);
}
else if (node instanceof LiteralCommandNode) {
String t = node.getName();
String h = "Literal command node";
if (node.getCommand() != null) {
t += "®";
h += "\nThis node has a command";
}
d = text(t);
if (!node.canUse(sender)) {
d.gray();
h += "\nPermission not granted for you";
}
d.hover(h);
}
else {
throw new IllegalArgumentException("Unknown command node type: " + node.getClass());
}
if (redirectTarget)
d.gray();
return d.get();
}
private static class DisplayCommandNode {
final List<CommandNode<CLICommandSender>> nodes = new ArrayList<>();
final List<DisplayCommandNode> children = new ArrayList<>();
void addInline(CommandNode<CLICommandSender> node) {
nodes.add(node);
}
void addChild(DisplayCommandNode child) {
children.add(child);
}
}
}

View File

@@ -0,0 +1,30 @@
package fr.pandacube.lib.cli.commands;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import fr.pandacube.lib.cli.CLIApplication;
/**
* the {@code stop} (or {@code end}) command for a {@link CLIApplication}.
*/
public class CommandStop extends CLIBrigadierCommand {
/**
* Initializes the admin command.
*/
public CommandStop() {}
@Override
protected LiteralArgumentBuilder<CLICommandSender> buildCommand() {
return literal("stop")
.executes(context -> {
CLIApplication.getInstance().stop();
return 1;
});
}
@Override
protected String[] getAliases() {
return new String[] { "end" };
}
}

View File

@@ -1,27 +1,56 @@
package fr.pandacube.lib.cli.log;
import java.io.PrintStream;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import fr.pandacube.lib.cli.CLI;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.cli.CLIApplication;
import fr.pandacube.lib.util.ThrowableUtil;
import fr.pandacube.lib.util.log.DailyLogRotateFileHandler;
import fr.pandacube.lib.util.log.Log;
import net.md_5.bungee.log.ColouredWriter;
import net.md_5.bungee.log.ConciseFormatter;
import net.md_5.bungee.log.LoggingOutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.PrintStream;
import java.util.Scanner;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
/**
* Initializer for the logging system of a CLI application.
*/
public class CLILogger {
static {
System.setProperty("java.util.logging.manager", ShutdownHookDelayerLogManager.class.getName());
}
private static Logger logger = null;
private static class ShutdownHookDelayerLogManager extends LogManager {
static ShutdownHookDelayerLogManager instance;
public ShutdownHookDelayerLogManager() { instance = this; }
@Override public void reset() { /* don't reset yet. */ }
private void actuallyReset() { super.reset(); }
}
/**
* Tells the LogManager to actually reset.
* <p>
* This method is called by the shutdown hook of {@link CLIApplication}, because the {@link CLILogger} uses a custom
* {@link LogManager} that bypass the reset process during the shutdown of the process.
*/
public static void actuallyResetLogManager() {
ShutdownHookDelayerLogManager.instance.actuallyReset();
}
/**
* Initialize and return the logger for this application.
* @param cli the CLI instance to use
* @return the logger of this application.
* @return the logger for this application.
*/
public static synchronized Logger getLogger(CLI cli) {
if (logger == null) {
@@ -38,12 +67,37 @@ public class CLILogger {
fileHandler.setFormatter(new ConciseFormatter(false));
logger.addHandler(fileHandler);
System.setErr(new PrintStream(new LoggingOutputStream(logger, Level.SEVERE), true));
System.setOut(new PrintStream(new LoggingOutputStream(logger, Level.INFO), true));
System.setErr(newRedirector(logger, Level.SEVERE));
System.setOut(newRedirector(logger, Level.INFO));
Log.setLogger(logger);
Thread.setDefaultUncaughtExceptionHandler((t, e) -> Log.severe("Uncaught Exception in thread " + t.getName(), e));
}
return logger;
}
private static PrintStream newRedirector(Logger logger, Level level) {
PipedOutputStream pos = new PipedOutputStream();
PrintStream ps = new PrintStream(pos);
PipedInputStream pis = new PipedInputStream();
ThrowableUtil.wrapEx(() -> pos.connect(pis));
Scanner s = new Scanner(pis);
Thread t = new Thread(() -> {
while(s.hasNextLine()) {
logger.logp(level, "", "", s.nextLine());
}
s.close();
}, "Logging Redirector Thread (" + level + ")");
t.setDaemon(true);
t.start();
return ps;
}
private CLILogger() {}
}

View File

@@ -0,0 +1,45 @@
package fr.pandacube.lib.commands;
import java.util.logging.Logger;
/**
* Throw an instance of this exception to indicate to the plugin command handler that the user has missused the command.
* The message, if provided, must indicate the reason of the mussusage of the command. It will be displayed on the
* screen with eventual indications of how to use the command (help command for example).
* If a {@link Throwable} cause is provided, it will be relayed to the plugin {@link Logger}.
*
*/
public class BadCommandUsage extends RuntimeException {
/**
* Constructs a new runtime exception with no message or cause.
*/
public BadCommandUsage() {
super();
}
/**
* Constructs a new runtime exception with the specified cause.
* @param cause the cause.
*/
public BadCommandUsage(Throwable cause) {
super(cause);
}
/**
* Constructs a new runtime exception with the specified message.
* @param message the message.
*/
public BadCommandUsage(String message) {
super(message);
}
/**
* Constructs a new runtime exception with the specified message and cause.
* @param message the message.
* @param cause the cause.
*/
public BadCommandUsage(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -10,9 +10,10 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.tree.LiteralCommandNode;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
@@ -24,12 +25,17 @@ import java.util.function.Predicate;
*/
public abstract class BrigadierCommand<S> {
/**
* Creates a Brigadier command.
*/
public BrigadierCommand() {}
/**
* Returns a builder for this command.
* Concrete class should include any element in the builder that is needed to build the command (sub-commands and
* arguments, requirements, redirection, ...).
* If any of the sub-commands and arguments needs to know the {@link LiteralCommandNode} builded from the returned
* If any of the sub-commands and arguments needs to know the {@link LiteralCommandNode} built from the returned
* {@link LiteralArgumentBuilder}, this can be done by overriding {@link #postBuildCommand(LiteralCommandNode)}.
* @return a builder for this command.
*/
@@ -37,16 +43,16 @@ public abstract class BrigadierCommand<S> {
/**
* Method to override if the reference to the command node has to be known when building the subcommands.
* @param commandNode the command node builded from {@link #buildCommand()}.
* @param commandNode the command node built from {@link #buildCommand()}.
*/
protected void postBuildCommand(LiteralCommandNode<S> commandNode) {
// default implementation does nothing.
}
/**
* Method to override if this command have any aliases.
* Method to override if this command has any aliases.
* @return an array of string corresponding to the aliases. This must not include the orignal command name (that
* is the name of the literal command node builded from {@link #buildCommand()}).
* is the name of the literal command node built from {@link #buildCommand()}).
*/
protected String[] getAliases() {
return new String[0];
@@ -235,9 +241,12 @@ public abstract class BrigadierCommand<S> {
args = Arrays.copyOf(args, args.length + 1);
args[args.length - 1] = message.substring(tokenStartPos);
for (String s : suggestions.getSuggestions(sender, args.length - 1, args[args.length - 1], args)) {
if (s != null)
builder.suggest(s);
List<String> wrappedResult = suggestions.getSuggestions(sender, args.length - 1, args[args.length - 1], args);
if (wrappedResult != null) {
for (String s : wrappedResult) {
if (s != null)
builder.suggest(s);
}
}
} catch (Throwable e) {
Log.severe("Error while tab-completing '" + message + "' for " + sender, e);

View File

@@ -6,14 +6,14 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.tree.LiteralCommandNode;
import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
import net.kyori.adventure.text.ComponentLike;
import java.util.concurrent.CompletableFuture;
/**
* Abstract class that holds a Brigadier {@link CommandDispatcher} instance.
* Subclasses contains logic to integrate this commands dispatcher into their environment (like Bungee or CLI app).
* Subclasses contain logic to integrate this commands dispatcher into their environment (like Bungee or CLI app).
* @param <S> the command source (or command sender) type.
*/
public abstract class BrigadierDispatcher<S> {
@@ -21,6 +21,11 @@ public abstract class BrigadierDispatcher<S> {
private final CommandDispatcher<S> dispatcher = new CommandDispatcher<>();
/**
* Creates a new Dispatcher instance.
*/
public BrigadierDispatcher() {}
/**
* Registers the provided command node into this dispatcher.
@@ -43,7 +48,7 @@ public abstract class BrigadierDispatcher<S> {
/**
* Executes the provided command as the provided sender.
* @param sender the command sender.
* @param commandWithoutSlash the command, without the eventual slash at the begining.
* @param commandWithoutSlash the command, without the eventual slash at the beginning.
* @return the value returned by the executed command.
*/
public int execute(S sender, String commandWithoutSlash) {

View File

@@ -18,7 +18,7 @@ import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* Utility methods to replace some functionalities of Brigadier, especialy suggestion sorting that we dont like.
* Utility methods to replace some functionalities of Brigadier, especially suggestion sorting that we dont like.
*/
public class BrigadierSuggestionsUtil {
@@ -140,4 +140,8 @@ public class BrigadierSuggestionsUtil {
}
private BrigadierSuggestionsUtil() {}
}

View File

@@ -14,7 +14,7 @@ import java.util.stream.LongStream;
import java.util.stream.Stream;
/**
* Functionnal interface providing suggestions for an argument of a command.
* Functional interface providing suggestions for an argument of a command.
* @param <S> the type of the command sender.
*/
@FunctionalInterface
@@ -66,7 +66,7 @@ public interface SuggestionsSupplier<S> {
* Filter the provided {@link Stream} of string according to the provided token, using the filter returned by {@link #filter(String)},
* then returns the strings collected into a {@link List}.
* <p>
* This methods consume the provided stream, so will not be usable anymore.
* This method consume the provided stream, so will not be usable anymore.
* @param stream the stream to filter and collet.
* @param token the token to consider for filtering.
* @return the stream, filtered and collected into a {@link List}.
@@ -336,7 +336,7 @@ public interface SuggestionsSupplier<S> {
/**
* List of all possible duration unit symbols for suggestions.
*/
public static final List<String> DURATION_SUFFIXES = List.of("y", "mo", "w", "d", "h", "m", "s");
List<String> DURATION_SUFFIXES = List.of("y", "mo", "w", "d", "h", "m", "s");
private static void scanAndRemovePastSuffixes(List<String> suffixes, String foundSuffix) {
@@ -505,7 +505,7 @@ public interface SuggestionsSupplier<S> {
/**
* Creates a new {@link SuggestionsSupplier} containing all the suggestions of this instance,
* but if this list is still empty, returns the suggestions from the provided one.
* @param other another {@link SuggestionsSupplier} to fallback to.
* @param other another {@link SuggestionsSupplier} to fall back to.
* @return a new {@link SuggestionsSupplier}.
*/
default SuggestionsSupplier<S> orIfEmpty(SuggestionsSupplier<S> other) {

33
pandalib-config/pom.xml Normal file
View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>pandalib-parent</artifactId>
<groupId>fr.pandacube.lib</groupId>
<version>1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>pandalib-config</artifactId>
<packaging>jar</packaging>
<repositories>
<repository>
<id>bungeecord-repo</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-config</artifactId>
<version>${bungeecord.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -1,23 +1,19 @@
package fr.pandacube.lib.core.config;
package fr.pandacube.lib.config;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import fr.pandacube.lib.chat.ChatColorUtil;
import fr.pandacube.lib.util.Log;
/**
* Class that loads a specific config file or directory.
*/
public abstract class AbstractConfig {
/**
* The {@link File} corresponging to this config file or directory.
* The {@link File} corresponding to this config file or directory.
*/
protected final File configFile;
@@ -57,7 +53,7 @@ public abstract class AbstractConfig {
while ((line = reader.readLine()) != null) {
String trimmedLine = line.trim();
if (ignoreEmpty && trimmedLine.equals(""))
if (ignoreEmpty && trimmedLine.isEmpty())
continue;
if (ignoreHashtagComment && trimmedLine.startsWith("#"))
@@ -94,7 +90,8 @@ public abstract class AbstractConfig {
* @return the list of files in the config directory, or null if this config is not a directory.
*/
protected List<File> getFileList() {
return configFile.isDirectory() ? Arrays.asList(configFile.listFiles()) : null;
File[] arr = configFile.listFiles();
return arr != null ? List.of(arr) : null;
}
@@ -105,7 +102,7 @@ public abstract class AbstractConfig {
* Splits the provided string into a list of permission nodes.
* The permission nodes must be separated by {@code ";"}.
* @param perms one or more permissions nodes, separated by {@code ";"}.
* @return {@code null} if the parameter is null or is equal to {@code "*"}, or the string splitted using {@code ";"}.
* @return {@code null} if the parameter is null or is equal to {@code "*"}, or the string split using {@code ";"}.
*/
public static List<String> splitPermissionsString(String perms) {
if (perms == null || perms.equals("*"))
@@ -114,25 +111,6 @@ public abstract class AbstractConfig {
}
/**
* Utility method to that translate the {@code '&'} formated string to the legacy format.
* @param string the string to convert.
* @return a legacy formated string (using {@code '§'}).
*/
public static String getTranslatedColorCode(String string) {
return ChatColorUtil.translateAlternateColorCodes('&', string);
}
/**
* Logs the message as a warning into console, prefixed with the context of this config.
* @param message the message to log.
*/
protected void warning(String message) {
Log.warning("Error in configuration '"+configFile.getName()+"': " + message);
}
/**
* The type of config.
*/

View File

@@ -1,17 +1,17 @@
package fr.pandacube.lib.core.config;
package fr.pandacube.lib.config;
import java.io.File;
import java.io.IOException;
/**
* An abstract manager for a set of configuration files and folders.
* Its uses is to manage the loading/reloading of the configuration of a plugin.
* It's uses to manage the loading/reloading of the configuration of a plugin.
*/
public abstract class AbstractConfigManager {
/**
* The global configuration directory.
* May be the one provided by the environmenet API (like Plugin.getPluginFolder() in Bukkit).
* It may be the one provided by the environment API (like Plugin.getPluginFolder() in Bukkit).
*/
protected final File configDir;

View File

@@ -37,6 +37,12 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<!-- Cron expression interpreter -->
<dependency>
<groupId>ch.eitchnet</groupId>
@@ -50,7 +56,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.3.0</version>
<version>3.5.2</version>
<executions>
<execution>
<phase>package</phase>
@@ -85,6 +91,28 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.googlecode.maven-download-plugin</groupId>
<artifactId>download-maven-plugin</artifactId>
<version>1.7.0</version>
<executions>
<execution>
<id>mcversion-download</id>
<phase>compile</phase>
<goals>
<goal>wget</goal>
</goals>
</execution>
</executions>
<configuration>
<url>https://api.pandacube.fr/rest/mcversion</url>
<outputDirectory>${project.basedir}/src/main/resources/fr/pandacube/lib/core/mc_version</outputDirectory>
<outputFileName>mcversion.json</outputFileName>
<skipCache>true</skipCache>
<overwrite>true</overwrite>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,8 +1,8 @@
package fr.pandacube.lib.core.backup;
import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.util.Log;
import net.md_5.bungee.api.ChatColor;
import fr.pandacube.lib.chat.LegacyChatFormat;
import fr.pandacube.lib.util.log.Log;
import java.io.File;
import java.time.LocalDateTime;
@@ -16,10 +16,19 @@ import java.util.stream.Collectors;
import static fr.pandacube.lib.chat.ChatStatic.text;
/**
* Cleanup a backup directory (i.e. removes old backup archives).
* It is possible to combine different instances to affect which archive to keep or delete.
*/
public abstract class BackupCleaner implements UnaryOperator<TreeSet<LocalDateTime>> {
private static final boolean testOnly = false; // if true, no files are deleted
/**
* Creates a {@link BackupCleaner} that keeps the n last archives in the backup directory.
* @param n the number of last archives to keep.
* @return a {@link BackupCleaner} that keeps the n last archives in the backup directory.
*/
public static BackupCleaner KEEPING_N_LAST(int n) {
return new BackupCleaner() {
@Override
@@ -32,15 +41,23 @@ public abstract class BackupCleaner implements UnaryOperator<TreeSet<LocalDateTi
}
/**
* Creates a {@link BackupCleaner} that keeps one archive every n month.
* <p>
* This cleaner divides each year into sections of n month. For each month, its compute a section id using the
* formula <code><i>YEAR</i> * (12 / <i>n</i>) + <i>MONTH</i> / <i>n</i></code>. It then keeps the first archive
* found in each section.
*
* @param n the interval in month between each kept archives. Must be a divider of 12 (1, 2, 3, 4, 6 or 12).
* @return a {@link BackupCleaner} that keeps one archive every n month.
*/
public static BackupCleaner KEEPING_1_EVERY_N_MONTH(int n) {
return new BackupCleaner() {
@Override
public TreeSet<LocalDateTime> apply(TreeSet<LocalDateTime> localDateTimes) {
return localDateTimes.stream()
.collect(Collectors.groupingBy(
ldt -> {
return ldt.getYear() * 4 + ldt.getMonthValue() / n;
},
ldt -> ldt.getYear() * (12 / n) + ldt.getMonthValue() / n,
TreeMap::new,
Collectors.minBy(LocalDateTime::compareTo))
)
@@ -53,9 +70,19 @@ public abstract class BackupCleaner implements UnaryOperator<TreeSet<LocalDateTi
};
}
/**
* Create a backup cleaner.
*/
public BackupCleaner() {}
/**
* Creates a new {@link BackupCleaner} that keeps the archives kept by this {@link BackupCleaner} or by the provided
* one.
* In other word, it makes a union operation with the set of archives kept by both original {@link BackupCleaner}.
* @param other the other {@link BackupCleaner} to merge with.
* @return a new {@link BackupCleaner}. The original ones are not affected.
*/
public BackupCleaner merge(BackupCleaner other) {
BackupCleaner self = this;
return new BackupCleaner() {
@@ -70,19 +97,24 @@ public abstract class BackupCleaner implements UnaryOperator<TreeSet<LocalDateTi
}
/**
* Performs the cleanup operation on the provided directory.
* @param archiveDir the backup directory to clean up.
* @param compressDisplayName the display name of the backup process that manages the backup directory. Used for logs.
*/
public void cleanupArchives(File archiveDir, String compressDisplayName) {
String[] files = archiveDir.list();
if (files == null)
return;
Log.info("[Backup] Cleaning up backup directory " + ChatColor.GRAY + compressDisplayName + ChatColor.RESET + "...");
Log.info("[Backup] Cleaning up backup directory " + LegacyChatFormat.GRAY + compressDisplayName + LegacyChatFormat.RESET + "...");
TreeMap<LocalDateTime, File> datedFiles = new TreeMap<>();
for (String filename : files) {
File file = new File(archiveDir, filename);
if (!filename.matches("\\d{8}-\\d{6}\\.zip")) {
Log.warning("[Backup] " + ChatColor.GRAY + compressDisplayName + ChatColor.RESET + " Invalid file in backup directory: " + filename);
Log.warning("[Backup] " + LegacyChatFormat.GRAY + compressDisplayName + LegacyChatFormat.RESET + " Invalid file in backup directory: " + filename);
continue;
}
@@ -91,7 +123,7 @@ public abstract class BackupCleaner implements UnaryOperator<TreeSet<LocalDateTi
try {
ldt = LocalDateTime.parse(dateTimeStr, BackupProcess.dateFileNameFormatter);
} catch (DateTimeParseException e) {
Log.warning("[Backup] " + ChatColor.GRAY + compressDisplayName + ChatColor.RESET + " Unable to parse file name to a date-time: " + filename, e);
Log.warning("[Backup] " + LegacyChatFormat.GRAY + compressDisplayName + LegacyChatFormat.RESET + " Unable to parse file name to a date-time: " + filename, e);
continue;
}
@@ -124,7 +156,7 @@ public abstract class BackupCleaner implements UnaryOperator<TreeSet<LocalDateTi
if (testOnly || oneDeleted)
Log.warning(c.getLegacyText());
Log.info("[Backup] Backup directory " + ChatColor.GRAY + compressDisplayName + ChatColor.RESET + " cleaned.");
Log.info("[Backup] Backup directory " + LegacyChatFormat.GRAY + compressDisplayName + LegacyChatFormat.RESET + " cleaned.");
}

View File

@@ -1,11 +1,8 @@
package fr.pandacube.lib.core.backup;
import fc.cron.CronExpression;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
import java.io.File;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Date;
@@ -13,22 +10,37 @@ import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.LongStream;
/**
* Handles the backup processes.
*/
public class BackupManager extends TimerTask {
private final File backupDirectory;
/**
* The {@link Persist} instance of this {@link BackupManager}.
*/
protected final Persist persist;
/**
* The list of backup processes that are scheduled.
*/
protected final List<BackupProcess> backupQueue = new ArrayList<>();
/* package */ final AtomicReference<BackupProcess> runningBackup = new AtomicReference<>();
private final Timer schedulerTimer = new Timer();
/**
* Instantiate a new backup manager.
* @param backupDirectory the root backup directory.
*/
public BackupManager(File backupDirectory) {
this.backupDirectory = backupDirectory;
if (!backupDirectory.exists()) {
backupDirectory.mkdirs();
}
persist = new Persist(this);
@@ -37,17 +49,32 @@ public class BackupManager extends TimerTask {
schedulerTimer.scheduleAtFixedRate(this, new Date(nextMinute), 60_000);
}
/**
* Add a new backup process to the queue.
* @param process the backup process to add.
*/
protected void addProcess(BackupProcess process) {
process.displayNextSchedule();
backupQueue.add(process);
}
/**
* Gets the backup root directory.
* @return the backup root directory.
*/
public File getBackupDirectory() {
return backupDirectory;
}
/**
* Tells if a backup is currently running.
* @return true if a backup is running, false otherwise.
*/
public synchronized boolean isBackupRunning() {
return runningBackup.get() != null;
}
public synchronized void run() {
BackupProcess tmp;
if ((tmp = runningBackup.get()) != null) {
@@ -65,12 +92,16 @@ public class BackupManager extends TimerTask {
}
/**
* Disables this backup manager, canceling scheduled backups.
* It will wait for a currently running backup to finish before returning.
*/
@SuppressWarnings("BusyWait")
public synchronized void onDisable() {
schedulerTimer.cancel();
if (runningBackup.get() != null) {
if (isBackupRunning()) {
Log.warning("[Backup] Waiting after the end of a backup...");
BackupProcess tmp;
while ((tmp = runningBackup.get()) != null) {
@@ -88,8 +119,6 @@ public class BackupManager extends TimerTask {
}
}
}
persist.save();
}

View File

@@ -1,48 +1,75 @@
package fr.pandacube.lib.core.backup;
import fc.cron.CronExpression;
import fr.pandacube.lib.chat.LegacyChatFormat;
import fr.pandacube.lib.core.cron.CronScheduler;
import fr.pandacube.lib.util.FileUtils;
import fr.pandacube.lib.util.Log;
import net.md_5.bungee.api.ChatColor;
import fr.pandacube.lib.util.log.Log;
import java.io.File;
import java.text.DateFormat;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.function.BiPredicate;
/**
* A backup process.
*/
public abstract class BackupProcess implements Comparable<BackupProcess>, Runnable {
private final BackupManager backupManager;
/**
* The process identifier.
*/
public final String identifier;
/**
* The zip compressor.
*/
protected ZipCompressor compressor = null;
private boolean enabled = true;
private String scheduling = "0 2 * * *"; // cron format, here is everyday at 2am
private String scheduling = "0 2 * * *"; // cron format, here is every day at 2am
private BackupCleaner backupCleaner = null;
private List<String> ignoreList = new ArrayList<>();
/**
* Instantiates a new backup process.
* @param bm the associated backup manager.
* @param n the process identifier.
*/
protected BackupProcess(BackupManager bm, final String n) {
backupManager = bm;
identifier = n;
}
/**
* Gets the associated backup manager.
* @return the associated backup manager.
*/
public BackupManager getBackupManager() {
return backupManager;
}
/**
* Gets the process identifier.
* @return the process identifier.
*/
public String getIdentifier() {
return identifier;
}
/**
* Gets the display name of this process.
* Default implementation returns {@link #getIdentifier()}.
* @return the display name of this process.
*/
protected String getDisplayName() {
return getIdentifier();
}
@@ -55,9 +82,11 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab
}
/**
* Provides a predicate that tells if a provided file must be included in the archive or not.
* The default implementation returns a filter based on the content of {@link #getIgnoreList()}.
* @return a predicate.
*/
public BiPredicate<File, String> getFilenameFilter() {
return (file, path) -> {
for (String exclude : ignoreList) {
@@ -75,47 +104,91 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab
};
}
/**
* Gets the source directory to back up.
* @return the source directory to back up.
*/
public abstract File getSourceDir();
/**
* Gets the directory in which to put the archives.
* @return the directory in which to put the archives.
*/
protected abstract File getTargetDir();
/**
* Called when the backup starts.
*/
protected abstract void onBackupStart();
/**
* Called when the backup ends.
* @param success true if the backup ended successfully.
*/
protected abstract void onBackupEnd(boolean success);
/**
* Tells if this backup process is enabled.
* A disabled backup process will not run.
* @return true if this backup process is enabled, false otherwise.
*/
public boolean isEnabled() {
return enabled;
}
/**
* Sets the enabled status of this backup process.
* @param enabled the enabled status of this backup process.
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
/**
* Gets the string representation of the scheduling, using cron format.
* @return the string representation of the scheduling.
*/
public String getScheduling() {
return scheduling;
}
/**
* Sets the string representation of the scheduling.
* @param scheduling the string representation of the scheduling, in the CRON format (without seconds).
*/
public void setScheduling(String scheduling) {
this.scheduling = scheduling;
}
/**
* Gets the associated backup cleaner, that is executed at the end of this backup process.
* @return the associated backup cleaner.
*/
public BackupCleaner getBackupCleaner() {
return backupCleaner;
}
/**
* Sets the backup cleaner of this backup process.
* @param backupCleaner the backup cleaner of this backup process.
*/
public void setBackupCleaner(BackupCleaner backupCleaner) {
this.backupCleaner = backupCleaner;
}
/**
* Gets the current list of files that are ignored during the backup process.
* @return the current list of files that are ignored during the backup process.
*/
public List<String> getIgnoreList() {
return ignoreList;
}
/**
* Sets a new list of files that will be ignored during the backup process.
* @param ignoreList the new list of files that are ignored during the backup process.
*/
public void setIgnoreList(List<String> ignoreList) {
this.ignoreList = ignoreList;
}
@@ -136,7 +209,7 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab
File sourceDir = getSourceDir();
if (!sourceDir.exists()) {
Log.warning("[Backup] Unable to compress " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + ": source directory " + sourceDir + " doesnt exist");
Log.warning("[Backup] Unable to compress " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + ": source directory " + sourceDir + " doesn't exist");
return;
}
@@ -146,7 +219,7 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab
onBackupStart();
new Thread(() -> {
Log.info("[Backup] Starting for " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " ...");
Log.info("[Backup] Starting for " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + " ...");
compressor = new ZipCompressor(sourceDir, target, 9, filter);
@@ -156,7 +229,7 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab
success = true;
Log.info("[Backup] Finished for " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET);
Log.info("[Backup] Finished for " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET);
try {
BackupCleaner cleaner = getBackupCleaner();
@@ -190,17 +263,18 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab
}
/**
* Logs the scheduling status of this backup process.
*/
public void displayNextSchedule() {
Log.info("[Backup] " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + " next backup on "
+ DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(new Date(getNext())));
}
public abstract void displayNextSchedule();
/**
* A formatter used to format and parse the name of backup archives, based on a date and time.
*/
public static final DateTimeFormatter dateFileNameFormatter = new DateTimeFormatterBuilder()
.appendValue(ChronoField.YEAR, 4)
.appendValue(ChronoField.MONTH_OF_YEAR, 2)
@@ -216,49 +290,67 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab
return dateFileNameFormatter.format(ZonedDateTime.now());
}
/**
* Logs the progress of this currently running backup process.
* Logs nothing if this backup is not in progress.
*/
public void logProgress() {
if (compressor == null)
return;
Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + ": " + compressor.getState().getLegacyText());
Log.info("[Backup] " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + ": " + compressor.getState().getLegacyText());
}
/**
* Tells if this backup process could start now.
* @return true if this backup process could start now, false otherwise.
*/
public boolean couldRunNow() {
if (!isEnabled())
return false;
if (!isDirty())
return false;
if (getNext() > System.currentTimeMillis())
return false;
return true;
return getNext() <= System.currentTimeMillis();
}
/**
* Gets the time of the next scheduled run.
* @return the time, in millis-timestamp, of the next scheduled run, or {@link Long#MAX_VALUE} if its not scheduled.
*/
public long getNext() {
if (!hasNextScheduled())
return Long.MAX_VALUE;
return getNextCompress(backupManager.persist.isDirtySince(identifier));
}
/**
* Tells if this backup is scheduled or not.
* @return true if this backup is scheduled, false otherwise.
*/
public boolean hasNextScheduled() {
return isEnabled() && isDirty();
}
/**
* Tells if the content to be backed up is dirty or not. The source data is not dirty if it has not changed since
* the last backup.
* @return the dirty status of the data to be backed-up by this backup process.
*/
public boolean isDirty() {
return backupManager.persist.isDirty(identifier);
}
/**
* Sets the source data as dirty since now.
*/
public void setDirtySinceNow() {
backupManager.persist.setDirtySinceNow(identifier);
}
/**
* Sets the source data as not dirty.
*/
public void setNotDirty() {
backupManager.persist.setNotDirty(identifier);
}
@@ -268,9 +360,9 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab
/**
* get the timestamp (in ms) of when the next compress will run, depending on since when the files to compress are dirty.
* @param dirtySince the timestamp in ms since the files are dirty
* @return the timestamp in ms when the next compress of the files should be run, or 0 if it is not yet scheduled
* Gets the millis-timestamp of when the next compress will run, depending on since when the files to compress are dirty.
* @param dirtySince the timestamp in ms since the files are dirty.
* @return the timestamp in ms when the next compress of the files should be run, or 0 if it is not yet scheduled.
*/
public long getNextCompress(long dirtySince) {
if (dirtySince == -1)

View File

@@ -3,7 +3,7 @@ package fr.pandacube.lib.core.backup;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import fr.pandacube.lib.core.json.Json;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
import java.io.File;
import java.io.FileReader;
@@ -12,6 +12,11 @@ import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* Handles the data stored used for backup manager, like dirty status of data to be backed up.
* The data is stored using JSON format, in a file in the root backup directory.
* The file is updated on disk on every call to a {@code set*(...)} method.
*/
public class Persist {
private Map<String, Long> dirtySince = new HashMap<>();
@@ -19,17 +24,18 @@ public class Persist {
private final File file;
// private final Set<String> dirtyWorldsSave = new HashSet<>();
/**
* Creates a new instance, immediately loading the data from the file if it exists, or creating an empty one if not.
* @param bm the associated backup manager.
*/
public Persist(BackupManager bm) {
file = new File(bm.getBackupDirectory(), "source-dirty-since.json");
load();
}
public void reload() {
load();
}
protected void load() {
private void load() {
boolean loaded = false;
try (FileReader reader = new FileReader(file)) {
dirtySince = Json.gson.fromJson(reader, new TypeToken<Map<String, Long>>(){}.getType());
@@ -48,8 +54,8 @@ public class Persist {
save();
}
}
public void save() {
private void save() {
try (FileWriter writer = new FileWriter(file, false)) {
Json.gsonPrettyPrinting.toJson(dirtySince, writer);
}
@@ -57,27 +63,41 @@ public class Persist {
Log.severe("could not save " + file, e);
}
}
public void setDirtySinceNow(String id) {
/**
* Sets the backup process with the provided id as dirty.
* @param id the id of the backup process.
*/
public synchronized void setDirtySinceNow(String id) {
dirtySince.put(id, System.currentTimeMillis());
save();
}
public void setNotDirty(String id) {
/**
* Sets the backup process with the provided id as not dirty.
* @param id the id of the backup process.
*/
public synchronized void setNotDirty(String id) {
dirtySince.put(id, -1L);
save();
}
public boolean isDirty(String id) {
/**
* Tells if the backup process with the provided id is dirty.
* @param id the id of the backup process.
* @return true if the process is marked as dirty, false otherwise.
*/
public synchronized boolean isDirty(String id) {
return isDirtySince(id) != -1;
}
public long isDirtySince(String id) {
/**
* Tells since when the backup process with the provided id is dirty.
* @param id the id of the backup process.
* @return the millis-timestamp of when the backup process has been marked dirty.
*/
public synchronized long isDirtySince(String id) {
if (!dirtySince.containsKey(id))
setDirtySinceNow(id);
return dirtySince.get(id);

View File

@@ -1,22 +1,32 @@
package fr.pandacube.lib.core.backup;
import com.google.common.io.Files;
import fr.pandacube.lib.util.Log;
import net.md_5.bungee.api.ChatColor;
import fr.pandacube.lib.chat.LegacyChatFormat;
import fr.pandacube.lib.util.log.Log;
import java.io.File;
import java.io.IOException;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.function.BiPredicate;
/**
* A special backup process that handle the backup of rotated log files.
*/
public class RotatedLogsBackupProcess extends BackupProcess {
final String logFileRegexPattern;
final File sourceLogDirectory;
final boolean inNewThread;
/**
* Create a new instance of this backup process.
* @param bm the backup manager.
* @param inNewThread tells if this process should be run in a separate thread (true) or in the same thread handling
* the backup manager (false).
* @param sourceLogDir the directory where the rotated log files are stored, usually {@code ./logs/}.
* @param logFileRegexPattern the pattern to match the rotated log files (usually dated log files, excluding the
* current log file).
*/
public RotatedLogsBackupProcess(BackupManager bm, boolean inNewThread, File sourceLogDir, String logFileRegexPattern) {
super(bm, "logs");
this.logFileRegexPattern = logFileRegexPattern;
@@ -43,7 +53,7 @@ public class RotatedLogsBackupProcess extends BackupProcess {
if (!getSourceDir().isDirectory())
return;
Log.info("[Backup] Starting for " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " ...");
Log.info("[Backup] Starting for " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + " ...");
try {
// wait a little after the log message above, in case the log file rotation has to be performed.
@@ -72,9 +82,9 @@ public class RotatedLogsBackupProcess extends BackupProcess {
success = true;
Log.info("[Backup] Finished for " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET);
Log.info("[Backup] Finished for " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET);
} catch (Exception e) {
Log.severe("[Backup] Failed for : " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET, e);
Log.severe("[Backup] Failed for : " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET, e);
} finally {
onBackupEnd(success);
@@ -84,7 +94,7 @@ public class RotatedLogsBackupProcess extends BackupProcess {
public List<File> getFilesToMove() {
private List<File> getFilesToMove() {
List<File> ret = new ArrayList<>();
for (File f : getSourceDir().listFiles()) {
if (f.getName().matches(logFileRegexPattern))
@@ -120,10 +130,4 @@ public class RotatedLogsBackupProcess extends BackupProcess {
protected void onBackupEnd(boolean success) {
setDirtySinceNow();
}
@Override
public void displayNextSchedule() {
Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " next backup on "
+ DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(new Date(getNext())));
}
}

View File

@@ -5,6 +5,7 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
@@ -117,7 +118,11 @@ public class ZipCompressor {
}
for (Entry entry : entriesToCompress) {
entry.zip();
try {
entry.zip();
} catch (NoSuchFileException ignored) {
// file has been deleted since
}
}
synchronized (stateLock) {
@@ -158,8 +163,8 @@ public class ZipCompressor {
}
private class Entry {
File file;
String entry;
final File file;
final String entry;
Entry(File f, String e) {
file = f;
entry = e;

View File

@@ -4,7 +4,7 @@ import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import fc.cron.CronExpression;
import fr.pandacube.lib.core.json.Json;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
import java.io.File;
import java.io.FileReader;
@@ -20,7 +20,6 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
// TODO Add support for persisted last execution timestamps
/**
* Application wide task scheduler using Cron expression.
*/
@@ -55,10 +54,10 @@ public class CronScheduler {
long now = System.currentTimeMillis();
if (!tasks.isEmpty()) {
CronTask next = tasks.get(0);
CronTask next = tasks.getFirst();
if (next.nextRun <= now) {
next.runAsync();
setLastRun(next.taskId, next.nextRun);
setLastRun(next.taskId, now);
onTaskUpdate(false);
continue;
}
@@ -78,8 +77,10 @@ public class CronScheduler {
/**
* Schedule a task.
* If a task with the provided taskId already exists, it will be replaced.
* @param taskId the id of the task.
* @param cronExpression the scheduling of the task. May use seconds (6 values) or not (5 values)
* @param cronExpression the scheduling of the task. May use seconds (6 values) or not (5 values).
* See {@link CronExpression} for the format.
* @param task the task to run.
*/
public static void schedule(String taskId, String cronExpression, Runnable task) {
@@ -101,7 +102,7 @@ public class CronScheduler {
/**
* Cancel a scheduled task.
* Will not stop a current execution of the task. If the task does not exists, it will not do anything.
* Will not stop a current execution of the task. If the task does not exist, it will not do anything.
* @param taskId the id of the task to cancel.
*/
public static void unSchedule(String taskId) {
@@ -185,8 +186,6 @@ public class CronScheduler {
catch (final JsonParseException e) {
Log.severe("cannot load " + lastRunFile, e);
}
finally {
}
if (!loaded) {
saveLastRuns();
@@ -225,5 +224,6 @@ public class CronScheduler {
.toEpochMilli();
}
private CronScheduler() {}
}

View File

@@ -1,47 +1,96 @@
package fr.pandacube.lib.core.json;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.RecordComponent;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import com.google.gson.ToNumberStrategy;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import com.google.gson.stream.MalformedJsonException;
import fr.pandacube.lib.core.mc_version.MinecraftVersionList.MinecraftVersionListAdapter;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
/**
* Provides pre-instanciated {@link Gson} instances, all with support for Java records.
* Provides pre-instanced {@link Gson} objects, all with support for Java records and additional
* {@link TypeAdapterFactory} provided with {@link #registerTypeAdapterFactory(TypeAdapterFactory)}.
*/
public class Json {
/**
* {@link Gson} instance with {@link GsonBuilder#setLenient()} and support for Java records.
* Makes Gson deserialize numbers to Number subclasses the same way SnakeYAML does
*/
private static final ToNumberStrategy YAML_EQUIVALENT_NUMBER_STRATEGY = in -> {
String value = in.nextString();
// YAML uses Regex to resolve values as INT or FLOAT (see org.yaml.snakeyaml.resolver.Resolver), trying FLOAT first.
// We see in the regex that FLOAT MUST have a "." in the string, but INT must not, so we try that.
boolean isFloat = value.contains(".");
if (isFloat) {
// if float, will only parse to Double
// (see org.yaml.snakeyaml.constructor.SafeConstructor.ConstructYamlFloat)
try {
Double d = Double.valueOf(value);
if ((d.isInfinite() || d.isNaN()) && !in.isLenient()) {
throw new MalformedJsonException("JSON forbids NaN and infinities: " + d + "; at path " + in.getPreviousPath());
}
return d;
} catch (NumberFormatException e) {
throw new JsonParseException("Cannot parse " + value + "; at path " + in.getPreviousPath(), e);
}
}
else {
// if integer, will try to parse int, then long, then BigDecimal
// (see org.yaml.snakeyaml.constructor.SafeConstructor.ConstructYamlInt
// then org.yaml.snakeyaml.constructor.SafeConstructor.createNumber)
try {
return Integer.valueOf(value);
} catch (NumberFormatException e) {
try {
return Long.valueOf(value);
} catch (NumberFormatException e2) {
try {
return new BigInteger(value);
} catch (NumberFormatException e3) {
throw new JsonParseException("Cannot parse " + value + "; at path " + in.getPreviousPath(), e3);
}
}
}
}
};
/**
* {@link Gson} instance with {@link GsonBuilder#setLenient()} and support for Java records and additional
* {@link TypeAdapterFactory} provided with {@link #registerTypeAdapterFactory(TypeAdapterFactory)}.
*/
public static final Gson gson = build(Function.identity());
/**
* {@link Gson} instance with {@link GsonBuilder#setLenient()}, {@link GsonBuilder#setPrettyPrinting()}
* and support for Java records.
* {@link Gson} instance with {@link GsonBuilder#setLenient()}, {@link GsonBuilder#setPrettyPrinting()} and support
* for Java records and additional {@link TypeAdapterFactory} provided with
* {@link #registerTypeAdapterFactory(TypeAdapterFactory)}.
*/
public static final Gson gsonPrettyPrinting = build(GsonBuilder::setPrettyPrinting);
/**
* {@link Gson} instance with {@link GsonBuilder#setLenient()}, {@link GsonBuilder#serializeNulls()}
* and support for Java records.
* {@link Gson} instance with {@link GsonBuilder#setLenient()}, {@link GsonBuilder#serializeNulls()} and support for
* Java records and additional {@link TypeAdapterFactory} provided with
* {@link #registerTypeAdapterFactory(TypeAdapterFactory)}.
*/
public static final Gson gsonSerializeNulls = build(GsonBuilder::serializeNulls);
/**
* {@link Gson} instance with {@link GsonBuilder#setLenient()}, {@link GsonBuilder#serializeNulls()},
* {@link GsonBuilder#setPrettyPrinting()} and support for Java records.
* {@link GsonBuilder#setPrettyPrinting()} and support for Java records and additional {@link TypeAdapterFactory}
* provided with {@link #registerTypeAdapterFactory(TypeAdapterFactory)}.
*/
public static final Gson gsonSerializeNullsPrettyPrinting = build(b -> b.serializeNulls().setPrettyPrinting());
@@ -52,84 +101,68 @@ public class Json {
private static Gson build(Function<GsonBuilder, GsonBuilder> builderModifier) {
return builderModifier
.apply(new GsonBuilder().registerTypeAdapterFactory(new RecordAdapterFactory()).setLenient()).create();
GsonBuilder base = new GsonBuilder()
.registerTypeAdapterFactory(new CustomAdapterFactory())
.disableHtmlEscaping()
.setObjectToNumberStrategy(YAML_EQUIVALENT_NUMBER_STRATEGY)
.setLenient();
return builderModifier.apply(base).create();
}
/**
* Adds the provided {@link TypeAdapterFactory} to all the static Gson instances of this class.
* @param factory the factory to add to the
*/
public static void registerTypeAdapterFactory(TypeAdapterFactory factory) {
synchronized (customTypeAdapterFactories) {
customTypeAdapterFactories.add(factory);
}
}
private static final List<TypeAdapterFactory> customTypeAdapterFactories = new ArrayList<>();
// from https://github.com/google/gson/issues/1794#issuecomment-812964421
private static class RecordAdapterFactory implements TypeAdapterFactory {
private static class CustomAdapterFactory implements TypeAdapterFactory {
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
@SuppressWarnings("unchecked")
Class<T> clazz = (Class<T>) type.getRawType();
if (!clazz.isRecord() || clazz == Record.class) {
return null;
synchronized (customTypeAdapterFactories) {
for (TypeAdapterFactory actualFactory : customTypeAdapterFactories) {
TypeAdapter<T> adapter = actualFactory.create(gson, type);
if (adapter != null)
return adapter;
}
}
return new RecordTypeAdapter<>(gson, this, type);
return null;
}
}
private static class RecordTypeAdapter<T> extends TypeAdapter<T> {
private final Gson gson;
private final TypeAdapterFactory factory;
private final TypeToken<T> type;
public RecordTypeAdapter(Gson gson, TypeAdapterFactory factory, TypeToken<T> type) {
this.gson = gson;
this.factory = factory;
this.type = type;
}
@Override
public void write(JsonWriter out, T value) throws IOException {
gson.getDelegateAdapter(factory, type).write(out, value);
}
@Override
public T read(JsonReader reader) throws IOException {
if (reader.peek() == JsonToken.NULL) {
reader.nextNull();
return null;
} else {
@SuppressWarnings("unchecked")
Class<T> clazz = (Class<T>) type.getRawType();
RecordComponent[] recordComponents = clazz.getRecordComponents();
Map<String, TypeToken<?>> typeMap = new HashMap<>();
for (RecordComponent recordComponent : recordComponents) {
typeMap.put(recordComponent.getName(), TypeToken.get(recordComponent.getGenericType()));
}
var argsMap = new HashMap<String, Object>();
reader.beginObject();
while (reader.hasNext()) {
String name = reader.nextName();
argsMap.put(name, gson.getAdapter(typeMap.get(name)).read(reader));
}
reader.endObject();
var argTypes = new Class<?>[recordComponents.length];
var args = new Object[recordComponents.length];
for (int i = 0; i < recordComponents.length; i++) {
argTypes[i] = recordComponents[i].getType();
args[i] = argsMap.get(recordComponents[i].getName());
}
Constructor<T> constructor;
try {
constructor = clazz.getDeclaredConstructor(argTypes);
constructor.setAccessible(true);
return constructor.newInstance(args);
} catch (NoSuchMethodException | InstantiationException | SecurityException | IllegalAccessException
| IllegalArgumentException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
static {
registerTypeAdapterFactory(StackTraceElementAdapter.FACTORY);
registerTypeAdapterFactory(ThrowableAdapter.FACTORY);
registerTypeAdapterFactory(MinecraftVersionListAdapter.FACTORY);
}
/*public static void main(String[] args) {
TypeToken<Map<String, Object>> MAP_STR_OBJ_TYPE = new TypeToken<>() { };
Map<String, Object> map = gson.fromJson("{" +
"\"int\":34," +
"\"long\":3272567356876864," +
"\"bigint\":-737868677777837833757846576245765," +
"\"float\":34.0" +
"}", MAP_STR_OBJ_TYPE.getType());
for (String key : map.keySet()) {
Object v = map.get(key);
System.out.println(key + ": " + v + " (type " + v.getClass() + ")");
}
}*/
private Json() {}
}

View File

@@ -0,0 +1,64 @@
package fr.pandacube.lib.core.json;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.bind.TreeTypeAdapter;
import java.lang.reflect.Type;
/* package */ class StackTraceElementAdapter implements JsonSerializer<StackTraceElement>, JsonDeserializer<StackTraceElement> {
public static final TypeAdapterFactory FACTORY = TreeTypeAdapter.newTypeHierarchyFactory(StackTraceElement.class, new StackTraceElementAdapter());
@Override
public StackTraceElement deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonObject obj = json.getAsJsonObject();
String classLoader = obj.has("classloader") && obj.get("classloader").isJsonPrimitive()
? obj.get("classloader").getAsString() : null;
String module = obj.has("module") && obj.get("module").isJsonPrimitive()
? obj.get("module").getAsString() : null;
String moduleVersion = obj.has("moduleversion") && obj.get("moduleversion").isJsonPrimitive()
? obj.get("moduleversion").getAsString() : null;
String clazz = obj.has("class") && obj.get("class").isJsonPrimitive()
? obj.get("class").getAsString() : null;
if (clazz == null) {
throw new JsonParseException("Missing 'class' entry");
}
String method = obj.has("method") && obj.get("method").isJsonPrimitive()
? obj.get("method").getAsString() : null;
if (method == null) {
throw new JsonParseException("Missing 'method' entry");
}
String file = obj.has("file") && obj.get("file").isJsonPrimitive()
? obj.get("file").getAsString() : null;
int line = obj.has("line") && obj.get("line").isJsonPrimitive()
? obj.get("line").getAsInt() : -1;
return new StackTraceElement(classLoader, module, moduleVersion, clazz, method, file, line);
}
@Override
public JsonElement serialize(StackTraceElement src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject obj = new JsonObject();
obj.addProperty("class", src.getClassName());
obj.addProperty("method", src.getMethodName());
obj.addProperty("line", src.getLineNumber());
if (src.getClassLoaderName() != null)
obj.addProperty("classloader", src.getClassLoaderName());
if (src.getModuleName() != null)
obj.addProperty("module", src.getModuleName());
if (src.getModuleVersion() != null)
obj.addProperty("moduleversion", src.getModuleVersion());
if (src.getFileName() != null)
obj.addProperty("file", src.getFileName());
return obj;
}
}

View File

@@ -0,0 +1,218 @@
package fr.pandacube.lib.core.json;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.bind.TreeTypeAdapter;
import fr.pandacube.lib.util.log.Log;
import fr.pandacube.lib.util.ThrowableUtil;
import java.lang.reflect.Constructor;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Function;
/**
* Gson Adapter that handles serialization and deserialization of {@link Throwable} instances properly.
*/
public class ThrowableAdapter implements JsonSerializer<Throwable>, JsonDeserializer<Throwable> {
/* package */ static final TypeAdapterFactory FACTORY = TreeTypeAdapter.newTypeHierarchyFactory(Throwable.class, new ThrowableAdapter());
private ThrowableAdapter() {}
@Override
public Throwable deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonObject obj = json.getAsJsonObject();
String message = obj.has("message") && !obj.get("message").isJsonNull()
? obj.get("message").getAsString() : null;
Throwable cause = obj.has("cause") && !obj.get("cause").isJsonNull()
? context.deserialize(obj.get("cause"), Throwable.class) : null;
// handle types
Throwable t = null;
if (obj.has("types") && obj.get("types").isJsonArray()) {
t = instantiate(obj.getAsJsonArray("types"), message, cause);
}
if (t == null) {
t = new Throwable(message, cause);
}
// handle suppressed
JsonArray suppressed = obj.has("suppressed") && !obj.get("suppressed").isJsonNull()
? obj.get("suppressed").getAsJsonArray() : null;
if (suppressed != null) {
for (JsonElement jsonEl : suppressed) {
t.addSuppressed(context.deserialize(jsonEl, Throwable.class));
}
}
// handle stacktrace
JsonArray stacktrace = obj.has("stacktrace") && !obj.get("stacktrace").isJsonNull()
? obj.get("stacktrace").getAsJsonArray() : null;
if (stacktrace != null) {
List<StackTraceElement> els = new ArrayList<>();
for (JsonElement jsonEl : stacktrace) {
els.add(context.deserialize(jsonEl, StackTraceElement.class));
}
t.setStackTrace(els.toArray(new StackTraceElement[0]));
}
return t;
}
@Override
public JsonElement serialize(Throwable src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject json = new JsonObject();
// toString for easy json reading (not used for deserialization)
json.addProperty("tostring", src.toString());
// handle types
JsonArray types = new JsonArray();
Class<?> cl = src.getClass();
while (cl != Throwable.class) {
if (cl.getCanonicalName() != null)
types.add(cl.getCanonicalName());
cl = cl.getSuperclass();
}
json.add("types", types);
// general data
if (src.getMessage() != null)
json.addProperty("message", src.getMessage());
if (src.getCause() != null)
json.add("cause", context.serialize(src.getCause()));
// handle suppressed
JsonArray suppressed = new JsonArray();
for (Throwable supp : src.getSuppressed()) {
suppressed.add(context.serialize(supp));
}
json.add("suppressed", suppressed);
// handle stacktrace
JsonArray stacktrace = new JsonArray();
for (StackTraceElement stackTraceElement : src.getStackTrace()) {
stacktrace.add(context.serialize(stackTraceElement));
}
json.add("stacktrace", stacktrace);
return json;
}
private static final Map<Class<? extends Throwable>, ThrowableSubAdapter<?>> subAdapters = Collections.synchronizedMap(new HashMap<>());
/**
* Register a new adapter for a specific {@link Throwable} subclass.
* @param clazz the type handled by the specified sub-adapter.
* @param subAdapter the sub-adapter.
* @param <T> the type.
*/
public static <T extends Throwable> void registerSubAdapter(Class<T> clazz, ThrowableSubAdapter<T> subAdapter) {
subAdapters.put(clazz, subAdapter);
}
private static <T extends Throwable> ThrowableSubAdapter<T> defaultSubAdapter(Class<T> clazz) {
BiFunction<String, Throwable, T> constructor = null;
// try (String, Throwable) constructor
try {
Constructor<T> constr = clazz.getConstructor(String.class, Throwable.class);
if (constr.canAccess(null)) {
constructor = (m, t) -> ThrowableUtil.wrapReflectEx(() -> constr.newInstance(m, t));
}
} catch (ReflectiveOperationException ignore) { }
// try (String) constructor
try {
Constructor<T> constr = clazz.getConstructor(String.class);
if (constr.canAccess(null)) {
constructor = ThrowableSubAdapter.messageOnly((m) -> ThrowableUtil.wrapReflectEx(() -> constr.newInstance(m)));
}
} catch (ReflectiveOperationException ignore) { }
if (constructor == null) {
Log.warning("Provided Throwable class '" + clazz + "' does not have any of those constructors or are not accessible: (String, Throwable), (String).");
return null;
}
return new ThrowableSubAdapter<>(constructor);
}
private Throwable instantiate(JsonArray types, String message, Throwable cause) {
Throwable t = null;
for (JsonElement clNameEl : types) {
String clName = clNameEl.getAsString();
try {
@SuppressWarnings("unchecked")
Class<? extends Throwable> cl = (Class<? extends Throwable>) Class.forName(clName);
ThrowableSubAdapter<? extends Throwable> subAdapter = subAdapters.get(cl);
if (subAdapter == null)
subAdapter = defaultSubAdapter(cl);
if (subAdapter != null) {
t = subAdapter.constructor.apply(message, cause);
break;
}
} catch (ReflectiveOperationException ignore) { }
}
return t;
}
/**
* Adapter for specific subclasses of {@link Throwable}.
* @param <T> the type handled by this adapter.
*/
public static class ThrowableSubAdapter<T extends Throwable> {
private final BiFunction<String, Throwable, T> constructor;
/**
* Creates a new adapter for a {@link Throwable}.
* @param constructor function that will construct a new throwable of the handled type, with prefilled message and cause if possible.
*/
protected ThrowableSubAdapter(BiFunction<String, Throwable, T> constructor) {
this.constructor = constructor;
}
/**
* Utility method to use on {@link Throwable} class that only have a message (no cause) constructor.
* @param constructorWithMessage function that will construct a new throwable, with prefilled message.
* @return a function that will construct a throwable using the provided function, then will try to init the cause of the throwable.
* @param <T> the type of the constructed {@link Throwable}.
*/
public static <T extends Throwable> BiFunction<String, Throwable, T> messageOnly(Function<String, T> constructorWithMessage) {
return (m, t) -> {
T inst = constructorWithMessage.apply(m);
try {
inst.initCause(t);
} catch (Exception ignore) { }
return inst;
};
}
}
}

View File

@@ -15,8 +15,8 @@ public class TypeConverter {
/**
* Converts the provided object to an {@link Integer}.
* @param o the object to convert.
* @return a the object converted to an {@link Integer}.
* @throws ConvertionException is a conversion error occurs.
* @return the object converted to an {@link Integer}.
* @throws ConversionException is a conversion error occurs.
*/
public static Integer toInteger(Object o) {
if (o == null) {
@@ -27,7 +27,7 @@ public class TypeConverter {
try {
return ((JsonElement)o).getAsInt();
} catch(UnsupportedOperationException e) {
throw new ConvertionException(e);
throw new ConversionException(e);
}
}
@@ -38,34 +38,34 @@ public class TypeConverter {
try {
return Integer.parseInt((String)o);
} catch (NumberFormatException e) {
throw new ConvertionException(e);
throw new ConversionException(e);
}
}
if (o instanceof Boolean) {
return ((Boolean)o) ? 1 : 0;
}
throw new ConvertionException("No integer convertion available for an instance of "+o.getClass());
throw new ConversionException("No integer conversion available for an instance of "+o.getClass());
}
/**
* Converts the provided object to a primitive int.
* @param o the object to convert.
* @return a the object converted to a primitive int.
* @throws ConvertionException is a conversion error occurs.
* @return the object converted to a primitive int.
* @throws ConversionException is a conversion error occurs.
*/
public static int toPrimInt(Object o) {
Integer val = toInteger(o);
if (val == null)
throw new ConvertionException("null values can't be converted to primitive int");
throw new ConversionException("null values can't be converted to primitive int");
return val;
}
/**
* Converts the provided object to a {@link Double}.
* @param o the object to convert.
* @return a the object converted to a {@link Double}.
* @throws ConvertionException is a conversion error occurs.
* @return the object converted to a {@link Double}.
* @throws ConversionException is a conversion error occurs.
*/
public static Double toDouble(Object o) {
if (o == null) {
@@ -76,7 +76,7 @@ public class TypeConverter {
try {
return ((JsonElement)o).getAsDouble();
} catch(UnsupportedOperationException e) {
throw new ConvertionException(e);
throw new ConversionException(e);
}
}
@@ -87,35 +87,35 @@ public class TypeConverter {
try {
return Double.parseDouble((String)o);
} catch (NumberFormatException e) {
throw new ConvertionException(e);
throw new ConversionException(e);
}
}
if (o instanceof Boolean) {
return ((Boolean)o) ? 1d : 0d;
}
throw new ConvertionException("No double convertion available for an instance of "+o.getClass());
throw new ConversionException("No double conversion available for an instance of "+o.getClass());
}
/**
* Converts the provided object to a primitive double.
* @param o the object to convert.
* @return a the object converted to a primitive double.
* @throws ConvertionException is a conversion error occurs.
* @return the object converted to a primitive double.
* @throws ConversionException is a conversion error occurs.
*/
public static double toPrimDouble(Object o) {
Double val = toDouble(o);
if (val == null)
throw new ConvertionException("null values can't converted to primitive int");
throw new ConversionException("null values can't converted to primitive int");
return val;
}
/**
* Converts the provided object to a {@link String}.
* @param o the object to convert.
* @return a the object converted to a {@link String}.
* @throws ConvertionException is a conversion error occurs.
* @return the object converted to a {@link String}.
* @throws ConversionException is a conversion error occurs.
*/
public static String toString(Object o) {
if (o == null) {
@@ -126,7 +126,7 @@ public class TypeConverter {
try {
return ((JsonElement)o).getAsString();
} catch(UnsupportedOperationException e) {
throw new ConvertionException(e);
throw new ConversionException(e);
}
}
@@ -134,7 +134,7 @@ public class TypeConverter {
return o.toString();
}
throw new ConvertionException("No string convertion available for an instance of "+o.getClass());
throw new ConversionException("No string conversion available for an instance of "+o.getClass());
}
@@ -144,8 +144,8 @@ public class TypeConverter {
* @param mapIntKeys if the String key representing an int should be duplicated as integer type,
* which map to the same value as the original String key. For example, if a key is "12" and map
* to the object <i>o</i>, an integer key 12 will be added and map to the same object <i>o</i>.
* @return a the object converted to a {@link Map}.
* @throws ConvertionException is a conversion error occurs.
* @return the object converted to a {@link Map}.
* @throws ConversionException is a conversion error occurs.
*/
@SuppressWarnings("unchecked")
public static Map<Object, Object> toMap(Object o, boolean mapIntKeys) {
@@ -186,15 +186,15 @@ public class TypeConverter {
return map;
}
throw new ConvertionException("No Map convertion available for an instance of "+o.getClass());
throw new ConversionException("No Map conversion available for an instance of "+o.getClass());
}
/**
* Converts the provided object to a {@link List}.
* @param o the object to convert.
* @return a the object converted to a {@link List}.
* @throws ConvertionException is a conversion error occurs.
* @return the object converted to a {@link List}.
* @throws ConversionException is a conversion error occurs.
*/
@SuppressWarnings("unchecked")
public static List<Object> toList(Object o) {
@@ -217,7 +217,7 @@ public class TypeConverter {
}
throw new ConvertionException("No Map convertion available for an instance of "+o.getClass());
throw new ConversionException("No Map conversion available for an instance of "+o.getClass());
@@ -225,17 +225,20 @@ public class TypeConverter {
/**
* Thrown when a convertion error occurs.
* Thrown when a conversion error occurs.
*/
public static class ConvertionException extends RuntimeException {
public static class ConversionException extends RuntimeException {
private ConvertionException(String m) {
private ConversionException(String m) {
super(m);
}
private ConvertionException(Throwable t) {
private ConversionException(Throwable t) {
super(t);
}
}
private TypeConverter() {}
}

View File

@@ -0,0 +1,85 @@
package fr.pandacube.lib.core.mc_version;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.bind.TreeTypeAdapter;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/**
* Record holding the data for {@link ProtocolVersion}, to facilitate serializing and deserializing.
* @param protocolOfVersion mapping from a version string to the corresponding protocol version number.
* @param versionsOfProtocol mapping from a protocol version number to a list of the supported MC versions.
*/
public record MinecraftVersionList(
Map<String, Integer> protocolOfVersion,
Map<Integer, List<String>> versionsOfProtocol
) {
/**
* Creates an empty {@link MinecraftVersionList}.
*/
public MinecraftVersionList() {
this(new TreeMap<>(MinecraftVersionUtil::compareVersions), new TreeMap<>());
}
/**
* Adds a new pair of version string and protocol version number.
* @param versionId the version string (e.g. "1.19.4").
* @param protocolVersion the protocol version number.
*/
public void add(String versionId, int protocolVersion) {
protocolOfVersion.put(versionId, protocolVersion);
List<String> versions = versionsOfProtocol.computeIfAbsent(protocolVersion, p -> new ArrayList<>());
versions.add(versionId);
versions.sort(MinecraftVersionUtil::compareVersions);
}
/**
* Gson Adapter that ensure the data in {@link MinecraftVersionList} is sorted correctly when deserializing.
*/
public static class MinecraftVersionListAdapter implements JsonSerializer<MinecraftVersionList>, JsonDeserializer<MinecraftVersionList> {
/**
* Gson adapter factory for {@link MinecraftVersionList}.
*/
public static final TypeAdapterFactory FACTORY = TreeTypeAdapter.newTypeHierarchyFactory(MinecraftVersionList.class, new MinecraftVersionListAdapter());
private static final TypeToken<Map<String, Integer>> MAP_STR_INT_TYPE = new TypeToken<>() { };
private static final TypeToken<Map<Integer, List<String>>> MAP_INT_LIST_STRING_TYPE = new TypeToken<>() { };
private MinecraftVersionListAdapter() {}
@Override
public MinecraftVersionList deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (!(json instanceof JsonObject jsonObj))
throw new JsonParseException("Expected JsonObject, got " + json.getClass().getSimpleName() + ".");
MinecraftVersionList mvList = new MinecraftVersionList();
mvList.protocolOfVersion.putAll(context.deserialize(jsonObj.get("protocolOfVersion"), MAP_STR_INT_TYPE.getType()));
mvList.versionsOfProtocol.putAll(context.deserialize(jsonObj.get("versionsOfProtocol"), MAP_INT_LIST_STRING_TYPE.getType()));
for (List<String> versionLists : mvList.versionsOfProtocol.values()) {
versionLists.sort(MinecraftVersionUtil::compareVersions);
}
return mvList;
}
@Override
public JsonElement serialize(MinecraftVersionList src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject obj = new JsonObject();
obj.add("protocolOfVersion", context.serialize(src.protocolOfVersion));
obj.add("versionsOfProtocol", context.serialize(src.versionsOfProtocol));
return obj;
}
}
}

View File

@@ -0,0 +1,141 @@
package fr.pandacube.lib.core.mc_version;
import fr.pandacube.lib.util.StringUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
/**
* Utility class to manipulate {@link String}s representing Minecraft versions.
*/
public class MinecraftVersionUtil {
/**
* Compare two Minecraft version strings. It uses the rules of semantic
* versioning to compare the versions.
* @param v1 the first version to compare.
* @param v2 the second version to compare.
* @return 0 if they are equal, &lt;0 if v1&lt;v2 and vice-versa.
*/
public static int compareVersions(String v1, String v2) {
int[] v1Int = decomposedVersion(v1);
int[] v2Int = decomposedVersion(v2);
for (int i = 0; i < Math.min(v1Int.length, v2Int.length); i++) {
int cmp = Integer.compare(v1Int[i], v2Int[i]);
if (cmp != 0)
return cmp;
}
return Integer.compare(v1Int.length, v2Int.length);
}
/**
* Decompose a version string into a series of integers.
* @param v a string representation of a version (eg. 1.19.1).
* @return an array of int representing the provided version (eg. [1, 19, 1]).
*/
public static int[] decomposedVersion(String v) {
try {
return Arrays.stream(v.split("\\.")).mapToInt(Integer::parseInt).toArray();
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid version format: '" + v + "'.", e);
}
}
/**
* Tells if the two provided Minecraft versions are consecutive.
* <p>
* Two versions are consecutive if (considering {@code 1.X[.Y]}):
* <ul>
* <li>They are part of the same main version (X value)</li>
* <li>v1 has no Y value, and v2 has Y = 1 (eg. 1.19 and 1.19.1) OR
* both v1 and v2 has a Y value and those values are consecutive.
* </li>
* </ul>
* @param v1 the first version.
* @param v2 the second version.
* @return thue if the second version is consecutive to the first one.
*/
public static boolean areConsecutive(String v1, String v2) {
int[] v1Int = decomposedVersion(v1);
int[] v2Int = decomposedVersion(v2);
if (v1Int.length == v2Int.length) {
for (int i = 0; i < v1Int.length - 1; i++) {
if (v1Int[i] != v2Int[i])
return false;
}
return v1Int[v1Int.length - 1] + 1 == v2Int[v2Int.length - 1];
}
else if (v1Int.length == v2Int.length - 1) {
for (int i = 0; i < v1Int.length; i++) {
if (v1Int[i] != v2Int[i])
return false;
}
return v2Int[v2Int.length - 1] == 1;
}
return false;
}
/**
* Generate a string representation of the provided list of version, with
* merged consecutive versions and using
* {@link StringUtil#joinGrammatically(CharSequence, CharSequence, List)}.
*
* @param versions the minecraft versions list to use.
* @param finalWordSeparator the word separator between the two last versions in the returned string, like "and",
* "or" or any other word of any language. The spaces before and after are already
* concatenated.
* @return a string representation of the provided list of version.
*/
public static String toString(List<String> versions, String finalWordSeparator) {
if (versions.isEmpty())
return "";
// put them in order and remove duplicates
versions = new ArrayList<>(toOrderedSet(versions));
List<String> keptVersions = new ArrayList<>(versions.size());
for (int i = 0, firstConsecutive = 0; i < versions.size(); i++) {
if (i == versions.size() - 1 || !areConsecutive(versions.get(i), versions.get(i + 1))) {
if (firstConsecutive == i) {
keptVersions.add(versions.get(i));
firstConsecutive++;
}
else {
// merge
if (i - firstConsecutive > 1)
keptVersions.add(versions.get(firstConsecutive) + "-" + versions.get(i));
else {
keptVersions.add(versions.get(firstConsecutive));
keptVersions.add(versions.get(i));
}
firstConsecutive = i + 1;
}
}
}
return StringUtil.joinGrammatically(", ", " " + finalWordSeparator + " ", keptVersions);
}
private static Set<String> toOrderedSet(List<String> versions) {
Set<String> set = new TreeSet<>(MinecraftVersionUtil::compareVersions);
set.addAll(versions);
return set;
}
private MinecraftVersionUtil() {}
}

View File

@@ -0,0 +1,249 @@
package fr.pandacube.lib.core.mc_version;
import fr.pandacube.lib.core.json.Json;
import fr.pandacube.lib.util.log.Log;
import fr.pandacube.lib.util.StringUtil;
import org.jetbrains.annotations.NotNull;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
/**
* Class handling a relationship table of known Minecraft version and their
* corresponding protocol version.
* <p>
* The data if fetch updated data from an external API on startup. If it fails,
* it uses the data stored in the current package at build time.
* <p>
* The public static methods are used to fetch an instance of {@link ProtocolVersion}
* based on the provided protocol version (e.g. 763) or Minecraft version (e.g. "1.20.1").
* An instance of this class provides information related to a protocol version
* (the protocol version number and all the corresponding Minecraft versions).
*/
public class ProtocolVersion implements Comparable<ProtocolVersion> {
private static final String ONLINE_DATA_URL = "https://api.pandacube.fr/rest/mcversion";
private static final AtomicReference<MinecraftVersionList> versionList = new AtomicReference<>();
private static void initIfNecessary() {
synchronized (versionList) {
if (versionList.get() == null) {
init();
}
}
}
/**
* Replace the currently used data cache by a new source.
* <p>
* <b>Note: </b>this method is not meant to be used by the final user of
* this class. Use it only if you have a better data source.
* @param data the data to use instead of the provided (external API or packaged file)
*/
public static void setRawData(MinecraftVersionList data) {
versionList.set(data);
}
/**
* Gets the raw data used internally by this class.
* <p>
* <b>Note: </b>this method is not meant to be used by the final user of
* this class. Use it only if you know what you do.
* @return the current instance of {@link MinecraftVersionUtil} uses
* internally by this class.
*/
public static MinecraftVersionList getRawData() {
initIfNecessary();
return versionList.get();
}
private static void init() {
// try online source first
try {
HttpResponse<String> response = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build()
.send(HttpRequest.newBuilder(URI.create(ONLINE_DATA_URL)).build(),
BodyHandlers.ofString()
);
if (response.statusCode() == 200) {
MinecraftVersionList data = Json.gson.fromJson(response.body(), MinecraftVersionList.class);
versionList.set(data);
}
} catch (Exception e) {
Log.warning(e);
}
if (versionList.get() != null) {
return;
}
Log.warning("Unable to get minecraft version data from API. Using local data instead.");
// try local source
try (InputStream is = ProtocolVersion.class.getResourceAsStream("mcversion.json")) {
if (is != null) {
try (InputStreamReader isr = new InputStreamReader(is)) {
MinecraftVersionList data = Json.gson.fromJson(isr, MinecraftVersionList.class);
versionList.set(data);
}
}
} catch (Exception e) {
Log.warning(e);
}
if (versionList.get() != null) {
return;
}
Log.severe("Unable to get Minecraft versions data from classpath. Using empty data instead.");
versionList.set(new MinecraftVersionList());
}
private static int getPVNOfVersion(String version) {
initIfNecessary();
Integer v = versionList.get().protocolOfVersion().get(version);
return v == null ? -1 : v;
}
private static List<String> getVersionsOfPVN(int pvn) {
initIfNecessary();
return versionList.get().versionsOfProtocol().get(pvn);
}
/**
* Gets the {@link ProtocolVersion} associated with the provided Minecraft version.
* @param version The Minecraft version, in the format "X.X[.X]" (eg. "1.17" or "1.8.8").
* @return an instance of {@link ProtocolVersion}.
*/
public static ProtocolVersion ofVersion(String version) {
int pvn = getPVNOfVersion(version);
if (pvn == -1)
return null;
List<String> versions = getVersionsOfPVN(pvn);
if (versions == null) {
versions = List.of(version);
}
return new ProtocolVersion(pvn, List.copyOf(versions));
}
/**
* Gets the {@link ProtocolVersion} associated with the provided protocol version number.
* @param pvn The protocol version number.
* @return an instance of {@link ProtocolVersion}.
*/
public static ProtocolVersion ofProtocol(int pvn) {
List<String> versions = getVersionsOfPVN(pvn);
if (versions == null) {
return null;
}
return new ProtocolVersion(pvn, List.copyOf(versions));
}
/**
* Returns all the {@link ProtocolVersion} currently known by this class.
* @return all the {@link ProtocolVersion} currently known by this class.
*/
public static List<ProtocolVersion> allKnownProtocolVersions() {
return versionList.get().versionsOfProtocol().entrySet().stream()
.filter(e -> e.getValue() != null && !e.getValue().isEmpty())
.map(e -> new ProtocolVersion(e.getKey(), List.copyOf(e.getValue())))
.toList();
}
/**
* Generate a string representation of the provided list of version, using
* {@link StringUtil#joinGrammatically(CharSequence, CharSequence, List)}.
*
* @param versions the minecraft versions to list
* @param finalWordSeparator the word separator between the two last versions in the returned string, like "and",
* "or" or any other word of any language. The spaces before and after are already
* concatenated.
* @return a string representation of the provided list of version.
*/
public static String displayOptimizedListOfVersions(List<ProtocolVersion> versions, String finalWordSeparator) {
return MinecraftVersionUtil.toString(versions.stream().flatMap(pv -> pv.versions.stream()).toList(), finalWordSeparator);
}
/**
* The protocol version number.
*/
public final int protocolVersionNumber;
/**
* All Minecraft version supported by this protocol version number.
*/
public final List<String> versions;
private ProtocolVersion(int protocolVersionNumber, List<String> versions) {
this.protocolVersionNumber = protocolVersionNumber;
this.versions = versions;
}
@Override
public String toString() {
return "ProtocolVersion{protocol=" + protocolVersionNumber + ", toString(\"and\")=" + toString("and") + "}";
}
/**
* Returns a string representation of all the Minecraft version of this enum value, using
* {@link StringUtil#joinGrammatically(CharSequence, CharSequence, List)}.
*
* @param finalWordSeparator the word separator between the two last versions in the returned string, like "and",
* "or" or any other word of any language. The spaces before and after are already
* concatenated.
* @return a string representation of this {@link ProtocolVersion}.
*/
public String toString(String finalWordSeparator) {
return displayOptimizedListOfVersions(List.of(this), finalWordSeparator);
}
/**
* Gets the first (earliest) Minecraft version that supports this protocol version.
* @return the first (earliest) Minecraft version that supports this protocol version.
*/
public String getFirstVersion() {
return versions.get(0);
}
/**
* Gets the last (latest) Minecraft version that supports this protocol version.
* @return the last (latest) Minecraft version that supports this protocol version.
*/
public String getLastVersion() {
return versions.get(versions.size() - 1);
}
@Override
public boolean equals(Object o) {
return o instanceof ProtocolVersion pv && protocolVersionNumber == pv.protocolVersionNumber;
}
@Override
public int hashCode() {
return protocolVersionNumber;
}
@Override
public int compareTo(@NotNull ProtocolVersion o) {
return Integer.compare(protocolVersionNumber, o.protocolVersionNumber);
}
}

View File

@@ -2,7 +2,7 @@ package fr.pandacube.lib.core.search;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
import java.util.ArrayList;
import java.util.HashMap;
@@ -15,7 +15,7 @@ import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
/**
* Utility class to manage searching among a set of {@link SearchResult} instances, using case insensitive keywords.
* Utility class to manage searching among a set of {@link SearchResult} instances, using case-insensitive keywords.
* The search engine is responsible for storing a database of entries ({@link SearchResult}) that can be searched using
* keywords. This class provides methods to returns a list of results for provided keywords, a list of keyword
* suggestions based on pre-typed keywords.

View File

@@ -0,0 +1,238 @@
{
"protocolOfVersion": {
"1.7.2": 4,
"1.7.3": 4,
"1.7.4": 4,
"1.7.5": 4,
"1.7.6": 5,
"1.7.7": 5,
"1.7.8": 5,
"1.7.9": 5,
"1.7.10": 5,
"1.8": 47,
"1.8.1": 47,
"1.8.2": 47,
"1.8.3": 47,
"1.8.4": 47,
"1.8.5": 47,
"1.8.6": 47,
"1.8.7": 47,
"1.8.8": 47,
"1.8.9": 47,
"1.9": 107,
"1.9.1": 108,
"1.9.2": 109,
"1.9.3": 110,
"1.9.4": 110,
"1.10": 210,
"1.10.1": 210,
"1.10.2": 210,
"1.11": 315,
"1.11.1": 316,
"1.11.2": 316,
"1.12": 335,
"1.12.1": 338,
"1.12.2": 340,
"1.13": 393,
"1.13.1": 401,
"1.13.2": 404,
"1.14": 477,
"1.14.1": 480,
"1.14.2": 485,
"1.14.3": 490,
"1.14.4": 498,
"1.15": 573,
"1.15.1": 575,
"1.15.2": 578,
"1.16": 735,
"1.16.1": 736,
"1.16.2": 751,
"1.16.3": 753,
"1.16.4": 754,
"1.16.5": 754,
"1.17": 755,
"1.17.1": 756,
"1.18": 757,
"1.18.1": 757,
"1.18.2": 758,
"1.19": 759,
"1.19.1": 760,
"1.19.2": 760,
"1.19.3": 761,
"1.19.4": 762,
"1.20": 763,
"1.20.1": 763,
"1.20.2": 764,
"1.20.3": 765,
"1.20.4": 765,
"1.20.5": 766,
"1.20.6": 766,
"1.21": 767,
"1.21.1": 767,
"1.21.2": 768,
"1.21.3": 768,
"1.21.4": 769
},
"versionsOfProtocol": {
"4": [
"1.7.2",
"1.7.3",
"1.7.4",
"1.7.5"
],
"5": [
"1.7.6",
"1.7.7",
"1.7.8",
"1.7.9",
"1.7.10"
],
"47": [
"1.8",
"1.8.1",
"1.8.2",
"1.8.3",
"1.8.4",
"1.8.5",
"1.8.6",
"1.8.7",
"1.8.8",
"1.8.9"
],
"107": [
"1.9"
],
"108": [
"1.9.1"
],
"109": [
"1.9.2"
],
"110": [
"1.9.3",
"1.9.4"
],
"210": [
"1.10",
"1.10.1",
"1.10.2"
],
"315": [
"1.11"
],
"316": [
"1.11.1",
"1.11.2"
],
"335": [
"1.12"
],
"338": [
"1.12.1"
],
"340": [
"1.12.2"
],
"393": [
"1.13"
],
"401": [
"1.13.1"
],
"404": [
"1.13.2"
],
"477": [
"1.14"
],
"480": [
"1.14.1"
],
"485": [
"1.14.2"
],
"490": [
"1.14.3"
],
"498": [
"1.14.4"
],
"573": [
"1.15"
],
"575": [
"1.15.1"
],
"578": [
"1.15.2"
],
"735": [
"1.16"
],
"736": [
"1.16.1"
],
"751": [
"1.16.2"
],
"753": [
"1.16.3"
],
"754": [
"1.16.4",
"1.16.5"
],
"755": [
"1.17"
],
"756": [
"1.17.1"
],
"757": [
"1.18",
"1.18.1"
],
"758": [
"1.18.2"
],
"759": [
"1.19"
],
"760": [
"1.19.1",
"1.19.2"
],
"761": [
"1.19.3"
],
"762": [
"1.19.4"
],
"763": [
"1.20",
"1.20.1"
],
"764": [
"1.20.2"
],
"765": [
"1.20.3",
"1.20.4"
],
"766": [
"1.20.5",
"1.20.6"
],
"767": [
"1.21",
"1.21.1"
],
"768": [
"1.21.2",
"1.21.3"
],
"769": [
"1.21.4"
]
}
}

View File

@@ -27,7 +27,7 @@
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>2.9.0</version>
<version>2.12.0</version>
</dependency>
</dependencies>
@@ -36,7 +36,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.3.0</version>
<version>3.5.2</version>
<executions>
<execution>
<phase>package</phase>
@@ -56,29 +56,32 @@
<artifact>org.apache.commons:commons-dbcp2</artifact>
<excludes>
<exclude>META-INF/MANIFEST.MF</exclude>
<exclude>META-INF/versions/9/**</exclude>
</excludes>
</filter>
<filter>
<artifact>org.apache.commons:commons-pool2</artifact>
<excludes>
<exclude>META-INF/MANIFEST.MF</exclude>
<exclude>META-INF/versions/9/**</exclude>
</excludes>
</filter>
<filter>
<artifact>commons-logging:commons-logging</artifact>
<excludes>
<exclude>META-INF/MANIFEST.MF</exclude>
<exclude>META-INF/versions/9/**</exclude>
</excludes>
</filter>
</filters>
<relocations>
<relocation>
<pattern>org.apache.commons</pattern>
<shadedPattern>fr.pandacube.lib.db.shaded.commons</shadedPattern>
<pattern>org.apache.commons.dbcp2</pattern>
<shadedPattern>fr.pandacube.lib.db.shaded.commons.dbcp2</shadedPattern>
</relocation>
<relocation>
<pattern>org.apache.commons</pattern>
<shadedPattern>fr.pandacube.lib.db.shaded.commons</shadedPattern>
<pattern>org.apache.commons.pool2</pattern>
<shadedPattern>fr.pandacube.lib.db.shaded.commons.pool2</shadedPattern>
</relocation>
</relocations>
<transformers>

View File

@@ -15,7 +15,7 @@ import java.util.Objects;
import java.util.function.Consumer;
import fr.pandacube.lib.reflect.Reflect;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
/**
* Static class to handle most of the database operations.
@@ -52,10 +52,10 @@ public final class DB {
}
/**
* Initialialize the table represented by the provided class.
* Initialize the table represented by the provided class.
* @param elemClass the class representing a table.
* @param <E> the type representing the table.
* @throws DBInitTableException if the table failed to initialized.
* @throws DBInitTableException if the table failed to initialize.
*/
public static synchronized <E extends SQLElement<E>> void initTable(Class<E> elemClass) throws DBInitTableException {
if (connection == null) {
@@ -111,7 +111,7 @@ public final class DB {
* @param elemClass the class representing a table.
* @return a table name.
* @param <E> the type representing the table.
* @throws DBInitTableException if the provided table had to be initialized and it failed to do so.
* @throws DBInitTableException if the provided table had to be initialized and failed to do so.
*/
public static <E extends SQLElement<E>> String getTableName(Class<E> elemClass) throws DBInitTableException {
initTable(elemClass);
@@ -130,7 +130,7 @@ public final class DB {
* @param elemClass the class representing a table.
* @return the {@code id} field of the provided table.
* @param <E> the type representing the table.
* @throws DBInitTableException if the provided table had to be initialized and it failed to do so.
* @throws DBInitTableException if the provided table had to be initialized and failed to do so.
*/
@SuppressWarnings("unchecked")
public static <E extends SQLElement<E>> SQLField<E, Integer> getSQLIdField(Class<E> elemClass) throws DBInitTableException {
@@ -226,8 +226,8 @@ public final class DB {
* @throws DBException if an error occurs when interacting with the database.
*/
public static <E extends SQLElement<E>> E getFirst(Class<E> elemClass, SQLWhere<E> where, SQLOrderBy<E> orderBy, Integer offset) throws DBException {
SQLElementList<E> elts = getAll(elemClass, where, orderBy, 1, offset);
return (elts.size() == 0) ? null : elts.get(0);
SQLElementList<E> elements = getAll(elemClass, where, orderBy, 1, offset);
return (elements.size() == 0) ? null : elements.get(0);
}
/**
@@ -294,15 +294,15 @@ public final class DB {
* @throws DBException if an error occurs when interacting with the database.
*/
public static <E extends SQLElement<E>> SQLElementList<E> getAll(Class<E> elemClass, SQLWhere<E> where, SQLOrderBy<E> orderBy, Integer limit, Integer offset) throws DBException {
SQLElementList<E> elmts = new SQLElementList<>();
forEach(elemClass, where, orderBy, limit, offset, elmts::add);
return elmts;
SQLElementList<E> elements = new SQLElementList<>();
forEach(elemClass, where, orderBy, limit, offset, elements::add);
return elements;
}
/**
* Iterate through all the entries from the provided table.
* @param elemClass the class representing a table.
* @param action the action to perform on each entries.
* @param action the action to perform on each entry.
* @param <E> the type representing the table.
* @throws DBException if an error occurs when interacting with the database.
*/
@@ -314,7 +314,7 @@ public final class DB {
* Iterate through the entries from the provided table, using the provided {@code WHERE} clause.
* @param elemClass the class representing a table.
* @param where the {@code WHERE} clause of the query.
* @param action the action to perform on each entries.
* @param action the action to perform on each entry.
* @param <E> the type representing the table.
* @throws DBException if an error occurs when interacting with the database.
*/
@@ -328,7 +328,7 @@ public final class DB {
* @param elemClass the class representing a table.
* @param where the {@code WHERE} clause of the query.
* @param orderBy the {@code ORDER BY} clause of the query.
* @param action the action to perform on each entries.
* @param action the action to perform on each entry.
* @param <E> the type representing the table.
* @throws DBException if an error occurs when interacting with the database.
*/
@@ -343,7 +343,7 @@ public final class DB {
* @param where the {@code WHERE} clause of the query.
* @param orderBy the {@code ORDER BY} clause of the query.
* @param limit the {@code LIMIT} clause of the query.
* @param action the action to perform on each entries.
* @param action the action to perform on each entry.
* @param <E> the type representing the table.
* @throws DBException if an error occurs when interacting with the database.
*/
@@ -359,7 +359,7 @@ public final class DB {
* @param orderBy the {@code ORDER BY} clause of the query.
* @param limit the {@code LIMIT} clause of the query.
* @param offset the {@code OFFSET} clause of the query.
* @param action the action to perform on each entries.
* @param action the action to perform on each entry.
* @param <E> the type representing the table.
* @throws DBException if an error occurs when interacting with the database.
*/
@@ -577,7 +577,7 @@ public final class DB {
@SuppressWarnings("unchecked")
private static <E extends SQLElement<E>> E getElementInstance(ResultSet set, Class<E> elemClass) throws DBException {
try {
E instance = Reflect.ofClass(elemClass).constructor(int.class).instanciate(set.getInt("id"));
E instance = Reflect.ofClass(elemClass).constructor(int.class).instantiate(set.getInt("id"));
int fieldCount = set.getMetaData().getColumnCount();
@@ -623,7 +623,7 @@ public final class DB {
return instance;
} catch (ReflectiveOperationException | IllegalArgumentException | SecurityException | SQLException e) {
throw new DBException("Can't instanciate " + elemClass.getName(), e);
throw new DBException("Can't instantiate " + elemClass.getName(), e);
}
}

View File

@@ -23,7 +23,8 @@ public class DBConnection {
public DBConnection(String host, int port, String dbname, String login, String password) {
this("jdbc:mysql://" + host + ":" + port + "/" + dbname
+ "?useUnicode=true"
+ "&useSSL=false"
+ "&sslMode=DISABLED"
+ "&allowPublicKeyRetrieval=true"
+ "&characterEncoding=utf8"
+ "&characterSetResults=utf8"
+ "&character_set_server=utf8mb4"

View File

@@ -1,7 +1,7 @@
package fr.pandacube.lib.db;
/**
* Exception thrown when something bad happends when using the {@link DB} API.
* Exception thrown when something bad happens when using the {@link DB} API.
*/
public class DBException extends Exception {

View File

@@ -1,7 +1,7 @@
package fr.pandacube.lib.db;
/**
* Exception thrown when something bad happends when initializing a new table using {@link DB#initTable(Class)}.
* Exception thrown when something bad happens when initializing a new table using {@link DB#initTable(Class)}.
*/
public class DBInitTableException extends DBException {

View File

@@ -26,7 +26,7 @@ import java.util.UUID;
import java.util.stream.Collectors;
import fr.pandacube.lib.util.EnumUtil;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
/**
* Represents an entry in a SQL table. Each subclass is for a specific table.
@@ -119,7 +119,7 @@ public abstract class SQLElement<E extends SQLElement<E>> {
/**
* Gets the name of the table in the database, without the prefix defined by {@link DB#init(DBConnection, String)}.
* @return The unprefixed name of the table in the database.
* @return The non-prefixed name of the table in the database.
*/
protected abstract String tableName();
@@ -133,7 +133,7 @@ public abstract class SQLElement<E extends SQLElement<E>> {
}
/**
* Fills the values of this entry that are known to be nullable or have a default value.
* Fills the entries values that are known to be nullable or have a default value.
*/
@SuppressWarnings("unchecked")
private void initDefaultValues() {
@@ -193,7 +193,7 @@ public abstract class SQLElement<E extends SQLElement<E>> {
/**
* Sets a value in this entry.
* <p>
* This is not good practice to set the {@code id} field of any entry, because its an unique auto-incremented
* This is not good practice to set the {@code id} field of any entry, because its a unique auto-incremented
* value. Use {@link #save()} and {@link #delete()} to set or unset the {@code id} instead, in consistence with the
* database.
* @param field the field to set.
@@ -241,25 +241,32 @@ public abstract class SQLElement<E extends SQLElement<E>> {
* Gets the value of the provided field in this entry.
* @param field the field to get the value from.
* @return the value of the provided field in this entry.
* @throws IllegalArgumentException if the provided field is null or not from the table represented by this class.
* @throws IllegalStateException if the field is not nullable and there is no value set
* @param <T> the Java type of the field.
*/
public <T> T get(SQLField<E, T> field) {
if (field == null) throw new IllegalArgumentException("field can't be null");
if (field == null)
throw new IllegalArgumentException("field can't be null");
if (!fields.containsKey(field.getName()) || !fields.get(field.getName()).equals(field))
throw new IllegalArgumentException("The provided field " + field + " is not from this table " + getClass().getName());
if (values.containsKey(field)) {
@SuppressWarnings("unchecked")
T val = (T) values.get(field);
return val;
}
throw new IllegalArgumentException("The field '" + field.getName() + "' in this instance of " + getClass().getName()
+ " does not exist or is not set");
if (field.nullable)
return null;
throw new IllegalStateException("The non-nullable field '" + field.getName() + "' in this instance of " + getClass().getName()
+ " is not set");
}
/**
* Gets the foreign table entry targeted by the provided foreignkey of this table.
* @param field a foreignkey of this table.
* @param <T> the type of the foreignkey field.
* Gets the foreign table entry targeted by the provided foreign key of this table.
* @param field a foreign key of this table.
* @param <T> the type of the foreign key field.
* @param <P> the targeted foreign table type.
* @return the foreign table entry targeted by the provided foreignkey of this table.
* @return the foreign table entry targeted by the provided foreign key of this table.
* @throws DBException if an error occurs when interacting with the database.
*/
public <T, P extends SQLElement<P>> P getReferencedEntry(SQLFKField<E, T, P> field) throws DBException {
@@ -271,11 +278,11 @@ public abstract class SQLElement<E extends SQLElement<E>> {
/**
* Gets the original table entry which the provided foreign key is targeting this entry, and following the provided
* {@code ORDER BY}, {@code LIMIT} and {@code OFFSET} clauses.
* @param field a foreignkey in the original table.
* @param field a foreign key in the original table.
* @param orderBy the {@code ORDER BY} clause of the query.
* @param limit the {@code LIMIT} clause of the query.
* @param offset the {@code OFFSET} clause of the query.
* @param <T> the type of the foreignkey field.
* @param <T> the type of the foreign key field.
* @param <F> the table class of the foreign key that reference a field of this entry.
* @return the original table entry which the provided foreign key is targeting this entry.
* @throws DBException if an error occurs when interacting with the database.
@@ -314,7 +321,7 @@ public abstract class SQLElement<E extends SQLElement<E>> {
/**
* Saves this entry into the database, either by updating the already existing entry in it, or by creating a new
* entry if it doesnt exist yet.
* entry if it doesn't exist yet.
* @return this.
* @throws DBException if an error occurs when interacting with the database.
*/
@@ -354,7 +361,7 @@ public abstract class SQLElement<E extends SQLElement<E>> {
first = false;
concatValues.append(" ? ");
concatFields.append("`").append(entry.getKey().getName()).append("`");
addValueToSQLObjectList(psValues, entry.getKey(), entry.getValue());
psValues.add(entry.getKey().fromJavaTypeToJDBCType(entry.getValue()));
}
try (Connection c = db.getConnection();
@@ -382,20 +389,6 @@ public abstract class SQLElement<E extends SQLElement<E>> {
return (E) this;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
/* package */ static <E extends SQLElement<E>> void addValueToSQLObjectList(List<Object> list, SQLField<E, ?> field, Object jValue) throws DBException {
if (jValue != null && field.type instanceof SQLCustomType) {
try {
jValue = ((SQLCustomType)field.type).javaToDbConv.apply(jValue);
} catch (Exception e) {
throw new DBException("Error while converting value of field '"+field.getName()+"' with SQLCustomType from "+field.type.getJavaType()
+"(java source) to "+((SQLCustomType<?, ?>)field.type).intermediateJavaType+"(jdbc destination). The original value is '"+jValue+"'", e);
}
}
list.add(jValue);
}
/**
* Tells if this entry is currently stored in DB or not.
* @return true if this entry is currently stored in DB, or false otherwise.
@@ -474,14 +467,14 @@ public abstract class SQLElement<E extends SQLElement<E>> {
* Creates a new SQL field.
* @param type the type of the field.
* @param nullable true if nullable, false if {@code NOT NULL}.
* @param autoIncr if {@code AUTO_INCREMENT}.
* @param autoIncrement if {@code AUTO_INCREMENT}.
* @param deflt a default value for this field. A null value indicate that this has no default value.
* @return the new SQL field.
* @param <E> the table type.
* @param <T> the Java type of this field.
*/
protected static <E extends SQLElement<E>, T> SQLField<E, T> field(SQLType<T> type, boolean nullable, boolean autoIncr, T deflt) {
return new SQLField<>(type, nullable, autoIncr, deflt);
protected static <E extends SQLElement<E>, T> SQLField<E, T> field(SQLType<T> type, boolean nullable, boolean autoIncrement, T deflt) {
return new SQLField<>(type, nullable, autoIncrement, deflt);
}
/**
@@ -500,13 +493,13 @@ public abstract class SQLElement<E extends SQLElement<E>> {
* Creates a new SQL field.
* @param type the type of the field.
* @param nullable true if nullable, false if {@code NOT NULL}.
* @param autoIncr if {@code AUTO_INCREMENT}.
* @param autoIncrement if {@code AUTO_INCREMENT}.
* @return the new SQL field.
* @param <E> the table type.
* @param <T> the Java type of this field.
*/
protected static <E extends SQLElement<E>, T> SQLField<E, T> field(SQLType<T> type, boolean nullable, boolean autoIncr) {
return new SQLField<>(type, nullable, autoIncr);
protected static <E extends SQLElement<E>, T> SQLField<E, T> field(SQLType<T> type, boolean nullable, boolean autoIncrement) {
return new SQLField<>(type, nullable, autoIncrement);
}
/**

View File

@@ -16,6 +16,11 @@ import java.util.stream.Collectors;
*/
public class SQLElementList<E extends SQLElement<E>> extends ArrayList<E> {
/**
* Creates an empty list of sql elements.
*/
public SQLElementList() {}
/**
* Stores all the values modified by {@link #setCommon(SQLField, Object)}.
*/
@@ -46,7 +51,7 @@ public class SQLElementList<E extends SQLElement<E>> extends ArrayList<E> {
E emptyElement = elemClass.getConstructor().newInstance();
emptyElement.set(field, value, false);
} catch (Exception e) {
throw new IllegalArgumentException("Illegal field or value or can't instanciante an empty instance of "
throw new IllegalArgumentException("Illegal field or value or can't instantiate an empty instance of "
+ elemClass.getName() + ". (the instance is only created to test validity of field and value)", e);
}
@@ -83,7 +88,7 @@ public class SQLElementList<E extends SQLElement<E>> extends ArrayList<E> {
@SuppressWarnings("unchecked")
private void applyNewValuesToElements(List<E> storedEl) {
// applique les valeurs dans chaques objets de la liste
// applique les valeurs dans chaque objet de la liste
for (E el : storedEl) {
for (@SuppressWarnings("rawtypes") SQLField entry : modifiedValues.keySet()) {
if (!el.isModified(entry)) {
@@ -100,7 +105,7 @@ public class SQLElementList<E extends SQLElement<E>> extends ArrayList<E> {
/**
* Removes all the entries of this list from the database.
* This method has the same effect as calling the {@link SQLElement#delete()} method individually on each element,
* but with only one SQL query to delete all of the entries.
* but with only one SQL query to delete all the entries.
* <p>
* If you intend to remove the entries from the database just after fetching them, call directly the
* {@link DB#delete(Class, SQLWhere)} method instead.
@@ -124,9 +129,9 @@ public class SQLElementList<E extends SQLElement<E>> extends ArrayList<E> {
/**
* Get all the entries targeted by the foreign key of all the entries in this list.
* @param foreignKey a foreignkey of this table.
* @param foreignKey a foreign key of this table.
* @param orderBy the {@code ORDER BY} clause of the query.
* @return a list of foreign table entries targeted by the provided foreignkey of this table.
* @return a list of foreign table entries targeted by the provided foreign key of this table.
* @param <T> the fields Java type.
* @param <P> the target table type.
* @throws DBException if an error occurs when interacting with the database.
@@ -144,7 +149,7 @@ public class SQLElementList<E extends SQLElement<E>> extends ArrayList<E> {
/**
* Get all the entries targeted by the foreign key of all the entries in this list, mapped from the foreign key value.
* @param foreignKey a foreignkey of this table.
* @param foreignKey a foreign key of this table.
* @return a map of the foreign key values, mapped to the foreign tables entries.
* @param <T> the fields Java type.
* @param <P> the target table type.
@@ -163,11 +168,11 @@ public class SQLElementList<E extends SQLElement<E>> extends ArrayList<E> {
/**
* Gets all the original tables entries which the provided foreign key is targeting the entries of this list, and
* following the provided {@code ORDER BY}, {@code LIMIT} and {@code OFFSET} clauses.
* @param foreignKey a foreignkey in the original table.
* @param foreignKey a foreign key in the original table.
* @param orderBy the {@code ORDER BY} clause of the query.
* @param limit the {@code LIMIT} clause of the query.
* @param offset the {@code OFFSET} clause of the query.
* @param <T> the type of the foreignkey field.
* @param <T> the type of the foreign key field.
* @param <F> the table class of the foreign key that reference a field of this entry.
* @return the original tables entries which the provided foreign key is targeting the entries of this list.
* @throws DBException if an error occurs when interacting with the database.
@@ -187,11 +192,11 @@ public class SQLElementList<E extends SQLElement<E>> extends ArrayList<E> {
* Gets all the original tables entries which the provided foreign key is targeting the entries of this list,
* following the provided {@code ORDER BY}, {@code LIMIT} and {@code OFFSET} clauses, and mapped from the foreign
* key value.
* @param foreignKey a foreignkey in the original table.
* @param foreignKey a foreign key in the original table.
* @param orderBy the {@code ORDER BY} clause of the query.
* @param limit the {@code LIMIT} clause of the query.
* @param offset the {@code OFFSET} clause of the query.
* @param <T> the type of the foreignkey field.
* @param <T> the type of the foreign key field.
* @param <F> the table class of the foreign key that reference a field of this entry.
* @return a map of the foreign key values, mapped to the orignal tables entries.
* @throws DBException if an error occurs when interacting with the database.

View File

@@ -1,6 +1,6 @@
package fr.pandacube.lib.db;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
/**
* A foreign key field in a SQL table.
@@ -28,7 +28,7 @@ public class SQLFKField<F extends SQLElement<F>, T, P extends SQLElement<P>> ext
SQLField<F, Integer> f = DB.getSQLIdField(fkEl);
return new SQLFKField<>(f.type, nul, deflt, fkEl, f);
} catch (DBInitTableException e) {
Log.severe("Can't create Foreign key Field targetting id field of '"+fkEl+"'", e);
Log.severe("Can't create Foreign key Field targeting id field of '"+fkEl+"'", e);
return null;
}
}
@@ -51,7 +51,7 @@ public class SQLFKField<F extends SQLElement<F>, T, P extends SQLElement<P>> ext
}
if (fkF.getSQLElementType() == null)
throw new RuntimeException("Can't initialize foreign key. The primary key in the table " + fkEl.getName() + " is not properly initialized and can't be targetted by a forein key");
throw new RuntimeException("Can't initialize foreign key. The primary key in the table " + fkEl.getName() + " is not properly initialized and can't be targeted by a foreign key");
sqlPrimaryKeyField = fkF;
sqlForeignKeyElemClass = fkEl;
}

View File

@@ -24,10 +24,10 @@ public class SQLField<E extends SQLElement<E>, T> {
/* package */ final boolean autoIncrement;
/* package */ final T defaultValue;
/* package */ SQLField(SQLType<T> type, boolean nullable, boolean autoIncr, T deflt) {
/* package */ SQLField(SQLType<T> type, boolean nullable, boolean autoIncrement, T deflt) {
this.type = type;
this.nullable = nullable;
autoIncrement = autoIncr;
this.autoIncrement = autoIncrement;
defaultValue = deflt;
}
@@ -35,8 +35,8 @@ public class SQLField<E extends SQLElement<E>, T> {
this(type, nullable, false, null);
}
/* package */ SQLField(SQLType<T> type, boolean nullable, boolean autoIncr) {
this(type, nullable, autoIncr, null);
/* package */ SQLField(SQLType<T> type, boolean nullable, boolean autoIncrement) {
this(type, nullable, autoIncrement, null);
}
/* package */ SQLField(SQLType<T> type, boolean nullable, T deflt) {
@@ -133,7 +133,7 @@ public class SQLField<E extends SQLElement<E>, T> {
/**
* Create a SQL {@code WHERE} expression comparing this field with the provided value using the {@code =} operator.
* @param r the value to compare with.
* @return a SQL {@code WHERE} expression.
* @return a new SQL {@code WHERE} expression.
*/
public fr.pandacube.lib.db.SQLWhere<E> eq(T r) {
return comp(SQLComparator.EQ, r);
@@ -142,7 +142,7 @@ public class SQLField<E extends SQLElement<E>, T> {
/**
* Create a SQL {@code WHERE} expression comparing this field with the provided value using the {@code >=} operator.
* @param r the value to compare with.
* @return a SQL {@code WHERE} expression.
* @return a new SQL {@code WHERE} expression.
*/
public fr.pandacube.lib.db.SQLWhere<E> geq(T r) {
return comp(SQLComparator.GEQ, r);
@@ -151,7 +151,7 @@ public class SQLField<E extends SQLElement<E>, T> {
/**
* Create a SQL {@code WHERE} expression comparing this field with the provided value using the {@code >} operator.
* @param r the value to compare with.
* @return a SQL {@code WHERE} expression.
* @return a new SQL {@code WHERE} expression.
*/
public fr.pandacube.lib.db.SQLWhere<E> gt(T r) {
return comp(SQLComparator.GT, r);
@@ -160,7 +160,7 @@ public class SQLField<E extends SQLElement<E>, T> {
/**
* Create a SQL {@code WHERE} expression comparing this field with the provided value using the {@code <=} operator.
* @param r the value to compare with.
* @return a SQL {@code WHERE} expression.
* @return a new SQL {@code WHERE} expression.
*/
public fr.pandacube.lib.db.SQLWhere<E> leq(T r) {
return comp(SQLComparator.LEQ, r);
@@ -169,7 +169,7 @@ public class SQLField<E extends SQLElement<E>, T> {
/**
* Create a SQL {@code WHERE} expression comparing this field with the provided value using the {@code <} operator.
* @param r the value to compare with.
* @return a SQL {@code WHERE} expression.
* @return a new SQL {@code WHERE} expression.
*/
public fr.pandacube.lib.db.SQLWhere<E> lt(T r) {
return comp(SQLComparator.LT, r);
@@ -178,7 +178,7 @@ public class SQLField<E extends SQLElement<E>, T> {
/**
* Create a SQL {@code WHERE} expression comparing this field with the provided value using the {@code !=} operator.
* @param r the value to compare with.
* @return a SQL {@code WHERE} expression.
* @return a new SQL {@code WHERE} expression.
*/
public fr.pandacube.lib.db.SQLWhere<E> neq(T r) {
return comp(SQLComparator.NEQ, r);
@@ -194,7 +194,7 @@ public class SQLField<E extends SQLElement<E>, T> {
* Create a SQL {@code WHERE} expression comparing this field with the provided value using the {@code LIKE}
* keyword.
* @param like the value to compare with.
* @return a SQL {@code WHERE} expression.
* @return a new SQL {@code WHERE} expression.
*/
public fr.pandacube.lib.db.SQLWhere<E> like(String like) {
return new SQLWhereLike<>(this, like);
@@ -206,7 +206,7 @@ public class SQLField<E extends SQLElement<E>, T> {
* Create a SQL {@code WHERE} expression testing the presence of this field in the provided collection of value
* using the {@code IN} keyword.
* @param v the value to compare with.
* @return a SQL {@code WHERE} expression.
* @return a new SQL {@code WHERE} expression.
*/
public fr.pandacube.lib.db.SQLWhere<E> in(Collection<T> v) {
return new SQLWhereIn<>(this, v);
@@ -216,7 +216,7 @@ public class SQLField<E extends SQLElement<E>, T> {
/**
* Create a SQL {@code WHERE} expression testing the nullity of this field using the {@code IS NULL} keyword.
* @return a SQL {@code WHERE} expression.
* @return a new SQL {@code WHERE} expression.
*/
public fr.pandacube.lib.db.SQLWhere<E> isNull() {
return new SQLWhereNull<>(this, true);
@@ -225,10 +225,35 @@ public class SQLField<E extends SQLElement<E>, T> {
/**
* Create a SQL {@code WHERE} expression testing the non-nullity of this field using the {@code IS NOT NULL}
* keyword.
* @return a SQL {@code WHERE} expression.
* @return a new SQL {@code WHERE} expression.
*/
public fr.pandacube.lib.db.SQLWhere<E> isNotNull() {
return new SQLWhereNull<>(this, false);
}
@SuppressWarnings({"rawtypes", "unchecked"})
/* package */ Object fromJavaTypeToJDBCType(Object value) throws DBException {
Object ret = value;
if (value != null && type instanceof SQLCustomType customType) {
try {
ret = customType.javaToDbConv.apply(value);
} catch (Exception e) {
throw new DBException("Error while converting value of field '" + name + "' with SQLCustomType from " + type.getJavaType()
+ "(java source) to " + customType.intermediateJavaType + "(jdbc destination). The original value is '" + value + "'", e);
}
}
return ret;
}
/* package */ Collection<Object> fromListJavaTypeToJDBCType(Collection<?> values) throws DBException {
if (values == null)
return null;
List<Object> ret = new ArrayList<>(values.size());
for (Object value : values) {
ret.add(fromJavaTypeToJDBCType(value));
}
return ret;
}
}

View File

@@ -6,7 +6,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
/**
* Builder for a SQL {@code UPDATE} query.
@@ -74,7 +74,7 @@ public class SQLUpdateBuilder<E extends SQLElement<E>> {
if (!first)
sql.append(", ");
sql.append("`").append(entry.getKey().getName()).append("` = ? ");
SQLElement.addValueToSQLObjectList(params, entry.getKey(), entry.getValue());
params.add(entry.getKey().fromJavaTypeToJDBCType(entry.getValue()));
first = false;
}

View File

@@ -3,15 +3,21 @@ package fr.pandacube.lib.db;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
/**
* A SQL {@code WHERE} expression.
* SQL {@code WHERE} expression.
* @param <E> the table type.
*/
public abstract class SQLWhere<E extends SQLElement<E>> {
/**
* Creates a SQL WHERE expression.
*/
protected SQLWhere() {}
/* package */ abstract ParameterizedSQLString toSQL() throws DBException;
@Override
@@ -29,7 +35,7 @@ public abstract class SQLWhere<E extends SQLElement<E>> {
* Create a SQL {@code WHERE} expression that is true when this expression {@code AND} the provided expression is
* true.
* @param other the other expression.
* @return a SQL {@code WHERE} expression.
* @return a new SQL {@code WHERE} expression.
*/
public SQLWhere<E> and(SQLWhere<E> other) {
return SQLWhere.<E>and().and(this).and(other);
@@ -39,7 +45,7 @@ public abstract class SQLWhere<E extends SQLElement<E>> {
* Create a SQL {@code WHERE} expression that is true when this expression {@code OR} the provided expression is
* true.
* @param other the other expression.
* @return a SQL {@code WHERE} expression.
* @return a new SQL {@code WHERE} expression.
*/
public SQLWhere<E> or(SQLWhere<E> other) {
return SQLWhere.<E>or().or(this).or(other);
@@ -48,7 +54,7 @@ public abstract class SQLWhere<E extends SQLElement<E>> {
/**
* Create a SQL {@code WHERE} expression builder joining multiple expressions with the {@code AND} operator.
* @return a SQL {@code WHERE} expression.
* @return a new SQL {@code WHERE} expression.
* @param <E> the table type.
*/
public static <E extends SQLElement<E>> SQLWhereAndBuilder<E> and() {
@@ -57,13 +63,63 @@ public abstract class SQLWhere<E extends SQLElement<E>> {
/**
* Create a SQL {@code WHERE} expression builder joining multiple expressions with the {@code OR} operator.
* @return a SQL {@code WHERE} expression.
* @return a new SQL {@code WHERE} expression.
* @param <E> the table type.
*/
public static <E extends SQLElement<E>> SQLWhereOrBuilder<E> or() {
return new SQLWhereOrBuilder<>();
}
/**
* Create a custom SQL {@code WHERE} expression.
* @param whereExpr the raw SQL {@code WHERE} expression.
* @return a new SQL {@code WHERE} expression.
* @param <E> the table type.
*/
public static <E extends SQLElement<E>> SQLWhere<E> expression(String whereExpr) {
return expression(whereExpr, List.of());
}
/**
* Create a custom SQL {@code WHERE} expression.
* @param whereExpr the raw SQL {@code WHERE} expression.
* @param params the parameters of the provided expression.
* @return a new SQL {@code WHERE} expression.
* @param <E> the table type.
*/
public static <E extends SQLElement<E>> SQLWhere<E> expression(String whereExpr, List<Object> params) {
return new SQLWhereCustomExpression<>(whereExpr, params);
}
/**
* Create a SQL {@code WHERE ... IN ...} expression with a custom left operand.
* @param leftExpr the raw SQL left operand.
* @param valuesIn the values on the right of the {@code IN} operator.
* @return a new SQL {@code WHERE} expression.
* @param <E> the table type.
*/
public static <E extends SQLElement<E>> SQLWhere<E> expressionIn(String leftExpr, Collection<?> valuesIn) {
return expressionIn(leftExpr, List.of(), valuesIn);
}
/**
* Create a SQL {@code WHERE ... IN ...} expression with a custom left operand.
* @param leftExpr the raw SQL left operand.
* @param leftParams the parameters of the left operand.
* @param valuesIn the values on the right of the {@code IN} operator.
* @return a new SQL {@code WHERE} expression.
* @param <E> the table type.
*/
public static <E extends SQLElement<E>> SQLWhere<E> expressionIn(String leftExpr, List<Object> leftParams, Collection<?> valuesIn) {
return new SQLWhereInCustom<>(leftExpr, leftParams, valuesIn);
}
/**
* A SQL {@code WHERE} expression builder joining multiple expressions with the {@code AND} or {@code OR} operator.
@@ -207,9 +263,8 @@ public abstract class SQLWhere<E extends SQLElement<E>> {
@Override
/* package */ ParameterizedSQLString toSQL() throws DBException {
List<Object> params = new ArrayList<>();
SQLElement.addValueToSQLObjectList(params, left, right);
return new ParameterizedSQLString("`" + left.getName() + "` " + comp.sql + " ? ", params);
return new ParameterizedSQLString("`" + left.getName() + "` " + comp.sql + " ? ",
List.of(left.fromJavaTypeToJDBCType(right)));
}
/* package */ enum SQLComparator {
@@ -241,33 +296,39 @@ public abstract class SQLWhere<E extends SQLElement<E>> {
/* package */ static class SQLWhereIn<E extends SQLElement<E>> extends SQLWhere<E> {
/* package */ static class SQLWhereInCustom<E extends SQLElement<E>> extends SQLWhere<E> {
private final SQLField<E, ?> field;
private final Collection<?> values;
private final String leftExpression;
private final List<Object> leftExpressionParameters;
protected Collection<?> collectionIn;
/* package */ <T> SQLWhereIn(SQLField<E, T> f, Collection<T> v) {
if (f == null || v == null)
throw new IllegalArgumentException("All arguments for SQLWhereIn constructor can't be null");
field = f;
values = v;
/* package */ <T> SQLWhereInCustom(String leftExpr, List<Object> leftExprParams, Collection<T> collectionIn) {
if (leftExpr == null)
throw new IllegalArgumentException("leftExpr can't be null");
if (leftExprParams == null)
leftExprParams = List.of();
if (collectionIn == null)
collectionIn = List.of();
leftExpression = leftExpr;
leftExpressionParameters = leftExprParams;
this.collectionIn = collectionIn;
}
@Override
/* package */ ParameterizedSQLString toSQL() throws DBException {
List<Object> params = new ArrayList<>();
if (values.isEmpty())
if (collectionIn.isEmpty())
return new ParameterizedSQLString(" 1=0 ", params);
for (Object v : values)
SQLElement.addValueToSQLObjectList(params, field, v);
params.addAll(leftExpressionParameters);
params.addAll(collectionIn);
char[] questions = new char[values.size() == 0 ? 0 : (values.size() * 2 - 1)];
char[] questions = new char[collectionIn.size() * 2 - 1];
for (int i = 0; i < questions.length; i++)
questions[i] = i % 2 == 0 ? '?' : ',';
return new ParameterizedSQLString("`" + field.getName() + "` IN (" + new String(questions) + ") ", params);
return new ParameterizedSQLString("(" + leftExpression + ") IN (" + new String(questions) + ") ", params);
}
}
@@ -277,6 +338,32 @@ public abstract class SQLWhere<E extends SQLElement<E>> {
/* package */ static class SQLWhereIn<E extends SQLElement<E>> extends SQLWhereInCustom<E> {
private final SQLField<E, ?> field;
private boolean collectionFiltered = false;
/* package */ <T> SQLWhereIn(SQLField<E, T> f, Collection<T> v) {
super("`" + Objects.requireNonNull(f).getName() + "`", List.of(), v);
field = f;
}
@Override
ParameterizedSQLString toSQL() throws DBException {
if (!collectionFiltered) {
collectionIn = field.fromListJavaTypeToJDBCType(collectionIn);
collectionFiltered = true;
}
return super.toSQL();
}
}
/* package */ static class SQLWhereLike<E extends SQLElement<E>> extends SQLWhere<E> {
@@ -334,6 +421,33 @@ public abstract class SQLWhere<E extends SQLElement<E>> {
/* package */ static class SQLWhereCustomExpression<E extends SQLElement<E>> extends SQLWhere<E> {
private final String sqlExpression;
private final List<Object> parameters;
/* package */ SQLWhereCustomExpression(String sqlExpression, List<Object> parameters) {
if (sqlExpression == null)
throw new IllegalArgumentException("sqlExpression can't be null");
if (parameters == null)
parameters = List.of();
this.sqlExpression = sqlExpression;
this.parameters = parameters;
}
@Override
/* package */ ParameterizedSQLString toSQL() {
return new ParameterizedSQLString(sqlExpression, parameters);
}
}
/**

View File

@@ -1,12 +0,0 @@
# pandalib-net
A TCP network library that uses the standard Java socket API, to ease the ommunication between the different processes
running the server network Pandacube.
Its still in development (actually not touched since years), and its supposed to be a replacement for the old
`pandalib-netapi`. This module is then marked as Beta using the Google Guava annotation.
- Packet based communication
- Supports Request/Answer packets
- Uses binary packet id and data
* Input streams are handled in separate Threads

View File

@@ -1,65 +0,0 @@
package fr.pandacube.lib.net;
import java.util.Arrays;
import com.google.common.annotations.Beta;
@Beta
public class Array8Bit {
public static final int BIT_COUNT = Byte.SIZE;
private boolean[] values = new boolean[BIT_COUNT];
public Array8Bit(byte b) {
fromByte(b);
}
/**
* @param bits (index 0 is the lowest significant bit)
*/
public Array8Bit(boolean[] bits) {
if (bits == null || bits.length != BIT_COUNT)
throw new IllegalArgumentException("bits is null or bits.length != "+BIT_COUNT);
values = Arrays.copyOf(bits, BIT_COUNT);
}
/**
* i = 0 is the lowest significant bit
*/
public boolean getBit(int i) {
return values[i];
}
/**
* i = 0 is the lowest significant bit
*/
public void setBit(int i, boolean b) {
values[i] = b;
}
public void fromByte(byte b) {
int mask = 1;
for (int i = 0; i < BIT_COUNT; i++) {
values[i] = (b & mask) != 0;
mask <<= 1;
}
}
public byte toByte() {
byte b = 0;
for (int i=BIT_COUNT-1; i>=0; i--) {
b <<= 1;
if (values[i]) b |= 1;
}
return b;
}
}

View File

@@ -1,275 +0,0 @@
package fr.pandacube.lib.net;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import com.google.common.annotations.Beta;
@Beta
public final class ByteBuffer implements Cloneable {
public static final Charset NETWORK_CHARSET = StandardCharsets.UTF_8;
private java.nio.ByteBuffer buff;
public ByteBuffer() {
this(16);
}
public ByteBuffer(int initSize) {
buff = java.nio.ByteBuffer.allocate(initSize);
}
/**
* Create a ByteBuffer that is initially <b>backed</b> by the provided byte array.
* The position of this buffer will be 0.
* If this ByteBuffer needs a biffer array, the provided array is replaced by a new one,
* making the provided array not related to this ByteBuffer anymore.
* @param data array of byte that serve as a backend for this ByteBuffer.
*/
public ByteBuffer(byte[] data) {
buff = java.nio.ByteBuffer.wrap(data);
}
private void askForBufferExtension(int needed) {
while (buff.remaining() < needed) {
java.nio.ByteBuffer newBuff = java.nio.ByteBuffer.wrap(Arrays.copyOf(buff.array(), buff.array().length * 2));
newBuff.position(buff.position());
buff = newBuff;
}
}
/**
* This clone method also clone the underlying array.
*/
@SuppressWarnings("MethodDoesntCallSuperMethod")
@Override
public ByteBuffer clone() {
return new ByteBuffer(Arrays.copyOf(buff.array(), buff.array().length));
}
/**
* @see java.nio.ByteBuffer#get()
*/
public byte getByte() {
return buff.get();
}
/**
* @see java.nio.ByteBuffer#get(byte[])
*/
public byte[] getByteArray(byte[] b) {
buff.get(b);
return b;
}
/**
* Return the next byte array wich is preceded with his size as integer,
* or null if the founded size is negative.
*/
public byte[] getSizedByteArray() {
int size = getInt();
if (size < 0) return null;
return getByteArray(new byte[size]);
}
/**
* @see java.nio.ByteBuffer#getChar()
*/
public char getChar() {
return buff.getChar();
}
/**
* @see java.nio.ByteBuffer#getShort()
*/
public short getShort() {
return buff.getShort();
}
/**
* @see java.nio.ByteBuffer#getInt()
*/
public int getInt() {
return buff.getInt();
}
/**
* @see java.nio.ByteBuffer#getLong()
*/
public long getLong() {
return buff.getLong();
}
/**
* @see java.nio.ByteBuffer#getFloat()
*/
public float getFloat() {
return buff.getFloat();
}
/**
* @see java.nio.ByteBuffer#getDouble()
*/
public double getDouble() {
return buff.getDouble();
}
/**
* @see java.nio.ByteBuffer#put(byte)
*/
public ByteBuffer putByte(byte b) {
askForBufferExtension(Byte.BYTES);
buff.put(b);
return this;
}
/**
* @see java.nio.ByteBuffer#put(byte[])
*/
public ByteBuffer putByteArray(byte[] b) {
askForBufferExtension(b.length * Byte.BYTES);
buff.put(b);
return this;
}
public ByteBuffer putSizedByteArray(byte[] b) {
if (b == null) {
return putInt(-1);
}
putInt(b.length);
return putByteArray(b);
}
/**
* @see java.nio.ByteBuffer#putChar(char)
*/
public ByteBuffer putChar(char value) {
askForBufferExtension(Character.BYTES);
buff.putChar(value);
return this;
}
/**
* @see java.nio.ByteBuffer#putShort(short)
*/
public ByteBuffer putShort(short value) {
askForBufferExtension(Short.BYTES);
buff.putShort(value);
return this;
}
/**
* @see java.nio.ByteBuffer#putInt(int)
*/
public ByteBuffer putInt(int value) {
askForBufferExtension(Integer.BYTES);
buff.putInt(value);
return this;
}
/**
* @see java.nio.ByteBuffer#putLong(long)
*/
public ByteBuffer putLong(long value) {
askForBufferExtension(Long.BYTES);
buff.putLong(value);
return this;
}
/**
* @see java.nio.ByteBuffer#putFloat(float)
*/
public ByteBuffer putFloat(float value) {
askForBufferExtension(Float.BYTES);
buff.putFloat(value);
return this;
}
/**
* @see java.nio.ByteBuffer#putDouble(double)
*/
public ByteBuffer putDouble(double value) {
askForBufferExtension(Double.BYTES);
buff.putDouble(value);
return this;
}
/**
* @see java.nio.ByteBuffer#position()
*/
public int getPosition() {
return buff.position();
}
/**
* @see java.nio.ByteBuffer#position(int)
*/
public void setPosition(int p) {
buff.position(p);
}
/**
* @see java.nio.ByteBuffer#capacity()
*/
public int capacity() {
return buff.capacity();
}
/**
*
* @param s null String are supported
*/
public ByteBuffer putString(String s) {
if (s == null) {
return putInt(-1);
}
return putSizedByteArray(s.getBytes(NETWORK_CHARSET));
}
/**
* returned string can be null
*/
public String getString() {
byte[] binaryString = getSizedByteArray();
return (binaryString == null) ? null : new String(binaryString, NETWORK_CHARSET);
}
/**
*
* @param list The list can be null, and any String can be null too.
*/
public ByteBuffer putListOfString(List<String> list) {
if (list == null) {
return putInt(-1);
}
putInt(list.size());
for (String str : list)
putString(str);
return this;
}
/**
* @return a List of String. The list can be null, and any element can be null too.
*/
public List<String> getListOfString() {
int size = getInt();
if (size < 0)
return null;
List<String> list = new ArrayList<>();
for (int i = 0; i < size; i++)
list.add(getString());
return list;
}
/**
* @see java.nio.ByteBuffer#array()
*/
public byte[] array() {
return buff.array();
}
}

View File

@@ -1,60 +0,0 @@
package fr.pandacube.lib.net;
import java.util.Arrays;
import com.google.common.annotations.Beta;
@Beta
public class PPacket {
public final String name;
/* package */ int id;
public final byte[] content;
/**
* Construct a new PPacket based on the content of the provided buffer before his position.
* @param n the name of the packet.
* @param buff the buffer where the data comes from. Only the content before {@link ByteBuffer#getPosition()} is copied.
*/
public PPacket(String n, ByteBuffer buff) {
this(n, Arrays.copyOf(buff.array(), buff.getPosition()));
}
public PPacket(String n, byte[] c) {
name = n;
content = c;
}
/* package */ PPacket(String n, int i, byte[] c) {
this(n, c);
id = i;
}
public ByteBuffer getContentAsBuffer() {
return new ByteBuffer(content);
}
public static PPacket buildSingleStringContentPacket(String name, String content) {
return new PPacket(name, new ByteBuffer().putString(content));
}
/* package */ static PPacket buildLoginPacket(String password) {
return buildSingleStringContentPacket("login", password);
}
/* package */ static PPacket buildBadFormatPacket(String message) {
return buildSingleStringContentPacket("bad_format", message);
}
/* package */ static PPacket buildLoginBadPacket() {
return new PPacket("login_bad", new byte[0]);
}
}

View File

@@ -1,47 +0,0 @@
package fr.pandacube.lib.net;
import java.util.Arrays;
import com.google.common.annotations.Beta;
@Beta
public class PPacketAnswer extends PPacket {
/* package */ final int answer;
/**
* Construct a new PPacketAnswer based on the content of the provided buffer before his position.
* @param n the name of the packet.
* @param buff the buffer where the data comes from. Only the content before {@link ByteBuffer#getPosition()} is copied.
*/
public PPacketAnswer(PPacket answered, String n, ByteBuffer buff) {
this(answered, n, Arrays.copyOf(buff.array(), buff.getPosition()));
}
public PPacketAnswer(PPacket answered, String n, byte[] c) {
super(n, c);
answer = answered.id;
}
/* package */ PPacketAnswer(String n, int i, int a, byte[] c) {
super(n, i, c);
answer = a;
}
public static PPacketAnswer buildSingleStringContentPacketAnswer(PPacket answered, String name, String content) {
ByteBuffer pwBuff = new ByteBuffer().putString(content);
return new PPacketAnswer(answered, name, Arrays.copyOf(pwBuff.array(), pwBuff.getPosition()));
}
/* package */ static PPacketAnswer buildLoginOkPacket(PPacket loginPacket) {
return new PPacketAnswer(loginPacket, "login_ok", new byte[0]);
}
/* package */ static PPacketAnswer buildExceptionPacket(PPacket answered, String message) {
return buildSingleStringContentPacketAnswer(answered, "exception", message);
}
}

View File

@@ -1,16 +0,0 @@
package fr.pandacube.lib.net;
import com.google.common.annotations.Beta;
@Beta
@FunctionalInterface
public interface PPacketListener<P extends PPacket> {
/**
* Called when we receive a packet (except responses)
* @param connection the connection from where the packet comes
* @param packet the received packet
*/
void onPacketReceive(PSocket connection, P packet);
}

View File

@@ -1,157 +0,0 @@
package fr.pandacube.lib.net;
import java.io.Closeable;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import com.google.common.annotations.Beta;
import fr.pandacube.lib.util.Log;
@Beta
public class PServer extends Thread implements Closeable {
private static final AtomicInteger connectionCounterId = new AtomicInteger(0);
private final int port;
private ServerSocket socket;
private final String socketName;
private final List<TCPServerClientConnection> clients = Collections.synchronizedList(new ArrayList<>());
private final AtomicBoolean isClosed = new AtomicBoolean(false);
private final List<PPacketListener<PPacket>> globalPacketListeners = Collections.synchronizedList(new ArrayList<>());
private final List<PSocketConnectionListener> clientConnectionListeners = Collections.synchronizedList(new ArrayList<>());
private final String password;
public PServer(int port, String sckName, String password) {
super("PServer " + sckName);
setDaemon(true);
if (port <= 0 || port > 65535) throw new IllegalArgumentException("le numéro de port est invalide");
socketName = sckName;
this.port = port;
this.password = password;
}
@Override
public void run() {
try {
socket = new ServerSocket();
socket.setReceiveBufferSize(PSocket.NETWORK_TCP_BUFFER_SIZE);
socket.setPerformancePreferences(0, 1, 0);
socket.bind(new InetSocketAddress(port));
while (true) {
Socket socketClient = socket.accept();
socketClient.setSendBufferSize(PSocket.NETWORK_TCP_BUFFER_SIZE);
socketClient.setSoTimeout(PSocket.NETWORK_TIMEOUT);
TCPServerClientConnection co = new TCPServerClientConnection(socketClient,
connectionCounterId.getAndIncrement());
co.start();
}
} catch (SocketException ignored) {
} catch (Exception e) {
Log.warning("Plus aucune connexion ne peux être acceptée", e);
}
}
public void addPacketListener(PPacketListener<PPacket> l) {
globalPacketListeners.add(l);
}
public boolean removePacketListener(PPacketListener<PPacket> l) {
return globalPacketListeners.remove(l);
}
public void addConnectionListener(PSocketConnectionListener l) {
clientConnectionListeners.add(l);
}
public void removeConnectionListener(PSocketConnectionListener l) {
clientConnectionListeners.remove(l);
}
protected class TCPServerClientConnection extends PSocket {
boolean loggedIn;
private TCPServerClientConnection(Socket s, int coId) {
super(s, "Conn#" + coId + " via PServer " + socketName, password);
addConnectionListener(new PSocketConnectionListener() {
@Override
public void onDisconnect(PSocket connection) {
try {
clientConnectionListeners.forEach(l -> l.onDisconnect(connection));
} finally {
clients.remove((TCPServerClientConnection)connection);
}
}
@Override
public void onConnect(PSocket connection) {
clients.add((TCPServerClientConnection)connection);
clientConnectionListeners.forEach(l -> l.onConnect(connection));
}
});
addPacketListener((conn, packet) ->
globalPacketListeners.forEach(l -> {
try {
l.onPacketReceive(conn, packet);
} catch (Exception e) {
Log.severe("Exception while calling PPacketListener.onPacketReceive().", e);
sendSilently(PPacketAnswer.buildExceptionPacket(packet, e.toString()));
}
})
);
}
}
@Override
public void close() {
try {
if (isClosed.get()) return;
isClosed.set(true);
clients.forEach(PSocket::close);
socket.close();
} catch (IOException ignored) {}
}
public boolean isClosed() {
return isClosed.get() || socket.isClosed();
}
public List<PSocket> getClients() {
synchronized (clients) {
return new ArrayList<>(clients);
}
}
@Override
public String toString() {
return this.getClass().getName() + "{thread=" + getName() + ", socket=" + socket + "}";
}
}

View File

@@ -1,350 +0,0 @@
package fr.pandacube.lib.net;
import java.io.BufferedOutputStream;
import java.io.Closeable;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import com.google.common.annotations.Beta;
import fr.pandacube.lib.util.Log;
/**
* A wrapper for a {@link Socket}. The connection must point to a software using {@link PServer}
* as wrapper for the target {@link ServerSocket}.
* <br>
* This class provides a simple way to exchange data between client and server :
* <ul>
* <li>Maintained connection with the server</li>
* <li>Login with a password (send in the first packet)</li>
* <li>Binary packet id</li>
* <li>Binary data</li>
* <li>Input stream in a separate Thread</li>
* </ul>
*
*/
@Beta
public class PSocket extends Thread implements Closeable {
public static final int NETWORK_TCP_BUFFER_SIZE = 1024 * 1024;
public static final int NETWORK_TIMEOUT = 0; // no timeout (milli-seconds)
private boolean server = false;
private Socket socket;
private final SocketAddress addr;
private DataInputStream in;
private DataOutputStream out;
private final Object outSynchronizer = new Object();
private String password;
private final AtomicBoolean isClosed = new AtomicBoolean(false);
private final List<PPacketListener<PPacket>> packetListeners = Collections.synchronizedList(new ArrayList<>());
private final List<PSocketConnectionListener> connectionListeners = Collections.synchronizedList(new ArrayList<>());
private final Map<Integer, PPacketListener<PPacketAnswer>> answersCallbacks = Collections.synchronizedMap(new HashMap<>());
private int nextSendId = 0;
/**
* Create a new PSocket that will connect to the specified SocketAddress.
* @param a The target server to connect to
* @param connName the name of the connection, used to name the Thread used to receive the packet.
* @param pass the password to send to the server.
*/
public PSocket(SocketAddress a, String connName, String pass) {
super("PSocket " + connName);
setDaemon(true);
if (a == null) throw new IllegalArgumentException("les arguments ne peuvent pas être null");
addr = a;
}
/* package */ PSocket(Socket s, String connName, String pass) {
this(s.getRemoteSocketAddress(), connName, pass);
socket = s;
server = true;
}
@Override
public void run() {
try {
if (socket == null) {
socket = new Socket();
socket.setReceiveBufferSize(NETWORK_TCP_BUFFER_SIZE);
socket.setSendBufferSize(NETWORK_TCP_BUFFER_SIZE);
socket.setSoTimeout(10000); // initial timeout before login
socket.connect(addr);
in = new DataInputStream(socket.getInputStream());
out = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream()));
}
// password check
if (server) {
PPacket packet = readPacket();
if (packet == null || packet instanceof PPacketAnswer || !"login".equals(packet.name)) {
send(PPacket.buildLoginBadPacket());
close();
return;
}
try {
String receivedPassword = new ByteBuffer(packet.content).getString();
if (!Objects.equals(receivedPassword, password)) {
send(PPacket.buildLoginBadPacket());
close();
return;
}
} catch(Exception e) {
send(PPacket.buildLoginBadPacket());
close();
return;
}
send(PPacketAnswer.buildLoginOkPacket(packet));
// login ok at this point
}
else {
send(PPacket.buildLoginPacket(password));
PPacket packet = readPacket();
if (packet == null) {
Log.severe("bad packet received from server. Disconnecting.");
close();
return;
}
if (packet.name.equals("login_bad")) {
Log.severe("Wrong password to connect to server. Disconnecting.");
close();
return;
}
if (!packet.name.equals("login_ok")) {
Log.severe("Unexpected packet from server. Disconnecting.");
close();
return;
}
// login ok at this point
}
password = null;
socket.setSoTimeout(NETWORK_TIMEOUT);
Log.info(getName() + " connected.");
connectionListeners.forEach(l -> {
try {
l.onConnect(this);
} catch (Exception e) {
Log.severe("Exception while calling PSocketConnectionListener.onConnect().", e);
}
});
while (!socket.isClosed()) {
PPacket packet = readPacket();
if (packet == null) {
send(PPacket.buildBadFormatPacket("Bad format for the last packet received. Closing connection."));
break;
}
if (packet instanceof PPacketAnswer) {
try {
answersCallbacks.remove(((PPacketAnswer)packet).answer).onPacketReceive(this, (PPacketAnswer)packet);
} catch (Exception e) {
Log.severe("Exception while calling PPacketListener.onPacketReceive().", e);
send(PPacketAnswer.buildExceptionPacket(packet, e.toString()));
}
}
else {
packetListeners.forEach(l -> {
try {
l.onPacketReceive(this, packet);
} catch (Exception e) {
Log.severe("Exception while calling PPacketListener.onPacketReceive().", e);
sendSilently(PPacketAnswer.buildExceptionPacket(packet, e.toString()));
}
});
}
}
} catch (Exception e) {
Log.severe(e);
}
close();
}
/**
* Return the packet read in the socket, or null if the packet is in a bad format.
* @return the packet
*
*/
private PPacket readPacket() throws IOException {
byte nSize = in.readByte();
if (nSize == 0) {
return null;
}
boolean answer = nSize < 0;
if (answer)
nSize *= -1;
byte[] nBytes = new byte[nSize];
in.readFully(nBytes);
String name = new String(nBytes, ByteBuffer.NETWORK_CHARSET);
int packetId = in.readInt();
int answerId = (answer) ? in.readInt() : -1;
int cSize = in.readInt();
if (cSize < 0 || cSize > 0xFFFFFF) { // can't be more that 16 MiB
return null;
}
byte[] content = new byte[cSize];
in.readFully(content);
return answer ? new PPacketAnswer(name, packetId, answerId, content) : new PPacket(name, packetId, content);
}
/**
* Send the provided packet, without waiting for an answer.
*/
public void send(PPacket packet) throws IOException {
if (packet == null)
throw new IllegalArgumentException("packet can't be null");
if (packet.name == null)
throw new IllegalArgumentException("packet.name can't be null");
if (packet.content == null)
throw new IllegalArgumentException("packet.content can't be null");
byte[] nameBytes = packet.name.getBytes(ByteBuffer.NETWORK_CHARSET);
if (nameBytes.length > 127)
throw new IllegalArgumentException("packet.name must take fewer than 128 bytes when converted to UTF-8");
byte nameSize = (byte)nameBytes.length;
boolean answer = packet instanceof PPacketAnswer;
if (answer) nameSize *= -1;
synchronized (outSynchronizer) {
int packetId = nextSendId++;
packet.id = packetId;
out.write(new byte[] {nameSize});
out.write(nameBytes);
out.write(packetId);
if (answer)
out.write(((PPacketAnswer)packet).answer);
out.write(packet.content.length);
out.write(packet.content);
out.flush();
}
}
public void sendSilently(PPacket packet) {
try {
send(packet);
} catch (IOException ignored) {}
}
public void send(PPacket packet, PPacketListener<PPacketAnswer> answerCallback) throws IOException {
synchronized (answersCallbacks) {
/*
* This synch block ensure that the callback will be put in the listeners Map before
* we receve the answer (in case this is really really fast)
*/
send(packet);
answersCallbacks.put(packet.id, answerCallback);
}
}
public void addPacketListener(PPacketListener<PPacket> l) {
packetListeners.add(l);
}
public boolean removePacketListener(PPacketListener<PPacket> l) {
return packetListeners.remove(l);
}
public void addConnectionListener(PSocketConnectionListener l) {
connectionListeners.add(l);
}
public void removeConnectionListener(PSocketConnectionListener l) {
connectionListeners.remove(l);
}
@Override
public void close() {
try {
synchronized (outSynchronizer) {
if (isClosed.get()) return;
Log.info(getName() + " closing...");
connectionListeners.forEach(l -> {
try {
l.onDisconnect(this);
} catch (Exception e) {
Log.severe("Exception while calling PSocketConnectionListener.onDisconnect().", e);
}
});
socket.close();
isClosed.set(true);
}
} catch (IOException e) {
Log.warning(e);
}
}
public SocketAddress getRemoteAddress() {
return addr;
}
public boolean isClosed() {
return isClosed.get() || socket.isClosed();
}
@Override
public String toString() {
return this.getClass().getName() + "{thread=" + getName() + ", socket=" + socket + "}";
}
}

View File

@@ -1,20 +0,0 @@
package fr.pandacube.lib.net;
import com.google.common.annotations.Beta;
@Beta
public interface PSocketConnectionListener {
/**
* Called when a socket is connected
* @param connection the connection
*/
void onConnect(PSocket connection);
/**
* Called just before a socket is disconnected
* @param connection the connection
*/
void onDisconnect(PSocket connection);
}

View File

@@ -21,4 +21,8 @@
</dependency>
</dependencies>
<properties>
<maven.javadoc.skip>true</maven.javadoc.skip>
</properties>
</project>

View File

@@ -18,7 +18,7 @@ public class ResponseAnalyser {
if (socket == null || socket.isClosed() || socket.isInputShutdown())
throw new IllegalArgumentException("le socket doit être non null et doit être ouvert sur le flux d'entrée");
// on lis la réponse
// on lit la réponse
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line;

View File

@@ -5,15 +5,15 @@ import java.io.PrintStream;
import java.net.InetAddress;
import java.net.Socket;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
public abstract class AbstractRequestExecutor {
public final String command;
public AbstractRequestExecutor(String cmd, NetworkAPIListener napiListener) {
public AbstractRequestExecutor(String cmd, NetworkAPIListener nAPIListener) {
command = cmd.toLowerCase();
napiListener.registerRequestExecutor(command, this);
nAPIListener.registerRequestExecutor(command, this);
}
public void execute(String data, Socket socket) throws IOException {
@@ -34,9 +34,8 @@ public abstract class AbstractRequestExecutor {
/**
*
* @param data La représentation sous forme de String des données envoyés
* dans la requête
* @return La réponse à retourner au client
* @param data The String representation of the request data.
* @return The response to send back to the client.
*/
protected abstract Response run(InetAddress source, String data);

View File

@@ -1,6 +1,6 @@
package fr.pandacube.lib.netapi.server;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
import java.io.IOException;
import java.net.InetAddress;
@@ -18,7 +18,7 @@ public class NetworkAPIListener implements Runnable {
private final String name;
/**
* Instencie le côté serveur du NetworkAPI.
* Instancie le côté serveur du NetworkAPI.
*
* @param n nom du networkAPI (permet l'identification dans les logs)
* @param p le port d'écoute
@@ -29,7 +29,7 @@ public class NetworkAPIListener implements Runnable {
}
/**
* Instencie le côté serveur du NetworkAPI.
* Instancie le côté serveur du NetworkAPI.
*
* @param n nom du networkAPI (permet l'identification dans les logs)
* @param p le port d'écoute
@@ -56,7 +56,6 @@ public class NetworkAPIListener implements Runnable {
Log.info("NetworkAPI '" + name + "' à l'écoute sur le socket " + serverSocket.getLocalSocketAddress());
try {
// réception des connexion client
while (!serverSocket.isClosed()) {
Thread t = new Thread(new PacketExecutor(serverSocket.accept(), this));
t.setDaemon(true);

View File

@@ -5,7 +5,7 @@ import java.io.PrintStream;
import java.net.Socket;
import fr.pandacube.lib.netapi.server.RequestAnalyser.BadRequestException;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
/**
* Prends en charge un socket client et le transmet au gestionnaire de paquet
@@ -20,9 +20,9 @@ public class PacketExecutor implements Runnable {
private final Socket socket;
private final NetworkAPIListener networkAPIListener;
public PacketExecutor(Socket s, NetworkAPIListener napiListener) {
public PacketExecutor(Socket s, NetworkAPIListener nAPIListener) {
socket = s;
networkAPIListener = napiListener;
networkAPIListener = nAPIListener;
}
@Override

View File

@@ -10,23 +10,23 @@ public class RequestAnalyser {
public final String command;
public final String data;
public RequestAnalyser(Socket socket, NetworkAPIListener napiListener) throws IOException, BadRequestException {
if (socket == null || socket.isClosed() || socket.isInputShutdown() || napiListener == null)
public RequestAnalyser(Socket socket, NetworkAPIListener nAPIListener) throws IOException, BadRequestException {
if (socket == null || socket.isClosed() || socket.isInputShutdown() || nAPIListener == null)
throw new IllegalArgumentException(
"le socket doit être non null et doit être ouvert sur le flux d'entrée et napiListener ne doit pas être null");
"le socket doit être non null et doit être ouvert sur le flux d'entrée et nAPIListener ne doit pas être null");
// on lis la réponse
// on lit la réponse
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line;
// lecture de la première ligne
line = in.readLine();
if (line == null || !line.equals(napiListener.pass)) throw new BadRequestException("wrong_password");
if (line == null || !line.equals(nAPIListener.pass)) throw new BadRequestException("wrong_password");
// lecture de la deuxième ligne
line = in.readLine();
if (line == null || napiListener.getRequestExecutor(line) == null)
if (line == null || nAPIListener.getRequestExecutor(line) == null)
throw new BadRequestException("command_not_exists");
command = line;

View File

@@ -16,7 +16,7 @@
<repositories>
<repository>
<id>papermc</id>
<url>https://papermc.io/repo/repository/maven-public/</url>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
<!-- WorldEdit -->
@@ -25,7 +25,7 @@
<url>https://maven.enginehub.org/repo/</url>
</repository>
<!-- Vault and maybe other dependecies -->
<!-- Vault and maybe other dependencies -->
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
@@ -33,12 +33,12 @@
</repositories>
<dependencies>
<dependency>
<!-- <dependency>
<groupId>fr.pandacube.lib</groupId>
<artifactId>pandalib-players-permissible</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
</dependency> -->
<dependency>
<groupId>fr.pandacube.lib</groupId>
<artifactId>pandalib-permissions</artifactId>
@@ -77,7 +77,7 @@
<dependency>
<groupId>com.sk89q.worldedit</groupId>
<artifactId>worldedit-bukkit</artifactId>
<version>7.2.9</version>
<version>7.2.19</version>
<scope>provided</scope>
<exclusions>
<exclusion>

View File

@@ -23,11 +23,11 @@ import org.bukkit.permissions.ServerOperator;
import org.bukkit.plugin.java.JavaPlugin;
import fr.pandacube.lib.permissions.Permissions;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
/**
* Class that integrates the {@code pandalib-permissions} system into a Bukkit/Spigot/Paper instance.
* The integration is made when calling {@link #init(JavaPlugin, String)}.
* The integration is made when calling {@link #onLoad(JavaPlugin, String)} and {@link #onEnable()}.
* The permission system must be initialized first, using {@link Permissions#init(Function)}.
* Dont forget that the permission system also needs a connection to a database, so dont forget to call
* {@link DB#init(DBConnection, String)} with the appropriate parameters before anything.
@@ -38,18 +38,26 @@ public class PandalibPaperPermissions implements Listener {
/* package */ static String serverName;
/* package */ static final Map<String, String> permissionMap = new HashMap<>();
/**
* Integrates the {@code pandalib-permissions} system into the Bukkit server.
* Integrates the {@code pandalib-permissions} system into the Bukkit server, during the loading phase of the plugin.
* @param plugin a Bukkit plugin.
* @param serverName the name of the current server, used to fetch server specific permissions. Cannot be null.
* If this server in not in a multi-server configuration, use a dummy server name, like
* If this server in not in a multiserver configuration, use a dummy server name, like
* {@code ""} (empty string).
*/
public static void init(JavaPlugin plugin, String serverName) {
public static void onLoad(JavaPlugin plugin, String serverName) {
PandalibPaperPermissions.plugin = plugin;
PandalibPaperPermissions.serverName = serverName;
PermissionsInjectorVault.onLoad();
}
/**
* Integrates the {@code pandalib-permissions} system into the Bukkit server, during the enabling phase of the plugin.
*/
public static void onEnable() {
PermissionsInjectorBukkit.inject(Bukkit.getConsoleSender());
PermissionsInjectorVault.inject();
PermissionsInjectorVault.onEnable();
PermissionsInjectorWEPIF.inject();
Bukkit.getPluginManager().registerEvents(new PandalibPaperPermissions(), plugin);
@@ -74,6 +82,12 @@ public class PandalibPaperPermissions implements Listener {
}
}
/**
* Creates a {@link PandalibPaperPermissions} instance.
*/
private PandalibPaperPermissions() {}
/**
* Player login event handler.
* @param event the event.

View File

@@ -1,5 +1,23 @@
package fr.pandacube.lib.paper.permissions;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import fr.pandacube.lib.permissions.Permissions;
import fr.pandacube.lib.reflect.Reflect;
import fr.pandacube.lib.util.log.Log;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.entity.Player;
import org.bukkit.permissions.Permissible;
import org.bukkit.permissions.PermissibleBase;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionAttachment;
import org.bukkit.permissions.PermissionAttachmentInfo;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.LinkedHashSet;
@@ -11,24 +29,6 @@ import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.entity.Player;
import org.bukkit.permissions.Permissible;
import org.bukkit.permissions.PermissibleBase;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionAttachment;
import org.bukkit.permissions.PermissionAttachmentInfo;
import org.bukkit.plugin.Plugin;
import fr.pandacube.lib.permissions.Permissions;
import fr.pandacube.lib.reflect.Reflect;
import fr.pandacube.lib.util.Log;
/* package */ class PermissionsInjectorBukkit
{
@@ -58,26 +58,24 @@ import fr.pandacube.lib.util.Log;
}
}
private static void setPermissible(CommandSender sender, Permissible newpermissible)
private static void setPermissible(CommandSender sender, Permissible newPermissible)
{
try {
Field perm = getPermField(sender);
if (perm == null)
return;
perm.setAccessible(true);
perm.set(sender, newpermissible);
perm.set(sender, newPermissible);
}
catch (Exception e) {
Log.severe(e);
throw new RuntimeException(e);
}
}
/* package */ static Permissible getPermissible(CommandSender sender)
{
Field perm = getPermField(sender);
if (perm == null)
return null;
try {
Field perm = getPermField(sender);
perm.setAccessible(true);
Permissible p = (Permissible) perm.get(sender);
if (p == null) {
@@ -86,26 +84,19 @@ import fr.pandacube.lib.util.Log;
return p;
}
catch (Exception e) {
Log.severe(e);
throw new RuntimeException(e);
}
return null;
}
private static Field getPermField(CommandSender sender)
{
private static Field getPermField(CommandSender sender) throws NoSuchFieldException {
if (sender == null) {
throw new IllegalArgumentException("sender cannot be null");
}
try {
if (sender instanceof Player || sender instanceof ConsoleCommandSender)
return Reflect.ofClassOfInstance(sender).field("perm").get();
else
throw new IllegalArgumentException("Unsupported type for sender: " + sender.getClass());
}
catch (Exception e) {
Log.severe(e);
}
return null;
if (sender instanceof Player || sender instanceof ConsoleCommandSender)
return Reflect.ofClassOfInstance(sender).field("perm").get();
else
throw new IllegalArgumentException("Unsupported type for sender: " + sender.getClass());
}
/* package */ static class PandaPermissible extends PermissibleBase
@@ -118,7 +109,7 @@ import fr.pandacube.lib.util.Log;
@SuppressWarnings("UnusedAssignment")
private boolean init = false;
/* assigment to false is necessary because of super class constructor calling the method recalculatePermission()
/* assignment to false is necessary because of super class constructor calling the method recalculatePermission()
* and we dont want that.
*/
@@ -143,7 +134,7 @@ import fr.pandacube.lib.util.Log;
public boolean hasPermission(String permission)
{
/*
* WARNING: dont call PermissibleOnlinePlayer#hasPermission(String) here or it will result on a stack overflow
* WARNING: dont call PermissibleOnlinePlayer#hasPermission(String) here, or it will result on a stack overflow
*/
if (permission.toLowerCase().startsWith("minecraft.command."))
@@ -180,7 +171,7 @@ import fr.pandacube.lib.util.Log;
if (res != null)
return res;
return oldPermissible.hasPermission(permission); // doesnt need to manage negative permission (should not happend)
return oldPermissible.hasPermission(permission); // doesn't need to manage negative permission (should not happen)
}
@Override
@@ -214,36 +205,34 @@ import fr.pandacube.lib.util.Log;
.build();
@Override
public Set<PermissionAttachmentInfo> getEffectivePermissions()
public @NotNull Set<PermissionAttachmentInfo> getEffectivePermissions()
{
// PlotSquared uses this method to optimize permission range (plots.limit.10 for example)
// MobArena uses this method when a player leave the arena
// LibsDisguises uses this method (and only this one) to parse all the permissions
//Log.warning("There is a plugin calling CommandSender#getEffectivePermissions(). See the stacktrace to understand the reason for that.", new Throwable());
String world = null;
if (sender instanceof Player player) {
world = player.getWorld().getName();
String world = player.getWorld().getName();
try {
return effectivePermissionsListCache.get(world, () -> {
// first get the superperms effective permissions (that take isOp into account)
Map<String, PermissionAttachmentInfo> perms = oldPermissible.getEffectivePermissions().stream()
.collect(Collectors.toMap(PermissionAttachmentInfo::getPermission, Function.identity()));
// then override them with the permissions from our permission system (that has priority, and take current world into account)
for (Map.Entry<String, Boolean> permE : getEffectivePermissionsOnServerInWorld().entrySet()) {
perms.put(permE.getKey(), new PermissionAttachmentInfo(this, permE.getKey(), null, permE.getValue()));
}
return new LinkedHashSet<>(perms.values());
});
} catch (ExecutionException e) {
Log.severe(e);
}
}
try {
return effectivePermissionsListCache.get(world, () -> {
// first get the superperms effective permissions (taht take isOp into accound)
Map<String, PermissionAttachmentInfo> perms = oldPermissible.getEffectivePermissions().stream()
.collect(Collectors.toMap(PermissionAttachmentInfo::getPermission, Function.identity()));
// then override them with the permissions from our permission system (that has priority, and take current world into account)
for (Map.Entry<String, Boolean> permE : getEffectivePermissionsOnServerInWorld().entrySet()) {
perms.put(permE.getKey(), new PermissionAttachmentInfo(this, permE.getKey(), null, permE.getValue()));
}
return new LinkedHashSet<>(perms.values());
});
} catch (ExecutionException e) {
Log.severe(e);
return oldPermissible.getEffectivePermissions();
}
return oldPermissible.getEffectivePermissions();
}
@@ -260,7 +249,7 @@ import fr.pandacube.lib.util.Log;
}
@Override
public boolean isPermissionSet(String permission)
public boolean isPermissionSet(@NotNull String permission)
{
Boolean res = hasPermissionOnServerInWorld(permission);
if (res != null)
@@ -278,31 +267,31 @@ import fr.pandacube.lib.util.Log;
}
@Override
public PermissionAttachment addAttachment(Plugin plugin)
public @NotNull PermissionAttachment addAttachment(@NotNull Plugin plugin)
{
return oldPermissible.addAttachment(plugin);
}
@Override
public PermissionAttachment addAttachment(Plugin plugin, int ticks)
public PermissionAttachment addAttachment(@NotNull Plugin plugin, int ticks)
{
return oldPermissible.addAttachment(plugin, ticks);
}
@Override
public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value)
public @NotNull PermissionAttachment addAttachment(@NotNull Plugin plugin, @NotNull String name, boolean value)
{
return oldPermissible.addAttachment(plugin, name, value);
}
@Override
public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value, int ticks)
public PermissionAttachment addAttachment(@NotNull Plugin plugin, @NotNull String name, boolean value, int ticks)
{
return oldPermissible.addAttachment(plugin, name, value, ticks);
}
@Override
public void removeAttachment(PermissionAttachment attachment)
public void removeAttachment(@NotNull PermissionAttachment attachment)
{
oldPermissible.removeAttachment(attachment);
}

View File

@@ -1,38 +1,66 @@
package fr.pandacube.lib.paper.permissions;
import java.util.List;
import fr.pandacube.lib.permissions.PermGroup;
import fr.pandacube.lib.permissions.Permissions;
import fr.pandacube.lib.util.log.Log;
import net.milkbowl.vault.chat.Chat;
import net.milkbowl.vault.permission.Permission;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.plugin.ServicePriority;
import fr.pandacube.lib.permissions.PermGroup;
import fr.pandacube.lib.permissions.Permissions;
import fr.pandacube.lib.util.Log;
import java.util.List;
/* package */ class PermissionsInjectorVault {
private static final ServicePriority servicePriority = ServicePriority.Highest;
public static PandaVaultPermission permInstance;
public static void inject() {
/**
* Vault injection needs to happen as soon as possible so other plugins detects it when they load.
*/
public static void onLoad() {
try {
permInstance = new PandaVaultPermission();
PandaVaultChat chat = new PandaVaultChat(permInstance);
Bukkit.getServicesManager().register(net.milkbowl.vault.permission.Permission.class, permInstance,
PandalibPaperPermissions.plugin, ServicePriority.High);
Bukkit.getServicesManager().register(net.milkbowl.vault.chat.Chat.class, chat,
PandalibPaperPermissions.plugin, ServicePriority.High);
Bukkit.getServicesManager().register(Permission.class, permInstance,
PandalibPaperPermissions.plugin, servicePriority);
Bukkit.getServicesManager().register(Chat.class, chat,
PandalibPaperPermissions.plugin, servicePriority);
Log.info("Providing permissions and chat prefix/suffix through Vault API.");
} catch (NoClassDefFoundError e) {
Log.warning("Vault plugin not detected. Not using it to provide permissions and prefix/suffix." + e.getMessage());
}
}
public static void onEnable() {
Bukkit.getScheduler().runTaskLater(PandalibPaperPermissions.plugin,
PermissionsInjectorVault::checkServicesRegistration, 1);
}
private static void checkServicesRegistration() {
Permission permService = Bukkit.getServicesManager().load(Permission.class);
if (!(permService instanceof PandaVaultPermission)) {
Log.severe("Check for Vault Permission service failed. "
+ (permService == null ? "Service manager returned null."
: ("Returned service is " + permService.getName() + " (" + permService.getClass().getName() + ").")));
}
Chat chatService = Bukkit.getServicesManager().load(Chat.class);
if (!(chatService instanceof PandaVaultChat)) {
Log.severe("Check for Vault Chat service failed. "
+ (chatService == null ? "Service manager returned null."
: ("Returned service is " + chatService.getName() + " (" + chatService.getClass().getName() + ").")));
}
}
/* package */ static class PandaVaultPermission extends net.milkbowl.vault.permission.Permission {
/* package */ static class PandaVaultPermission extends Permission {
private PandaVaultPermission() { }
@@ -46,6 +74,11 @@ import fr.pandacube.lib.util.Log;
return PandalibPaperPermissions.plugin != null && PandalibPaperPermissions.plugin.isEnabled();
}
private void checkEnabled() {
if (!isEnabled())
throw new IllegalStateException("Cannot provide permission service because plugin is disabled.");
}
@Override
public boolean hasSuperPermsCompat() {
return true;
@@ -59,6 +92,7 @@ import fr.pandacube.lib.util.Log;
@Override
public boolean playerHas(String world, OfflinePlayer player, String permission) {
checkEnabled();
Boolean res = Permissions.getPlayer(player.getUniqueId()).hasPermission(permission, PandalibPaperPermissions.serverName, world);
if (res != null)
return res;
@@ -73,17 +107,38 @@ import fr.pandacube.lib.util.Log;
@Deprecated
@Override
public boolean playerAdd(String world, String player, String permission) {
return false;
return playerAdd(world, Bukkit.getOfflinePlayer(player), permission);
}
@Override
public boolean playerAdd(String world, OfflinePlayer player, String permission) {
checkEnabled();
String server = PandalibPaperPermissions.serverName;
Permissions.getPlayer(player.getUniqueId()).addSelfPermission(permission, server, world);
Permissions.clearPlayerCache(player.getUniqueId());
Log.info("A plugin added permission " + permission + " (server=" + server + ",world=" + world + ") to player " + player.getName() + " through Vault.");
return true;
}
@Deprecated
@Override
public boolean playerRemove(String world, String player, String permission) {
return false;
return playerRemove(world, Bukkit.getOfflinePlayer(player), permission);
}
@Override
public boolean playerRemove(String world, OfflinePlayer player, String permission) {
checkEnabled();
String server = PandalibPaperPermissions.serverName;
Permissions.getPlayer(player.getUniqueId()).removeSelfPermission(permission, server, world);
Permissions.clearPlayerCache(player.getUniqueId());
Log.info("A plugin removed permission " + permission + " (server=" + server + ",world=" + world + ") to player " + player.getName() + " through Vault.");
return true;
}
@Override
public boolean groupHas(String world, String group, String permission) {
checkEnabled();
Boolean res = Permissions.getGroup(group).hasPermission(permission, PandalibPaperPermissions.serverName, world);
if (res != null)
return res;
@@ -97,11 +152,15 @@ import fr.pandacube.lib.util.Log;
@Override
public boolean groupAdd(String world, String group, String permission) {
Log.severe(new Throwable("A plugin tried to add to group " + group + " (world=" + world + ") the permission " + permission
+ " through Vault but Pandalib does not support it."));
return false;
}
@Override
public boolean groupRemove(String world, String group, String permission) {
Log.severe(new Throwable("A plugin tried to remove from group " + group + " (world=" + world + ") the permission " + permission
+ " through Vault but Pandalib does not support it."));
return false;
}
@@ -113,18 +172,23 @@ import fr.pandacube.lib.util.Log;
@Override
public boolean playerInGroup(String world, OfflinePlayer player, String group) {
checkEnabled();
return Permissions.getPlayer(player.getUniqueId()).isInGroup(group);
}
@Deprecated
@Override
public boolean playerAddGroup(String world, String player, String group) {
Log.severe(new Throwable("A plugin tried to add player " + player + " (world=" + world + ") to permission group " + group
+ " through Vault but Pandalib does not support it."));
return false;
}
@Deprecated
@Override
public boolean playerRemoveGroup(String world, String player, String group) {
Log.severe(new Throwable("A plugin tried to remove player " + player + " (world=" + world + ") from permission group " + group
+ " through Vault but Pandalib does not support it."));
return false;
}
@@ -136,6 +200,7 @@ import fr.pandacube.lib.util.Log;
@Override
public String[] getPlayerGroups(String world, OfflinePlayer player) {
checkEnabled();
List<String> groups = Permissions.getPlayer(player.getUniqueId()).getGroupsString();
return groups.toArray(new String[0]);
}
@@ -148,12 +213,14 @@ import fr.pandacube.lib.util.Log;
@Override
public String getPrimaryGroup(String world, OfflinePlayer player) {
checkEnabled();
return Permissions.getPlayer(player.getUniqueId()).getGroupsString().stream()
.findFirst().orElse(null);
}
@Override
public String[] getGroups() {
checkEnabled();
return Permissions.getGroups().stream()
.map(PermGroup::getName).toArray(String[]::new);
}
@@ -166,9 +233,9 @@ import fr.pandacube.lib.util.Log;
}
private static class PandaVaultChat extends net.milkbowl.vault.chat.Chat {
private static class PandaVaultChat extends Chat {
public PandaVaultChat(net.milkbowl.vault.permission.Permission perms) {
public PandaVaultChat(Permission perms) {
super(perms);
}
@@ -182,6 +249,11 @@ import fr.pandacube.lib.util.Log;
return PandalibPaperPermissions.plugin != null && PandalibPaperPermissions.plugin.isEnabled();
}
private void checkEnabled() {
if (!isEnabled())
throw new IllegalStateException("Cannot provide permission service because plugin is disabled.");
}
@Deprecated
@Override
public String getPlayerPrefix(String world, String player) {
@@ -190,6 +262,7 @@ import fr.pandacube.lib.util.Log;
@Override
public String getPlayerPrefix(String world, OfflinePlayer player) {
checkEnabled();
return Permissions.getPlayer(player.getUniqueId()).getPrefix();
}
@@ -201,16 +274,19 @@ import fr.pandacube.lib.util.Log;
@Override
public String getPlayerSuffix(String world, OfflinePlayer player) {
checkEnabled();
return Permissions.getPlayer(player.getUniqueId()).getSuffix();
}
@Override
public String getGroupPrefix(String world, String group) {
checkEnabled();
return Permissions.getGroup(group).getPrefix();
}
@Override
public String getGroupSuffix(String world, String group) {
checkEnabled();
return Permissions.getGroup(group).getSuffix();
}

View File

@@ -11,7 +11,7 @@ import org.bukkit.plugin.ServicePriority;
import fr.pandacube.lib.permissions.PermPlayer;
import fr.pandacube.lib.permissions.Permissions;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
/* package */ class PermissionsInjectorWEPIF {

View File

@@ -16,7 +16,7 @@
<repositories>
<repository>
<id>papermc</id>
<url>https://papermc.io/repo/repository/maven-public/</url>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
<repository>
<id>fabricmc</id>
@@ -71,6 +71,12 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>fr.pandacube.lib</groupId>
<artifactId>pandalib-bungee-chat</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>fr.pandacube.lib</groupId>
<artifactId>pandalib-paper-permissions</artifactId>
@@ -84,19 +90,6 @@
<artifactId>paper-api</artifactId>
<version>${paper.version}-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-mojangapi</artifactId>
<version>${paper.version}-SNAPSHOT</version>
</dependency>
<!-- Needed to read obfuscation mapping file. Already included in Paper -->
<dependency>
<groupId>net.fabricmc</groupId>
<artifactId>mapping-io</artifactId>
<version>0.3.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>

View File

@@ -1,27 +1,51 @@
package fr.pandacube.lib.paper;
import fr.pandacube.lib.paper.event.ServerStopEvent;
import fr.pandacube.lib.paper.json.PaperJson;
import fr.pandacube.lib.paper.modules.PerformanceAnalysisManager;
import org.bukkit.plugin.Plugin;
/**
* Main class for pandalib-paper.
*/
public class PandaLibPaper {
private static Plugin plugin;
/**
* Method to call in plugin's {@link Plugin#onLoad()} method.
* @param plugin the plugin instance.
*/
public static void onLoad(Plugin plugin) {
PandaLibPaper.plugin = plugin;
PaperJson.init();
}
/**
* Method to call in plugin's {@link Plugin#onEnable()} method.
*/
public static void onEnable() {
PerformanceAnalysisManager.getInstance(); // initialize
ServerStopEvent.init();
}
/**
* Method to call in plugin's {@link Plugin#onDisable()} method.
*/
public static void disable() {
PerformanceAnalysisManager.getInstance().cancelInternalBossBar();
PerformanceAnalysisManager.getInstance().deinit();
}
/**
* Gets the plugin instance.
* @return the plugin instance provided with {@link #onLoad(Plugin)}.
*/
public static Plugin getPlugin() {
return plugin;
}
private PandaLibPaper() {}
}

View File

@@ -6,13 +6,65 @@ import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* A basic class holding configuration for {@link PaperBackupManager}.
*/
@SuppressWarnings("CanBeFinal")
public class PaperBackupConfig {
/**
* Creates a new Paper backup config.
*/
public PaperBackupConfig() {}
/**
* Set to true to enable worlds backup.
* Defaults to true.
*/
public boolean worldBackupEnabled = true;
/**
* Set to true to enable the backup of the working directory.
* The workdir backup will already ignore the logs directory and any world folder (folder with a level.dat file in it).
* Defaults to true.
*/
public boolean workdirBackupEnabled = true;
/**
* Set to true to enable the backup of logs.
* Defaults to true.
*/
public boolean logsBackupEnabled = true;
public String scheduling = "0 2 * * *"; // cron format, here is everyday at 2am
/**
* The cron-formatted scheduling of the worlds and workdir backups.
* The default value is {@code "0 2 * * *"}, that is every day at 2am.
*/
public String scheduling = "0 2 * * *"; // cron format, here is every day at 2am
/**
* The backup target directory.
* Must be set (defaults to null).
*/
public File backupDirectory = null;
/**
* The backup cleaner for the worlds backup.
* Defaults to keep 1 backup every 3 month + the last 5 backups.
*/
public BackupCleaner worldBackupCleaner = BackupCleaner.KEEPING_1_EVERY_N_MONTH(3).merge(BackupCleaner.KEEPING_N_LAST(5));
/**
* The backup cleaner for the workdir backup.
* Defaults to keep 1 backup every 3 month + the last 5 backups.
*/
public BackupCleaner workdirBackupCleaner = BackupCleaner.KEEPING_1_EVERY_N_MONTH(3).merge(BackupCleaner.KEEPING_N_LAST(5));
/**
* The list of files or directory to ignore.
* Defaults to none.
* The workdir backup will already ignore the logs directory and any world folder (folder with a level.dat file in it).
*/
public List<String> workdirIgnoreList = new ArrayList<>();
}

View File

@@ -21,13 +21,21 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CancellationException;
/**
* The backup manager for Paper servers.
*/
public class PaperBackupManager extends BackupManager implements Listener {
private final Map<String, PaperWorldProcess> compressWorlds = new HashMap<>();
PaperBackupConfig config;
/**
* Instantiate a new backup manager.
* @param config the configuration of the backups.
*/
public PaperBackupManager(PaperBackupConfig config) {
super(config.backupDirectory);
setConfig(config);
@@ -48,13 +56,17 @@ public class PaperBackupManager extends BackupManager implements Listener {
super.addProcess(process);
}
/**
* Updates the backups config
* @param config the new config.
*/
public void setConfig(PaperBackupConfig config) {
this.config = config;
backupQueue.forEach(this::updateProcessConfig);
}
public void updateProcessConfig(BackupProcess process) {
private void updateProcessConfig(BackupProcess process) {
if (process instanceof PaperWorkdirProcess) {
process.setEnabled(config.workdirBackupEnabled);
process.setBackupCleaner(config.workdirBackupCleaner);
@@ -76,7 +88,9 @@ public class PaperBackupManager extends BackupManager implements Listener {
public void run() {
try {
SchedulerUtil.runOnServerThreadAndWait(super::run);
} catch (Exception e) {
} catch (CancellationException ignored) {
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@@ -116,12 +130,12 @@ public class PaperBackupManager extends BackupManager implements Listener {
private final Set<String> dirtyForSave = new HashSet<>();
@EventHandler(priority = EventPriority.MONITOR)
public void onWorldLoad(WorldLoadEvent event) {
void onWorldLoad(WorldLoadEvent event) {
initWorldProcess(event.getWorld().getName());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onWorldSave(WorldSaveEvent event) {
void onWorldSave(WorldSaveEvent event) {
if (event.getWorld().getLoadedChunks().length > 0
|| dirtyForSave.contains(event.getWorld().getName())) {
compressWorlds.get(event.getWorld().getName()).setDirtyAfterSave();
@@ -134,18 +148,18 @@ public class PaperBackupManager extends BackupManager implements Listener {
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerChangeWorldEvent(PlayerChangedWorldEvent event) {
void onPlayerChangeWorldEvent(PlayerChangedWorldEvent event) {
dirtyForSave.add(event.getFrom().getName());
dirtyForSave.add(event.getPlayer().getWorld().getName());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerJoin(PlayerJoinEvent event) {
void onPlayerJoin(PlayerJoinEvent event) {
dirtyForSave.add(event.getPlayer().getWorld().getName());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerQuit(PlayerQuitEvent event) {
void onPlayerQuit(PlayerQuitEvent event) {
dirtyForSave.add(event.getPlayer().getWorld().getName());
}

View File

@@ -10,11 +10,19 @@ import net.kyori.adventure.bossbar.BossBar.Color;
import net.kyori.adventure.bossbar.BossBar.Overlay;
import org.bukkit.Bukkit;
/**
* A backup process with specific logic around Paper server.
*/
public abstract class PaperBackupProcess extends BackupProcess {
private BossBar bossBar;
/**
* Instantiates a new backup process.
* @param bm the associated backup manager.
* @param id the process identifier.
*/
protected PaperBackupProcess(PaperBackupManager bm, String id) {
super(bm, id);
}

View File

@@ -1,31 +1,31 @@
package fr.pandacube.lib.paper.backup;
import fr.pandacube.lib.util.Log;
import java.io.File;
import java.text.DateFormat;
import java.util.Date;
import java.util.function.BiPredicate;
/**
* A backup process with specific logic around Paper server working directory.
*/
public class PaperWorkdirProcess extends PaperBackupProcess {
/**
* Instantiates a new backup process for the paper server working directory.
* @param bm the associated backup manager.
*/
protected PaperWorkdirProcess(PaperBackupManager bm) {
super(bm, "workdir");
}
public BiPredicate<File, String> getFilenameFilter() {
return new BiPredicate<File, String>() {
@Override
public boolean test(File file, String path) {
if (file.isDirectory() && new File(file, "level.dat").exists())
return false;
if (new File(getSourceDir(), "logs").equals(file))
return false;
if (file.isFile() && file.getName().endsWith(".lck"))
return false;
return PaperWorkdirProcess.super.getFilenameFilter().test(file, path);
}
return (file, path) -> {
if (file.isDirectory() && new File(file, "level.dat").exists())
return false;
if (new File(getSourceDir(), "logs").equals(file))
return false;
if (file.isFile() && file.getName().endsWith(".lck"))
return false;
return PaperWorkdirProcess.super.getFilenameFilter().test(file, path);
};
}
@@ -53,10 +53,4 @@ public class PaperWorkdirProcess extends PaperBackupProcess {
return "workdir";
}
public void displayNextSchedule() {
Log.info("[Backup] " + net.md_5.bungee.api.ChatColor.GRAY + getDisplayName() + net.md_5.bungee.api.ChatColor.RESET + " next backup on "
+ DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(new Date(getNext())));
}
}

View File

@@ -1,25 +1,32 @@
package fr.pandacube.lib.paper.backup;
import fr.pandacube.lib.chat.LegacyChatFormat;
import fr.pandacube.lib.paper.scheduler.SchedulerUtil;
import fr.pandacube.lib.paper.util.WorldUtil;
import fr.pandacube.lib.util.Log;
import net.md_5.bungee.api.ChatColor;
import fr.pandacube.lib.paper.world.WorldUtil;
import fr.pandacube.lib.util.log.Log;
import org.bukkit.Bukkit;
import org.bukkit.World;
import java.io.File;
import java.text.DateFormat;
import java.util.Date;
import java.util.function.BiPredicate;
/**
* A backup process with specific logic around Paper server world.
*/
public class PaperWorldProcess extends PaperBackupProcess {
private final String worldName;
private boolean autoSave = true;
protected PaperWorldProcess(PaperBackupManager bm, final String n) {
super(bm, "worlds/" + n);
worldName = n;
private boolean autoSave = true;
/**
* Instantiates a new backup process for a world.
* @param bm the associated backup manager.
* @param worldName the name of the world.
*/
protected PaperWorldProcess(PaperBackupManager bm, final String worldName) {
super(bm, "worlds/" + worldName);
this.worldName = worldName;
}
private World getWorld() {
@@ -63,11 +70,11 @@ public class PaperWorldProcess extends PaperBackupProcess {
public void displayNextSchedule() {
if (hasNextScheduled()) {
Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " is dirty. Next backup on "
Log.info("[Backup] " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + " is dirty. Next backup on "
+ DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(new Date(getNext())));
}
else {
Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " is clean. Next backup not scheduled.");
Log.info("[Backup] " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + " is clean. Next backup not scheduled.");
}
}
@@ -81,7 +88,7 @@ public class PaperWorldProcess extends PaperBackupProcess {
public void setDirtyAfterSave() {
if (!isDirty()) { // don't set dirty if it is already
setDirtySinceNow();
Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " was saved and is now dirty. Next backup on "
Log.info("[Backup] " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + " was saved and is now dirty. Next backup on "
+ DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG)
.format(new Date(getNext()))
);

View File

@@ -1,8 +1,8 @@
package fr.pandacube.lib.paper.commands;
import com.destroystokyo.paper.brigadier.BukkitBrigadierCommandSource;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.SuggestionProvider;
@@ -10,132 +10,153 @@ import com.mojang.brigadier.tree.CommandNode;
import com.mojang.brigadier.tree.LiteralCommandNode;
import com.mojang.brigadier.tree.RootCommandNode;
import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.commands.BadCommandUsage;
import fr.pandacube.lib.commands.BrigadierCommand;
import fr.pandacube.lib.commands.SuggestionsSupplier;
import fr.pandacube.lib.paper.permissions.PandalibPaperPermissions;
import fr.pandacube.lib.paper.reflect.PandalibPaperReflect;
import fr.pandacube.lib.paper.reflect.wrapper.craftbukkit.CraftServer;
import fr.pandacube.lib.paper.PandaLibPaper;
import fr.pandacube.lib.paper.reflect.wrapper.craftbukkit.CraftVector;
import fr.pandacube.lib.paper.reflect.wrapper.craftbukkit.VanillaCommandWrapper;
import fr.pandacube.lib.paper.reflect.wrapper.minecraft.commands.BlockPosArgument;
import fr.pandacube.lib.paper.reflect.wrapper.minecraft.commands.Commands;
import fr.pandacube.lib.paper.reflect.wrapper.minecraft.commands.ComponentArgument;
import fr.pandacube.lib.paper.reflect.wrapper.minecraft.commands.Coordinates;
import fr.pandacube.lib.paper.reflect.wrapper.minecraft.commands.EntityArgument;
import fr.pandacube.lib.paper.reflect.wrapper.minecraft.commands.EntitySelector;
import fr.pandacube.lib.paper.reflect.wrapper.minecraft.commands.Vec3Argument;
import fr.pandacube.lib.paper.reflect.wrapper.minecraft.core.BlockPos;
import fr.pandacube.lib.paper.reflect.wrapper.minecraft.server.ServerPlayer;
import fr.pandacube.lib.paper.reflect.wrapper.paper.PaperAdventure;
import fr.pandacube.lib.paper.reflect.wrapper.paper.commands.BukkitCommandNode;
import fr.pandacube.lib.paper.reflect.wrapper.paper.commands.PluginCommandNode;
import fr.pandacube.lib.players.standalone.AbstractOffPlayer;
import fr.pandacube.lib.players.standalone.AbstractOnlinePlayer;
import fr.pandacube.lib.players.standalone.AbstractPlayerManager;
import fr.pandacube.lib.reflect.wrapper.ReflectWrapper;
import fr.pandacube.lib.util.Log;
import net.kyori.adventure.text.Component;
import fr.pandacube.lib.reflect.Reflect;
import fr.pandacube.lib.reflect.ReflectClass;
import fr.pandacube.lib.util.log.Log;
import io.papermc.paper.command.brigadier.CommandSourceStack;
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.command.Command;
import org.bukkit.command.CommandMap;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.command.PluginCommand;
import org.bukkit.command.defaults.BukkitCommand;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerCommandSendEvent;
import org.bukkit.event.server.ServerLoadEvent;
import org.bukkit.plugin.Plugin;
import org.bukkit.util.BlockVector;
import org.bukkit.util.Vector;
import java.util.ArrayList;
import java.lang.reflect.InvocationTargetException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Stream;
import static fr.pandacube.lib.reflect.wrapper.ReflectWrapper.unwrap;
import static fr.pandacube.lib.reflect.wrapper.ReflectWrapper.wrap;
/**
* Abstract class to hold a command to be integrated into a Paper server vanilla command dispatcher.
*/
public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBrigadierCommandSource> implements Listener {
@SuppressWarnings("UnstableApiUsage")
public abstract class PaperBrigadierCommand extends BrigadierCommand<CommandSourceStack> implements Listener {
private static final Commands vanillaCommandDispatcher;
private static final CommandDispatcher<BukkitBrigadierCommandSource> nmsDispatcher;
private static CommandDispatcher<CommandSourceStack> vanillaPaperDispatcher = null;
static {
PandalibPaperReflect.init();
vanillaCommandDispatcher = ReflectWrapper.wrapTyped(Bukkit.getServer(), CraftServer.class)
.getServer()
.vanillaCommandDispatcher();
nmsDispatcher = vanillaCommandDispatcher.dispatcher();
/**
* Gets the Brigadier dispatcher provided by paper API during {@link LifecycleEvents#COMMANDS}.
* <p>
* This Dispatcher is not the vanilla one. Instead, Paper implementation wraps the vanilla one to handle proper registration
* of commands from plugins.
* @return the Brigadier dispatcher.
*/
public static CommandDispatcher<CommandSourceStack> getVanillaPaperDispatcher() {
return vanillaPaperDispatcher;
}
/**
* Removes a plugin command that overrides a vanilla command, so the vanilla command functionnalities are fully
* restored (so, not only the usage, but also the suggestions and the command structure sent to the client).
* @param name the name of the command to restore.
* Gets the root node of the dispatcher from {@link #getVanillaPaperDispatcher()}.
* @return the root node, or null if {@link #getVanillaPaperDispatcher()} is also null.
*/
public static void restoreVanillaCommand(String name) {
CommandMap bukkitCmdMap = Bukkit.getCommandMap();
Command bukkitCommand = bukkitCmdMap.getCommand(name);
if (bukkitCommand != null) {
if (VanillaCommandWrapper.REFLECT.get().isInstance(bukkitCommand)) {
//Log.info("Command /" + name + " is already a vanilla command.");
return;
}
Log.info("Removing Bukkit command /" + name + " (" + getCommandIdentity(bukkitCommand) + ")");
bukkitCmdMap.getKnownCommands().remove(name.toLowerCase(java.util.Locale.ENGLISH));
bukkitCommand.unregister(bukkitCmdMap);
public static RootCommandNode<CommandSourceStack> getRootNode() {
return vanillaPaperDispatcher == null ? null : vanillaPaperDispatcher.getRoot();
}
LiteralCommandNode<BukkitBrigadierCommandSource> node = (LiteralCommandNode<BukkitBrigadierCommandSource>) getRootNode().getChild(name);
Command newCommand = new VanillaCommandWrapper(vanillaCommandDispatcher, node).__getRuntimeInstance();
bukkitCmdMap.getKnownCommands().put(name.toLowerCase(), newCommand);
newCommand.register(bukkitCmdMap);
private static void updateVanillaPaperDispatcher(CommandDispatcher<CommandSourceStack> newDispatcher) {
if (vanillaPaperDispatcher == null || newDispatcher != vanillaPaperDispatcher) {
vanillaPaperDispatcher = newDispatcher;
// vanillaPaperDispatcher.getRoot() is not the real root but a wrapped root. Trying to map the fake root with the real one to trick the Paper/Brigadier (un)wrapper
RootCommandNode<CommandSourceStack> wrappedRoot = vanillaPaperDispatcher.getRoot();
ReflectClass<?> apiMirrorRootNodeClass = Reflect.ofClassOfInstance(wrappedRoot);
try {
RootCommandNode<?> unwrappedRoot = ((CommandDispatcher<?>) apiMirrorRootNodeClass.method("getDispatcher").invoke(wrappedRoot)).getRoot();
Reflect.ofClass(CommandNode.class).field("unwrappedCached").setValue(wrappedRoot, unwrappedRoot);
Reflect.ofClass(CommandNode.class).field("wrappedCached").setValue(unwrappedRoot, wrappedRoot);
} catch (InvocationTargetException|IllegalAccessException|NoSuchMethodException|NoSuchFieldException e) {
Log.severe("Unable to trick the Paper/Brigadier unwrapper to properly handle commands redirecting to root command node.", e);
}
}
}
/**
* Returns the vanilla instance of the Brigadier dispatcher.
* @return the vanilla instance of the Brigadier dispatcher.
*/
public static CommandDispatcher<BukkitBrigadierCommandSource> getNMSDispatcher() {
return nmsDispatcher;
}
/**
* Returns the root command node of the Brigadier dispatcher.
* @return the root command node of the Brigadier dispatcher.
* Removes a plugin command that overrides a vanilla command, so the vanilla command functionalities are fully
* restored (so, not only the usage, but also the suggestions and the command structure sent to the client).
* @param name the name of the command to restore.
*/
protected static RootCommandNode<BukkitBrigadierCommandSource> getRootNode() {
return nmsDispatcher.getRoot();
public static void restoreVanillaCommand(String name) {
PandaLibPaper.getPlugin().getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS,
event -> updateVanillaPaperDispatcher(event.registrar().getDispatcher()));
Bukkit.getServer().getScheduler().runTask(PandaLibPaper.getPlugin(), () -> {
if (vanillaPaperDispatcher == null)
return;
CommandNode<CommandSourceStack> targetCommand = vanillaPaperDispatcher.getRoot().getChild("minecraft:" + name);
if (targetCommand == null) {
Log.warning("There is no vanilla command '" + name + "' to restore.");
return;
}
CommandNode<CommandSourceStack> eventuallyBadCommandToReplace = vanillaPaperDispatcher.getRoot().getChild(name);
Boolean isPluginCommand = isPluginCommand(eventuallyBadCommandToReplace);
if (isPluginCommand != null && isPluginCommand) {
Log.info(getCommandIdentity(eventuallyBadCommandToReplace) + " found in the dispatcher. Restoring the vanilla command.");
vanillaPaperDispatcher.getRoot().getChildren().removeIf(c -> c.getName().equals(name));
vanillaPaperDispatcher.getRoot().addChild(getAliasNode(targetCommand, name));
}
/*else if (isPluginCommand == null) {
Log.info(getCommandIdentity(eventuallyBadCommandToReplace) + " found in the dispatcher. Unsure if we restore the vanilla command.");
}*/
});
}
private final Plugin plugin;
/**
* The command node of this command.
*/
protected final LiteralCommandNode<BukkitBrigadierCommandSource> commandNode;
protected LiteralCommandNode<CommandSourceStack> commandNode;
/**
* The command requested aliases.
*/
protected final String[] aliases;
/**
* The command description.
*/
protected final String description;
private final RegistrationPolicy registrationPolicy;
private Set<String> registeredAliases;
/**
* Instanciate this command instance.
* Instantiate this command instance.
*
* @param pl the plugin instance.
* @param regPolicy the registration policy for this command.
@@ -143,17 +164,17 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
public PaperBrigadierCommand(Plugin pl, RegistrationPolicy regPolicy) {
plugin = pl;
registrationPolicy = regPolicy;
commandNode = buildCommand().build();
postBuildCommand(commandNode);
String[] aliasesTmp = getAliases();
aliases = aliasesTmp == null ? new String[0] : aliasesTmp;
description = getDescription();
register();
Bukkit.getPluginManager().registerEvents(this, plugin);
try {
PandalibPaperPermissions.addPermissionMapping("minecraft.command." + commandNode.getLiteral().toLowerCase(), getTargetPermission().toLowerCase());
} catch (NoClassDefFoundError ignored) { }
//try {
// PandalibPaperPermissions.addPermissionMapping("minecraft.command." + commandNode.getLiteral().toLowerCase(), getTargetPermission().toLowerCase());
//} catch (NoClassDefFoundError ignored) { }
}
/**
* Instanciate this command isntance with a registration policy of {@link RegistrationPolicy#ONLY_BASE_COMMAND}.
* Instantiate this command instance with a registration policy of {@link RegistrationPolicy#ONLY_BASE_COMMAND}.
* @param pl the plugin instance.
*/
public PaperBrigadierCommand(Plugin pl) {
@@ -164,163 +185,179 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
private void register() {
plugin.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event -> {
updateVanillaPaperDispatcher(event.registrar().getDispatcher());
String[] aliases = getAliases();
if (aliases == null)
aliases = new String[0];
commandNode = buildCommand().build();
postBuildCommand(commandNode);
String pluginName = plugin.getName().toLowerCase();
if (vanillaPaperDispatcher.getRoot().getChild(commandNode.getName()) != null) {
Log.info("Command /" + commandNode.getName() + " found in the vanilla dispatcher during initial command registration. Replacing it by force.");
vanillaPaperDispatcher.getRoot().getChildren().removeIf(c -> c.getName().equals(commandNode.getName()));
}
registeredAliases = new HashSet<>();
registerNode(commandNode, false);
registerAlias(pluginName + ":" + commandNode.getLiteral(), true);
registeredAliases = new HashSet<>(event.registrar().register(commandNode, description, List.of(aliases)));
doPostRegistrationFixes();
for (String alias : aliases) {
registerAlias(alias, false);
registerAlias(pluginName + ":" + alias, true);
if (registrationPolicy == RegistrationPolicy.ALL) {
// enforce registration of aliases
for (String alias : aliases) {
if (!registeredAliases.contains(alias)) {
Log.info("Command /" + commandNode.getName() + ": forcing registration of alias " + alias);
registeredAliases.addAll(event.registrar().register(getAliasNode(commandNode, alias), description));
}
}
}
Bukkit.getServer().getScheduler().runTask(plugin, () -> {
if (vanillaPaperDispatcher == null)
return;
Set<String> forceRegistrationAgain = new HashSet<>();
forceRegistrationAgain.add(commandNode.getName());
if (registrationPolicy == RegistrationPolicy.ALL)
forceRegistrationAgain.addAll(List.of(aliases));
for (String aliasToForce : forceRegistrationAgain) {
CommandNode<CommandSourceStack> actualNode = vanillaPaperDispatcher.getRoot().getChild(aliasToForce);
if (actualNode != null) {
//Log.info("Forcing registration of alias /" + aliasToForce + " for command /" + commandNode.getName() + ": replacing " + getCommandIdentity(actualNode) + "?");
if (PluginCommandNode.REFLECT.get().isInstance(actualNode)) {
PluginCommandNode pcn = wrap(actualNode, PluginCommandNode.class);
if (pcn.getPlugin().equals(plugin))
return;
}
else if (BukkitCommandNode.REFLECT.get().isInstance(actualNode)) {
BukkitCommandNode bcn = wrap(actualNode, BukkitCommandNode.class);
if (bcn.getBukkitCommand() instanceof PluginCommand pc && pc.getPlugin().equals(plugin))
return;
}
vanillaPaperDispatcher.getRoot().getChildren().removeIf(c -> c.getName().equals(aliasToForce));
}
/*else {
Log.info("Forcing registration of alias /" + aliasToForce + " for command /" + commandNode.getName() + ": no command found for alias. Adding alias.");
}*/
LiteralCommandNode<CommandSourceStack> newPCN = unwrap(new PluginCommandNode(aliasToForce, plugin.getPluginMeta(), commandNode, description));
vanillaPaperDispatcher.getRoot().addChild(newPCN);
}
});
});
}
private void doPostRegistrationFixes() {
postRegistrationFixNode(new HashSet<>(), commandNode);
}
private void postRegistrationFixNode(Set<CommandNode<CommandSourceStack>> fixedNodes, CommandNode<CommandSourceStack> originalNode) {
if (originalNode instanceof RootCommandNode)
return;
if (fixedNodes.contains(originalNode))
return;
fixedNodes.add(originalNode);
if (originalNode.getRedirect() != null) {
try {
@SuppressWarnings("rawtypes")
ReflectClass<CommandNode> cmdNodeClass = Reflect.ofClass(CommandNode.class);
@SuppressWarnings("unchecked")
CommandNode<CommandSourceStack> unwrappedNode = (CommandNode<CommandSourceStack>) cmdNodeClass.field("unwrappedCached").getValue(originalNode);
if (unwrappedNode != null) {
cmdNodeClass.field("modifier").setValue(unwrappedNode, cmdNodeClass.field("modifier").getValue(originalNode));
cmdNodeClass.field("forks").setValue(unwrappedNode, cmdNodeClass.field("forks").getValue(originalNode));
}
} catch (IllegalAccessException | NoSuchFieldException e) {
throw new RuntimeException(e);
}
postRegistrationFixNode(fixedNodes, originalNode.getRedirect());
}
else {
try {
for (CommandNode<CommandSourceStack> child : originalNode.getChildren())
postRegistrationFixNode(fixedNodes, child);
} catch (UnsupportedOperationException ignored) {
// in case getChildren is not possible (vanilla commands are wrapped by Paper API)
}
}
}
private void registerAlias(String alias, boolean prefixed) {
LiteralCommandNode<BukkitBrigadierCommandSource> node = literal(alias)
private static LiteralCommandNode<CommandSourceStack> getAliasNode(CommandNode<CommandSourceStack> commandNode, String alias) {
return LiteralArgumentBuilder.<CommandSourceStack>literal(alias)
.requires(commandNode.getRequirement())
.executes(commandNode.getCommand())
.redirect(commandNode)
.build();
registerNode(node, prefixed);
}
private void registerNode(LiteralCommandNode<BukkitBrigadierCommandSource> node, boolean prefixed) {
RootCommandNode<BukkitBrigadierCommandSource> root = getRootNode();
String name = node.getLiteral();
boolean isAlias = node.getRedirect() == commandNode;
boolean forceRegistration = switch (registrationPolicy) {
case NONE -> false;
case ONLY_BASE_COMMAND -> prefixed || !isAlias;
case ALL -> true;
};
// nmsDispatcher integration and conflit resolution
boolean nmsRegister = false, nmsRegistered = false;
CommandNode<BukkitBrigadierCommandSource> nmsConflited = root.getChild(name);
if (nmsConflited != null) {
if (isFromThisCommand(nmsConflited)) {
// this command is already registered in NMS. Dont need to register again
nmsRegistered = true;
private static String getCommandIdentity(CommandNode<CommandSourceStack> command) {
if (PluginCommandNode.REFLECT.get().isInstance(command)) {
PluginCommandNode wrappedPCN = wrap(command, PluginCommandNode.class);
return "Node /" + command.getName() + " from plugin " + wrappedPCN.getPlugin().getName();
}
else if (BukkitCommandNode.REFLECT.get().isInstance(command)) {
BukkitCommandNode wrappedBCN = wrap(command, BukkitCommandNode.class);
Command bukkitCmd = wrappedBCN.getBukkitCommand();
if (bukkitCmd instanceof PluginCommand cmd) {
return "Node /" + command.getName() + " wrapping Bukkit command /" + bukkitCmd.getName() + " from plugin " + cmd.getPlugin().getName();
}
else if (forceRegistration) {
nmsRegister = true;
Log.info("Overwriting Brigadier command /" + name);
}
else if (prefixed || !isAlias) {
Log.severe("/" + name + " already in NMS Brigadier instance."
+ " Wont replace it because registration is not forced for prefixed or initial name of a command.");
}
else { // conflict, wont replace, not forced but only an alias anyway
Log.info("/" + name + " already in NMS Brigadier instance."
+ " Wont replace it because registration is not forced for a non-prefixed alias.");
else if (VanillaCommandWrapper.REFLECT.get().isInstance(bukkitCmd)) {
VanillaCommandWrapper vcw = wrap(bukkitCmd, VanillaCommandWrapper.class);
CommandNode<CommandSourceStack> vanillaCmd = vcw.vanillaCommand();
if (vanillaCmd != command)
return "Node /" + command.getName() + " wrapping non-plugin command /" + bukkitCmd.getName() + " wrapping: " + getCommandIdentity(vcw.vanillaCommand());
else
return "Node /" + command.getName() + " wrapping non-plugin command /" + bukkitCmd.getName() + " wrapping back the node (risk of StackOverflow?)";
}
else
return "Node /" + command.getName() + " wrapping " + bukkitCmd.getClass().getName() + " /" + bukkitCmd.getName();
}
else {
nmsRegister = true;
return "Node /" + command.getName() + " (unspecific)";
}
}
if (nmsRegister) {
@SuppressWarnings("unchecked")
var rCommandNode = ReflectWrapper.wrapTyped(root, fr.pandacube.lib.paper.reflect.wrapper.brigadier.CommandNode.class);
rCommandNode.removeCommand(name);
root.addChild(node);
nmsRegistered = true;
private static Boolean isPluginCommand(CommandNode<CommandSourceStack> command) {
if (PluginCommandNode.REFLECT.get().isInstance(command)) {
return true;
}
if (!nmsRegistered) {
return;
}
registeredAliases.add(name);
// bukkit dispatcher conflict resolution
boolean bukkitRegister = false;
CommandMap bukkitCmdMap = Bukkit.getCommandMap();
Command bukkitConflicted = bukkitCmdMap.getCommand(name);
if (bukkitConflicted != null) {
if (!isFromThisCommand(bukkitConflicted)) {
if (forceRegistration) {
bukkitRegister = true;
Log.info("Overwriting Bukkit command /" + name
+ " (" + getCommandIdentity(bukkitConflicted) + ")");
}
else if (prefixed || !isAlias) {
Log.severe("/" + name + " already in Bukkit dispatcher (" + getCommandIdentity(bukkitConflicted) + ")." +
" Wont replace it because registration is not forced for prefixed or initial name of a command.");
}
else {
Log.info("/" + name + " already in Bukkit dispatcher (" + getCommandIdentity(bukkitConflicted) + ")." +
" Wont replace it because registration is not forced for a non-prefixed alias.");
}
else if (BukkitCommandNode.REFLECT.get().isInstance(command)) {
BukkitCommandNode wrappedBCN = wrap(command, BukkitCommandNode.class);
Command bukkitCmd = wrappedBCN.getBukkitCommand();
if (bukkitCmd instanceof PluginCommand) {
return true;
}
else if (VanillaCommandWrapper.REFLECT.get().isInstance(bukkitCmd)) {
VanillaCommandWrapper vcw = wrap(bukkitCmd, VanillaCommandWrapper.class);
CommandNode<CommandSourceStack> vanillaCmd = vcw.vanillaCommand();
if (vanillaCmd != command)
return isPluginCommand(vcw.vanillaCommand());
else
return false;
}
else
return null;
}
else {
bukkitRegister = true;
return false;
}
if (bukkitRegister) {
bukkitCmdMap.getKnownCommands().remove(name.toLowerCase());
if (bukkitConflicted != null)
bukkitConflicted.unregister(bukkitCmdMap);
Command newCommand = new VanillaCommandWrapper(vanillaCommandDispatcher, node).__getRuntimeInstance();
bukkitCmdMap.getKnownCommands().put(name.toLowerCase(), newCommand);
newCommand.register(bukkitCmdMap);
}
}
private boolean isFromThisCommand(CommandNode<BukkitBrigadierCommandSource> node) {
return node == commandNode || node.getRedirect() == commandNode;
}
private boolean isFromThisCommand(Command bukkitCmd) {
if (VanillaCommandWrapper.REFLECT.get().isInstance(bukkitCmd)) {
return isFromThisCommand(ReflectWrapper.wrapTyped((BukkitCommand) bukkitCmd, VanillaCommandWrapper.class).vanillaCommand());
}
return false;
}
private static String getCommandIdentity(Command bukkitCmd) {
if (bukkitCmd instanceof PluginCommand cmd) {
return "Bukkit command: /" + cmd.getName() + " from plugin " + cmd.getPlugin().getName();
}
else if (VanillaCommandWrapper.REFLECT.get().isInstance(bukkitCmd)) {
return "Vanilla command: /" + bukkitCmd.getName();
}
else
return bukkitCmd.getClass().getName() + ": /" + bukkitCmd.getName();
}
/**
* Player command sender event handler.
* @param event the event.
* Gets the aliases that are actually registered in the server.
* @return the actually registered aliases.
*/
@EventHandler
public void onPlayerCommandSend(PlayerCommandSendEvent event) {
event.getCommands().removeAll(registeredAliases.stream().map(s -> "minecraft:" + s).toList());
protected Set<String> getRegisteredAliases() {
return Set.copyOf(registeredAliases);
}
/**
* Server load event handler.
* @param event the event.
*/
@EventHandler
public void onServerLoad(ServerLoadEvent event) {
register();
}
@@ -339,6 +376,15 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
*/
protected abstract String getTargetPermission();
/**
* Returns the permission that should be tested instead of "minecraft.command.cmdName". The conversion from the
* minecraft prefixed permission node to the returned node is done by the {@code pandalib-paper-permissions} if it
* is present in the classpath during runtime.
* @return the permission that should be tested instead of "minecraft.command.cmdName".
*/
protected String getDescription() {
return "A command from " + plugin.getName();
}
@@ -352,13 +398,17 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
public boolean isConsole(BukkitBrigadierCommandSource wrapper) {
@Override
public boolean isConsole(CommandSourceStack wrapper) {
return isConsole(getCommandSender(wrapper));
}
public boolean isPlayer(BukkitBrigadierCommandSource wrapper) {
@Override
public boolean isPlayer(CommandSourceStack wrapper) {
return isPlayer(getCommandSender(wrapper));
}
public Predicate<BukkitBrigadierCommandSource> hasPermission(String permission) {
@Override
public Predicate<CommandSourceStack> hasPermission(String permission) {
return wrapper -> getCommandSender(wrapper).hasPermission(permission);
}
@@ -392,7 +442,7 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
* @param context the command context from which to get the Bukkit command sender.
* @return the Bukkit command sender.
*/
public static CommandSender getCommandSender(CommandContext<BukkitBrigadierCommandSource> context) {
public static CommandSender getCommandSender(CommandContext<CommandSourceStack> context) {
return getCommandSender(context.getSource());
}
@@ -401,8 +451,8 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
* @param wrapper the wrapper from which to get the Bukkit command sender.
* @return the Bukkit command sender.
*/
public static CommandSender getCommandSender(BukkitBrigadierCommandSource wrapper) {
return wrapper.getBukkitSender();
public static CommandSender getCommandSender(CommandSourceStack wrapper) {
return wrapper.getSender();
}
/**
@@ -410,13 +460,13 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
* @param sender the command sender.
* @return a new instance of a command sender wrapper for the provided command sender.
*/
public static BukkitBrigadierCommandSource getBrigadierCommandSource(CommandSender sender) {
return VanillaCommandWrapper.getListener(sender);
public static CommandSourceStack getBrigadierCommandSource(CommandSender sender) {
throw new UnsupportedOperationException("The 1.20.6 Paper API update uses a different wrapper for Brigadier command sender.");
}
/**
* A suggestions supplier that suggests the names of the currently connected players (that the command sender can see).
* A suggestion supplier that suggests the names of the currently connected players (that the command sender can see).
*/
public static final SuggestionsSupplier<CommandSender> TAB_PLAYER_CURRENT_SERVER = (sender, ti, token, a) -> {
@SuppressWarnings("unchecked")
@@ -430,7 +480,7 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
};
/**
* A suggestions supplier that suggests the names of the worlds currently loaded on this server.
* A suggestion supplier that suggests the names of the worlds currently loaded on this server.
*/
public static final SuggestionsSupplier<CommandSender> TAB_WORLDS = SuggestionsSupplier.fromStreamSupplier(() -> Bukkit.getWorlds().stream().map(World::getName));
@@ -440,7 +490,7 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
* @param suggestions the suggestions to wrap.
* @return a {@link SuggestionProvider} generating the suggestions from the provided {@link SuggestionsSupplier}.
*/
protected SuggestionProvider<BukkitBrigadierCommandSource> wrapSuggestions(SuggestionsSupplier<CommandSender> suggestions) {
public SuggestionProvider<CommandSourceStack> wrapSuggestions(SuggestionsSupplier<CommandSender> suggestions) {
return wrapSuggestions(suggestions, PaperBrigadierCommand::getCommandSender);
}
@@ -453,12 +503,15 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
* @param cmd the command executor to wrap.
* @return a wrapper command executor.
*/
protected static com.mojang.brigadier.Command<BukkitBrigadierCommandSource> wrapCommand(com.mojang.brigadier.Command<BukkitBrigadierCommandSource> cmd) {
protected static com.mojang.brigadier.Command<CommandSourceStack> wrapCommand(com.mojang.brigadier.Command<CommandSourceStack> cmd) {
return context -> {
try {
return cmd.run(context);
} catch(CommandSyntaxException e) {
throw e;
} catch (BadCommandUsage e) {
getCommandSender(context).sendMessage(Chat.failureText("Error while using the command: " + e.getMessage()));
return 0;
} catch (Throwable t) {
Log.severe(t);
getCommandSender(context).sendMessage(Chat.failureText("Error while executing the command: " + t));
@@ -475,120 +528,9 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
/*
* Minecraft argument type
* Minecraft's argument type
*/
/**
* Creates a new instance of the Brigadier argument type {@code minecraft:entity}.
* @param singleTarget if this argument takes only a single target.
* @param playersOnly if this argument takes players only.
* @return the {@code minecraft:entity} argument type with the specified parameters.
*/
public static ArgumentType<Object> argumentMinecraftEntity(boolean singleTarget, boolean playersOnly) {
if (playersOnly) {
return singleTarget ? EntityArgument.player() : EntityArgument.players();
}
else {
return singleTarget ? EntityArgument.entity() : EntityArgument.entities();
}
}
/**
* Gets the value of the provided argument of type {@code minecraft:entity} (list of entities), from the provided context.
* @param context the command execution context.
* @param argument the argument name.
* @return the value of the argument, or null if not found.
*/
public List<Entity> tryGetMinecraftEntityArgument(CommandContext<BukkitBrigadierCommandSource> context, String argument) {
EntitySelector es = ReflectWrapper.wrap(tryGetArgument(context, argument, EntitySelector.MAPPING.runtimeClass()), EntitySelector.class);
if (es == null)
return null;
List<fr.pandacube.lib.paper.reflect.wrapper.minecraft.world.Entity> nmsEntityList = es.findEntities(context.getSource());
List<Entity> entityList = new ArrayList<>(nmsEntityList.size());
for (fr.pandacube.lib.paper.reflect.wrapper.minecraft.world.Entity nmsEntity : nmsEntityList) {
entityList.add(nmsEntity.getBukkitEntity());
}
return entityList;
}
/**
* Gets the value of the provided argument of type {@code minecraft:entity} (list of players), from the provided context.
* @param context the command execution context.
* @param argument the argument name.
* @return the value of the argument, or null if not found.
*/
public List<Player> tryGetMinecraftEntityArgumentPlayers(CommandContext<BukkitBrigadierCommandSource> context, String argument) {
EntitySelector es = ReflectWrapper.wrap(tryGetArgument(context, argument, EntitySelector.MAPPING.runtimeClass()), EntitySelector.class);
if (es == null)
return null;
List<ServerPlayer> nmsPlayerList = es.findPlayers(context.getSource());
List<Player> playerList = new ArrayList<>(nmsPlayerList.size());
for (ServerPlayer nmsPlayer : nmsPlayerList) {
playerList.add(nmsPlayer.getBukkitEntity());
}
return playerList;
}
/**
* Gets the value of the provided argument of type {@code minecraft:entity} (one entity), from the provided context.
* @param context the command execution context.
* @param argument the argument name.
* @return the value of the argument, or null if not found.
*/
public Entity tryGetMinecraftEntityArgumentOneEntity(CommandContext<BukkitBrigadierCommandSource> context, String argument) {
EntitySelector es = ReflectWrapper.wrap(tryGetArgument(context, argument, EntitySelector.MAPPING.runtimeClass()), EntitySelector.class);
if (es == null)
return null;
fr.pandacube.lib.paper.reflect.wrapper.minecraft.world.Entity nmsEntity = es.findSingleEntity(context.getSource());
return nmsEntity == null ? null : nmsEntity.getBukkitEntity();
}
/**
* Gets the value of the provided argument of type {@code minecraft:entity} (one player), from the provided context.
* @param context the command execution context.
* @param argument the argument name.
* @return the value of the argument, or null if not found.
*/
public Player tryGetMinecraftEntityArgumentOnePlayer(CommandContext<BukkitBrigadierCommandSource> context, String argument) {
EntitySelector es = ReflectWrapper.wrap(tryGetArgument(context, argument, EntitySelector.MAPPING.runtimeClass()), EntitySelector.class);
if (es == null)
return null;
ServerPlayer nmsPlayer = es.findSinglePlayer(context.getSource());
return nmsPlayer == null ? null : nmsPlayer.getBukkitEntity();
}
/**
* Creates a new instance of the Brigadier argument type {@code minecraft:block_pos}.
* @return the {@code minecraft:block_pos} argument type.
*/
public static ArgumentType<Object> argumentMinecraftBlockPosition() {
return BlockPosArgument.blockPos();
}
/**
* Gets the value of the provided argument of type {@code minecraft:block_pos}, from the provided context.
* @param context the command execution context.
* @param argument the argument name.
* @param deflt a defualt value if the argument is not found.
* @return the value of the argument.
*/
public BlockVector tryGetMinecraftBlockPositionArgument(CommandContext<BukkitBrigadierCommandSource> context,
String argument, BlockVector deflt) {
return tryGetArgument(context, argument, Coordinates.MAPPING.runtimeClass(), nmsCoord -> {
BlockPos bp = ReflectWrapper.wrap(nmsCoord, Coordinates.class).getBlockPos(context.getSource());
return new BlockVector(bp.getX(), bp.getY(), bp.getZ());
}, deflt);
}
/**
* Creates a new instance of the Brigadier argument type {@code minecraft:vec3}.
* @return the {@code minecraft:vec3} argument type.
@@ -601,14 +543,14 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
* Gets the value of the provided argument of type {@code minecraft:vec3}, from the provided context.
* @param context the command execution context.
* @param argument the argument name.
* @param deflt a defualt value if the argument is not found.
* @param deflt a default value if the argument is not found.
* @return the value of the argument.
*/
public Vector tryGetMinecraftVec3Argument(CommandContext<BukkitBrigadierCommandSource> context, String argument,
public Vector tryGetMinecraftVec3Argument(CommandContext<CommandSourceStack> context, String argument,
Vector deflt) {
return tryGetArgument(context, argument, Coordinates.MAPPING.runtimeClass(),
nmsCoord -> CraftVector.toBukkit(
ReflectWrapper.wrap(nmsCoord, Coordinates.class).getPosition(context.getSource())
return tryGetArgument(context, argument, Coordinates.REFLECT.get(),
nmsCoordinate -> CraftVector.toBukkit(
wrap(nmsCoordinate, Coordinates.class).getPosition(context.getSource())
),
deflt);
}
@@ -616,44 +558,11 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
/**
* Creates a new instance of the Brigadier argument type {@code minecraft:component}.
* @return the {@code minecraft:component} argument type.
*/
public static ArgumentType<Object> argumentMinecraftChatComponent() {
return ComponentArgument.textComponent();
}
/**
* Gets the value of the provided argument of type {@code minecraft:component}, from the provided context.
* @param context the command execution context.
* @param argument the argument name.
* @param deflt a defualt value if the argument is not found.
* @return the value of the argument.
*/
public Component tryGetMinecraftChatComponentArgument(CommandContext<BukkitBrigadierCommandSource> context,
String argument, Component deflt) {
return tryGetArgument(context, argument,
fr.pandacube.lib.paper.reflect.wrapper.minecraft.network.chat.Component.MAPPING.runtimeClass(),
nmsComp -> PaperAdventure.asAdventure(
ReflectWrapper.wrap(nmsComp,
fr.pandacube.lib.paper.reflect.wrapper.minecraft.network.chat.Component.class)
),
deflt);
}
/**
* All possible choices on how to force the registration of a command, based on certain conditions.
*/
public enum RegistrationPolicy {
/**
* Do not force to register a command node or an alias if there is already a command with that name in the
* vanilla Brigadier dispatcher.
* Note that all plugin-name-prefixed aliases will be registered anyway.
*/
NONE,
/**
* Force only the base command (but not the aliases) to be registered, even if a command with that name already
* exists in the vanilla Brigadier dispatcher.

Some files were not shown because too many files have changed in this diff Show More