Compare commits

...

145 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
279 changed files with 6851 additions and 5287 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
/.idea
/*/target
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,32 +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.md-5</groupId>
<artifactId>bungeecord-chat</artifactId>
<version>${bungeecord.version}</version>
<scope>compile</scope>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-minimessage</artifactId>
<version>4.15.0</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10</version>
<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);
/* 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]));
}
/**
* 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 });
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,11 +15,6 @@
<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>
@@ -32,27 +27,16 @@
<artifactId>pandalib-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>fr.pandacube.lib</groupId>
<artifactId>pandalib-reflect</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>fr.pandacube.lib</groupId>
<artifactId>pandalib-commands</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-log</artifactId>
<version>${bungeecord.version}</version>
</dependency>
<dependency>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-config</artifactId>
<version>${bungeecord.version}</version>
</dependency>
</dependencies>

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();
}
} catch (IOException e) {
Log.severe(e);
Thread.ofVirtual().name("CLICmdThread #"+(i++))
.start(() -> CLIBrigadierDispatcher.instance.execute(cmdLine));
}
} 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,14 +64,22 @@ public abstract class CLIApplication {
}
}
/**
* Returns the application's {@link Logger}.
* @return the application's {@link Logger}.
*/
public Logger getLogger() {
return cli.getLogger();
}
private final Thread shutdownThread = new Thread(this::stop);
private final AtomicBoolean stopping = new AtomicBoolean(false);
@SuppressWarnings("finally")
/**
* Stops this application.
*/
public final void stop() {
synchronized (stopping) {
if (stopping.get())
@@ -77,23 +93,45 @@ public abstract class CLIApplication {
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

@@ -39,13 +39,6 @@ public abstract class CLIBrigadierCommand extends BrigadierCommand<CLICommandSen
protected abstract LiteralArgumentBuilder<CLICommandSender> buildCommand();
protected String[] getAliases() {
return new String[0];
}
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

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;
}
@@ -71,4 +98,6 @@ public class CLILogger {
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,10 +241,13 @@ 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)) {
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) {
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

@@ -238,4 +238,7 @@ 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,11 +68,11 @@ public class ProtocolVersion implements Comparable<ProtocolVersion> {
private static void init() {
// try online source first
try {
HttpResponse<String> response = HttpClient.newBuilder()
try (HttpClient cl = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build()
.send(HttpRequest.newBuilder(URI.create(ONLINE_DATA_URL)).build(),
.build()) {
HttpResponse<String> response = cl.send(
HttpRequest.newBuilder(URI.create(ONLINE_DATA_URL)).build(),
BodyHandlers.ofString()
);
if (response.statusCode() == 200) {
@@ -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.
@@ -361,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();
@@ -389,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,6 +88,8 @@ public class PaperBackupManager extends BackupManager implements Listener {
public void run() {
try {
SchedulerUtil.runOnServerThreadAndWait(super::run);
} 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;
/**
* 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();
@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) {
registerAlias(alias, false);
registerAlias(pluginName + ":" + alias, true);
if (!registeredAliases.contains(alias)) {
Log.info("Command /" + commandNode.getName() + ": forcing registration of alias " + alias);
registeredAliases.addAll(event.registrar().register(getAliasNode(commandNode, alias), description));
}
}
}
private void registerAlias(String alias, boolean prefixed) {
LiteralCommandNode<BukkitBrigadierCommandSource> node = literal(alias)
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 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;
}
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 {
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;
}
if (!nmsRegistered) {
return;
}
registeredAliases.add(name);
// bukkit dispatcher conflict resolution
boolean bukkitRegister = false;
CommandMap bukkitCmdMap = Bukkit.getCommandMap();
Command bukkitConflicted = bukkitCmdMap.getCommand(name);
if (bukkitConflicted != null) {
if (!isFromThisCommand(bukkitConflicted)) {
if (forceRegistration) {
bukkitRegister = true;
Log.info("Overwriting Bukkit command /" + name
+ " (" + getCommandIdentity(bukkitConflicted) + ")");
}
else if (prefixed || !isAlias) {
Log.severe("/" + name + " already in Bukkit dispatcher (" + getCommandIdentity(bukkitConflicted) + ")." +
" Wont replace it because registration is not forced for prefixed or initial name of a command.");
}
else {
Log.info("/" + name + " already in Bukkit dispatcher (" + getCommandIdentity(bukkitConflicted) + ")." +
" Wont replace it because registration is not forced for a non-prefixed alias.");
}
}
}
else {
bukkitRegister = true;
}
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 (BukkitCommandNode.REFLECT.get().isInstance(command)) {
BukkitCommandNode wrappedBCN = wrap(command, BukkitCommandNode.class);
Command bukkitCmd = wrappedBCN.getBukkitCommand();
if (bukkitCmd instanceof PluginCommand cmd) {
return "Bukkit command: /" + cmd.getName() + " from plugin " + cmd.getPlugin().getName();
return "Node /" + command.getName() + " wrapping Bukkit command /" + bukkitCmd.getName() + " from plugin " + cmd.getPlugin().getName();
}
else if (VanillaCommandWrapper.REFLECT.get().isInstance(bukkitCmd)) {
return "Vanilla command: /" + bukkitCmd.getName();
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 bukkitCmd.getClass().getName() + ": /" + bukkitCmd.getName();
return "Node /" + command.getName() + " wrapping " + bukkitCmd.getClass().getName() + " /" + bukkitCmd.getName();
}
else {
@SuppressWarnings("unchecked")
fr.pandacube.lib.paper.reflect.wrapper.brigadier.CommandNode<CommandSourceStack> wrappedCommandNode = wrap(command, fr.pandacube.lib.paper.reflect.wrapper.brigadier.CommandNode.class);
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 {
@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;
}
}
/**
* 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

@@ -63,6 +63,7 @@ public class DirectionalVector {
* 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(
@@ -79,6 +80,7 @@ public class DirectionalVector {
/**
* 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
@@ -126,7 +128,10 @@ public class DirectionalVector {
}
/**
* 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);
}
@@ -135,7 +140,8 @@ public class DirectionalVector {
/**
* Set the yaw and the pitch of the provided {@link Location}
* with the values inside the current {@link DirectionalVector}
* after conversion of these values
* after conversion of these values.
* @param l the location.
*/
public void putIntoLocation(Location l) {
/* std : -PI/2 : <0 ? +2PI : MC
@@ -148,7 +154,10 @@ public class DirectionalVector {
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,
@@ -163,6 +172,7 @@ public class DirectionalVector {
* 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();
@@ -171,6 +181,7 @@ public class DirectionalVector {
/**
* 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(
@@ -182,6 +193,7 @@ public class DirectionalVector {
/**
* 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(
@@ -194,6 +206,7 @@ public class DirectionalVector {
/**
* 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(
@@ -206,6 +219,7 @@ public class DirectionalVector {
/**
* 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(

View File

@@ -2,24 +2,29 @@ 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>.
*/
public static final double PI = Math.PI;
static final double PI = Math.PI;
/**
* Value equal to <code>{@link Math#PI} / 2</code>.
*/
public static final double PId2 = PI/2;
static final double PId2 = PI/2;
/**
* Value equal to <code>{@link Math#PI} * 2</code>.
*/
public static final double PIx2 = PI*2;
static final double PIx2 = PI*2;
@@ -55,9 +60,21 @@ public class GeometryUtil {
* 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;
@@ -72,11 +89,10 @@ public class GeometryUtil {
/**
* 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 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).
@@ -129,27 +145,22 @@ public class GeometryUtil {
/**
* 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) {
final double epsilon = 0.0001f;
Vector d = end.clone().subtract(start).multiply(0.5);
Vector e = max.clone().subtract(min).multiply(0.5);
Vector c = start.clone().add(d).subtract(min.clone().add(max).multiply(0.5));
Vector ad = d.clone();
ad.setX(Math.abs(ad.getX()));
ad.setY(Math.abs(ad.getY()));
ad.setZ(Math.abs(ad.getZ()));
return !(
Math.abs(c.getX()) > e.getX() + ad.getX()
|| Math.abs(c.getY()) > e.getY() + ad.getY()
|| Math.abs(c.getZ()) > e.getX() + ad.getZ()
|| Math.abs(d.getY() * c.getZ() - d.getZ() * c.getY()) > e.getY() * ad.getZ() + e.getZ() * ad.getY() + epsilon
|| Math.abs(d.getZ() * c.getX() - d.getX() * c.getZ()) > e.getZ() * ad.getX() + e.getX() * ad.getZ() + epsilon
|| Math.abs(d.getX() * c.getY() - d.getY() * c.getX()) > e.getX() * ad.getY() + e.getY() * ad.getX() + epsilon
);
RayTraceResult res = BoundingBox.of(min, max).rayTrace(start, end.clone().subtract(start), end.distance(start));
return res != null;
}
private GeometryUtil() {}
}

View File

@@ -2,11 +2,10 @@ package fr.pandacube.lib.paper.geometry.blocks;
import fr.pandacube.lib.util.RandomUtil;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.util.BlockVector;
import org.bukkit.util.BoundingBox;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import java.util.Iterator;
@@ -30,10 +29,19 @@ public class AABBBlock implements BlockSet, Cloneable {
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();
@@ -42,14 +50,34 @@ public class AABBBlock implements BlockSet, Cloneable {
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
@@ -74,22 +102,45 @@ public class AABBBlock implements BlockSet, Cloneable {
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);
}
@@ -101,7 +152,6 @@ public class AABBBlock implements BlockSet, Cloneable {
}
public boolean overlaps(BoundingBox bb) {
return asBukkitBoundingBox().overlaps(bb);
}
@@ -110,6 +160,10 @@ public class AABBBlock implements BlockSet, Cloneable {
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();
}
@@ -118,6 +172,10 @@ public class AABBBlock implements BlockSet, Cloneable {
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(),
@@ -134,7 +192,7 @@ public class AABBBlock implements BlockSet, Cloneable {
}
@Override
public Iterator<BlockVector> iterator() {
public @NotNull Iterator<BlockVector> iterator() {
return new Iterator<>() {
private int x = pos1.getBlockX(),
y = pos1.getBlockY(),
@@ -164,23 +222,16 @@ public class AABBBlock implements BlockSet, Cloneable {
}
public Iterable<Block> asBlockIterable(World w) {
return () -> new Iterator<>() {
final Iterator<BlockVector> nested = AABBBlock.this.iterator();
@Override
public boolean hasNext() {
return nested.hasNext();
public String toString() {
return "{(" + pos1.getBlockX() +
", " + pos1.getBlockY() +
", " + pos1.getBlockZ() +
"), (" + pos2.getBlockX() +
", " + pos2.getBlockY() +
", " + pos2.getBlockZ() +
")}";
}
@Override
public Block next() {
BlockVector bv = nested.next();
return w.getBlockAt(bv.getBlockX(), bv.getBlockY(), bv.getBlockZ());
}
};
}
static boolean overlap(AABBBlock aabb1, AABBBlock aabb2) {
return aabb1.asBukkitBoundingBox().overlaps(aabb2.asBukkitBoundingBox());

View File

@@ -12,12 +12,22 @@ 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.");
@@ -25,6 +35,10 @@ public class AABBBlockGroup implements BlockSet {
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));
}
@@ -35,11 +49,11 @@ public class AABBBlockGroup implements BlockSet {
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.getBlockZ()));
pos1.setZ(Math.min(pos1.getBlockY(), aabb.pos1.getBlockZ()));
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.getBlockZ() - 1));
pos2.setZ(Math.max(pos2.getBlockY(), aabb.pos2.getBlockZ() - 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);
}
@@ -86,9 +100,13 @@ public class AABBBlockGroup implements BlockSet {
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))

View File

@@ -1,23 +1,58 @@
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)
@@ -37,20 +72,78 @@ public interface BlockSet extends Iterable<BlockVector> {
}
/**
* 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));
}
default boolean isInside(Entity p) {
return isInside(p.getLocation());
/**
* 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;

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;
/**
@@ -38,12 +38,20 @@ public class GUIHotBar implements Listener {
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);
@@ -53,9 +61,10 @@ public class GUIHotBar implements Listener {
/**
* 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);
@@ -69,8 +78,9 @@ public class GUIHotBar implements Listener {
/**
* 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))
@@ -85,6 +95,7 @@ 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);
@@ -92,6 +103,8 @@ public class GUIHotBar implements Listener {
/**
* 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))
@@ -107,17 +120,27 @@ public class GUIHotBar implements Listener {
}
/**
* 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, clearMenuItems);
@@ -127,7 +150,7 @@ public class GUIHotBar implements Listener {
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))
@@ -135,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);
}
@@ -144,7 +167,7 @@ public class GUIHotBar implements Listener {
@EventHandler
public void onPlayerDropItem(PlayerDropItemEvent event) {
void onPlayerDropItem(PlayerDropItemEvent event) {
if (!currentPlayers.contains(event.getPlayer()))
return;
@@ -159,7 +182,7 @@ public class GUIHotBar implements Listener {
@EventHandler
public void onPlayerInteract(PlayerInteractEvent event) {
void onPlayerInteract(PlayerInteractEvent event) {
if (!currentPlayers.contains(event.getPlayer()))
return;
@@ -188,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;
@@ -213,20 +236,20 @@ 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;

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);
@@ -50,6 +50,10 @@ public class GUIInventory implements Listener {
}
/**
* Sets the action when the player closes the inventory window.
* @param closeEventAction the action to run.
*/
protected void setCloseEvent(Consumer<InventoryCloseEvent> closeEventAction) {
onCloseEvent = closeEventAction;
}
@@ -190,11 +194,11 @@ public class GUIInventory implements Listener {
}
/**
* 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