Compare commits

...

156 Commits

Author SHA1 Message Date
b42bbb4887 ItemStackAdapter for Json: removed backward compatibility of serialized data (was temporary during the server upgrade) 2025-07-20 00:22:41 +02:00
34809d4618 ItemStackAdapter for Json: fix again deserialization 2025-07-18 17:17:08 +02:00
843d9c3509 ItemStackAdapter for Json: deserialized json cannot contains both old and new data. When both are present (because old server needs the old one), keeping only the new data. 2025-07-18 17:09:30 +02:00
1716e0b5a8 ItemStackAdapter for Json: make the generated json compatible with pre 1.21.5 deserializer
-> temporary fix so the older servers does not throw an error when trying to deserialize (e.g. when using the lobby compass menu)
2025-07-18 16:53:22 +02:00
254b885648 Fix reflection for 1.21.7/8 (round 6) 2025-07-18 15:12:38 +02:00
e2c0098eb9 Fix reflection for 1.21.7/8 (round 5) 2025-07-18 14:37:12 +02:00
2fc3eb50f5 Added missing javadoc 2025-07-18 01:56:28 +02:00
fc44151f2b Fix reflection for 1.21.7/8 (round 4) 2025-07-18 01:52:17 +02:00
f8a7c5f1e7 Small dependency upgrade 2025-07-18 01:39:58 +02:00
a9ea8c3038 Fix reflection for 1.21.7/8 (round 3) 2025-07-18 01:37:57 +02:00
e611d06987 MC 1.21.8 2025-07-17 19:12:03 +02:00
638e57bb7f Fix reflection wrappers for 1.21.7 (round 2) 2025-07-17 14:41:44 +02:00
0009dd22cd Fix reflection wrappers for 1.21.7 2025-07-17 13:22:05 +02:00
79474b14d2 Make pandalib compile against Paper API 1.21.7 (prepare to full 1.21.7 update) 2025-07-14 22:31:55 +02:00
ee4812bdbb Adjust debugging message for command registration 2025-07-14 00:29:32 +02:00
7d2a5e7862 Fix another reflection wrapper inheritance 2025-07-14 00:26:04 +02:00
c09362c75e Fix reflection wrapper inheritance 2025-07-14 00:20:00 +02:00
457262049d Fix some reflection wrapping issues 2025-07-14 00:16:38 +02:00
ebbbc3a1f0 Fixes to support last Paper 1.21.4 build 2025-07-14 00:02:07 +02:00
8c0db895da Added MC 1.21.7 2025-07-01 21:39:39 +02:00
5943b10d16 Added MC 1.21.6 2025-06-21 18:14:18 +02:00
cda7ebadcc Updated Bungeecord version 2025-06-21 18:13:51 +02:00
dbdf1eeb7c Fix ItemStackBuilder#canBreak 2025-05-24 17:26:42 +02:00
500163d8f4 Spaces around hyphen of versions range in MinecraftVersionUtil#toString() 2025-04-05 00:14:06 +02:00
9374f8d280 Updated MC version json file 2025-04-04 00:29:52 +02:00
f5194334de Fix PerformanceAnalysisManager 2025-02-20 21:54:00 +01:00
21777d4b9e Fully use chat components in PerformanceAnalysisManager lag bar 2025-02-20 21:31:13 +01:00
3b4cf63c48 Updated Skull class handling custom heads. No more relying on MHF_* accounts 2025-02-17 00:30:38 +01:00
e2b2ab466d New RandomUtil method shuffleMap 2025-02-12 23:10:48 +01:00
50e21896ba Override VaultAPI Permission extra methods #playerAdd and @playerRemove to ignore the current player world when another plugin changes permissions 2025-02-06 23:19:06 +01:00
07af67b33f Less verbose client websocket when reconnecting 2025-01-20 00:00:14 +01:00
f4d0ccca51 Do not put the HttpClient of a persistent websocket into a try-with-resources 2025-01-19 23:30:56 +01:00
51bc0bd6e8 Removed unused reflected method. 2025-01-19 17:53:53 +01:00
c229b14779 ItemStackBuilder: make canPlaceOn and canBreak actually working (must use old deprecated API because the new API is not yet working) 2025-01-18 19:17:33 +01:00
49942b35da Add support for item data components in ItemStackBuilder 2025-01-17 00:50:15 +01:00
0ffe3198e6 Fixed hundreds of small issues, code improvements, typos, ... 2025-01-16 00:25:23 +01:00
ace34fc0e8 MC 1.21.4 + small fixes 2025-01-13 23:57:48 +01:00
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
282 changed files with 7729 additions and 5826 deletions

4
.gitignore vendored
View File

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

View File

@@ -9,17 +9,18 @@ that are detailed in their respective Readme file (if any).
- `pandalib-util` General purpose utility and helper classes;
- `pandalib-chat` A chat API working on top of the Adventure API;
- `pandalib-db` An ORM working with a MySQL server through JDBC;
- `pandalib-bungee` Utility and helper classes to use in Bungeecord plugins. Also provides platform implementation for `pandalib-players` and `pandalib-commands`;
- `pandalib-bungee` Utility and helper classes to use in BungeeCord plugins. Also provides platform implementation for `pandalib-players` and `pandalib-commands`;
- `pandalib-paper` Utility and helper classes to use in Spigot/Paper plugins. Also provides platform implementation for `pandalib-players` and `pandalib-commands`;
- `pandalib-reflect` A reflection wrapper to make reflective operation easier;
- `pandalib-permissions` A general purpose permission system;
- `pandalib-bungee-permissions` Integration of the permission system `pandalib-permissions` into Bungeecord;
- `pandalib-bungee-permissions` Integration of the permission system `pandalib-permissions` into BungeeCord;
- `pandalib-paper-permissions` Integration of the permission system `pandalib-permissions` into Bukkit, Vault and WEPIF permission systems;
- `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-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-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,44 @@
<?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-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.FormattableChat;
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 FormattableChat} from the provided Bungee {@link BaseComponent}.
* @param c the {@link BaseComponent}.
* @return a new {@link FormattableChat}.
*/
public static FormattableChat chatComponent(BaseComponent c) {
return Chat.chatComponent(toAdventure(c));
}
/**
* Creates a {@link FormattableChat} from the provided Bungee {@link BaseComponent BaseComponent[]}.
* @param c the array of {@link BaseComponent}.
* @return a new {@link FormattableChat}.
*/
public static FormattableChat 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.BungeeDailyLogRotateFileHandler;
import fr.pandacube.lib.bungee.util.PluginMessagePassthrough;
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);
PluginMessagePassThrough.init(plugin);
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,12 +6,40 @@ import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* Class that holds the configuration variables 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;
/**
* 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;
/**
* Creates 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

@@ -5,8 +5,15 @@ import fr.pandacube.lib.core.backup.BackupProcess;
import java.io.File;
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");
}

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;

View File

@@ -1,6 +1,6 @@
package fr.pandacube.lib.bungee.commands;
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;
@@ -71,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

@@ -6,7 +6,7 @@ import net.md_5.bungee.api.connection.ProxiedPlayer;
import fr.pandacube.lib.players.standalone.AbstractOffPlayer;
/**
* Represents any player on a Bungeecord proxy, either offline or online.
* Represents any player on a BungeeCord proxy, either offline or online.
*/
public interface BungeeOffPlayer extends AbstractOffPlayer {

View File

@@ -1,6 +1,6 @@
package fr.pandacube.lib.bungee.players;
import fr.pandacube.lib.chat.Chat;
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;
@@ -20,7 +20,7 @@ import net.md_5.bungee.protocol.packet.PluginMessage;
import java.util.Locale;
/**
* Represents any online player on a Bungeecord proxy.
* Represents any online player on a BungeeCord proxy.
*/
public interface BungeeOnlinePlayer extends BungeeOffPlayer, AbstractOnlinePlayer {
@@ -84,13 +84,13 @@ public interface BungeeOnlinePlayer extends BungeeOffPlayer, AbstractOnlinePlaye
@Override
default void sendMessage(Component message) {
getBungeeProxiedPlayer().sendMessage(Chat.toBungee(message));
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

@@ -1,6 +1,6 @@
package fr.pandacube.lib.bungee.util;
import fr.pandacube.lib.util.logs.DailyLogRotateFileHandler;
import fr.pandacube.lib.util.log.DailyLogRotateFileHandler;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.log.ConciseFormatter;

View File

@@ -19,16 +19,16 @@ import net.md_5.bungee.event.EventHandler;
* <p>
* Usage example, in your plugin init code:
* <pre>{@code
* PluginMessagePassthrough.init(yourPluginInstance);
* PluginMessagePassthrough.register("worldedit:cui"); // plugin message used by WorldEdit
* PluginMessagePassThrough.init(yourPluginInstance);
* PluginMessagePassThrough.register("worldedit:cui"); // plugin message used by WorldEdit
* }</pre>
*/
public class PluginMessagePassthrough implements Listener {
public class PluginMessagePassThrough implements Listener {
private static final List<String> channels = Collections.synchronizedList(new ArrayList<>());
private static final PluginMessagePassthrough instance = new PluginMessagePassthrough();
private static final PluginMessagePassThrough instance = new PluginMessagePassThrough();
/**
* Initialize the {@link PluginMessagePassthrough}.
* Initialize the {@link PluginMessagePassThrough}.
* It registers the required event listener.
* @param plugin the plugin instance.
*/
@@ -92,7 +92,7 @@ public class PluginMessagePassthrough implements Listener {
}
private PluginMessagePassthrough() { }
private PluginMessagePassThrough() { }
/**

View File

@@ -26,26 +26,35 @@
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<version>4.14.0</version>
<version>4.15.0</version>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-platform-bungeecord</artifactId>
<version>4.3.0</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.14.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>${gson.version}</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,13 +15,18 @@ 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.
* <p>
@@ -33,8 +35,8 @@ import org.jetbrains.annotations.NotNull;
* This class implements {@link ComponentLike} and {@link HoverEventSource} so they can be used directly in
* 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 formatting of the
* built component. The rationale for this design is explained in the documentation of {@link FormatableChat}.
* The unique possible concrete subclass of this class, {@link FormattableChat}, takes care of the formatting of the
* built component. The rationale for this design is explained in the documentation of {@link FormattableChat}.
*/
public abstract sealed class Chat extends ChatStatic implements HoverEventSource<Component>, ComponentLike {
@@ -63,26 +65,10 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
* Builds the component into Adventure Component instance.
* @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} built from this {@link Chat} component.
*/
public BaseComponent get() {
return toBungee(getAdv());
}
/**
* Builds the component into BungeeCord {@link BaseComponent} array.
* @return the {@link BaseComponent} array built from this {@link Chat} component.
*/
public BaseComponent[] getAsArray() {
return toBungeeArray(getAdv());
}
private static final LegacyComponentSerializer LEGACY_SERIALIZER_BUNGEE_FRIENDLY = LegacyComponentSerializer.builder()
.hexColors()
.useUnusualXRepeatedCharacterHexFormat()
@@ -93,7 +79,7 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
* @return the legacy text. RGB colors are in BungeeCord format.
*/
public String getLegacyText() {
return LEGACY_SERIALIZER_BUNGEE_FRIENDLY.serialize(getAdv());
return LEGACY_SERIALIZER_BUNGEE_FRIENDLY.serialize(get());
}
/**
@@ -101,12 +87,12 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
* @return the plain text of this component.
*/
public String getPlainText() {
return PlainTextComponentSerializer.plainText().serializeOr(getAdv(), "");
return PlainTextComponentSerializer.plainText().serializeOr(get(), "");
}
@Override
public @NotNull HoverEvent<Component> asHoverEvent(@NotNull UnaryOperator<Component> op) {
return HoverEvent.showText(op.apply(getAdv()));
return HoverEvent.showText(op.apply(get()));
}
/**
@@ -115,7 +101,32 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
*/
@Override
public @NotNull Component asComponent() {
return getAdv();
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());
}
@@ -155,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.
@@ -178,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));
}
@@ -261,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.
@@ -270,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.
@@ -289,7 +296,7 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
* @param key the keybinding to display.
* @return this.
*/
public Chat thenKeyBind(String key) { return then(keybind(key)); }
public Chat thenKeyBind(String key) { return then(keyBind(key)); }
/**
* Appends a component with the provided score name and objective.
@@ -447,69 +454,34 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
* 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 chat line with the configured decoration character
* @return a new {@link FormattableChat} 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 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 chat line 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 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 chat line with the configured decoration character
* @return a new {@link FormattableChat} 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 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 chat line 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 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 chat line with the configured decoration character
* @return a new {@link FormattableChat} filling a chat line with the configured decoration character
* and color and a centered text.
*/
public Chat thenCenterText(ComponentLike centerText) {
return then(centerText(centerText, console));
}
/**
* 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 chat line 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 chat line with the configured decoration character and color.
* @return a new {@link FormatableChat} filling a chat line with a decoration character and color.
* @return a new {@link FormattableChat} filling a chat line with a decoration character and color.
*/
public Chat thenFilledLine() { return then(filledLine(console)); }
@@ -548,10 +520,10 @@ 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, cast
* When calling {@link #then(Component) #then(...)} on a {@link FormattableChat}, 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.
* class which only subclass is {@link FormattableChat}, you can cast the builder, and use the format methods again.
* <pre>{@code
* Chat component = Chat.text("Hello ").red()
* .then(Chat.text("world").darkRed().bold())
@@ -560,8 +532,8 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
* ((FormatableChat)component).underlined(); // this will not format only the last appended text.
* }</pre>
*/
public static final class FormatableChat extends Chat {
/* package */ FormatableChat(ComponentBuilder<?, ?> c) {
public static final class FormattableChat extends Chat {
/* package */ FormattableChat(ComponentBuilder<?, ?> c) {
super(c);
}
@@ -571,177 +543,180 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
* @param c true for console, false for game UI.
* @return this.
*/
public FormatableChat console(boolean c) { console = c; return this; }
public FormattableChat console(boolean c) { console = c; return this; }
/**
* Configure the width of the line.
* @param w the width to consider when rendering the line. In pixel for game UI rendering, n character for
* console rendering.
* @return this.
*/
public FormatableChat maxWidth(int w) { maxWidth = w; return this; }
public FormattableChat maxWidth(int w) { maxWidth = w; return this; }
/**
* Sets the color of this component.
* @param c the color.
* @return this.
*/
public FormatableChat color(TextColor c) { builder.color(c); return this; }
public FormattableChat 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())); }
public FormattableChat color(Color c) { return color(c == null ? null : TextColor.color(c.getRGB())); }
/**
* Sets the color of this component.
* @param c the color.
* @return this.
*/
public FormatableChat color(Color c) { return color(c == null ? null : TextColor.color(c.getRGB())); }
/**
* Sets the color of this component.
* @param c the color.
* @return this.
*/
public FormatableChat color(String c) { return color(c == null ? null : ChatColor.of(c)); }
public FormattableChat 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);
}
/**
* Sets the color of this component to {@link NamedTextColor#BLACK}.
* @return this.
*/
public FormatableChat black() { return color(NamedTextColor.BLACK); }
public FormattableChat black() { return color(NamedTextColor.BLACK); }
/**
* Sets the color of this component to {@link NamedTextColor#DARK_BLUE}.
* @return this.
*/
public FormatableChat darkBlue() { return color(NamedTextColor.DARK_BLUE); }
public FormattableChat darkBlue() { return color(NamedTextColor.DARK_BLUE); }
/**
* Sets the color of this component to {@link NamedTextColor#DARK_GREEN}.
* @return this.
*/
public FormatableChat darkGreen() { return color(NamedTextColor.DARK_GREEN); }
public FormattableChat darkGreen() { return color(NamedTextColor.DARK_GREEN); }
/**
* Sets the color of this component to {@link NamedTextColor#DARK_AQUA}.
* @return this.
*/
public FormatableChat darkAqua() { return color(NamedTextColor.DARK_AQUA); }
public FormattableChat darkAqua() { return color(NamedTextColor.DARK_AQUA); }
/**
* Sets the color of this component to {@link NamedTextColor#DARK_RED}.
* @return this.
*/
public FormatableChat darkRed() { return color(NamedTextColor.DARK_RED); }
public FormattableChat darkRed() { return color(NamedTextColor.DARK_RED); }
/**
* Sets the color of this component to {@link NamedTextColor#DARK_PURPLE}.
* @return this.
*/
public FormatableChat darkPurple() { return color(NamedTextColor.DARK_PURPLE); }
public FormattableChat darkPurple() { return color(NamedTextColor.DARK_PURPLE); }
/**
* Sets the color of this component to {@link NamedTextColor#GOLD}.
* @return this.
*/
public FormatableChat gold() { return color(NamedTextColor.GOLD); }
public FormattableChat gold() { return color(NamedTextColor.GOLD); }
/**
* Sets the color of this component to {@link NamedTextColor#GRAY}.
* @return this.
*/
public FormatableChat gray() { return color(NamedTextColor.GRAY); }
public FormattableChat gray() { return color(NamedTextColor.GRAY); }
/**
* Sets the color of this component to {@link NamedTextColor#DARK_GRAY}.
* @return this.
*/
public FormatableChat darkGray() { return color(NamedTextColor.DARK_GRAY); }
public FormattableChat darkGray() { return color(NamedTextColor.DARK_GRAY); }
/**
* Sets the color of this component to {@link NamedTextColor#BLUE}.
* @return this.
*/
public FormatableChat blue() { return color(NamedTextColor.BLUE); }
public FormattableChat blue() { return color(NamedTextColor.BLUE); }
/**
* Sets the color of this component to {@link NamedTextColor#GREEN}.
* @return this.
*/
public FormatableChat green() { return color(NamedTextColor.GREEN); }
public FormattableChat green() { return color(NamedTextColor.GREEN); }
/**
* Sets the color of this component to {@link NamedTextColor#AQUA}.
* @return this.
*/
public FormatableChat aqua() { return color(NamedTextColor.AQUA); }
public FormattableChat aqua() { return color(NamedTextColor.AQUA); }
/**
* Sets the color of this component to {@link NamedTextColor#RED}.
* @return this.
*/
public FormatableChat red() { return color(NamedTextColor.RED); }
public FormattableChat red() { return color(NamedTextColor.RED); }
/**
* Sets the color of this component to {@link NamedTextColor#LIGHT_PURPLE}.
* @return this.
*/
public FormatableChat lightPurple() { return color(NamedTextColor.LIGHT_PURPLE); }
public FormattableChat lightPurple() { return color(NamedTextColor.LIGHT_PURPLE); }
/**
* Sets the color of this component to {@link NamedTextColor#YELLOW}.
* @return this.
*/
public FormatableChat yellow() { return color(NamedTextColor.YELLOW); }
public FormattableChat yellow() { return color(NamedTextColor.YELLOW); }
/**
* Sets the color of this component to {@link NamedTextColor#WHITE}.
* @return this.
*/
public FormatableChat white() { return color(NamedTextColor.WHITE); }
public FormattableChat white() { return color(NamedTextColor.WHITE); }
/**
* Sets the color of this component to {@link ChatConfig#successColor}.
* @return this.
*/
public FormatableChat successColor() { return color(ChatConfig.successColor); }
public FormattableChat successColor() { return color(ChatConfig.successColor); }
/**
* Sets the color of this component to {@link ChatConfig#failureColor}.
* @return this.
*/
public FormatableChat failureColor() { return color(ChatConfig.failureColor); }
public FormattableChat failureColor() { return color(ChatConfig.failureColor); }
/**
* Sets the color of this component to {@link ChatConfig#infoColor}.
* @return this.
*/
public FormatableChat infoColor() { return color(ChatConfig.infoColor); }
public FormattableChat infoColor() { return color(ChatConfig.infoColor); }
/**
* Sets the color of this component to {@link ChatConfig#warningColor}.
* @return this.
*/
public FormatableChat warningColor() { return color(ChatConfig.warningColor); }
public FormattableChat warningColor() { return color(ChatConfig.warningColor); }
/**
* Sets the color of this component to {@link ChatConfig#dataColor}.
* @return this.
*/
public FormatableChat dataColor() { return color(ChatConfig.dataColor); }
public FormattableChat dataColor() { return color(ChatConfig.dataColor); }
/**
* Sets the color of this component to {@link ChatConfig#decorationColor}.
* @return this.
*/
public FormatableChat decorationColor() { return color(ChatConfig.decorationColor); }
public FormattableChat decorationColor() { return color(ChatConfig.decorationColor); }
/**
* Sets the color of this component to {@link ChatConfig#urlColor}.
* @return this.
*/
public FormatableChat urlColor() { return color(ChatConfig.urlColor); }
public FormattableChat urlColor() { return color(ChatConfig.urlColor); }
/**
* Sets the color of this component to {@link ChatConfig#commandColor}.
* @return this.
*/
public FormatableChat commandColor() { return color(ChatConfig.commandColor); }
public FormattableChat commandColor() { return color(ChatConfig.commandColor); }
/**
* Sets the color of this component to {@link ChatConfig#highlightedCommandColor}.
* @return this.
*/
public FormatableChat highlightedCommandColor() { return color(ChatConfig.highlightedCommandColor); }
public FormattableChat highlightedCommandColor() { return color(ChatConfig.highlightedCommandColor); }
/**
* Sets the color of this component to {@link ChatConfig#broadcastColor}.
* @return this.
*/
public FormatableChat broadcastColor() { return color(ChatConfig.broadcastColor); }
public FormattableChat broadcastColor() { return color(ChatConfig.broadcastColor); }
private FormatableChat setStyle(Consumer<Style.Builder> styleOp) { builder.style(styleOp); return this; }
private FormatableChat setDecoration(TextDecoration deco, Boolean state) {
private FormattableChat setStyle(Consumer<Style.Builder> styleOp) { builder.style(styleOp); return this; }
private FormattableChat setDecoration(TextDecoration deco, Boolean state) {
return setStyle(b -> b.decoration(deco, State.byBoolean(state)));
}
@@ -751,56 +726,56 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
* @param b true to enable, false to disable, or null to inherit from parent.
* @return this.
*/
public FormatableChat bold(Boolean b) { return setDecoration(TextDecoration.BOLD, b); }
public FormattableChat bold(Boolean b) { return setDecoration(TextDecoration.BOLD, b); }
/**
* Enables the bold status of this component.
* @return this.
*/
public FormatableChat bold() { return bold(true); }
public FormattableChat bold() { return bold(true); }
/**
* Sets the italic status of this component.
* @param i true to enable, false to disable, or null to inherit from parent.
* @return this.
*/
public FormatableChat italic(Boolean i) { return setDecoration(TextDecoration.ITALIC, i); }
public FormattableChat italic(Boolean i) { return setDecoration(TextDecoration.ITALIC, i); }
/**
* Enables the italic status of this component.
* @return this.
*/
public FormatableChat italic() { return italic(true); }
public FormattableChat italic() { return italic(true); }
/**
* Sets the underlined status of this component.
* @param u true to enable, false to disable, or null to inherit from parent.
* @return this.
*/
public FormatableChat underlined(Boolean u) { return setDecoration(TextDecoration.UNDERLINED, u); }
public FormattableChat underlined(Boolean u) { return setDecoration(TextDecoration.UNDERLINED, u); }
/**
* Enables the underlined status of this component.
* @return this.
*/
public FormatableChat underlined() { return underlined(true); }
public FormattableChat underlined() { return underlined(true); }
/**
* Sets the strikethrough status of this component.
* @param s true to enable, false to disable, or null to inherit from parent.
* @return this.
*/
public FormatableChat strikethrough(Boolean s) { return setDecoration(TextDecoration.STRIKETHROUGH, s); }
public FormattableChat strikethrough(Boolean s) { return setDecoration(TextDecoration.STRIKETHROUGH, s); }
/**
* Enables the strikethrough status of this component.
* @return this.
*/
public FormatableChat strikethrough() { return strikethrough(true); }
public FormattableChat strikethrough() { return strikethrough(true); }
/**
* Sets the obfuscated status of this component.
* @param o true to enable, false to disable, or null to inherit from parent.
* @return this.
*/
public FormatableChat obfuscated(Boolean o) { return setDecoration(TextDecoration.OBFUSCATED, o); }
public FormattableChat obfuscated(Boolean o) { return setDecoration(TextDecoration.OBFUSCATED, o); }
/**
* Enables the obfuscated status of this component.
* @return this.
*/
public FormatableChat obfuscated() { return obfuscated(true); }
public FormattableChat obfuscated() { return obfuscated(true); }
/**
@@ -808,7 +783,7 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
* @param f the font namespaced key.
* @return this.
*/
public FormatableChat font(Key f) { return setStyle(s -> s.font(f)); }
public FormattableChat font(Key f) { return setStyle(s -> s.font(f)); }
/**
@@ -816,7 +791,7 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
* @param i the text to insert.
* @return this.
*/
public FormatableChat shiftClickInsertion(String i) { builder.insertion(i); return this; }
public FormattableChat shiftClickInsertion(String i) { builder.insertion(i); return this; }
/**
@@ -824,37 +799,37 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
* @param e the {@link ClickEvent}.
* @return this.
*/
private FormatableChat click(ClickEvent e) { builder.clickEvent(e); return this; }
private FormattableChat click(ClickEvent e) { builder.clickEvent(e); return this; }
/**
* Configure this component to execute the specified command when clicked.
* @param cmdWithSlash the command to execute.
* @return this.
*/
public FormatableChat clickCommand(String cmdWithSlash) { return click(ClickEvent.runCommand(cmdWithSlash)); }
public FormattableChat clickCommand(String cmdWithSlash) { return click(ClickEvent.runCommand(cmdWithSlash)); }
/**
* Configure this component to insert in the chat-box the specified command when clicked.
* @param cmdWithSlash the command to suggest.
* @return this.
*/
public FormatableChat clickSuggest(String cmdWithSlash) { return click(ClickEvent.suggestCommand(cmdWithSlash)); }
public FormattableChat clickSuggest(String cmdWithSlash) { return click(ClickEvent.suggestCommand(cmdWithSlash)); }
/**
* Configure this component to copy into clipboard the specified text when clicked.
* @param value the text to copy.
* @return this.
*/
public FormatableChat clickClipboard(String value) { return click(ClickEvent.copyToClipboard(value)); }
public FormattableChat clickClipboard(String value) { return click(ClickEvent.copyToClipboard(value)); }
/**
* Configure this component to open the specified URL when clicked.
* @param url the URL to open.
* @return this.
*/
public FormatableChat clickURL(String url) { return click(ClickEvent.openUrl(url)); }
public FormattableChat clickURL(String url) { return click(ClickEvent.openUrl(url)); }
/**
* Configure this component to change the page of the opened book when clicked.
* @param page the page to go to.
* @return this.
*/
public FormatableChat clickBookPage(int page) { return click(ClickEvent.changePage(page)); }
public FormattableChat clickBookPage(int page) { return click(ClickEvent.changePage(page)); }
/**
@@ -862,43 +837,31 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
* @param e the {@link HoverEventSource}.
* @return this.
*/
public FormatableChat hover(HoverEventSource<?> e) { builder.hoverEvent(e); return this; }
public FormattableChat hover(HoverEventSource<?> e) { builder.hoverEvent(e); return this; }
/**
* Configure this component to show the provided component when hovered.
* @param v the component to show.
* @return this.
*/
public FormatableChat hover(Component v) { return hover((HoverEventSource<Component>) v); }
public FormattableChat hover(Component v) { return hover((HoverEventSource<Component>) v); }
/**
* Configure this component to show the provided component when hovered.
* @param v the component to show.
* @return this.
*/
public FormatableChat hover(Chat v) { return hover((HoverEventSource<Component>) v); }
public FormattableChat hover(Chat v) { return hover((HoverEventSource<Component>) v); }
/**
* Configure this component to show the provided component when hovered.
* @param v the component to show.
* @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)); }
public FormattableChat hover(ComponentLike v) { return hover(v.asComponent()); }
/**
* Configure this component to show the provided legacy text when hovered.
* @param legacyText the legacy text to show.
* @return this.
*/
public FormatableChat hover(String legacyText) { return hover(legacyText(legacyText)); }
public FormattableChat hover(String legacyText) { return hover(legacyText(legacyText)); }
}
@@ -920,7 +883,7 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
@Override
public int hashCode() {
return getAdv().hashCode();
return get().hashCode();
}
@Override
@@ -931,71 +894,52 @@ 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 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.
* This is useful for item lore 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.
*/
public static Chat italicFalseIfNotSet(Chat c) {
c.builder.style(b -> {
if (b.build().decoration(TextDecoration.ITALIC) == State.NOT_SET) {
((FormatableChat) c).italic(false);
((FormattableChat) c).italic(false);
}
});
return c;

View File

@@ -10,6 +10,7 @@ import org.jetbrains.annotations.NotNull;
* A custom gradient with at least 2 colors in it.
*/
public class ChatColorGradient {
private record GradientColor(
float location,
TextColor color
@@ -22,6 +23,11 @@ public class ChatColorGradient {
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.
@@ -43,7 +49,7 @@ public class ChatColorGradient {
if (colors.isEmpty())
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();
int i = 0;
for (; i < colors.size(); i++) {
@@ -54,7 +60,7 @@ public class ChatColorGradient {
if (i == 0)
return colors.get(i).color();
if (i == colors.size())
return colors.get(colors.size() - 1).color();
return colors.getLast().color();
int p = i - 1;
float pLoc = colors.get(p).location();

View File

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

@@ -50,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.
@@ -87,7 +87,7 @@ 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);
}
@@ -157,5 +157,9 @@ public class ChatConfig {
.thenText("] ");
}
private PandaTheme() {}
}
private ChatConfig() {}
}

View File

@@ -5,7 +5,7 @@ import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.format.TextColor;
import org.jetbrains.annotations.NotNull;
import fr.pandacube.lib.chat.Chat.FormatableChat;
import fr.pandacube.lib.chat.Chat.FormattableChat;
/**
* Builder for a {@link Chat} component for filling a chat line, with decoration and eventual aligned text.
@@ -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.
@@ -139,10 +150,10 @@ public class ChatFilledLine implements ComponentLike {
/**
* Renders this line to a {@link FormatableChat}.
* @return a new {@link FormatableChat} built by this {@link ChatFilledLine}.
* Renders this line to a {@link FormattableChat}.
* @return a new {@link FormattableChat} built by this {@link ChatFilledLine}.
*/
public FormatableChat toChat() {
public FormattableChat toChat() {
int maxWidth = (this.maxWidth != null)
? this.maxWidth
: console ? ChatUtil.CONSOLE_NB_CHAR_DEFAULT : ChatUtil.DEFAULT_CHAT_WIDTH;
@@ -159,7 +170,7 @@ public class ChatFilledLine implements ComponentLike {
int textWidth = ChatUtil.componentWidth(text.asComponent(), console);
if (textWidth > maxWidth)
return (FormatableChat) text;
return (FormattableChat) text;
int repeatedCharWidth = ChatUtil.charW(decorationChar, console, decorationBold);
int nbCharLeft = 0, nbCharRight = 0;
@@ -168,12 +179,12 @@ public class ChatFilledLine implements ComponentLike {
case CENTER -> {
nbCharLeft = nbCharRight = (maxWidth - textWidth) / 2 / repeatedCharWidth;
if (nbCharLeft == 0)
return (FormatableChat) text;
return (FormattableChat) text;
}
case LEFT, RIGHT -> {
int remWidth = textWidth + nbSide * repeatedCharWidth;
if (remWidth > maxWidth)
return (FormatableChat) text;
return (FormattableChat) text;
boolean left = alignment == Alignment.LEFT;
int nbOtherSide = (maxWidth - remWidth) / repeatedCharWidth;
nbCharLeft = left ? nbSide : nbOtherSide;
@@ -184,9 +195,9 @@ 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;
return (FormattableChat) d;
}

View File

@@ -1,7 +1,6 @@
package fr.pandacube.lib.chat;
import java.util.Objects;
import fr.pandacube.lib.chat.Chat.FormattableChat;
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.
@@ -28,45 +27,27 @@ public abstract class ChatStatic {
private static FormatableChat chatComponent(Component c) {
return new FormatableChat(componentToBuilder(c));
private static FormattableChat chatComponent(Component c) {
return new FormattableChat(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}.
* Creates a {@link FormattableChat} from the provided {@link ComponentLike}.
* If the provided component is an instance of {@link Chat}, its content will be duplicated, and the provided one
* will be untouched.
* @param c the {@link ComponentLike}.
* @return a new {@link FormatableChat}.
* @return a new {@link FormattableChat}.
*/
public static FormatableChat chatComponent(ComponentLike c) {
public static FormattableChat chatComponent(ComponentLike c) {
return chatComponent(c.asComponent());
}
/**
* Creates a {@link FormatableChat} with an empty main text content.
* @return a new empty {@link FormatableChat}.
* Creates a {@link FormattableChat} with an empty main text content.
* @return a new empty {@link FormattableChat}.
*/
public static FormatableChat chat() {
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));
public static FormattableChat chat() {
return new FormattableChat(Component.text());
}
@@ -75,140 +56,180 @@ public abstract class ChatStatic {
/**
* Creates a {@link FormatableChat} with the provided plain text as its main text content.
* Creates a {@link FormattableChat} with the provided plain text as its main text content.
* @param plainText the text to use as the content.
* @return a new {@link FormatableChat} with the provided text as its main text content.
* @return a new {@link FormattableChat} with the provided text as its main text content.
* @throws IllegalArgumentException if the {@code plainText} parameter is instance of {@link Chat} or
* {@link Component}. The caller should use {@link #chatComponent(ComponentLike)}
* instead.
*/
public static FormatableChat text(Object plainText) {
public static FormattableChat text(Object plainText) {
if (plainText instanceof ComponentLike) {
throw new IllegalArgumentException("Expected any object except instance of " + ComponentLike.class + ". Received " + plainText + ". Please use ChatStatic.chatComponent(ComponentLike) instead.");
}
return new FormatableChat(Component.text().content(Objects.toString(plainText)));
return new FormattableChat(Component.text().content(Objects.toString(plainText)));
}
/**
* Creates a {@link FormatableChat} with the provided legacy text as its content.
* @param legacyText the legacy text to use as the content.
* @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
* Creates a {@link FormattableChat} 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 FormattableChat} 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 legacyText(Object legacyText) {
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)));
public static FormattableChat legacyText(Object legacyText) {
return legacyText(legacyText, LegacyComponentSerializer.SECTION_CHAR);
}
/**
* Creates a {@link FormatableChat} with the provided plain text as its main text content, and colored using the
* Creates a {@link FormattableChat} 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 FormattableChat} 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 FormattableChat legacyAmpersandText(Object legacyText) {
return legacyText(legacyText, LegacyComponentSerializer.AMPERSAND_CHAR);
}
/**
* Creates a {@link FormattableChat} 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 FormattableChat} 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 FormattableChat 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.legacy(legacyCharacter).deserialize(Objects.toString(legacyText)));
}
/**
* Creates a {@link FormattableChat} with the provided MiniMessage text as its content.
* @param miniMessageText the MiniMessage text to use as the content.
* @return a new {@link FormattableChat} with the provided text as its content.
*/
public static FormattableChat miniMessageText(String miniMessageText) {
return chatComponent(MiniMessage.miniMessage().deserialize(miniMessageText));
}
/**
* Creates a {@link FormattableChat} with the provided plain text as its main text content, and colored using the
* {@link ChatConfig#infoColor configured info color}.
* @param plainText the text to use as the content.
* @return a new {@link FormatableChat} with the provided text as its main text content, and the configured color.
* @return a new {@link FormattableChat} with the provided text as its main text content, and the configured color.
* @throws IllegalArgumentException if the {@code plainText} parameter is instance of {@link Chat} or
* {@link Component}. The caller should use {@link #chatComponent(ComponentLike)} and
* {@link FormatableChat#infoColor()} instead.
* {@link FormattableChat#infoColor()} instead.
*/
public static FormatableChat infoText(Object plainText) {
public static FormattableChat infoText(Object plainText) {
return text(plainText).infoColor();
}
/**
* Creates a {@link FormatableChat} with the provided plain text as its main text content, and colored using the
* Creates a {@link FormattableChat} with the provided plain text as its main text content, and colored using the
* {@link ChatConfig#warningColor configured warning color}.
* @param plainText the text to use as the content.
* @return a new {@link FormatableChat} with the provided text as its main text content, and the configured color.
* @return a new {@link FormattableChat} with the provided text as its main text content, and the configured color.
* @throws IllegalArgumentException if the {@code plainText} parameter is instance of {@link Chat} or
* {@link Component}. The caller should use {@link #chatComponent(ComponentLike)} and
* {@link FormatableChat#warningColor()} instead.
* {@link FormattableChat#warningColor()} instead.
*/
public static FormatableChat warningText(Object plainText) {
public static FormattableChat warningText(Object plainText) {
return text(plainText).warningColor();
}
/**
* Creates a {@link FormatableChat} with the provided plain text as its main text content, and colored using the
* Creates a {@link FormattableChat} with the provided plain text as its main text content, and colored using the
* {@link ChatConfig#dataColor configured data color}.
* @param plainText the text to use as the content.
* @return a new {@link FormatableChat} with the provided text as its main text content, and the configured color.
* @return a new {@link FormattableChat} with the provided text as its main text content, and the configured color.
* @throws IllegalArgumentException if the {@code plainText} parameter is instance of {@link Chat} or
* {@link Component}. The caller should use {@link #chatComponent(ComponentLike)} and
* {@link FormatableChat#dataColor()} instead.
* {@link FormattableChat#dataColor()} instead.
*/
public static FormatableChat dataText(Object plainText) {
public static FormattableChat dataText(Object plainText) {
return text(plainText).dataColor();
}
/**
* Creates a {@link FormatableChat} with the provided plain text as its main text content, and colored using the
* Creates a {@link FormattableChat} with the provided plain text as its main text content, and colored using the
* {@link ChatConfig#decorationColor configured decorationColor color}.
* @param plainText the text to use as the content.
* @return a new {@link FormatableChat} with the provided text as its main text content, and the configured color.
* @return a new {@link FormattableChat} with the provided text as its main text content, and the configured color.
* @throws IllegalArgumentException if the {@code plainText} parameter is instance of {@link Chat} or
* {@link Component}. The caller should use {@link #chatComponent(ComponentLike)} and
* {@link FormatableChat#decorationColor()} instead.
* {@link FormattableChat#decorationColor()} instead.
*/
public static FormatableChat decorationText(Object plainText) {
public static FormattableChat decorationText(Object plainText) {
return text(plainText).decorationColor();
}
/**
* Creates a {@link FormatableChat} with the provided plain text as its main text content, and colored using the
* Creates a {@link FormattableChat} with the provided plain text as its main text content, and colored using the
* {@link ChatConfig#successColor configured success color}.
* @param plainText the text to use as the content.
* @return a new {@link FormatableChat} with the provided text as its main text content, and the configured color.
* @return a new {@link FormattableChat} with the provided text as its main text content, and the configured color.
* @throws IllegalArgumentException if the {@code plainText} parameter is instance of {@link Chat} or
* {@link Component}. The caller should use {@link #chatComponent(ComponentLike)} and
* {@link FormatableChat#successColor()} instead.
* {@link FormattableChat#successColor()} instead.
*/
public static FormatableChat successText(Object plainText) {
public static FormattableChat successText(Object plainText) {
return text(plainText).successColor();
}
/**
* Creates a {@link FormatableChat} with the provided plain text as its main text content, and colored using the
* Creates a {@link FormattableChat} with the provided plain text as its main text content, and colored using the
* {@link ChatConfig#failureColor configured failure color}.
* @param plainText the text to use as the content.
* @return a new {@link FormatableChat} with the provided text as its main text content, and the configured color.
* @return a new {@link FormattableChat} with the provided text as its main text content, and the configured color.
* @throws IllegalArgumentException if the {@code plainText} parameter is instance of {@link Chat} or
* {@link Component}. The caller should use {@link #chatComponent(ComponentLike)} and
* {@link FormatableChat#failureColor()} instead.
* {@link FormattableChat#failureColor()} instead.
*/
public static FormatableChat failureText(Object plainText) {
public static FormattableChat failureText(Object plainText) {
return text(plainText).failureColor();
}
/**
* Creates a {@link FormatableChat} with the provided legacy text as its main text content, and colored in white in
* Creates a {@link FormattableChat} with the provided legacy text as its main text content, and colored in white in
* case there is no color on the generated parent component.
* @param legacyText the legacy text to use as the content.
* @return a new {@link FormatableChat} with the provided text as its main text content, and the configured color.
* @return a new {@link FormattableChat} with the provided text as its main text content, and the configured color.
* @throws IllegalArgumentException if the {@code plainText} parameter is instance of {@link Chat} or
* {@link Component}. The caller should use {@link #chatComponent(ComponentLike)} and
* {@link FormatableChat#failureColor()} instead.
* {@link FormattableChat#failureColor()} instead.
*/
public static FormatableChat playerNameText(String legacyText) {
FormatableChat fc = legacyText(legacyText);
public static FormattableChat playerNameText(String legacyText) {
FormattableChat fc = legacyText(legacyText);
fc.builder.colorIfAbsent(NamedTextColor.WHITE);
return fc;
}
/**
* Creates a {@link FormatableChat} from the provided {@link Component}, coloring in white the generated parent
* Creates a {@link FormattableChat} from the provided {@link Component}, coloring in white the generated parent
* component in case there is no color defined.
* If the provided component is an instance of {@link Chat}, its content will be duplicated, and the provided one
* will be untouched.
* @param c the {@link Component}.
* @return a new {@link FormatableChat}.
* @return a new {@link FormattableChat}.
*/
public static FormatableChat playerNameComponent(Component c) {
FormatableChat fc = chatComponent(c);
public static FormattableChat playerNameComponent(ComponentLike c) {
FormattableChat fc = chatComponent(c);
fc.builder.colorIfAbsent(NamedTextColor.WHITE);
return fc;
}
@@ -217,32 +238,32 @@ public abstract class ChatStatic {
/**
* Creates a {@link FormatableChat} with the provided translation key and parameters.
* Creates a {@link FormattableChat} with the provided translation key and parameters.
* @param key the translation key.
* @param with the translation parameters.
* @return a new {@link FormatableChat} with the provided translation key and parameters.
* @return a new {@link FormattableChat} 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)));
public static FormattableChat translation(String key, Object... with) {
return new FormattableChat(Component.translatable().key(key).arguments(Chat.filterObjToTranslationArgumentLike(with)));
}
/**
* Creates a {@link FormatableChat} with the provided keybinding.
* Creates a {@link FormattableChat} with the provided keybinding.
* @param key the keybinding to display.
* @return a new {@link FormatableChat} with the provided keybinding.
* @return a new {@link FormattableChat} with the provided keybinding.
*/
public static FormatableChat keybind(String key) {
return new FormatableChat(Component.keybind().keybind(key));
public static FormattableChat keyBind(String key) {
return new FormattableChat(Component.keybind().keybind(key));
}
/**
* Creates a {@link FormatableChat} with the provided score name and objective.
* Creates a {@link FormattableChat} with the provided score name and objective.
* @param name the score name.
* @param objective the score objective.
* @return a new {@link FormatableChat} with the provided score name and objective.
* @return a new {@link FormattableChat} with the provided score name and objective.
*/
public static FormatableChat score(String name, String objective) {
return new FormatableChat(Component.score().name(name).objective(objective));
public static FormattableChat score(String name, String objective) {
return new FormattableChat(Component.score().name(name).objective(objective));
}
@@ -251,49 +272,49 @@ public abstract class ChatStatic {
/**
* Creates a {@link FormatableChat} that leads to a URL when clicked.
* Creates a {@link FormattableChat} that leads to a URL when clicked.
* @param inner the component to make clickable.
* @param url the target url. Must start with {@code "http://"} or {@code "https://"}.
* @param hover the content to display when hovering the component.
* @return a new {@link FormatableChat} that leads to a URL when clicked.
* @return a new {@link FormattableChat} that leads to a URL when clicked.
*/
public static FormatableChat clickableURL(ComponentLike inner, String url, HoverEventSource<?> hover) {
public static FormattableChat clickableURL(ComponentLike inner, String url, HoverEventSource<?> hover) {
Objects.requireNonNull(url, "url");
if (inner == null)
inner = text(url);
if (hover == null)
hover = text(ChatUtil.wrapInLimitedPixels(url, 240));
return (FormatableChat) chat().clickURL(url).urlColor().hover(hover).then(inner);
return (FormattableChat) chat().clickURL(url).urlColor().hover(hover).then(inner);
}
/**
* Creates a {@link FormatableChat} that leads to a URL when clicked.
* Creates a {@link FormattableChat} that leads to a URL when clicked.
* <p>
* When hovered, the component will display the url. To customize the hover content, use
* {@link #clickableURL(ComponentLike, String, HoverEventSource)}.
* @param inner the component to make clickable.
* @param url the target url. Must start with {@code "http://"} or {@code "https://"}.
* @return a new {@link FormatableChat} that leads to a URL when clicked.
* @return a new {@link FormattableChat} that leads to a URL when clicked.
*/
public static FormatableChat clickableURL(ComponentLike inner, String url) {
public static FormattableChat clickableURL(ComponentLike inner, String url) {
return clickableURL(inner, url, null);
}
/**
* Creates a {@link FormatableChat} that leads to a URL when clicked.
* Creates a {@link FormattableChat} that leads to a URL when clicked.
* <p>
* The text on which to click will be the URL itself. To configure the clicked text, use
* {@link #clickableURL(ComponentLike, String, HoverEventSource)}.
* @param url the target url. Must start with {@code "http://"} or {@code "https://"}.
* @param hover the content to display when hovering the component.
* @return a new {@link FormatableChat} that leads to a URL when clicked.
* @return a new {@link FormattableChat} that leads to a URL when clicked.
*/
public static FormatableChat clickableURL(String url, HoverEventSource<?> hover) {
public static FormattableChat clickableURL(String url, HoverEventSource<?> hover) {
return clickableURL(null, url, hover);
}
/**
* Creates a {@link FormatableChat} that leads to a URL when clicked.
* Creates a {@link FormattableChat} that leads to a URL when clicked.
* <p>
* The text on which to click will be the URL itself. To configure the clicked text, use
* {@link #clickableURL(ComponentLike, String)}.
@@ -301,9 +322,9 @@ public abstract class ChatStatic {
* When hovered, the component will display the url. To customize the hover content, use
* {@link #clickableURL(String, HoverEventSource)}.
* @param url the target url. Must start with {@code "http://"} or {@code "https://"}.
* @return a new {@link FormatableChat} that leads to a URL when clicked.
* @return a new {@link FormattableChat} that leads to a URL when clicked.
*/
public static FormatableChat clickableURL(String url) {
public static FormattableChat clickableURL(String url) {
return clickableURL(null, url, null);
}
@@ -313,14 +334,14 @@ public abstract class ChatStatic {
/**
* Creates a {@link FormatableChat} that runs a command when clicked.
* Creates a {@link FormattableChat} that runs a command when clicked.
* @param inner the component to make clickable.
* @param commandWithSlash the command to run. Must start with {@code "/"}.
* @param hover the content to display when hovering the component.
* @return a new {@link FormatableChat} that runs a command when clicked.
* @return a new {@link FormattableChat} that runs a command when clicked.
* @throws IllegalArgumentException if {@code commandWithSlash} does not start with a {@code "/"}.
*/
public static FormatableChat clickableCommand(ComponentLike inner, String commandWithSlash, HoverEventSource<?> hover) {
public static FormattableChat clickableCommand(ComponentLike inner, String commandWithSlash, HoverEventSource<?> hover) {
Objects.requireNonNull(commandWithSlash, "commandWithSlash");
if (!commandWithSlash.startsWith("/"))
throw new IllegalArgumentException("commandWithSlash must start with a '/' character.");
@@ -328,39 +349,39 @@ public abstract class ChatStatic {
inner = text(commandWithSlash);
if (hover == null)
hover = text(ChatUtil.wrapInLimitedPixels(commandWithSlash, 240));
return (FormatableChat) chat().clickCommand(commandWithSlash).commandColor().hover(hover).then(inner);
return (FormattableChat) chat().clickCommand(commandWithSlash).commandColor().hover(hover).then(inner);
}
/**
* Creates a {@link FormatableChat} that runs a command when clicked.
* Creates a {@link FormattableChat} that runs a command when clicked.
* <p>
* When hovered, the component will display the command itself. To customize the hover content, use
* {@link #clickableCommand(ComponentLike, String, HoverEventSource)}.
* @param inner the component to make clickable.
* @param commandWithSlash the command to run. Must start with {@code "/"}.
* @return a new {@link FormatableChat} that runs a command when clicked.
* @return a new {@link FormattableChat} that runs a command when clicked.
* @throws IllegalArgumentException if {@code commandWithSlash} does not start with a {@code "/"}.
*/
public static FormatableChat clickableCommand(ComponentLike inner, String commandWithSlash) {
public static FormattableChat clickableCommand(ComponentLike inner, String commandWithSlash) {
return clickableCommand(inner, commandWithSlash, null);
}
/**
* Creates a {@link FormatableChat} that runs a command when clicked.
* Creates a {@link FormattableChat} that runs a command when clicked.
* <p>
* The text on which to click will be the command itself. To configure the clicked text, use
* {@link #clickableCommand(ComponentLike, String, HoverEventSource)}.
* @param commandWithSlash the command to run. Must start with {@code "/"}.
* @param hover the content to display when hovering the component.
* @return a new {@link FormatableChat} that runs a command when clicked.
* @return a new {@link FormattableChat} that runs a command when clicked.
* @throws IllegalArgumentException if {@code commandWithSlash} does not start with a {@code "/"}.
*/
public static FormatableChat clickableCommand(String commandWithSlash, HoverEventSource<?> hover) {
public static FormattableChat clickableCommand(String commandWithSlash, HoverEventSource<?> hover) {
return clickableCommand(null, commandWithSlash, hover);
}
/**
* Creates a {@link FormatableChat} that runs a command when clicked.
* Creates a {@link FormattableChat} that runs a command when clicked.
* <p>
* The text on which to click will be the command itself. To configure the clicked text, use
* {@link #clickableCommand(ComponentLike, String)}.
@@ -368,10 +389,10 @@ public abstract class ChatStatic {
* When hovered, the component will display the command itself. To customize the hover content, use
* {@link #clickableCommand(String, HoverEventSource)}.
* @param commandWithSlash the command to run. Must start with {@code "/"}.
* @return a new {@link FormatableChat} that runs a command when clicked.
* @return a new {@link FormattableChat} that runs a command when clicked.
* @throws IllegalArgumentException if {@code commandWithSlash} does not start with a {@code "/"}.
*/
public static FormatableChat clickableCommand(String commandWithSlash) {
public static FormattableChat clickableCommand(String commandWithSlash) {
return clickableCommand(null, commandWithSlash, null);
}
@@ -381,14 +402,14 @@ public abstract class ChatStatic {
/**
* Creates a {@link FormatableChat} that pre-fill the chat box with a command when clicked.
* Creates a {@link FormattableChat} that pre-fill the chat box with a command when clicked.
* @param inner the component to make clickable.
* @param commandWithSlash the command to suggest. Must start with {@code "/"}.
* @param hover the content to display when hovering the component.
* @return a new {@link FormatableChat} that pre-fill the chat box with a command when clicked.
* @return a new {@link FormattableChat} that pre-fill the chat box with a command when clicked.
* @throws IllegalArgumentException if {@code commandWithSlash} does not start with a {@code "/"}.
*/
public static FormatableChat clickableSuggest(ComponentLike inner, String commandWithSlash, HoverEventSource<?> hover) {
public static FormattableChat clickableSuggest(ComponentLike inner, String commandWithSlash, HoverEventSource<?> hover) {
Objects.requireNonNull(commandWithSlash, "commandWithSlash");
if (!commandWithSlash.startsWith("/"))
throw new IllegalArgumentException("commandWithSlash must start with a '/' character.");
@@ -396,39 +417,39 @@ public abstract class ChatStatic {
inner = text(commandWithSlash);
if (hover == null)
hover = text(ChatUtil.wrapInLimitedPixels(commandWithSlash, 240));
return (FormatableChat) chat().clickSuggest(commandWithSlash).commandColor().hover(hover).then(inner);
return (FormattableChat) chat().clickSuggest(commandWithSlash).commandColor().hover(hover).then(inner);
}
/**
* Creates a {@link FormatableChat} that pre-fill the chat box with a command when clicked.
* Creates a {@link FormattableChat} that pre-fill the chat box with a command when clicked.
* <p>
* When hovered, the component will display the command itself. To customize the hover content, use
* {@link #clickableSuggest(ComponentLike, String, HoverEventSource)}.
* @param inner the component to make clickable.
* @param commandWithSlash the command to suggest. Must start with {@code "/"}.
* @return a new {@link FormatableChat} that pre-fill the chat box with a command when clicked.
* @return a new {@link FormattableChat} that pre-fill the chat box with a command when clicked.
* @throws IllegalArgumentException if {@code commandWithSlash} does not start with a {@code "/"}.
*/
public static FormatableChat clickableSuggest(ComponentLike inner, String commandWithSlash) {
public static FormattableChat clickableSuggest(ComponentLike inner, String commandWithSlash) {
return clickableSuggest(inner, commandWithSlash, null);
}
/**
* Creates a {@link FormatableChat} that pre-fill the chat box with a command when clicked.
* Creates a {@link FormattableChat} that pre-fill the chat box with a command when clicked.
* <p>
* The text on which to click will be the command itself. To configure the clicked text, use
* {@link #clickableSuggest(ComponentLike, String, HoverEventSource)}.
* @param commandWithSlash the command to suggest. Must start with {@code "/"}.
* @param hover the content to display when hovering the component.
* @return a new {@link FormatableChat} that pre-fill the chat box with a command when clicked.
* @return a new {@link FormattableChat} that pre-fill the chat box with a command when clicked.
* @throws IllegalArgumentException if {@code commandWithSlash} does not start with a {@code "/"}.
*/
public static FormatableChat clickableSuggest(String commandWithSlash, HoverEventSource<?> hover) {
public static FormattableChat clickableSuggest(String commandWithSlash, HoverEventSource<?> hover) {
return clickableSuggest(null, commandWithSlash, hover);
}
/**
* Creates a {@link FormatableChat} that pre-fill the chat box with a command when clicked.
* Creates a {@link FormattableChat} that pre-fill the chat box with a command when clicked.
* <p>
* The text on which to click will be the command itself. To configure the clicked text, use
* {@link #clickableSuggest(ComponentLike, String)}.
@@ -436,10 +457,10 @@ public abstract class ChatStatic {
* When hovered, the component will display the command itself. To customize the hover content, use
* {@link #clickableSuggest(String, HoverEventSource)}.
* @param commandWithSlash the command to suggest. Must start with {@code "/"}.
* @return a new {@link FormatableChat} that pre-fill the chat box with a command when clicked.
* @return a new {@link FormattableChat} that pre-fill the chat box with a command when clicked.
* @throws IllegalArgumentException if {@code commandWithSlash} does not start with a {@code "/"}.
*/
public static FormatableChat clickableSuggest(String commandWithSlash) {
public static FormattableChat clickableSuggest(String commandWithSlash) {
return clickableSuggest(null, commandWithSlash, null);
}
@@ -451,112 +472,112 @@ public abstract class ChatStatic {
/**
* Creates a {@link FormatableChat} filling a chat line with decoration and a left-aligned text.
* Creates a {@link FormattableChat} 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 chat line with decoration and a left-aligned text.
* @return a new {@link FormattableChat} 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) {
public static FormattableChat leftText(ComponentLike text, char decorationChar, TextColor decorationColor, boolean console) {
return ChatFilledLine.leftText(text).decoChar(decorationChar).decoColor(decorationColor).spacesAroundText().console(console).toChat();
}
/**
* Creates a {@link FormatableChat} filling a chat line with the configured decoration character and
* Creates a {@link FormattableChat} 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 chat line with the configured decoration character
* @return a new {@link FormattableChat} filling a chat line with the configured decoration character
* and color and a left-aligned text.
* @see ChatFilledLine#leftText(ComponentLike)
* @see ChatConfig#decorationChar
* @see ChatConfig#decorationColor
*/
public static FormatableChat leftText(ComponentLike text, boolean console) {
public static FormattableChat leftText(ComponentLike text, boolean console) {
return ChatFilledLine.leftText(text).spacesAroundText().console(console).toChat();
}
/**
* Creates a {@link FormatableChat} filling a chat line with decoration and a right-aligned text.
* Creates a {@link FormattableChat} 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 chat line with decoration and a right-aligned
* @return a new {@link FormattableChat} filling a chat line with decoration and a right-aligned
* text.
* @see ChatFilledLine#rightText(ComponentLike)
*/
public static FormatableChat rightText(ComponentLike text, char decorationChar, TextColor decorationColor, boolean console) {
public static FormattableChat rightText(ComponentLike text, char decorationChar, TextColor decorationColor, boolean console) {
return ChatFilledLine.rightText(text).decoChar(decorationChar).decoColor(decorationColor).spacesAroundText().console(console).toChat();
}
/**
* Creates a {@link FormatableChat} filling a chat line with the configured decoration character and
* Creates a {@link FormattableChat} 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 chat line with the configured decoration character
* @return a new {@link FormattableChat} filling a chat line with the configured decoration character
* and color and a right-aligned text.
* @see ChatFilledLine#rightText(ComponentLike)
* @see ChatConfig#decorationChar
* @see ChatConfig#decorationColor
*/
public static FormatableChat rightText(ComponentLike text, boolean console) {
public static FormattableChat rightText(ComponentLike text, boolean console) {
return ChatFilledLine.rightText(text).spacesAroundText().console(console).toChat();
}
/**
* Creates a {@link FormatableChat} filling a chat line with decoration and a centered text.
* Creates a {@link FormattableChat} 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 chat line with decoration and a centered text.
* @return a new {@link FormattableChat} 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) {
public static FormattableChat centerText(ComponentLike text, char decorationChar, TextColor decorationColor, boolean console) {
return ChatFilledLine.centerText(text).decoChar(decorationChar).decoColor(decorationColor).spacesAroundText().console(console).toChat();
}
/**
* Creates a {@link FormatableChat} filling a chat line with the configured decoration character and
* Creates a {@link FormattableChat} 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 chat line with the configured decoration character
* @return a new {@link FormattableChat} filling a chat line with the configured decoration character
* and color and a centered text.
* @see ChatFilledLine#centerText(ComponentLike)
* @see ChatConfig#decorationChar
* @see ChatConfig#decorationColor
*/
public static FormatableChat centerText(ComponentLike text, boolean console) {
public static FormattableChat centerText(ComponentLike text, boolean console) {
return ChatFilledLine.centerText(text).spacesAroundText().console(console).toChat();
}
/**
* Creates a {@link FormatableChat} filling a chat line with a decoration character and color.
* Creates a {@link FormattableChat} 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 chat line with a decoration character and color.
* @return a new {@link FormattableChat} filling a chat line with a decoration character and color.
* @see ChatFilledLine#filled()
*/
public static FormatableChat filledLine(char decorationChar, TextColor decorationColor, boolean console) {
public static FormattableChat filledLine(char decorationChar, TextColor decorationColor, boolean console) {
return ChatFilledLine.filled().decoChar(decorationChar).decoColor(decorationColor).console(console).toChat();
}
/**
* Creates a {@link FormatableChat} filling a chat line with the configured decoration character and
* Creates a {@link FormattableChat} 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 chat line with a decoration character and color.
* @return a new {@link FormattableChat} filling a chat line with a decoration character and color.
* @see ChatFilledLine#filled()
* @see ChatConfig#decorationChar
* @see ChatConfig#decorationColor
*/
public static FormatableChat filledLine(boolean console) {
public static FormattableChat filledLine(boolean console) {
return ChatFilledLine.filled().console(console).toChat();
}
@@ -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("Unknown 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.
@@ -30,7 +33,7 @@ public class ChatTreeNode {
* Construct a new {@link ChatTreeNode}.
* @param cmp the component for the current node.
*/
public ChatTreeNode(Chat cmp) {
public ChatTreeNode(ComponentLike cmp) {
component = cmp;
}
@@ -50,7 +53,7 @@ public class ChatTreeNode {
* 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 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.FormattableChat;
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,18 +22,6 @@ import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
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.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;
/**
@@ -152,7 +152,7 @@ public class ChatUtil {
else
first = false;
FormatableChat pDisplay = Chat.clickableCommand(Chat.text(page), String.format(cmdFormat, page), Chat.text("Aller à la page " + page));
FormattableChat pDisplay = Chat.clickableCommand(Chat.text(page), String.format(cmdFormat, page), Chat.text("Aller à la page " + page));
if (page == currentPage) {
pDisplay.highlightedCommandColor();
}
@@ -180,12 +180,12 @@ public class ChatUtil {
* @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) {
public static FormattableChat 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))
default -> (FormattableChat) join(regularSeparator, elements.subList(0, last))
.then(finalSeparator)
.then(elements.get(last));
};
@@ -199,10 +199,11 @@ public class ChatUtil {
/**
* 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();
public static FormattableChat join(ComponentLike separator, Iterable<? extends ComponentLike> elements) {
FormattableChat c = chat();
if (elements == null)
return c;
boolean first = true;
@@ -264,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())
@@ -349,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);
@@ -431,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);
}
@@ -479,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;
@@ -502,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;
@@ -518,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 + ")");
}
@@ -595,7 +596,7 @@ public class ChatUtil {
for (int i = 0; i < sizes.length; i++) {
sumSizes += sizes[i];
FormatableChat subC = ChatStatic.text(repeatedChar(PROGRESS_BAR_FULL_CHAR, sizes[i]));
FormattableChat subC = ChatStatic.text(repeatedChar(PROGRESS_BAR_FULL_CHAR, sizes[i]));
if (colors != null && i < colors.length && colors[i] != null)
subC.color(colors[i]);
@@ -656,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,26 @@
<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-commands</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

@@ -1,22 +1,25 @@
package fr.pandacube.lib.cli;
import fr.pandacube.lib.cli.commands.CLIBrigadierDispatcher;
import fr.pandacube.lib.cli.log.CLILogger;
import fr.pandacube.lib.util.log.Log;
import org.jline.reader.EndOfFileException;
import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder;
import org.jline.reader.UserInterruptException;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import java.io.IOException;
import java.util.logging.Logger;
import fr.pandacube.lib.cli.commands.CLIBrigadierDispatcher;
import fr.pandacube.lib.cli.log.CLILogger;
import jline.console.ConsoleReader;
import org.fusesource.jansi.AnsiConsole;
import fr.pandacube.lib.util.Log;
/**
* Class to handle 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 LineReader} for the
* console rendering, a JUL {@link Logger} for logging, and Brigadier to handle commands.
*/
public class CLI extends Thread {
private final ConsoleReader reader;
private final LineReader reader;
private final Logger logger;
@@ -28,10 +31,11 @@ public class CLI extends Thread {
super("Console Thread");
setDaemon(true);
AnsiConsole.systemInstall();
reader = new ConsoleReader();
reader.setPrompt(">");
reader.addCompleter(CLIBrigadierDispatcher.instance);
Terminal terminal = TerminalBuilder.builder().build();
reader = LineReaderBuilder.builder().terminal(terminal)
.completer(CLIBrigadierDispatcher.instance)
.build()
;
// configure logger's formatter
System.setProperty("net.md_5.bungee.log-date-format", "yyyy-MM-dd HH:mm:ss");
@@ -40,10 +44,10 @@ public class CLI extends Thread {
/**
* Gets the Jline {@link ConsoleReader} of this CLI instance.
* @return the Jline {@link ConsoleReader} of this CLI instance.
* Gets the Jline {@link LineReader} of this CLI instance.
* @return the Jline {@link LineReader} of this CLI instance.
*/
public ConsoleReader getConsoleReader() {
public LineReader getConsoleReader() {
return reader;
}
@@ -65,15 +69,14 @@ public class CLI extends Thread {
int i = 0;
String line;
try {
while((line = reader.readLine()) != null) {
if (line.trim().equals(""))
while((line = reader.readLine(">")) != null) {
if (line.trim().isEmpty())
continue;
String cmdLine = line;
new Thread(() -> CLIBrigadierDispatcher.instance.execute(cmdLine), "CLICmdThread #"+(i++)).start();
Thread.ofVirtual().name("CLICmdThread #"+(i++))
.start(() -> CLIBrigadierDispatcher.instance.execute(cmdLine));
}
} catch (IOException e) {
Log.severe(e);
}
} catch (UserInterruptException | EndOfFileException ignore) { }
}

View File

@@ -2,7 +2,8 @@ package fr.pandacube.lib.cli;
import fr.pandacube.lib.cli.commands.CommandAdmin;
import fr.pandacube.lib.cli.commands.CommandStop;
import fr.pandacube.lib.util.Log;
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;
@@ -14,17 +15,24 @@ 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.
*/
@SuppressWarnings("CallToPrintStackTrace")
protected CLIApplication() {
instance = this;
CLI tmpCLI = null;
@@ -46,7 +54,7 @@ public abstract class CLIApplication {
new CommandAdmin();
new CommandStop();
Runtime.getRuntime().addShutdownHook(new Thread(this::stop));
Runtime.getRuntime().addShutdownHook(shutdownThread);
cli.start(); // actually starts the CLI thread
@@ -56,48 +64,74 @@ public abstract class CLIApplication {
}
}
/**
* Returns the application's {@link Logger}.
* @return the application's {@link Logger}.
*/
public Logger getLogger() {
return cli.getLogger();
}
private final Object stopLock = new Object();
private final Thread shutdownThread = new Thread(this::stop);
private final AtomicBoolean stopping = new AtomicBoolean(false);
@SuppressWarnings("finally")
/**
* Stops this application.
*/
public final void stop() {
synchronized (stopLock) {
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.");
System.exit(0);
}
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

@@ -38,16 +38,9 @@ public abstract class CLIBrigadierCommand extends BrigadierCommand<CLICommandSen
}
protected abstract LiteralArgumentBuilder<CLICommandSender> buildCommand();
protected String[] getAliases() {
return new String[0];
}
public boolean isPlayer(CLICommandSender sender) {
public boolean isPlayer(CLICommandSender sender) {
return sender.isPlayer();
}

View File

@@ -3,8 +3,11 @@ package fr.pandacube.lib.cli.commands;
import com.mojang.brigadier.suggestion.Suggestion;
import com.mojang.brigadier.suggestion.Suggestions;
import fr.pandacube.lib.commands.BrigadierDispatcher;
import jline.console.completer.Completer;
import net.kyori.adventure.text.ComponentLike;
import org.jline.reader.Candidate;
import org.jline.reader.Completer;
import org.jline.reader.LineReader;
import org.jline.reader.ParsedLine;
import java.util.List;
@@ -25,6 +28,9 @@ public class CLIBrigadierDispatcher extends BrigadierDispatcher<CLICommandSender
public static final CLICommandSender CLI_CONSOLE_COMMAND_SENDER = new CLIConsoleCommandSender();
private CLIBrigadierDispatcher() {}
/**
* Executes the provided command as the console.
* @param commandWithoutSlash the command, without the eventual slash at the beginning.
@@ -36,17 +42,15 @@ public class CLIBrigadierDispatcher extends BrigadierDispatcher<CLICommandSender
@Override
public int complete(String buffer, int cursor, List<CharSequence> candidates) {
String bufferBeforeCursor = buffer.substring(0, cursor);
public void complete(LineReader lineReader, ParsedLine parsedLine, List<Candidate> candidates) {
String bufferBeforeCursor = parsedLine.line().substring(0, parsedLine.cursor());
Suggestions completeResult = getSuggestions(bufferBeforeCursor);
completeResult.getList().stream()
.map(Suggestion::getText)
.map(Candidate::new)
.forEach(candidates::add);
return completeResult.getRange().getStart();
}
/**

View File

@@ -41,6 +41,9 @@ public interface CLICommandSender extends Audience {
*/
void sendMessage(String message);
@SuppressWarnings({"UnstableApiUsage", "deprecation"})
@Override // force implementation of super-interface default method
void sendMessage(@NotNull Identity source, @NotNull Component message, @NotNull MessageType type);
}

View File

@@ -1,7 +1,7 @@
package fr.pandacube.lib.cli.commands;
import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.util.Log;
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;
@@ -11,6 +11,12 @@ 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";
}
@@ -32,7 +38,7 @@ public class CLIConsoleCommandSender implements CLICommandSender {
}
@Override
public void sendMessage(@NotNull Identity source, @NotNull Component message, @NotNull MessageType type) {
public void sendMessage(@NotNull Identity source, @NotNull Component message, @SuppressWarnings({"UnstableApiUsage", "deprecation"}) @NotNull MessageType type) {
sendMessage(Chat.chatComponent(message).getLegacyText());
}
}

View File

@@ -14,11 +14,11 @@ 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.Chat.FormattableChat;
import fr.pandacube.lib.chat.ChatTreeNode;
import fr.pandacube.lib.cli.CLIApplication;
import fr.pandacube.lib.util.Log;
import net.md_5.bungee.api.chat.BaseComponent;
import fr.pandacube.lib.util.log.Log;
import net.kyori.adventure.text.Component;
import java.util.ArrayList;
import java.util.Arrays;
@@ -31,8 +31,16 @@ 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")
@@ -184,16 +192,16 @@ public class CommandAdmin extends CLIBrigadierCommand {
private BaseComponent displayCurrentNode(CommandNode<CLICommandSender> node, boolean redirectTarget, CLICommandSender sender) {
private Component displayCurrentNode(CommandNode<CLICommandSender> node, boolean redirectTarget, CLICommandSender sender) {
if (node == null)
throw new IllegalArgumentException("node must not be null");
FormatableChat d;
FormattableChat d;
if (node instanceof RootCommandNode) {
d = text("(root)").italic()
.hover("Root command node");
}
else if (node instanceof ArgumentCommandNode) {
ArgumentType<?> type = ((ArgumentCommandNode<?, ?>) node).getType();
else if (node instanceof ArgumentCommandNode<?, ?> argNode) {
ArgumentType<?> type = argNode.getType();
String typeStr = type.getClass().getSimpleName();
if (type instanceof IntegerArgumentType
|| type instanceof LongArgumentType
@@ -252,10 +260,10 @@ public class CommandAdmin extends CLIBrigadierCommand {
return d.get();
}
private static class DisplayCommandNode {
final List<CommandNode<CLICommandSender>> nodes = new ArrayList<>();
final List<DisplayCommandNode> children = new ArrayList<>();

View File

@@ -5,10 +5,15 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import fr.pandacube.lib.cli.CLIApplication;
/**
* /stop (/end) command.
* 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")

View File

@@ -1,9 +1,10 @@
package fr.pandacube.lib.cli.log;
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.logs.DailyLogRotateFileHandler;
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;
@@ -13,6 +14,7 @@ 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;
/**
@@ -20,8 +22,31 @@ import java.util.logging.Logger;
*/
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
@@ -46,6 +71,8 @@ public class CLILogger {
System.setOut(newRedirector(logger, Level.INFO));
Log.setLogger(logger);
Thread.setDefaultUncaughtExceptionHandler((t, e) -> Log.severe("Uncaught Exception in thread " + t.getName(), e));
}
return logger;
}
@@ -70,5 +97,7 @@ public class CLILogger {
t.start();
return ps;
}
private CLILogger() {}
}

View File

@@ -45,7 +45,7 @@
<dependency>
<groupId>com.mojang</groupId>
<artifactId>brigadier</artifactId>
<version>1.0.18</version>
<version>${brigadier.version}</version>
</dependency>
</dependencies>

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 badly used the command.
* The message, if provided, must indicate the reason of the bad usage 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,6 +25,11 @@ import java.util.function.Predicate;
*/
public abstract class BrigadierCommand<S> {
/**
* Creates a Brigadier command.
*/
public BrigadierCommand() {}
/**
* Returns a builder for this command.
@@ -217,14 +223,14 @@ public abstract class BrigadierCommand<S> {
/**
* Wraps the provided {@link SuggestionsSupplier} into a Brigadiers {@link SuggestionProvider}.
* @param suggestions the suggestions to wrap.
* @param senderUnwrapper function to convert the command sender provided by brigadier into the command sender
* @param senderUnWrapper function to convert the command sender provided by brigadier into the command sender
* supported by {@link SuggestionsSupplier}.
* @return a {@link SuggestionProvider} generating the suggestions from the provided {@link SuggestionsSupplier}.
* @param <AS> the type of command sender supported by the {@link SuggestionsSupplier}.
*/
protected <AS> SuggestionProvider<S> wrapSuggestions(SuggestionsSupplier<AS> suggestions, Function<S, AS> senderUnwrapper) {
protected <AS> SuggestionProvider<S> wrapSuggestions(SuggestionsSupplier<AS> suggestions, Function<S, AS> senderUnWrapper) {
return (context, builder) -> {
AS sender = senderUnwrapper.apply(context.getSource());
AS sender = senderUnWrapper.apply(context.getSource());
String message = builder.getInput();
try {
int tokenStartPos = builder.getStart();
@@ -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,7 +6,7 @@ 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;
@@ -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.

View File

@@ -140,4 +140,8 @@ public class BrigadierSuggestionsUtil {
}
private BrigadierSuggestionsUtil() {}
}

View File

@@ -241,7 +241,7 @@ public interface SuggestionsSupplier<S> {
return (s, ti, token, a) -> {
try {
List<Long> proposedValues = new ArrayList<>();
if (token.length() == 0) {
if (token.isEmpty()) {
long start = Math.max(Math.max(Math.min(-4, max - 9), min), -9);
long end = Math.min(Math.min(start + 9, max), 9);
ListUtil.addLongRangeToList(proposedValues, start, end);
@@ -399,7 +399,7 @@ public interface SuggestionsSupplier<S> {
*/
default SuggestionsSupplier<S> quotableString() {
return (s, ti, token, a) -> {
boolean startWithQuote = token.length() > 0 && (token.charAt(0) == '"' || token.charAt(0) == '\'');
boolean startWithQuote = !token.isEmpty() && (token.charAt(0) == '"' || token.charAt(0) == '\'');
String realToken = startWithQuote ? unescapeBrigadierQuotable(token.substring(1), token.charAt(0)) : token;
String[] argsCopy = Arrays.copyOf(a, a.length);
argsCopy[a.length - 1] = realToken;

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,7 +1,4 @@
package fr.pandacube.lib.core.config;
import fr.pandacube.lib.chat.ChatColorUtil;
import fr.pandacube.lib.util.Log;
package fr.pandacube.lib.config;
import java.io.BufferedReader;
import java.io.File;
@@ -56,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("#"))
@@ -114,25 +111,6 @@ public abstract class AbstractConfig {
}
/**
* Utility method to that translate the {@code '&'} formatted string to the legacy format.
* @param string the string to convert.
* @return a legacy formatted 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,4 +1,4 @@
package fr.pandacube.lib.core.config;
package fr.pandacube.lib.config;
import java.io.File;
import java.io.IOException;

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>
@@ -104,6 +110,7 @@
<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>

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;
@@ -70,6 +70,11 @@ 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
@@ -102,14 +107,14 @@ public abstract class BackupCleaner implements UnaryOperator<TreeSet<LocalDateTi
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;
}
@@ -118,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;
}
@@ -151,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,6 +1,6 @@
package fr.pandacube.lib.core.backup;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
import java.io.File;
import java.time.ZonedDateTime;
@@ -66,7 +66,10 @@ public class BackupManager extends TimerTask {
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;
}
@@ -93,6 +96,7 @@ 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();

View File

@@ -1,10 +1,10 @@
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;
@@ -209,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 + " doesn't exist");
Log.warning("[Backup] Unable to compress " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + ": source directory " + sourceDir + " doesn't exist");
return;
}
@@ -219,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);
@@ -229,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();
@@ -267,7 +267,7 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab
* Logs the scheduling status of this backup process.
*/
public void displayNextSchedule() {
Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " next backup on "
Log.info("[Backup] " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + " next backup on "
+ DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(new Date(getNext())));
}
@@ -297,7 +297,7 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab
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());
}

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;

View File

@@ -1,8 +1,8 @@
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;
@@ -53,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.
@@ -82,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);

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) {

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;
@@ -54,7 +54,7 @@ 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, now);
@@ -224,5 +224,6 @@ public class CronScheduler {
.toEpochMilli();
}
private CronScheduler() {}
}

View File

@@ -2,10 +2,16 @@ package fr.pandacube.lib.core.json;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import com.google.gson.Strictness;
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.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;
@@ -16,6 +22,52 @@ import java.util.function.Function;
*/
public class Json {
/**
* 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 is 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)}.
@@ -23,21 +75,21 @@ public class Json {
public static final Gson gson = build(Function.identity());
/**
* {@link Gson} instance with {@link GsonBuilder#setLenient()}, {@link GsonBuilder#setPrettyPrinting()} and support
* {@link Gson} instance with {@link Strictness#LENIENT}, {@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
* {@link Gson} instance with {@link Strictness#LENIENT}, {@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 Gson} instance with {@link Strictness#LENIENT}, {@link GsonBuilder#serializeNulls()},
* {@link GsonBuilder#setPrettyPrinting()} and support for Java records and additional {@link TypeAdapterFactory}
* provided with {@link #registerTypeAdapterFactory(TypeAdapterFactory)}.
*/
@@ -52,7 +104,9 @@ public class Json {
private static Gson build(Function<GsonBuilder, GsonBuilder> builderModifier) {
GsonBuilder base = new GsonBuilder()
.registerTypeAdapterFactory(new CustomAdapterFactory())
.setLenient();
.disableHtmlEscaping()
.setObjectToNumberStrategy(YAML_EQUIVALENT_NUMBER_STRATEGY)
.setStrictness(Strictness.LENIENT);
return builderModifier.apply(base).create();
}
@@ -92,6 +146,24 @@ public class Json {
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

@@ -10,7 +10,7 @@ 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;
import fr.pandacube.lib.util.log.Log;
import fr.pandacube.lib.util.ThrowableUtil;
import java.lang.reflect.Constructor;
@@ -30,6 +30,8 @@ public class ThrowableAdapter implements JsonSerializer<Throwable>, JsonDeserial
/* 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 {
@@ -132,30 +134,30 @@ public class ThrowableAdapter implements JsonSerializer<Throwable>, JsonDeserial
}
private static <T extends Throwable> ThrowableSubAdapter<T> defaultSubAdapter(Class<T> clazz) {
BiFunction<String, Throwable, T> constructor = null;
BiFunction<String, Throwable, T> constructionFunction = 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));
Constructor<T> constructor = clazz.getConstructor(String.class, Throwable.class);
if (constructor.canAccess(null)) {
constructionFunction = (m, t) -> ThrowableUtil.wrapReflectEx(() -> constructor.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)));
Constructor<T> constructor = clazz.getConstructor(String.class);
if (constructor.canAccess(null)) {
constructionFunction = ThrowableSubAdapter.messageOnly((m) -> ThrowableUtil.wrapReflectEx(() -> constructor.newInstance(m)));
}
} catch (ReflectiveOperationException ignore) { }
if (constructor == null) {
if (constructionFunction == 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);
return new ThrowableSubAdapter<>(constructionFunction);
}

View File

@@ -237,5 +237,8 @@ public class TypeConverter {
}
}
private TypeConverter() {}
}

View File

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

@@ -37,8 +37,8 @@ public class MinecraftVersionUtil {
/**
* 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]).
* @param v a string representation of a version (e.g. 1.19.1).
* @return an array of int representing the provided version (e.g. [1, 19, 1]).
*/
public static int[] decomposedVersion(String v) {
try {
@@ -114,7 +114,7 @@ public class MinecraftVersionUtil {
else {
// merge
if (i - firstConsecutive > 1)
keptVersions.add(versions.get(firstConsecutive) + "-" + versions.get(i));
keptVersions.add(versions.get(firstConsecutive) + " - " + versions.get(i));
else {
keptVersions.add(versions.get(firstConsecutive));
keptVersions.add(versions.get(i));
@@ -135,4 +135,7 @@ public class MinecraftVersionUtil {
return set;
}
private MinecraftVersionUtil() {}
}

View File

@@ -1,7 +1,7 @@
package fr.pandacube.lib.core.mc_version;
import fr.pandacube.lib.core.json.Json;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
import fr.pandacube.lib.util.StringUtil;
import org.jetbrains.annotations.NotNull;
@@ -68,12 +68,12 @@ public class ProtocolVersion implements Comparable<ProtocolVersion> {
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()
try (HttpClient cl = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build()) {
HttpResponse<String> response = cl.send(
HttpRequest.newBuilder(URI.create(ONLINE_DATA_URL)).build(),
BodyHandlers.ofString()
);
if (response.statusCode() == 200) {
MinecraftVersionList data = Json.gson.fromJson(response.body(), MinecraftVersionList.class);
@@ -123,7 +123,7 @@ public class ProtocolVersion implements Comparable<ProtocolVersion> {
/**
* 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").
* @param version The Minecraft version, in the format "X.X[.X]" (e.g. "1.17" or "1.8.8").
* @return an instance of {@link ProtocolVersion}.
*/
public static ProtocolVersion ofVersion(String version) {
@@ -156,8 +156,9 @@ public class ProtocolVersion implements Comparable<ProtocolVersion> {
* @return all the {@link ProtocolVersion} currently known by this class.
*/
public static List<ProtocolVersion> allKnownProtocolVersions() {
return versionList.get().versionsOfProtocol().keySet().stream()
.map(ProtocolVersion::ofProtocol)
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();
}
@@ -215,6 +216,22 @@ public class ProtocolVersion implements Comparable<ProtocolVersion> {
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;

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;

View File

@@ -1,181 +1,95 @@
{
"protocolOfVersion": {
"1.20.1": 763,
"1.20": 763,
"1.19.4": 762,
"1.19.3": 761,
"1.19.2": 760,
"1.19.1": 760,
"1.19": 759,
"1.18.2": 758,
"1.18.1": 757,
"1.18": 757,
"1.17.1": 756,
"1.17": 755,
"1.16.5": 754,
"1.16.4": 754,
"1.16.3": 753,
"1.16.2": 751,
"1.16.1": 736,
"1.16": 735,
"1.15.2": 578,
"1.15.1": 575,
"1.15": 573,
"1.14.4": 498,
"1.14.3": 490,
"1.14.2": 485,
"1.14.1": 480,
"1.14": 477,
"1.13.2": 404,
"1.13.1": 401,
"1.13": 393,
"1.12.2": 340,
"1.12.1": 338,
"1.12": 335,
"1.11.2": 316,
"1.11.1": 316,
"1.11": 315,
"1.10.2": 210,
"1.10.1": 210,
"1.10": 210,
"1.9.4": 110,
"1.9.3": 110,
"1.9.2": 109,
"1.9.1": 108,
"1.9": 107,
"1.8.9": 47,
"1.8.8": 47,
"1.8.7": 47,
"1.8.6": 47,
"1.8.5": 47,
"1.8.4": 47,
"1.8.3": 47,
"1.8.2": 47,
"1.8.1": 47,
"1.8": 47,
"1.7.10": 5,
"1.7.9": 5,
"1.7.8": 5,
"1.7.7": 5,
"1.7.6": 5,
"1.7.5": 4,
"1.7.4": 4,
"1.7.2": 4,
"1.7.3": 4,
"1.7.2": 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,
"1.21.5": 770,
"1.21.6": 771,
"1.21.7": 772,
"1.21.8": 772
},
"versionsOfProtocol": {
"763": [
"1.20",
"1.20.1"
"4": [
"1.7.2",
"1.7.3",
"1.7.4",
"1.7.5"
],
"762": [
"1.19.4"
],
"761": [
"1.19.3"
],
"760": [
"1.19.1",
"1.19.2"
],
"759": [
"1.19"
],
"758": [
"1.18.2"
],
"757": [
"1.18",
"1.18.1"
],
"756": [
"1.17.1"
],
"755": [
"1.17"
],
"754": [
"1.16.4",
"1.16.5"
],
"753": [
"1.16.3"
],
"751": [
"1.16.2"
],
"736": [
"1.16.1"
],
"735": [
"1.16"
],
"578": [
"1.15.2"
],
"575": [
"1.15.1"
],
"573": [
"1.15"
],
"498": [
"1.14.4"
],
"490": [
"1.14.3"
],
"485": [
"1.14.2"
],
"480": [
"1.14.1"
],
"477": [
"1.14"
],
"404": [
"1.13.2"
],
"401": [
"1.13.1"
],
"393": [
"1.13"
],
"340": [
"1.12.2"
],
"338": [
"1.12.1"
],
"335": [
"1.12"
],
"316": [
"1.11.1",
"1.11.2"
],
"315": [
"1.11"
],
"210": [
"1.10",
"1.10.1",
"1.10.2"
],
"110": [
"1.9.3",
"1.9.4"
],
"109": [
"1.9.2"
],
"108": [
"1.9.1"
],
"107": [
"1.9"
"5": [
"1.7.6",
"1.7.7",
"1.7.8",
"1.7.9",
"1.7.10"
],
"47": [
"1.8",
@@ -189,18 +103,150 @@
"1.8.8",
"1.8.9"
],
"5": [
"1.7.6",
"1.7.7",
"1.7.8",
"1.7.9",
"1.7.10"
"107": [
"1.9"
],
"4": [
"1.7.2",
"1.7.3",
"1.7.4",
"1.7.5"
"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"
],
"770": [
"1.21.5"
],
"771": [
"1.21.6"
],
"772": [
"1.21.7",
"1.21.8"
]
}
}

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.
@@ -227,7 +227,7 @@ public final class DB {
*/
public static <E extends SQLElement<E>> E getFirst(Class<E> elemClass, SQLWhere<E> where, SQLOrderBy<E> orderBy, Integer offset) throws DBException {
SQLElementList<E> elements = getAll(elemClass, where, orderBy, 1, offset);
return (elements.size() == 0) ? null : elements.get(0);
return (elements.isEmpty()) ? null : elements.get(0);
}
/**

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

@@ -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.
@@ -241,17 +241,24 @@ 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");
}
/**
@@ -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.

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)}.
*/

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.

View File

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

@@ -42,7 +42,7 @@ public class SQLType<T> {
@Override
public boolean equals(Object obj) {
return obj instanceof SQLType o
return obj instanceof SQLType<?> o
&& toString().equals(o.toString());
}

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

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

View File

@@ -5,7 +5,7 @@ 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 {

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;

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

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 -->
@@ -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,7 +23,7 @@ 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.
@@ -82,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

@@ -6,7 +6,7 @@ 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;
import fr.pandacube.lib.util.log.Log;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.entity.Player;

View File

@@ -2,11 +2,12 @@ package fr.pandacube.lib.paper.permissions;
import fr.pandacube.lib.permissions.PermGroup;
import fr.pandacube.lib.permissions.Permissions;
import fr.pandacube.lib.util.Log;
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.entity.Player;
import org.bukkit.plugin.ServicePriority;
import java.util.List;
@@ -115,10 +116,18 @@ import java.util.List;
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;
}
@Override
public boolean playerAdd(Player player, String permission) {
// override because the super class sets the permission on the current world of the player, that is probably
// not intended by the calling plugin
return playerAdd(null, player, permission);
}
@Deprecated
@Override
public boolean playerRemove(String world, String player, String permission) {
@@ -130,10 +139,19 @@ import java.util.List;
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 playerRemove(Player player, String permission) {
// to stay coherent with the override of #playerAdd(Player, String), we also override this method.
// Will try first to remove the permission on the world itself (like super-method), then if it doesn't exist,
// removes on the server level.
return super.playerRemove(player, permission) || playerRemove(null, player, permission);
}
@Override
public boolean groupHas(String world, String group, String permission) {
checkEnabled();
@@ -150,14 +168,14 @@ import java.util.List;
@Override
public boolean groupAdd(String world, String group, String permission) {
Log.warning(new Throwable("A plugin tried to add to group " + group + " (world=" + world + ") the permission " + 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.warning(new Throwable("A plugin tried to remove from group " + group + " (world=" + world + ") the permission " + 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;
}
@@ -177,7 +195,7 @@ import java.util.List;
@Deprecated
@Override
public boolean playerAddGroup(String world, String player, String group) {
Log.warning(new Throwable("A plugin tried to add player " + player + " (world=" + world + ") to permission group " + 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;
}
@@ -185,7 +203,7 @@ import java.util.List;
@Deprecated
@Override
public boolean playerRemoveGroup(String world, String player, String group) {
Log.warning(new Throwable("A plugin tried to remove player " + player + " (world=" + world + ") from permission group " + 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;
}

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,12 +16,17 @@
<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>
<url>https://maven.fabricmc.net/</url>
</repository>
<repository>
<id>minecraft-libraries</id>
<name>Minecraft Libraries</name>
<url>https://libraries.minecraft.net</url>
</repository>
</repositories>
<dependencies>
@@ -71,6 +76,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>
@@ -78,25 +89,18 @@
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.mojang</groupId>
<artifactId>datafixerupper</artifactId>
<version>${datafixerupper.version}</version>
</dependency>
<!-- Paper -->
<dependency>
<groupId>io.papermc.paper</groupId>
<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

@@ -5,27 +5,47 @@ 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,14 +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;
/**
* 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

@@ -3,8 +3,15 @@ package fr.pandacube.lib.paper.backup;
import java.io.File;
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");
}

View File

@@ -1,9 +1,9 @@
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;
@@ -11,14 +11,22 @@ import java.io.File;
import java.text.DateFormat;
import java.util.Date;
/**
* 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() {
@@ -62,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.");
}
}
@@ -80,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,118 +10,127 @@ 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.APICommandMeta;
import fr.pandacube.lib.paper.reflect.wrapper.paper.commands.BukkitCommandNode;
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.util.ThrowableUtil.wrapEx;
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 {
wrapEx(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;
}
/**
* Gets the root node of the dispatcher from {@link #getVanillaPaperDispatcher()}.
* @return the root node, or null if {@link #getVanillaPaperDispatcher()} is also null.
*/
public static RootCommandNode<CommandSourceStack> getRootNode() {
return vanillaPaperDispatcher == null ? null : vanillaPaperDispatcher.getRoot();
}
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);
}
}
}
/**
* 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.
*/
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.");
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;
}
Log.info("Removing Bukkit command /" + name + " (" + getCommandIdentity(bukkitCommand) + ")");
bukkitCmdMap.getKnownCommands().remove(name.toLowerCase(java.util.Locale.ENGLISH));
bukkitCommand.unregister(bukkitCmdMap);
LiteralCommandNode<BukkitBrigadierCommandSource> node = (LiteralCommandNode<BukkitBrigadierCommandSource>) getRootNode().getChild(name);
Command newCommand = new VanillaCommandWrapper(vanillaCommandDispatcher, node).__getRuntimeInstance();
bukkitCmdMap.getKnownCommands().put(name.toLowerCase(), newCommand);
newCommand.register(bukkitCmdMap);
}
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.");
}*/
});
}
/**
* 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.
*/
protected static RootCommandNode<BukkitBrigadierCommandSource> getRootNode() {
return nmsDispatcher.getRoot();
}
@@ -130,7 +139,16 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
/**
* 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;
@@ -145,13 +163,13 @@ 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) { }
}
/**
@@ -166,163 +184,193 @@ 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);
@SuppressWarnings("unchecked")
fr.pandacube.lib.paper.reflect.wrapper.brigadier.CommandNode<CommandSourceStack> registeredNode = wrap(vanillaPaperDispatcher.getRoot().getChild(commandNode.getName()), fr.pandacube.lib.paper.reflect.wrapper.brigadier.CommandNode.class);
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);
@SuppressWarnings("unchecked")
fr.pandacube.lib.paper.reflect.wrapper.brigadier.CommandNode<CommandSourceStack> wrappedCommandNode = wrap(actualNode, fr.pandacube.lib.paper.reflect.wrapper.brigadier.CommandNode.class);
if (actualNode != null) {
if (wrappedCommandNode.apiCommandMeta() != null) {
APICommandMeta meta = wrappedCommandNode.apiCommandMeta();
if (meta.plugin().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;
}
Log.warning("Forcing registration of alias /" + aliasToForce + " for command /" + commandNode.getName() + ": replacing " + getCommandIdentity(actualNode));
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 = getAliasNode(commandNode, aliasToForce);
@SuppressWarnings("unchecked")
fr.pandacube.lib.paper.reflect.wrapper.brigadier.CommandNode<CommandSourceStack> wrappedNewPCN = wrap(newPCN, fr.pandacube.lib.paper.reflect.wrapper.brigadier.CommandNode.class);
wrappedNewPCN.apiCommandMeta(registeredNode.apiCommandMeta());
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 static String getCommandIdentity(CommandNode<CommandSourceStack> command) {
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> nmsConflicted = root.getChild(name);
if (nmsConflicted != null) {
if (isFromThisCommand(nmsConflicted)) {
// this command is already registered in NMS. Dont need to register again
nmsRegistered = true;
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, won't 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;
}
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;
}
fr.pandacube.lib.paper.reflect.wrapper.brigadier.CommandNode<CommandSourceStack> wrappedCommandNode = wrap(command, fr.pandacube.lib.paper.reflect.wrapper.brigadier.CommandNode.class);
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.");
}
if (wrappedCommandNode.apiCommandMeta() != null) {
APICommandMeta meta = wrappedCommandNode.apiCommandMeta();
return "Node /" + command.getName() + " from plugin " + meta.plugin().getName();
}
else {
return "Node /" + command.getName() + " (unspecific)";
}
}
}
private static Boolean isPluginCommand(CommandNode<CommandSourceStack> command) {
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;
@SuppressWarnings("unchecked")
fr.pandacube.lib.paper.reflect.wrapper.brigadier.CommandNode<CommandSourceStack> wrappedCommandNode = wrap(command, fr.pandacube.lib.paper.reflect.wrapper.brigadier.CommandNode.class);
return wrappedCommandNode.apiCommandMeta() != null;
}
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();
}
@@ -341,6 +389,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();
}
@@ -354,13 +411,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);
}
@@ -394,7 +455,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());
}
@@ -403,8 +464,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();
}
/**
@@ -412,8 +473,8 @@ 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.");
}
@@ -442,7 +503,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);
}
@@ -455,12 +516,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));
@@ -480,117 +544,6 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
* 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 default 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(), nmsCoordinate -> {
BlockPos bp = ReflectWrapper.wrap(nmsCoordinate, 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.
@@ -606,11 +559,11 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
* @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(),
return tryGetArgument(context, argument, Coordinates.REFLECT.get(),
nmsCoordinate -> CraftVector.toBukkit(
ReflectWrapper.wrap(nmsCoordinate, Coordinates.class).getPosition(context.getSource())
wrap(nmsCoordinate, Coordinates.class).getPosition(context.getSource())
),
deflt);
}
@@ -618,44 +571,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 default 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.

View File

@@ -10,28 +10,38 @@ import org.bukkit.event.server.PluginDisableEvent;
import org.bukkit.event.server.ServerEvent;
import org.jetbrains.annotations.NotNull;
/**
* Fired at the beginning of the server stop process.
* More specifically, this event is called when the first plugin is disabling ({@link PluginDisableEvent}) while
* {@link Bukkit#isStopping()} returns true.
* <p>
* This event can be useful when a plugin want to execute stuff on server stop as soon as possible in the process,
* but not when the plugin itself is disabling (because some part of the Bukkit API is not usable at that moment).
*/
public class ServerStopEvent extends ServerEvent {
private static final HandlerList handlers = new HandlerList();
@NotNull
@Override
public HandlerList getHandlers() {
return handlers;
}
/**
* Gets the handler list of the event.
* @return the handler list of the event.
*/
@NotNull
public static HandlerList getHandlerList() {
return handlers;
}
private static boolean hasTriggered = false;
private static boolean isInit = false;
/**
* Register the event used to detect the server stop.
*/
public static void init() {
if (isInit)
return;
BukkitEvent.register(new Listener() {
@EventHandler(priority = EventPriority.LOWEST)
@@ -45,7 +55,25 @@ public class ServerStopEvent extends ServerEvent {
}
});
isInit = true;
}
private ServerStopEvent() {}
@NotNull
@Override
public HandlerList getHandlers() {
return handlers;
}
}

View File

@@ -0,0 +1,232 @@
package fr.pandacube.lib.paper.geometry;
import org.bukkit.Location;
import org.bukkit.util.Vector;
/**
* This vector considers Minecraft X Y Z axis orientation,
* but consider standard (not Minecraft) radian values for yaw and pitch.<br/>
* The length of this Vector (based on {@link #x}, {@link #y} and {@link #z} values)
* Is always 1.
*
* <pre>Yaw :
* North (-z) = -PI/2
* East (+x) = 0
* South (+z) = PI/2
* West (-x) = ±PI
*
* Pitch :
* Up (+y) = PI/2
* Down (-y) = -PI/2</pre>
*/
public class DirectionalVector {
/**
* The X cartesian coordinate of this {@link DirectionalVector}.
* It corresponds to the X (west to east) axis in a Minecraft world.
*/
public final double x;
/**
* The Y cartesian coordinate of this {@link DirectionalVector}.
* It corresponds to the Y (bottom to top) axis in a Minecraft world.
*/
public final double y;
/**
* The Z cartesian coordinate of this {@link DirectionalVector}.
* It corresponds to the Z (north to south) axis in a Minecraft world.
*/
public final double z;
/**
* The azimuthal angle φ (phi) of this {@link DirectionalVector}, in radian.
* It corresponds with Minecraft world as follows :
* <pre>Yaw :
* North (-z) = -PI/2
* East (+x) = 0
* South (+z) = PI/2
* West (-x) = ±PI</pre>
*/
public final double yaw;
/**
* The polar angle θ (theta) of this {@link DirectionalVector}, in radian.
* It corresponds with Minecraft world as follows :
* <pre>Pitch :
* Down (-y) = -PI/2
* Up (+y) = PI/2</pre>
*/
public final double pitch;
/**
* Initialize this {@link DirectionalVector} with the yaw and pitch
* contained in the provided {@link Location}.
* {@link Location#getYaw()} and {@link Location#getPitch()} values are automatically
* converted to conform {@link #yaw} and {@link #pitch} specification.
* @param l the location.
*/
public DirectionalVector(Location l) {
this(
Math.toRadians(((l.getYaw() + 90) % 360) > 180 ? ((l.getYaw() + 90) % 360) - 360 : ((l.getYaw() + 90) % 360)),
-Math.toRadians(l.getPitch())
);
/* MC : +90 : %360 : >180 -> -360
* South (+z) = 0, 360 : 90-450 : 90 : 90 : PI/2
* West (-x) = 90 : 180 : 180 : ±180 : ±PI
* North (-z) = 180 : 270 : 270 : -90 : -PI/2
* East (+x) = 270 : 360 : 0-360 : 0 : 0
*/
}
/**
* Creates a new {@link DirectionalVector} from a simple {@link Vector}.
* @param v the vector representing the direction. If v.getX() and v.getZ() are 0,
* the yaw will be 0. This may have inconsistency if the vector is calculated
* from a {@link Location}'s yaw and pitch. In this case, prefer using
* {@link #DirectionalVector(Location)}. The {@link Vector} is
* normalized if necessary (does not modify the provided {@link Vector}).
*/
public DirectionalVector(Vector v) {
this(v.getX(), v.getY(), v.getZ());
// this((v = v.clone().normalize()).getX(), v.getY(), v.getZ());
}
private DirectionalVector(double x, double y, double z) {
double vecSize = Math.sqrt(x * x + y * y + z * z);
this.x = x / vecSize;
this.y = y / vecSize;
this.z = z / vecSize;
if (x == 0.0 && z == 0.0) {
pitch = y > 0.0 ? GeometryUtil.PId2 : -GeometryUtil.PId2;
yaw = 0;
} else {
yaw = Math.atan2(z, x);
pitch = Math.atan(y / Math.sqrt(x * x + z * z));
}
}
private DirectionalVector(double x, double y, double z, double yaw, double pitch) {
this.x = x;
this.y = y;
this.z = z;
this.yaw = yaw;
this.pitch = pitch;
}
private DirectionalVector(double yaw, double pitch) {
this.yaw = yaw;
this.pitch = pitch;
y = Math.sin(pitch);
double cosPitch = Math.cos(pitch);
x = cosPitch * Math.cos(yaw);
z = cosPitch * Math.sin(yaw);
}
/**
* Gets a Vector using the internal X, Y and Z values, that is a simple directional 3D vector.
* @return this vector as a simple 3D {@link Vector}.
*/
public Vector toVector() {
return new Vector(x, y, z);
}
/**
* Set the yaw and the pitch of the provided {@link Location}
* with the values inside the current {@link DirectionalVector}
* after conversion of these values.
* @param l the location.
*/
public void putIntoLocation(Location l) {
/* std : -PI/2 : <0 ? +2PI : MC
* South (+z) = PI/2 : 0 : 0 : 0, 360
* West (-x) = ±PI : -3PI/2 - PI/2 : PI/2 : 90
* North (-z) = -PI/2 : -PI : PI : 180
* East (+x) = 0 : -PI/2 : 3PI/2 : 270
*/
l.setYaw((float) Math.toDegrees(yaw < GeometryUtil.PId2 ? yaw + GeometryUtil.PIx2 - GeometryUtil.PId2 : yaw - GeometryUtil.PId2));
l.setPitch((float) Math.toDegrees(-pitch));
}
/**
* Gets the vector pointing to the opposite direction.
* @return the opposite vector.
*/
public DirectionalVector getOpposite() {
return new DirectionalVector(
-x,
-y,
-z,
(yaw > 0 ? (yaw - GeometryUtil.PI) : (yaw + GeometryUtil.PI)),
-pitch
);
}
/**
* If the current direction is the player face direction,
* this method return the direction of the back of the head.
* This is an alias of {@link #getOpposite()}
* @return the opposite of this vector.
*/
public DirectionalVector getBackDirection() {
return getOpposite();
}
/**
* If the current direction is the player face direction,
* this method return the direction of the bottom of the head.
* @return the vector pointing on the bottom, as if this vector was the front orientation of a player head.
*/
public DirectionalVector getBottomDirection() {
return new DirectionalVector(
(pitch > 0 ? yaw : (yaw > 0 ? (yaw - GeometryUtil.PI) : (yaw + GeometryUtil.PI))),
(pitch > 0 ? (pitch - GeometryUtil.PId2) : (-GeometryUtil.PId2 - pitch))
);
}
/**
* If the current direction is the player face direction,
* this method return the direction of the top of the head.
* @return the vector pointing on the top, as if this vector was the front orientation of a player head.
*/
public DirectionalVector getTopDirection() {
return new DirectionalVector(
(pitch < 0 ? yaw : (yaw > 0 ? (yaw - GeometryUtil.PI) : (yaw + GeometryUtil.PI))),
(pitch < 0 ? (pitch + GeometryUtil.PId2) : (GeometryUtil.PId2 - pitch))
);
}
/**
* If the current direction is the player face direction,
* this method return the direction of the left of the head.
* @return the vector pointing on the left, as if this vector was the front orientation of a player head.
*/
public DirectionalVector getLeftDirection() {
return new DirectionalVector(
yaw > -GeometryUtil.PId2 ? (yaw - GeometryUtil.PId2) : (yaw - GeometryUtil.PId2 + GeometryUtil.PIx2),
0
);
}
/**
* If the current direction is the player face direction,
* this method return the direction of the right of the head.
* @return the vector pointing on the right, as if this vector was the front orientation of a player head.
*/
public DirectionalVector getRightDirection() {
return new DirectionalVector(
yaw < GeometryUtil.PId2 ? (yaw + GeometryUtil.PId2) : (yaw + GeometryUtil.PId2 - GeometryUtil.PIx2),
0
);
}
}

View File

@@ -0,0 +1,166 @@
package fr.pandacube.lib.paper.geometry;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.util.BoundingBox;
import org.bukkit.util.RayTraceResult;
import org.bukkit.util.Vector;
/**
* Utility class related to geometry and Minecraft.
*/
public class GeometryUtil {
/**
* Value equal to <code>{@link Math#PI}</code>.
*/
static final double PI = Math.PI;
/**
* Value equal to <code>{@link Math#PI} / 2</code>.
*/
static final double PId2 = PI/2;
/**
* Value equal to <code>{@link Math#PI} * 2</code>.
*/
static final double PIx2 = PI*2;
/*
* Player geometry
*/
/**
* The visual height of a Minecraft player skin, when he is standing up and not sneaking,
* from the ground where the player is standing on, to the above of the first layer of the head skin.
* It doesn't correspond to the player hit box height.<br/>
* <br/>
* The value is provided in Minecraft Wiki.
*/
public static final double PLAYER_SKIN_HEIGHT = 1.85;
/**
* Value provided by net.minecraft.world.entity.player.Player#getStandingEyeHeight
*/
public static final double PLAYER_EYE_HEIGHT = 1.62;
/**
* The visual height of a Minecraft player skin, when he is standing up and sneaking,
* from the ground where the player is standing on, to the above of the first layer of the head skin.
* It may not correspond to the player hit box height.<br/>
* <br/>
* The current value is the height of the player's hit box when sneaking. Even if this
* is close to the real value (tested in game), this is not the exact value.
*/
public static final double PLAYER_SKIN_HEIGHT_SNEAK = 1.50;
/**
* Value provided by net.minecraft.world.entity.player.Player#getStandingEyeHeight
*/
public static final double PLAYER_EYE_HEIGHT_SNEAK = 1.27;
/**
* The size of a skin pixel.
*/
public static final double PLAYER_SKIN_PIXEL_SIZE = PLAYER_SKIN_HEIGHT / 32;
/**
* The height of the center of rotation of the head, that is the distance between neck and the player's foot.
*/
public static final double PLAYER_HEAD_ROTATION_HEIGHT = PLAYER_SKIN_PIXEL_SIZE * 24;
/**
* The height of the center of rotation of the head, that is the distance between neck and the player's foot, but when the player is sneaking.
*/
public static final double PLAYER_HEAD_ROTATION_HEIGHT_SNEAK = PLAYER_HEAD_ROTATION_HEIGHT - (PLAYER_SKIN_HEIGHT - PLAYER_SKIN_HEIGHT_SNEAK);
/**
* The size of the first layer of the players head.
*/
public static final double PLAYER_HEAD_SIZE = PLAYER_SKIN_PIXEL_SIZE * 8;
/**
* Get the {@link Location}s of the 8 vertex of the player head<br/>
* This method only work if the player is standing up
* (not dead, not gliding, not sleeping, not swimming).
* @param playerLocation the location of the player, generally provided by {@link Player#getLocation()}
* @param isSneaking if the player is sneaking. Generally {@link Player#isSneaking()}
* @return an array of 8 {@link Location}s with x, y, and z values filled (yaw and pitch are ignored).
* <pre>
* return[0] // top front left
* return[1] // top front right
* return[2] // bottom front left
* return[3] // bottom front right
* return[4] // top back left
* return[5] // top back right
* return[6] // bottom back left
* return[7] // bottom back right
* </pre>
*/
public static Location[] getPlayerHeadGeometry(Location playerLocation, boolean isSneaking) {
Location[] headAnglesPoints = new Location[8];
Location playerHeadRotationLocation = playerLocation.clone()
.add(0, isSneaking ? PLAYER_HEAD_ROTATION_HEIGHT_SNEAK : PLAYER_HEAD_ROTATION_HEIGHT, 0);
DirectionalVector frontDirection = new DirectionalVector(playerHeadRotationLocation);
Vector frontHalfVector = frontDirection.toVector().multiply(PLAYER_HEAD_SIZE/2);
Vector backHalfDirection = frontDirection.getBackDirection().toVector().multiply(PLAYER_HEAD_SIZE/2);
Vector leftHalfVector = frontDirection.getLeftDirection().toVector().multiply(PLAYER_HEAD_SIZE/2);
Vector rightHalfVector = frontDirection.getRightDirection().toVector().multiply(PLAYER_HEAD_SIZE/2);
Vector topVector = frontDirection.getTopDirection().toVector().multiply(PLAYER_HEAD_SIZE);
Location bottomFrontMiddle = playerHeadRotationLocation.clone().add(frontHalfVector);
Location bottomBackMiddle = playerHeadRotationLocation.clone().add(backHalfDirection);
Location topFrontMiddle = bottomFrontMiddle.clone().add(topVector);
Location topBackMiddle = bottomBackMiddle.clone().add(topVector);
headAnglesPoints[0] = topFrontMiddle.clone().add(leftHalfVector);
headAnglesPoints[1] = topFrontMiddle.clone().add(rightHalfVector);
headAnglesPoints[2] = bottomFrontMiddle.clone().add(leftHalfVector);
headAnglesPoints[3] = bottomFrontMiddle.clone().add(rightHalfVector);
headAnglesPoints[4] = topBackMiddle.clone().add(leftHalfVector);
headAnglesPoints[5] = topBackMiddle.clone().add(rightHalfVector);
headAnglesPoints[6] = bottomBackMiddle.clone().add(leftHalfVector);
headAnglesPoints[7] = bottomBackMiddle.clone().add(rightHalfVector);
return headAnglesPoints;
}
/**
* Check if the path from <i>start</i> location to <i>end</i> pass through
* the axis aligned bounding box defined by <i>min</i> and <i>max</i>.
* @param start the start of the path.
* @param end the end of the path.
* @param min the min of the bounding box.
* @param max the max of the bounding box.
* @return true if the path intersects the bounding box.
* @deprecated use {@link BoundingBox#rayTrace(Vector, Vector, double)} instead.
*/
@Deprecated
public static boolean hasIntersection(Vector start, Vector end, Vector min, Vector max) {
RayTraceResult res = BoundingBox.of(min, max).rayTrace(start, end.clone().subtract(start), end.distance(start));
return res != null;
}
private GeometryUtil() {}
}

View File

@@ -0,0 +1,250 @@
package fr.pandacube.lib.paper.geometry.blocks;
import fr.pandacube.lib.util.RandomUtil;
import org.bukkit.Location;
import org.bukkit.util.BlockVector;
import org.bukkit.util.BoundingBox;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import java.util.Iterator;
/**
* Block Aligned Bounding Box (sort of AABB).
* Represent the littlest cuboid selection of blocks that contains the bounding box
* passed to the constructor.
*/
public class AABBBlock implements BlockSet, Cloneable {
/* package */ final Vector pos1, pos2;
private final Vector center;
private final long volume;
private BoundingBox bukkitBoundingBox;
private AABBBlock(AABBBlock original, int shiftX, int shiftY, int shiftZ) {
Vector shiftVec = new Vector(shiftX, shiftY, shiftZ);
pos1 = original.pos1.clone().add(shiftVec);
pos2 = original.pos2.clone().add(shiftVec);
center = original.center.clone().add(shiftVec);
volume = original.volume;
}
/**
* Construct a {@link AABBBlock} based on the two provided Bukkit's {@link Vector}.
* @param p1 the first vector.
* @param p2 the second vector.
*/
public AABBBlock(Vector p1, Vector p2) {
this(p1.getBlockX(), p1.getBlockY(), p1.getBlockZ(), p2.getBlockX(), p2.getBlockY(), p2.getBlockZ());
}
/**
* Construct a {@link AABBBlock} based on the provided Bukkit's {@link BoundingBox}.
* @param bb the bounding box.
*/
public AABBBlock(BoundingBox bb) {
pos1 = bb.getMin();
pos2 = bb.getMax();
center = bb.getCenter();
volume = (int) bb.getVolume();
bukkitBoundingBox = bb;
}
/**
* Construct a {@link AABBBlock} based on the two provided Bukkit's {@link Location}.
* The worlds defined in the provided locations are ignored.
* @param l1 the first location.
* @param l2 the second location.
*/
public AABBBlock(Location l1, Location l2) {
this(l1.getBlockX(), l1.getBlockY(), l1.getBlockZ(), l2.getBlockX(), l2.getBlockY(), l2.getBlockZ());
}
/**
* Construct a {@link AABBBlock} based on the two provided Bukkit's {@link BlockVector}.
* @param l1 the first block vector.
* @param l2 the second block vector.
*/
public AABBBlock(BlockVector l1, BlockVector l2) {
this(l1.getBlockX(), l1.getBlockY(), l1.getBlockZ(), l2.getBlockX(), l2.getBlockY(), l2.getBlockZ());
}
/**
* Construct a {@link AABBBlock} based on the individual coordinates of the 2 vectors.
* @param p1x the x value of the first vector.
* @param p1y the y value of the first vector.
* @param p1z the z value of the first vector.
* @param p2x the x value of the second vector.
* @param p2y the y value of the second vector.
* @param p2z the z value of the second vector.
*/
public AABBBlock(int p1x, int p1y, int p1z, int p2x, int p2y, int p2z) {
/*
* Prends les points extérieurs permettant de former un bounding box englobant
* celui représenté par v1 et v2, et étant aligné au quadrillage des blocs.
*/
int p1x_ = Math.min(p1x, p2x);
int p1y_ = Math.min(p1y, p2y);
int p1z_ = Math.min(p1z, p2z);
int p2x_ = Math.max(p1x, p2x) + 1;
int p2y_ = Math.max(p1y, p2y) + 1;
int p2z_ = Math.max(p1z, p2z) + 1;
pos1 = new Vector(p1x_, p1y_, p1z_);
pos2 = new Vector(p2x_, p2y_, p2z_);
center = new Vector((p1x_ + p2x_) / 2d, (p1y_ + p2y_) / 2d, (p1z_ + p2z_) / 2d);
volume = (long) Math.abs(p2x_ - p1x_) * Math.abs(p2x_ - p1x_) * Math.abs(p2x_ - p1x_);
}
@Override
public AABBBlock getEnglobingAABB() {
return this;
}
/**
* Gets the value of the "minimum" {@link Vector}, that is the vector with the lowest coordinates that is inside this bounding box.
* @return the minimum vector.
*/
public Vector getMin() {
return pos1.clone();
}
/**
* Gets the value of the "maximum" {@link Vector}, that is the vector with the highest coordinates that is inside this bounding box.
* @return the maximum vector.
*/
public Vector getMax() {
return pos2.clone();
}
/**
* Gets the {@link BlockVector} with the lowest coordinates in this bounding box.
* @return the minimum block vector.
*/
public BlockVector getMinBlock() {
return pos1.toBlockVector();
}
/**
* Gets the {@link BlockVector} with the highest coordinates in this bounding box.
* @return the maximum block vector.
*/
public BlockVector getMaxBlock() {
return pos2.clone().add(new Vector(-1, -1, -1)).toBlockVector();
}
/**
* Gets a new {@link AABBBlock} with its coordinates shifted by the provided amount.
* @param x the x shift.
* @param y the y shift.
* @param z the z shift.
* @return a shifted bounding box.
*/
public AABBBlock shift(int x, int y, int z) {
return new AABBBlock(this, x, y, z);
}
@SuppressWarnings("MethodDoesntCallSuperMethod")
@Override
public AABBBlock clone() {
return new AABBBlock(this, 0, 0, 0);
}
public boolean overlaps(BoundingBox bb) {
return asBukkitBoundingBox().overlaps(bb);
}
public boolean isInside(Vector v) {
return asBukkitBoundingBox().contains(v);
}
/**
* Gets the coordinate of the center of this bounding box.
* @return the center of this bounding box.
*/
public Vector getCenter() {
return center.clone();
}
public long getVolume() {
return volume;
}
/**
* Gets the Bukkit equivalent of this bounding box.
* @return a {@link BoundingBox} corresponding to this {@link AABBBlock}.
*/
public BoundingBox asBukkitBoundingBox() {
if (bukkitBoundingBox == null) {
bukkitBoundingBox = new BoundingBox(pos1.getX(), pos1.getY(), pos1.getZ(),
pos2.getX(), pos2.getY(), pos2.getZ());
}
return bukkitBoundingBox;
}
public Vector getRandomPosition() {
double x = RandomUtil.rand.nextDouble(pos1.getX(), pos2.getX());
double y = RandomUtil.rand.nextDouble(pos1.getY(), pos2.getY());
double z = RandomUtil.rand.nextDouble(pos1.getZ(), pos2.getZ());
return new Vector(x, y, z);
}
@Override
public @NotNull Iterator<BlockVector> iterator() {
return new Iterator<>() {
private int x = pos1.getBlockX(),
y = pos1.getBlockY(),
z = pos1.getBlockZ();
@Override
public boolean hasNext() {
return x < pos2.getBlockX();
}
@Override
public BlockVector next() {
BlockVector bv = new BlockVector(x, y, z);
z++;
if (z >= pos2.getBlockZ()) {
y++;
z = pos1.getBlockZ();
if (y >= pos2.getBlockY()) {
x++;
y = pos1.getBlockY();
}
}
return bv;
}
};
}
@Override
public String toString() {
return "{(" + pos1.getBlockX() +
", " + pos1.getBlockY() +
", " + pos1.getBlockZ() +
"), (" + pos2.getBlockX() +
", " + pos2.getBlockY() +
", " + pos2.getBlockZ() +
")}";
}
static boolean overlap(AABBBlock aabb1, AABBBlock aabb2) {
return aabb1.asBukkitBoundingBox().overlaps(aabb2.asBukkitBoundingBox());
}
static boolean overlap(AABBBlock aabb1, BlockSet bs) {
if (!overlap(aabb1, bs.getEnglobingAABB()))
return false;
AABBBlock intersection = new AABBBlock(aabb1.asBukkitBoundingBox().intersection(bs.getEnglobingAABB().asBukkitBoundingBox()));
for (BlockVector bv : intersection)
if (bs.isInside(bv))
return true;
return false;
}
}

View File

@@ -0,0 +1,156 @@
package fr.pandacube.lib.paper.geometry.blocks;
import fr.pandacube.lib.util.IteratorIterator;
import fr.pandacube.lib.util.RandomUtil;
import org.bukkit.util.BlockVector;
import org.bukkit.util.BoundingBox;
import org.bukkit.util.Vector;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
/**
* A group of {@link AABBBlock}.
*/
public class AABBBlockGroup implements BlockSet {
/**
* The list of {@link AABBBlock} contained in this group. This list is unmodifiable.
*/
public final List<AABBBlock> subAABB;
private final AABBBlock englobingAABB;
/**
* Creates a new {@link AABBBlockGroup}.
* @param in the list of {@link AABBBlock} that will be contained in this group.
*/
public AABBBlockGroup(Collection<AABBBlock> in) {
if (in.isEmpty())
throw new IllegalArgumentException("Provided collection must not be empty.");
subAABB = List.copyOf(in);
englobingAABB = initEnglobingAABB();
}
/**
* Creates a new {@link AABBBlockGroup}.
* @param in an array of {@link AABBBlock} that will be contained in this group.
*/
public AABBBlockGroup(AABBBlock... in) {
this(Arrays.asList(in));
}
private AABBBlock initEnglobingAABB() {
Vector pos1 = subAABB.get(0).pos1.clone();
Vector pos2 = subAABB.get(0).pos2.clone().add(new Vector(-1, -1, -1));
for (int i = 1; i < subAABB.size(); i++) {
AABBBlock aabb = subAABB.get(i);
pos1.setX(Math.min(pos1.getBlockX(), aabb.pos1.getBlockX()));
pos1.setY(Math.min(pos1.getBlockY(), aabb.pos1.getBlockY()));
pos1.setZ(Math.min(pos1.getBlockZ(), aabb.pos1.getBlockZ()));
pos2.setX(Math.max(pos2.getBlockX(), aabb.pos2.getBlockX() - 1));
pos2.setY(Math.max(pos2.getBlockY(), aabb.pos2.getBlockY() - 1));
pos2.setZ(Math.max(pos2.getBlockZ(), aabb.pos2.getBlockZ() - 1));
}
return new AABBBlock(pos1, pos2);
}
@Override
public AABBBlock getEnglobingAABB() {
return englobingAABB;
}
public boolean isInside(Vector v) {
if (!englobingAABB.isInside(v))
return false;
for (AABBBlock b : subAABB)
if (b.isInside(v))
return true;
return false;
}
public Vector getRandomPosition() {
double[] freq = subAABB.stream().mapToDouble(AABBBlock::getVolume).toArray();
int i = RandomUtil.randomIndexOfFrequencies(freq);
return subAABB.get(i).getRandomPosition();
}
public long getVolume() {
long v = 0;
for (AABBBlock b : subAABB)
v += b.getVolume();
return v;
}
@Override
public boolean overlaps(BoundingBox bb) {
if (!englobingAABB.overlaps(bb))
return false;
for (AABBBlock b : subAABB)
if (b.overlaps(bb))
return true;
return false;
}
@Override
public Iterator<BlockVector> iterator() {
return IteratorIterator.ofCollectionOfIterator(subAABB.stream().map(AABBBlock::iterator).toList());
}
@Override
public String toString() {
return "AABBBlockGroup{" +
"subAABB=" + subAABB +
", englobingAABB=" + englobingAABB +
'}';
}
/* package */ static boolean overlap(AABBBlockGroup aabbGroup, AABBBlock aabb) {
if (!aabbGroup.englobingAABB.overlaps(aabb))
return false;
for (AABBBlock b : aabbGroup.subAABB)
if (b.overlaps(aabb))
return true;
return false;
}
/* package */ static boolean overlap(AABBBlockGroup aabbGroup1, AABBBlockGroup aabbGroup2) {
if (!aabbGroup1.englobingAABB.overlaps(aabbGroup2.englobingAABB))
return false;
List<AABBBlock> group1SubList = new ArrayList<>();
for (AABBBlock b : aabbGroup1.subAABB) {
if (b.overlaps(aabbGroup2.englobingAABB))
group1SubList.add(b);
}
if (group1SubList.isEmpty())
return false;
List<AABBBlock> group2SubList = new ArrayList<>();
for (AABBBlock b : aabbGroup2.subAABB) {
if (b.overlaps(aabbGroup1.englobingAABB))
group2SubList.add(b);
}
if (group2SubList.isEmpty())
return false;
for (AABBBlock b1 : group1SubList)
for (AABBBlock b2 : group2SubList)
if (b1.overlaps(b2))
return true;
return false;
}
static boolean overlap(AABBBlockGroup aabbGroup, BlockSet bs) {
if (!aabbGroup.englobingAABB.overlaps(bs.getEnglobingAABB()))
return false;
for (AABBBlock b : aabbGroup.subAABB) {
if (b.overlaps(bs)) // already checks for englobingAABB before checking block per block
return true;
}
return false;
}
}

View File

@@ -0,0 +1,157 @@
package fr.pandacube.lib.paper.geometry.blocks;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.entity.Entity;
import org.bukkit.util.BlockVector;
import org.bukkit.util.BoundingBox;
import org.bukkit.util.Vector;
import java.util.Iterator;
/**
* Represents a set of blocks in a world.
*/
public interface BlockSet extends Iterable<BlockVector> {
/**
* Gets a random coordinate that is inside this block set.
* @return a random coordinate inside this block set.
*/
Vector getRandomPosition();
/**
* Gets the volume, in blocks (or cubic meters), of this block set.
* @return the volume of this block set.
*/
long getVolume();
/**
* Gets the englobing bounding box if this block set.
* @return the englobing bounding box if this block set.
*/
AABBBlock getEnglobingAABB();
/**
* Tells if this block set overlaps the provided bounding box.
* @param bb the provided bounding box
* @return true if its overlaps, false otherwise.
*/
boolean overlaps(BoundingBox bb);
/**
* Tells if this block set overlaps the bounding box of the provided entity.
* @param e the provided entity.
* @return true if its overlaps, false otherwise.
*/
default boolean overlaps(Entity e) {
return overlaps(e.getBoundingBox());
}
/**
* Tells if this block set overlaps the provided one. that is there is at least one block in common.
* @param bs the provided block set.
* @return true if its overlaps, false otherwise.
*/
default boolean overlaps(BlockSet bs) {
if (this instanceof AABBBlock b1) {
if (bs instanceof AABBBlock b2)
return AABBBlock.overlap(b1, b2);
if (bs instanceof AABBBlockGroup bg2)
return AABBBlockGroup.overlap(bg2, b1);
return AABBBlock.overlap(b1, bs);
}
if (this instanceof AABBBlockGroup bg1) {
if (bs instanceof AABBBlock b2)
return AABBBlockGroup.overlap(bg1, b2);
if (bs instanceof AABBBlockGroup bg2)
return AABBBlockGroup.overlap(bg1, bg2);
return AABBBlockGroup.overlap(bg1, bs);
}
return overlap(this, bs);
}
/**
* Tells if the provided vector is inside this bounding box.
* @param v the vector.
* @return true if its inside, false otherwise.
*/
boolean isInside(Vector v);
/**
* Tells if the provided location is inside this bounding box.
* The world of the location is ignored.
* @param l the location.
* @return true if its inside, false otherwise.
*/
default boolean isInside(Location l) {
return isInside(l.toVector());
}
/**
* Tells if the provided block is inside this bounding box.
* The world of the block is ignored.
* @param b the block.
* @return true if its inside, false otherwise.
*/
default boolean isInside(Block b) {
return isInside(b.getLocation().add(.5, .5, .5));
}
/**
* Tells if the provided entity is inside this bounding box.
* It calls {@link #isInside(Location)} using the returned value of {@link Entity#getLocation()}.
* The world of the entity is ignored.
* @param e the entity.
* @return true if its inside, false otherwise.
*/
default boolean isInside(Entity e) {
return isInside(e.getLocation());
}
/**
* Gets an iterator iterating through all the blocks of this block set.
* @param w the world of the blocks.
* @return a new iterator.
*/
default Iterable<Block> asBlockIterable(World w) {
return () -> new Iterator<>() {
final Iterator<BlockVector> nested = BlockSet.this.iterator();
@Override
public boolean hasNext() {
return nested.hasNext();
}
@Override
public Block next() {
BlockVector bv = nested.next();
return w.getBlockAt(bv.getBlockX(), bv.getBlockY(), bv.getBlockZ());
}
};
}
/**
* Tests the two block set overlap each other.
* This method works on any implementation of this interface, but they should override the
* {@link #overlaps(BlockSet)} method to provide a more optimized code.
* @param bs1 the first block set.
* @param bs2 the second block set.
* @return true if the two block set overlap, false otherwise.
*/
static boolean overlap(BlockSet bs1, BlockSet bs2) {
if (!bs1.getEnglobingAABB().overlaps(bs2.getEnglobingAABB()))
return false;
AABBBlock intersection = new AABBBlock(bs1.getEnglobingAABB().asBukkitBoundingBox().intersection(bs2.getEnglobingAABB().asBukkitBoundingBox()));
for (BlockVector bv : intersection)
if (bs1.isInside(bv) && bs2.isInside(bv))
return true;
return false;
}
}

View File

@@ -20,7 +20,7 @@ import org.bukkit.event.player.PlayerRespawnEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
import fr.pandacube.lib.paper.util.BukkitEvent;
/**
@@ -37,19 +37,34 @@ public class GUIHotBar implements Listener {
private final int defaultSlot;
private final List<Player> currentPlayers = new ArrayList<>();
/**
* Set up a new gui hot bar. You should not instantiate more than one hot bar.
* @param defaultSlot the default slot (currently held item) when the player joins the hot bar.
*/
public GUIHotBar(int defaultSlot) {
this.defaultSlot = Math.max(0, Math.min(8, defaultSlot));
BukkitEvent.register(this);
}
/**
* Disables this hot bar.
* @param clearPlayerMenuItems if the items of this hot bar should be removed from the players inventories.
*/
public void disable(boolean clearPlayerMenuItems) {
removeAllPlayers(clearPlayerMenuItems);
BukkitEvent.unregister(this);
}
/**
* Add the item to this hot bar menu. if there is already players hooked to this hot bar, the item will be directly added to
* their inventories.
* @param i the item stack
* @param i the item stack.
* @param setter code executed to put the item in the inventory. Additionally, check for permission before doing the addition.
* @param run the Runnable to run when the user right-click on the item in the hot bar.
* @return itself for daisy-chaining.
*/
public GUIHotBar addItem(ItemStack i, BiConsumer<PlayerInventory, ItemStack> setter, Consumer<Player> run) {
itemsAndSetters.put(i, setter);
@@ -62,9 +77,10 @@ public class GUIHotBar implements Listener {
}
/**
* Add the hot bar elements to this player.
*
* The player is automatically removed when they quit. You can remove it before by calling {@link #removePlayer(Player)}.
* Add the hot bar elements to this player, or update them if applicable.
* <br>
* The player is automatically removed when they quit. You can remove it before by calling {@link #removePlayer(Player, boolean)}.
* @param p the player to add.
*/
public void addPlayer(Player p) {
if (!currentPlayers.contains(p))
@@ -79,36 +95,62 @@ public class GUIHotBar implements Listener {
/**
* Detach this player from this hot bar manager and removes the managed items from the players inventory.
* @param p the player to remove.
*/
public void removePlayer(Player p) {
removePlayer(p, true);
}
/**
* Detach this player from this hot bar manager and optionally removes the managed items from the players inventory.
* @param p the player to remove.
* @param clearMenuItems if the items from this hot bar should be removed from the player inventory.
*/
public void removePlayer(Player p, boolean clearMenuItems) {
if (!currentPlayers.contains(p))
return;
for (ItemStack is : itemsAndSetters.keySet()) {
removeItemFromPlayer(p, is);
if (clearMenuItems) {
for (ItemStack is : itemsAndSetters.keySet()) {
removeItemFromPlayer(p, is);
}
}
currentPlayers.remove(p);
}
/**
* Tells if the provided player is attached to this hot bar.
* @param p the player to check.
* @return true if the player is attached, false otherwise.
*/
public boolean containsPlayer(Player p) {
return currentPlayers.contains(p);
}
/**
* Detach all players from this hot bar.
*/
public void removeAllPlayers() {
removeAllPlayers(true);
}
/**
* Detach all players from this hot bar.
* @param clearMenuItems if the items from this hot bar should be removed from the player inventory.
*/
public void removeAllPlayers(boolean clearMenuItems) {
for (Player p : new ArrayList<>(currentPlayers))
removePlayer(p);
removePlayer(p, clearMenuItems);
}
public void addItemToPlayer(Player p, ItemStack is) {
private void addItemToPlayer(Player p, ItemStack is) {
if (!itemsAndSetters.containsKey(is))
throw new IllegalArgumentException("The provided ItemStack is not registered in this GUIHotBar");
if (!currentPlayers.contains(p))
@@ -116,7 +158,7 @@ public class GUIHotBar implements Listener {
itemsAndSetters.get(is).accept(p.getInventory(), is.clone());
}
public void removeItemFromPlayer(Player p, ItemStack is) {
private void removeItemFromPlayer(Player p, ItemStack is) {
p.getInventory().remove(is);
}
@@ -125,7 +167,7 @@ public class GUIHotBar implements Listener {
@EventHandler
public void onPlayerDropItem(PlayerDropItemEvent event) {
void onPlayerDropItem(PlayerDropItemEvent event) {
if (!currentPlayers.contains(event.getPlayer()))
return;
@@ -140,7 +182,7 @@ public class GUIHotBar implements Listener {
@EventHandler
public void onPlayerInteract(PlayerInteractEvent event) {
void onPlayerInteract(PlayerInteractEvent event) {
if (!currentPlayers.contains(event.getPlayer()))
return;
@@ -169,7 +211,7 @@ public class GUIHotBar implements Listener {
@EventHandler
public void onInventoryClick(InventoryClickEvent event) {
void onInventoryClick(InventoryClickEvent event) {
if (event.getClickedInventory() == null || !(event.getClickedInventory() instanceof PlayerInventory inv))
return;
@@ -194,24 +236,23 @@ public class GUIHotBar implements Listener {
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
void onPlayerQuit(PlayerQuitEvent event) {
removePlayer(event.getPlayer());
}
@EventHandler
public void onPlayerDeath(PlayerDeathEvent event) {
void onPlayerDeath(PlayerDeathEvent event) {
if (!currentPlayers.contains(event.getEntity()))
return;
event.getDrops().removeAll(itemsAndSetters.keySet());
}
@EventHandler
public void onPlayerRespawn(PlayerRespawnEvent event) {
void onPlayerRespawn(PlayerRespawnEvent event) {
if (!currentPlayers.contains(event.getPlayer()))
return;
currentPlayers.remove(event.getPlayer());
addPlayer(event.getPlayer());
}

View File

@@ -38,7 +38,7 @@ public class GUIInventory implements Listener {
if (title == null)
inv = Bukkit.createInventory(null, nbLines * 9);
else
inv = Bukkit.createInventory(null, nbLines * 9, title.getAdv());
inv = Bukkit.createInventory(null, nbLines * 9, title.get());
setCloseEvent(closeEventAction);
@@ -49,7 +49,11 @@ public class GUIInventory implements Listener {
Bukkit.getPluginManager().registerEvents(this, PandaLibPaper.getPlugin());
}
/**
* Sets the action when the player closes the inventory window.
* @param closeEventAction the action to run.
*/
protected void setCloseEvent(Consumer<InventoryCloseEvent> closeEventAction) {
onCloseEvent = closeEventAction;
}
@@ -188,13 +192,13 @@ public class GUIInventory implements Listener {
onCloseEvent.accept(event);
isOpened = false;
}
/**
* Tells the number of inventory line needed to show the provided number of slot.
* @param nb the number of slot.
* @return the number of line.
*/
public static int nbLineForNbElements(int nb) {
return nb / 9 + ((nb % 9 == 0) ? 0 : 1);
}

View File

@@ -0,0 +1,232 @@
package fr.pandacube.lib.paper.inventory;
import com.google.common.base.Preconditions;
import org.bukkit.Material;
import org.bukkit.entity.HumanEntity;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.Objects;
/**
* Dummy implementation of a player inventory.
*/
public class DummyPlayerInventory extends InventoryWrapper implements PlayerInventory {
/**
* Total number of item slots in the player inventory.
*/
public static final int PLAYER_INVENTORY_SIZE = 43; // 36 base inventory + 4 armor slots + 1 off hand + 2 hidden slots (body and saddle)
private int heldItemSlot;
/**
* Creates a dummy player inventory.
* @param base the inventory itself.
* @param heldItemSlot the currently held item slot, from 0 to 8.
*/
public DummyPlayerInventory(Inventory base, int heldItemSlot) {
super(base);
if (base.getSize() < PLAYER_INVENTORY_SIZE)
throw new IllegalArgumentException("base inventory should have a size of " + PLAYER_INVENTORY_SIZE + " (" + base.getSize() + " given).");
if (heldItemSlot < 0 || heldItemSlot > 8)
throw new IllegalArgumentException("heldItemSlot should be between 0 and 8 inclusive.");
this.heldItemSlot = heldItemSlot;
}
@Override
public @Nullable ItemStack @NotNull [] getStorageContents() {
return Arrays.copyOfRange(getContents(), 0, 36);
}
@Override
public @Nullable ItemStack @NotNull [] getArmorContents() {
return Arrays.copyOfRange(getContents(), 36, 40);
}
@Override
public void setArmorContents(@Nullable ItemStack[] items) {
this.setSlots(items, 36, 4);
}
@Override
public @Nullable ItemStack @NotNull [] getExtraContents() {
return Arrays.copyOfRange(getContents(), 40, getSize());
}
@Override
public void setExtraContents(@Nullable ItemStack[] items) {
this.setSlots(items, 40, getSize() - 40);
}
private void setSlots(ItemStack[] items, int baseSlot, int length) {
if (items == null) {
items = new ItemStack[length];
}
Preconditions.checkArgument(items.length <= length, "items.length must be < %s", length);
for (int i = 0; i < length; i++) {
if (i >= items.length) {
this.setItem(baseSlot + i, null);
} else {
this.setItem(baseSlot + i, items[i]);
}
}
}
@Override
public ItemStack getHelmet() {
return getItem(39);
}
@Override
public void setHelmet(@Nullable ItemStack helmet) {
setItem(39, helmet);
}
@Override
public ItemStack getChestplate() {
return getItem(38);
}
@Override
public void setChestplate(@Nullable ItemStack chestplate) {
setItem(38, chestplate);
}
@Override
public ItemStack getLeggings() {
return getItem(37);
}
@Override
public void setLeggings(@Nullable ItemStack leggings) {
setItem(37, leggings);
}
@Override
public ItemStack getBoots() {
return getItem(36);
}
@Override
public void setBoots(@Nullable ItemStack boots) {
setItem(36, boots);
}
/**
* Gets the item stack in the SADDLE {@link EquipmentSlot}.
* @return the SADDLE item stack.
*/
public ItemStack getSaddle() {
return getItem(42);
}
/**
* Puts the provided item stack in the SADDLE {@link EquipmentSlot}.
* @param saddle the item.
*/
public void setSaddle(@Nullable ItemStack saddle) {
setItem(42, saddle);
}
/**
* Gets the item stack in the BODY {@link EquipmentSlot}.
* @return the BODY item stack.
*/
public ItemStack getBody() {
return getItem(41);
}
/**
* Puts the provided item stack in the BODY {@link EquipmentSlot}.
* @param body the item.
*/
public void setBody(@Nullable ItemStack body) {
setItem(41, body);
}
@Override
public void setItem(EquipmentSlot slot, ItemStack item) {
Preconditions.checkArgument(slot != null, "slot must not be null");
switch (slot) {
case HAND -> this.setItemInMainHand(item);
case OFF_HAND -> this.setItemInOffHand(item);
case FEET -> this.setBoots(item);
case LEGS -> this.setLeggings(item);
case CHEST -> this.setChestplate(item);
case HEAD -> this.setHelmet(item);
case BODY -> this.setBody(item);
case SADDLE -> this.setSaddle(item);
}
}
@Override
public @NotNull ItemStack getItem(@NotNull EquipmentSlot slot) {
return switch (slot) {
case HAND -> this.getItemInMainHand();
case OFF_HAND -> this.getItemInOffHand();
case FEET -> Objects.requireNonNullElseGet(this.getBoots(), () -> new ItemStack(Material.AIR));
case LEGS -> Objects.requireNonNullElseGet(this.getLeggings(), () -> new ItemStack(Material.AIR));
case CHEST -> Objects.requireNonNullElseGet(this.getChestplate(), () -> new ItemStack(Material.AIR));
case HEAD -> Objects.requireNonNullElseGet(this.getHelmet(), () -> new ItemStack(Material.AIR));
case BODY -> Objects.requireNonNullElseGet(this.getBody(), () -> new ItemStack(Material.AIR)); // for horses/wolves armor
case SADDLE -> Objects.requireNonNullElseGet(this.getSaddle(), () -> new ItemStack(Material.AIR));
};
}
@Override
public @NotNull ItemStack getItemInMainHand() {
return Objects.requireNonNullElse(getItem(heldItemSlot), new ItemStack(Material.AIR));
}
@Override
public void setItemInMainHand(@Nullable ItemStack item) {
setItem(heldItemSlot, item);
}
@Override
public @NotNull ItemStack getItemInOffHand() {
return Objects.requireNonNullElse(getItem(40), new ItemStack(Material.AIR));
}
@Override
public void setItemInOffHand(@Nullable ItemStack item) {
setItem(40, item);
}
@Override
public @NotNull ItemStack getItemInHand() {
return getItemInMainHand();
}
@Override
public void setItemInHand(@Nullable ItemStack stack) {
setItemInMainHand(stack);
}
@Override
public int getHeldItemSlot() {
return heldItemSlot;
}
@Override
public void setHeldItemSlot(int slot) {
if (slot < 0 || slot > 8)
throw new IllegalArgumentException("Slot is not between 0 and 8 inclusive");
heldItemSlot = slot;
}
@Override
public @Nullable HumanEntity getHolder() {
return null;
}
}

View File

@@ -1,4 +1,4 @@
package fr.pandacube.lib.paper.util;
package fr.pandacube.lib.paper.inventory;
import org.bukkit.Location;
import org.bukkit.Material;
@@ -13,12 +13,22 @@ import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Objects;
/**
* Wrapper for an {@link Inventory}.
* Can be overridden to add specific behaviour to some methods.
*/
public class InventoryWrapper implements Inventory {
private final Inventory base;
/**
* Creates a wrapper for the provided inventory.
* @param base the wrapped inventory. Cannot be null.
* @throws NullPointerException if the base inventory is null.
*/
public InventoryWrapper(Inventory base) {
this.base = base;
this.base = Objects.requireNonNull(base, "base inventory cannot be null.");
}

View File

@@ -0,0 +1,413 @@
package fr.pandacube.lib.paper.inventory;
import com.google.common.collect.Streams;
import fr.pandacube.lib.chat.Chat;
import io.papermc.paper.datacomponent.DataComponentType;
import io.papermc.paper.datacomponent.DataComponentType.Valued;
import io.papermc.paper.datacomponent.DataComponentTypes;
import io.papermc.paper.datacomponent.item.ResolvableProfile;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.Damageable;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import static fr.pandacube.lib.chat.ChatStatic.chatComponent;
/**
* A builder for {@link ItemStack}.
*/
public class ItemStackBuilder {
/**
* Create a builder with a clone of the provided ItemStack.
* <p>
* The returned builder will not alter the provided ItemStack.
* If you want to modify the ItemStack with the builder, please use {@link #wrap(ItemStack)}.
* @param base the original ItemStack.
* @return the builder
*/
public static ItemStackBuilder of(ItemStack base) {
return wrap(base.clone());
}
/**
* Create a builder of a new ItemStack with the specified Material.
* @param mat the material of the new built ItemStack
* @return the builder
*/
public static ItemStackBuilder of(Material mat) {
return wrap(new ItemStack(mat));
}
/**
* Create a builder that will alter the data of the provided ItemStack.
* <p>
* The {@link #build()} method of the returned builder will return the same instance
* of ItemStack as the parameter of this method.
* <p>
* To create a builder that doesn't modify the provided ItemStack, use {@link #of(ItemStack)}.
* @param stack the wrapped item stack.
* @return the builder
*/
public static ItemStackBuilder wrap(ItemStack stack) {
return new ItemStackBuilder(stack);
}
private final ItemStack stack;
private ItemStackBuilder(ItemStack base) {
stack = base;
}
/**
* Runs the provided updater on the {@link ItemMeta} instance of the built stack.
* @param metaUpdater the updater that will modify the meta.
* @return itself.
*/
public ItemStackBuilder meta(Consumer<ItemMeta> metaUpdater) {
return meta(metaUpdater, ItemMeta.class);
}
/**
* Runs the provided updater on the {@link ItemMeta} instance of the built stack.
* @param metaUpdater the updater that will modify the meta.
* @param metaType the type of the meta instance.
* @param <T> the type of item meta.
* @return itself.
*/
public <T extends ItemMeta> ItemStackBuilder meta(Consumer<T> metaUpdater, Class<T> metaType) {
stack.editMeta(metaType, metaUpdater);
return this;
}
/**
* Sets the amount of the built stack.
* @param a the new amount.
* @return itself.
*/
public ItemStackBuilder amount(int a) {
stack.setAmount(a);
return this;
}
/**
* Sets the display name of the item, directly passing to {@link ItemMeta#displayName(Component)}.
* @param displayName the new display name. Can be null to unset.
* @return itself.
*/
public ItemStackBuilder rawDisplayName(Component displayName) {
return meta(m -> m.displayName(displayName));
}
/**
* Sets the display name of the item, filtering to make default italic to false.
* @param displayName the new display name. Can be null to unset.
* @return itself.
*/
public ItemStackBuilder displayName(ComponentLike displayName) {
return rawDisplayName(displayName != null
? Chat.italicFalseIfNotSet(chatComponent(displayName)).asComponent()
: null);
}
/**
* Sets the lore of the item, directly passing to {@link ItemMeta#lore(List)}.
* @param lore the new lore. Can be null to unset.
* @return itself.
*/
public ItemStackBuilder rawLore(List<Component> lore) {
return meta(m -> m.lore(lore));
}
/**
* Sets the lore of the item, filtering to make default italic to false.
* @param lore the new lore. Can be null to unset.
* @return itself.
*/
public ItemStackBuilder lore(List<? extends ComponentLike> lore) {
if (lore != null) {
return rawLore(lore.stream()
.map(line -> Chat.italicFalseIfNotSet(chatComponent(line)).get())
.toList());
}
else
return rawLore(Collections.emptyList());
}
/**
* Adds new lore lines to the existing lore of the item.
* @param lores the added lore lines.
* @return itself.
*/
public ItemStackBuilder addLoreAfter(List<? extends ComponentLike> lores) {
if (lores != null) {
List<Component> baseLore = stack.getItemMeta().lore();
if (baseLore == null) baseLore = Collections.emptyList();
return rawLore(
Streams.concat(
baseLore.stream(),
lores.stream()
.map(line -> Chat.italicFalseIfNotSet(chatComponent(line)).get())
)
.toList());
}
else
return this;
}
/**
* Adds new lore lines to the existing lore of the item.
* @param lores the added lore lines.
* @return itself.
*/
public ItemStackBuilder addLoreAfter(ComponentLike... lores) {
if (lores == null || lores.length == 0)
return this;
return addLoreAfter(Arrays.asList(lores));
}
/**
* Enchant the item.
* Supports unsafe enchants.
* @param enchantment the enchantment.
* @param level the enchant level.
* @return itself.
*/
public ItemStackBuilder enchant(Enchantment enchantment, int level) {
return meta(m -> m.addEnchant(enchantment, level, true));
}
/**
* Adds the provided flags to the item.
* @param flags he flags to add.
* @return itself.
*/
public ItemStackBuilder flags(ItemFlag... flags) {
return flags(true, flags);
}
/**
* Adds or removes the provided flags to the item.
* @param add true to add, false to remove.
* @param flags he flags to add.
* @return itself.
*/
public ItemStackBuilder flags(boolean add, ItemFlag... flags) {
return add ? meta(m -> m.addItemFlags(flags))
: meta(m -> m.removeItemFlags(flags));
}
/**
* Hides the enchants from the tooltip of the item.
* Will set the {@link ItemFlag#HIDE_ENCHANTS} flag of the item.
* @return itself.
*/
public ItemStackBuilder hideEnchants() {
return hideEnchants(true);
}
/**
* Sets or unsets the {@link ItemFlag#HIDE_ENCHANTS} flag of the item.
* @param hide true to hide, false to show.
* @return itself.
*/
public ItemStackBuilder hideEnchants(boolean hide) {
return flags(hide, ItemFlag.HIDE_ENCHANTS);
}
/**
* Hides the attributes from the tooltip of the item.
* Will set the {@link ItemFlag#HIDE_ATTRIBUTES} flag of the item.
* @return itself.
*/
public ItemStackBuilder hideAttributes() {
return hideAttributes(true);
}
/**
* Sets or unsets the {@link ItemFlag#HIDE_ATTRIBUTES} flag of the item.
* @param hide true to hide, false to show.
* @return itself.
*/
public ItemStackBuilder hideAttributes(boolean hide) {
return flags(hide, ItemFlag.HIDE_ATTRIBUTES);
}
/**
* Apply the enchantment glint to the item, event if it's not enchant.
* @return itself.
*/
public ItemStackBuilder fakeEnchant() {
return fakeEnchant(true);
}
/**
* Sets the enchantment glint override to the item.
* @param apply true to enforce the enchantment glint, false to set to default.
* @return itself.
*/
public ItemStackBuilder fakeEnchant(boolean apply) {
return meta(m -> m.setEnchantmentGlintOverride(apply ? true : null));
}
/**
* Sets this item as unbreakable.
* @return itself.
*/
public ItemStackBuilder unbreakable() {
return unbreakable(true);
}
/**
* Sets the unbreakable status of this item.
* @param unbreakable the unbreakable status.
* @return itself.
*/
public ItemStackBuilder unbreakable(boolean unbreakable) {
return meta(m -> m.setUnbreakable(unbreakable));
}
/**
* Sets the damage value of this item.
* @param d the new damage value.
* @return itself.
*/
public ItemStackBuilder damage(int d) {
return meta(m -> m.setDamage(d), Damageable.class);
}
/**
* Sets a value for a data component of this item.
* @param dataType the data component type.
* @param dataValue the data component value.
* @return itself.
* @param <T> the data component API type.
*/
public <T> ItemStackBuilder data(Valued<T> dataType, T dataValue) {
stack.setData(dataType, dataValue);
return this;
}
/**
* Unset (set to empty) a value for a data component of this item.
* @param dataType the data component type.
* @return itself.
*/
public ItemStackBuilder unsetData(DataComponentType dataType) {
stack.unsetData(dataType);
return this;
}
/**
* Reset (act as default) a value for a data component of this item.
* @param dataType the data component type.
* @return itself.
*/
public ItemStackBuilder resetData(DataComponentType dataType) {
stack.resetData(dataType);
return this;
}
/**
* Sets the {@code can_break} data component to the provided list of {@link Material}.
* @param canBreak a list of {@link Material}.
* @return itself.
*/
public ItemStackBuilder canBreakMaterials(Collection<Material> canBreak) {
return canBreak(canBreak.stream().map(Material::getKey).toList());
}
/**
* Sets the {@code can_break} data component to the provided list of {@link NamespacedKey}.
* @param canBreak a list of block predicate. If empty, unsets the data component. If null, reset to default.
* @return itself.
*/
@SuppressWarnings("removal")
public ItemStackBuilder canBreak(Collection<NamespacedKey> canBreak) {
@SuppressWarnings("unchecked")
Collection<com.destroystokyo.paper.Namespaced> nsCanBreak = (Collection<com.destroystokyo.paper.Namespaced>) (Collection<?>) canBreak;
return meta(m -> m.setDestroyableKeys(nsCanBreak));
/*
if (canBreak == null)
return resetData(DataComponentTypes.CAN_BREAK);
else if (canBreak.isEmpty())
return unsetData(DataComponentTypes.CAN_BREAK);
else
return data(DataComponentTypes.CAN_BREAK, ItemAdventurePredicate.itemAdventurePredicate(canBreak));*/
}
/**
* Sets the {@code can_place_on} data component to the provided list of {@link Material}.
* @param canPlaceOn a list of {@link Material}.
* @return itself.
*/
public ItemStackBuilder canPlaceOnMaterials(Collection<Material> canPlaceOn) {
return canPlaceOn(canPlaceOn.stream().map(Material::getKey).toList());
}
/**
* Sets the {@code can_place_on} data component to the provided list of {@link NamespacedKey}.
* @param canPlaceOn a list of block predicate. If empty, unsets the data component. If null, reset to default.
* @return itself.
*/
@SuppressWarnings("removal")
public ItemStackBuilder canPlaceOn(Collection<NamespacedKey> canPlaceOn) {
@SuppressWarnings("unchecked")
Collection<com.destroystokyo.paper.Namespaced> nsCanPlaceOn = (Collection<com.destroystokyo.paper.Namespaced>) (Collection<?>) canPlaceOn;
return meta(m -> m.setPlaceableKeys(nsCanPlaceOn));
/* if (canPlaceOn == null)
return resetData(DataComponentTypes.CAN_PLACE_ON);
else if (canPlaceOn.isEmpty())
return unsetData(DataComponentTypes.CAN_PLACE_ON);
else
return data(DataComponentTypes.CAN_PLACE_ON, ItemAdventurePredicate.itemAdventurePredicate(canPlaceOn)); */
}
/**
* Sets the {@code profile} data component to the provided profile.
* @param profile the profile to use as the component value.
* @return itself.
*/
public ItemStackBuilder profile(ResolvableProfile profile) {
return data(DataComponentTypes.PROFILE, profile);
}
/**
* Build the {@link ItemStack}.
* @return the build item stack.
*/
public ItemStack build() {
return stack;
}
}

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