Compare commits
	
		
			156 Commits
		
	
	
		
			3e0297c8af
			...
			medrias
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 27c444f3b4 | |||
| c589da2a14 | |||
| 3fe4a1b244 | |||
| 1925dd9b36 | |||
| d637b92f6c | |||
| af4bab0d12 | |||
| 44dc690736 | |||
| c9af5ad308 | |||
| 27403a6e20 | |||
| 38a42dcca0 | |||
| 5782046b7a | |||
| 2b407d7f27 | |||
| 8f5f880754 | |||
| 3d92c3afb6 | |||
| 5e1f98ab70 | |||
| 276f5b2dc1 | |||
| 9c72b8cda4 | |||
| ee023b5d2c | |||
| 974347cbde | |||
| e6b77bcad6 | |||
| 36eb1227cf | |||
| 4be37945cb | |||
| 3e6cf96040 | |||
| d1a04a7a66 | |||
| fcac9af7d1 | |||
| e16487431d | |||
| 50f5ab671a | |||
| 5a04052f8e | |||
| c86855ac16 | |||
| 001239fe57 | |||
| 0c7fb9b370 | |||
| f416e30d45 | |||
| e271ac2964 | |||
| 2bb09ad42b | |||
| 4f55890092 | |||
| 76470b963e | |||
| 76fc673e98 | |||
| 307b5132df | |||
| ac52e024f3 | |||
| bb6d221ced | |||
| 2acfa53b63 | |||
| 640b255e1d | |||
| ed0db5391d | |||
| cef4af80f0 | |||
| 7f56645ba5 | |||
| 827c13887c | |||
| 0ff2ae1296 | |||
| e7b528718c | |||
| d411618e63 | |||
| decf302851 | |||
| d3097781bc | |||
| 2942a030a6 | |||
| 69af006001 | |||
| c60fb613d4 | |||
| 33e4e053cf | |||
| e02ccc2b60 | |||
| 1c22518dd9 | |||
| 5d294ea172 | |||
| 56632867ec | |||
| 0c074b9354 | |||
| bdf60785e8 | |||
| ca7a51af2c | |||
| d7705d8904 | |||
| 649e1a56c8 | |||
| 7d89f0c376 | |||
| f494c3bdb3 | |||
| 23a7b940b7 | |||
| bd0e0484cd | |||
| 92a9afa22c | |||
| 2393352770 | |||
| 7795d94dfb | |||
| 3c6d77f0bb | |||
| e4eb6dc9c9 | |||
| 2d6d905b54 | |||
| 5a3831ba74 | |||
| 07f3841ee6 | |||
| d84e4c87dc | |||
| 49a32421c0 | |||
| 177733950d | |||
| 8149d8fb54 | |||
| eb72633dd8 | |||
| ece1bc70bf | |||
| 90009b8703 | |||
| a39f3d8143 | |||
| ecc9932f5e | |||
| e9b401f43d | |||
| 77b0a0c73c | |||
| 93960b83c2 | |||
| 075468854d | |||
| b82b59d57b | |||
| b85c5acb21 | |||
| ba496b0968 | |||
| ecd8b3b0c9 | |||
| 2f0b59a032 | |||
| 8f31ea54d1 | |||
| e2506d5d53 | |||
| 0e016881d7 | |||
| c7b33132a9 | |||
| a24eab67b6 | |||
| db06ab1be9 | |||
| 728961d19f | |||
| 8b6fe63df1 | |||
| da1ee9d882 | |||
| 455226b762 | |||
| 3ee806c1ea | |||
| 69a4f2fe6f | |||
| 62949948e1 | |||
| bd3bea8381 | |||
| 463a4d7e78 | |||
| 84298b08b3 | |||
| 9ac7a98257 | |||
| f16389d33d | |||
| a49061eb9f | |||
| 378e79b8ad | |||
| ae634ab560 | |||
| 45ab550d06 | |||
| 0fcd02c80d | |||
| 2d950117d3 | |||
| 2f476ce8f2 | |||
| 75e292b1b8 | |||
| 2969d51f72 | |||
| c0e0097b7b | |||
| d047be35d9 | |||
| 5fb17be4c7 | |||
| d7bb56e0b2 | |||
| 9e7d89cf70 | |||
| 79e4bb90f7 | |||
| 736e0f0c23 | |||
| 7481b12111 | |||
| 7c4fd78680 | |||
| 8c25fb0dd1 | |||
| d5a2aa1c30 | |||
| 7d5060d09b | |||
| a46e066669 | |||
| a4b33a1af7 | |||
| 5edd8cdfec | |||
| c984b63cee | |||
| 69b72ef90d | |||
| 555f5ab65c | |||
| 9f9fb55726 | |||
| 98d1a21aab | |||
| d59ae22970 | |||
| e6fc31e5ca | |||
| 70c4d59fdc | |||
| edd5b06a46 | |||
| 436c9b9381 | |||
| f3f616cdca | |||
| 20643fec62 | |||
| c79d9b8006 | |||
| 61fb7b3142 | |||
| f0a9fca952 | |||
| 913d5d91dd | |||
| 1cd3749d7d | |||
| e640cbb1a3 | |||
| edd4e89a6e | |||
| 9b83f9699c | 
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,3 +1,5 @@ | |||||||
| /.idea | /.idea | ||||||
| /*/target | /*/target | ||||||
| dependency-reduced-pom.xml | dependency-reduced-pom.xml | ||||||
|  |  | ||||||
|  | *.iml | ||||||
| @@ -3,7 +3,7 @@ | |||||||
| ### Development library for Minecraft server applications and plugins | ### Development library for Minecraft server applications and plugins | ||||||
|  |  | ||||||
| This repository contains a collection of maven modules that are used for the development of our Minecraft server. Those | This repository contains a collection of maven modules that are used for the development of our Minecraft server. Those | ||||||
| modules are made open source so they can be used by other developpers. Each of them provides different functionalities | modules are made open source, so they can be used by other developers. Each of them provides different functionalities | ||||||
| that are detailed in their respective Readme file (if any). | that are detailed in their respective Readme file (if any). | ||||||
|  |  | ||||||
| - `pandalib-util` General purpose utility and helper classes; | - `pandalib-util` General purpose utility and helper classes; | ||||||
| @@ -18,10 +18,10 @@ that are detailed in their respective Readme file (if any). | |||||||
| - `pandalib-players` A library to handle classes representing online or offline players; | - `pandalib-players` 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-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-netapi` A poorly designed, but working TCP network library; | ||||||
| - `pandalib-net` A better-designed, packet-based TCP network library (_still in development_); | - `pandalib-config` Utility and helper classes to handle configuration related files and folders; | ||||||
| - `pandalib-commands` An abstract command manager working on top of [Brigadier](https://github.com/Mojang/brigadier); | - `pandalib-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; | - `pandalib-core` A catch-all module for some helper classes that didn't have their own module yet; | ||||||
|  |  | ||||||
| ### Use in your projects | ### Use in your projects | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								pandalib-bungee-chat/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								pandalib-bungee-chat/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | /target/ | ||||||
							
								
								
									
										49
									
								
								pandalib-bungee-chat/pom.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								pandalib-bungee-chat/pom.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <project xmlns="http://maven.apache.org/POM/4.0.0" | ||||||
|  |          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||||||
|  |          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||||||
|  |     <parent> | ||||||
|  |         <artifactId>pandalib-parent</artifactId> | ||||||
|  |         <groupId>fr.pandacube.lib</groupId> | ||||||
|  |         <version>1.0-SNAPSHOT</version> | ||||||
|  |         <relativePath>../pom.xml</relativePath> | ||||||
|  |     </parent> | ||||||
|  |     <modelVersion>4.0.0</modelVersion> | ||||||
|  |  | ||||||
|  |     <artifactId>pandalib-bungee-chat</artifactId> | ||||||
|  |     <packaging>jar</packaging> | ||||||
|  |  | ||||||
|  |     <repositories> | ||||||
|  |         <repository> | ||||||
|  |             <id>bungeecord-repo</id> | ||||||
|  |             <url>https://oss.sonatype.org/content/repositories/snapshots</url> | ||||||
|  |         </repository> | ||||||
|  |     </repositories> | ||||||
|  |  | ||||||
|  |     <dependencies> | ||||||
|  |         <dependency> | ||||||
|  |             <groupId>fr.pandacube.lib</groupId> | ||||||
|  |             <artifactId>pandalib-util</artifactId> | ||||||
|  |             <version>${project.version}</version> | ||||||
|  |         </dependency> | ||||||
|  |         <dependency> | ||||||
|  |             <groupId>fr.pandacube.lib</groupId> | ||||||
|  |             <artifactId>pandalib-chat</artifactId> | ||||||
|  |             <version>${project.version}</version> | ||||||
|  |         </dependency> | ||||||
|  |  | ||||||
|  |         <dependency> | ||||||
|  |             <groupId>net.md-5</groupId> | ||||||
|  |             <artifactId>bungeecord-chat</artifactId> | ||||||
|  |             <version>${bungeecord.version}</version> | ||||||
|  |             <scope>provided</scope> | ||||||
|  |         </dependency> | ||||||
|  |  | ||||||
|  |         <dependency> | ||||||
|  |             <groupId>net.kyori</groupId> | ||||||
|  |             <artifactId>adventure-platform-bungeecord</artifactId> | ||||||
|  |             <version>4.3.0</version> | ||||||
|  |         </dependency> | ||||||
|  |     </dependencies> | ||||||
|  |  | ||||||
|  | </project> | ||||||
| @@ -0,0 +1,77 @@ | |||||||
|  | package fr.pandacube.lib.bungee.chat; | ||||||
|  |  | ||||||
|  | import fr.pandacube.lib.chat.Chat; | ||||||
|  | import fr.pandacube.lib.chat.Chat.FormatableChat; | ||||||
|  | import net.kyori.adventure.text.Component; | ||||||
|  | import net.kyori.adventure.text.ComponentLike; | ||||||
|  | import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer; | ||||||
|  | import net.md_5.bungee.api.chat.BaseComponent; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Utility class to ease conversion between our Adventure backed Chat API and BungeeCord chat API. | ||||||
|  |  */ | ||||||
|  | public class ChatBungee { | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Creates a {@link FormatableChat} from the provided Bungee {@link BaseComponent}. | ||||||
|  |      * @param c the {@link BaseComponent}. | ||||||
|  |      * @return a new {@link FormatableChat}. | ||||||
|  |      */ | ||||||
|  |     public static FormatableChat chatComponent(BaseComponent c) { | ||||||
|  |         return Chat.chatComponent(toAdventure(c)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Creates a {@link FormatableChat} from the provided Bungee {@link BaseComponent BaseComponent[]}. | ||||||
|  |      * @param c the array of {@link BaseComponent}. | ||||||
|  |      * @return a new {@link FormatableChat}. | ||||||
|  |      */ | ||||||
|  |     public static FormatableChat chatComponent(BaseComponent[] c) { | ||||||
|  |         return Chat.chatComponent(toAdventure(c)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Converts the Bungee {@link BaseComponent} array into Adventure {@link Component}. | ||||||
|  |      * @param components the Bungee {@link BaseComponent} array. | ||||||
|  |      * @return a {@link Component}. | ||||||
|  |      */ | ||||||
|  |     public static Component toAdventure(BaseComponent[] components) { | ||||||
|  |         return BungeeComponentSerializer.get().deserialize(components); | ||||||
|  |     } | ||||||
|  |     /** | ||||||
|  |      * Converts the Bungee {@link BaseComponent} into Adventure {@link Component}. | ||||||
|  |      * @param component the Bungee {@link BaseComponent}. | ||||||
|  |      * @return a {@link Component}. | ||||||
|  |      */ | ||||||
|  |     public static Component toAdventure(BaseComponent component) { | ||||||
|  |         return toAdventure(new BaseComponent[] { component }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Converts the Adventure {@link Component} into Bungee {@link BaseComponent} array. | ||||||
|  |      * @param component the Adventure {@link Component}. | ||||||
|  |      * @return a {@link BaseComponent} array. | ||||||
|  |      */ | ||||||
|  |     public static BaseComponent[] toBungeeArray(ComponentLike component) { | ||||||
|  |         return BungeeComponentSerializer.get().serialize(component.asComponent()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Converts the Adventure {@link Component} into Bungee {@link BaseComponent}. | ||||||
|  |      * @param component the Adventure {@link Component}. | ||||||
|  |      * @return a {@link BaseComponent}. | ||||||
|  |      */ | ||||||
|  |     public static BaseComponent toBungee(ComponentLike component) { | ||||||
|  |         BaseComponent[] arr = toBungeeArray(component); | ||||||
|  |         return arr.length == 1 ? arr[0] : new net.md_5.bungee.api.chat.TextComponent(arr); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     private ChatBungee() {} | ||||||
|  | } | ||||||
| @@ -25,7 +25,6 @@ import java.util.function.Function; | |||||||
|  */ |  */ | ||||||
| public class PandalibBungeePermissions implements Listener { | public class PandalibBungeePermissions implements Listener { | ||||||
|  |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Registers event listener to redirect permission checks to {@code pandalib-permissions}. |      * Registers event listener to redirect permission checks to {@code pandalib-permissions}. | ||||||
|      * @param bungeePlugin a BungeeCord plugin. |      * @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. |      * Event handler called when a plugin asks if a player has a permission. | ||||||
|      * @param event the permission check event. |      * @param event the permission check event. | ||||||
|   | |||||||
| @@ -33,7 +33,7 @@ | |||||||
|         </dependency> |         </dependency> | ||||||
|         <dependency> |         <dependency> | ||||||
|             <groupId>fr.pandacube.lib</groupId> |             <groupId>fr.pandacube.lib</groupId> | ||||||
|             <artifactId>pandalib-chat</artifactId> |             <artifactId>pandalib-bungee-chat</artifactId> | ||||||
|             <version>${project.version}</version> |             <version>${project.version}</version> | ||||||
|         </dependency> |         </dependency> | ||||||
|         <dependency> |         <dependency> | ||||||
|   | |||||||
| @@ -1,29 +1,49 @@ | |||||||
| package fr.pandacube.lib.bungee; | package fr.pandacube.lib.bungee; | ||||||
|  |  | ||||||
| import fr.pandacube.lib.bungee.util.DailyLogRotateFileHandler; | import fr.pandacube.lib.bungee.util.BungeeDailyLogRotateFileHandler; | ||||||
| import fr.pandacube.lib.bungee.util.PluginMessagePassthrough; | import fr.pandacube.lib.bungee.util.PluginMessagePassthrough; | ||||||
| import net.md_5.bungee.api.plugin.Plugin; | 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 { | public class PandaLibBungee { | ||||||
|  |  | ||||||
|     private static Plugin plugin; |     private static Plugin plugin; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Method to be called in {@link Plugin#onLoad()} method. | ||||||
|  |      * @param plugin the plugin instance. | ||||||
|  |      */ | ||||||
|     public static void onLoad(Plugin plugin) { |     public static void onLoad(Plugin plugin) { | ||||||
|         PandaLibBungee.plugin = plugin; |         PandaLibBungee.plugin = plugin; | ||||||
|  |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Method to be called in {@link Plugin#onEnable()} method. | ||||||
|  |      */ | ||||||
|     public static void onEnable() { |     public static void onEnable() { | ||||||
|         PluginMessagePassthrough.init(plugin); |         PluginMessagePassthrough.init(plugin); | ||||||
|         DailyLogRotateFileHandler.init(true); |         BungeeDailyLogRotateFileHandler.init(true); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Method to be called in {@link Plugin#onDisable()} method. | ||||||
|  |      */ | ||||||
|     public static void disable() { |     public static void disable() { | ||||||
|  |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns the plugin instance. | ||||||
|  |      * @return the plugin instance. | ||||||
|  |      */ | ||||||
|     public static Plugin getPlugin() { |     public static Plugin getPlugin() { | ||||||
|         return plugin; |         return plugin; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     private PandaLibBungee() {} | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,11 +6,40 @@ import java.io.File; | |||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.List; | import java.util.List; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Class that holds the configuration varables for {@link BungeeBackupManager}. | ||||||
|  |  */ | ||||||
|  | @SuppressWarnings("CanBeFinal") | ||||||
| public class BungeeBackupConfig { | public class BungeeBackupConfig { | ||||||
|  |     /** | ||||||
|  |      * Tells if the working directory of the current bungee instance should be backed up. | ||||||
|  |      */ | ||||||
|     public boolean workdirBackupEnabled = true; |     public boolean workdirBackupEnabled = true; | ||||||
|  |     /** | ||||||
|  |      * Tells if the old logs of the current bungee instance should be backed up. | ||||||
|  |      */ | ||||||
|     public boolean logsBackupEnabled = true; |     public boolean logsBackupEnabled = true; | ||||||
|     public String scheduling = "0 2 * * *"; // cron format, here is everyday at 2am |     /** | ||||||
|  |      * The cron scheduling of when the workdir backup occurs. | ||||||
|  |      */ | ||||||
|  |     public String scheduling = "0 2 * * *"; // cron format, here is every day at 2am | ||||||
|  |     /** | ||||||
|  |      * The destination directory for the backups. | ||||||
|  |      */ | ||||||
|     public File backupDirectory = null; |     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)); |     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<>(); |     public List<String> workdirIgnoreList = new ArrayList<>(); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Creates a new {@link BungeeBackupConfig}. | ||||||
|  |      */ | ||||||
|  |     public BungeeBackupConfig() { | ||||||
|  |  | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,10 +6,17 @@ import fr.pandacube.lib.core.backup.RotatedLogsBackupProcess; | |||||||
|  |  | ||||||
| import java.io.File; | import java.io.File; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Handles the backup processes for a Bungeecord instance. | ||||||
|  |  */ | ||||||
| public class BungeeBackupManager extends BackupManager { | public class BungeeBackupManager extends BackupManager { | ||||||
|  |  | ||||||
| 	BungeeBackupConfig config; | 	BungeeBackupConfig config; | ||||||
| 	 |  | ||||||
|  | 	/** | ||||||
|  | 	 * Instanciate a new {@link BungeeBackupManager}. | ||||||
|  | 	 * @param config the configuration. | ||||||
|  | 	 */ | ||||||
| 	public BungeeBackupManager(BungeeBackupConfig config) { | 	public BungeeBackupManager(BungeeBackupConfig config) { | ||||||
| 		super(config.backupDirectory); | 		super(config.backupDirectory); | ||||||
| 		setConfig(config); | 		setConfig(config); | ||||||
| @@ -24,12 +31,19 @@ public class BungeeBackupManager extends BackupManager { | |||||||
| 		super.addProcess(process); | 		super.addProcess(process); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * Sets a new configuration for this backup manager. | ||||||
|  | 	 * @param config the new configuration. | ||||||
|  | 	 */ | ||||||
| 	public void setConfig(BungeeBackupConfig config) { | 	public void setConfig(BungeeBackupConfig config) { | ||||||
| 		this.config = config; | 		this.config = config; | ||||||
| 		backupQueue.forEach(this::updateProcessConfig); | 		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) { | 	public void updateProcessConfig(BackupProcess process) { | ||||||
| 		if (process instanceof BungeeWorkdirProcess) { | 		if (process instanceof BungeeWorkdirProcess) { | ||||||
| 			process.setEnabled(config.workdirBackupEnabled); | 			process.setEnabled(config.workdirBackupEnabled); | ||||||
|   | |||||||
| @@ -5,8 +5,15 @@ import fr.pandacube.lib.core.backup.BackupProcess; | |||||||
| import java.io.File; | import java.io.File; | ||||||
| import java.util.function.BiPredicate; | import java.util.function.BiPredicate; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * The backup process responsible for the working directory of the current Bungeecord instance. | ||||||
|  |  */ | ||||||
| public class BungeeWorkdirProcess extends BackupProcess { | public class BungeeWorkdirProcess extends BackupProcess { | ||||||
| 	 |  | ||||||
|  | 	/** | ||||||
|  | 	 * Instantiates this backup process. | ||||||
|  | 	 * @param bm the backup manager. | ||||||
|  | 	 */ | ||||||
| 	protected BungeeWorkdirProcess(BungeeBackupManager bm) { | 	protected BungeeWorkdirProcess(BungeeBackupManager bm) { | ||||||
| 		super(bm, "workdir"); | 		super(bm, "workdir"); | ||||||
| 	} | 	} | ||||||
| @@ -18,15 +25,12 @@ public class BungeeWorkdirProcess extends BackupProcess { | |||||||
| 	 | 	 | ||||||
| 	 | 	 | ||||||
| 	public BiPredicate<File, String> getFilenameFilter() { | 	public BiPredicate<File, String> getFilenameFilter() { | ||||||
| 		return new BiPredicate<>() { | 		return (file, path) -> { | ||||||
| 			@Override | 			if (new File(getSourceDir(), "logs").equals(file)) | ||||||
| 			public boolean test(File file, String path) { | 				return false; | ||||||
| 				if (new File(getSourceDir(), "logs").equals(file)) | 			if (file.isFile() && file.getName().endsWith(".lck")) | ||||||
| 					return false; | 				return false; | ||||||
| 				if (file.isFile() && file.getName().endsWith(".lck")) | 			return BungeeWorkdirProcess.super.getFilenameFilter().test(file, path); | ||||||
| 					return false; |  | ||||||
| 				return BungeeWorkdirProcess.super.getFilenameFilter().test(file, path); |  | ||||||
| 			} |  | ||||||
| 		}; | 		}; | ||||||
| 	} | 	} | ||||||
| 	 | 	 | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ import com.mojang.brigadier.suggestion.Suggestions; | |||||||
| import com.mojang.brigadier.tree.LiteralCommandNode; | import com.mojang.brigadier.tree.LiteralCommandNode; | ||||||
| import fr.pandacube.lib.commands.BrigadierCommand; | import fr.pandacube.lib.commands.BrigadierCommand; | ||||||
| import fr.pandacube.lib.commands.SuggestionsSupplier; | 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.CommandSender; | ||||||
| import net.md_5.bungee.api.ProxyServer; | import net.md_5.bungee.api.ProxyServer; | ||||||
| import net.md_5.bungee.api.connection.ProxiedPlayer; | import net.md_5.bungee.api.connection.ProxiedPlayer; | ||||||
| @@ -28,10 +28,10 @@ public abstract class BungeeBrigadierCommand extends BrigadierCommand<CommandSen | |||||||
| 	/** | 	/** | ||||||
| 	 * The command dispatcher. | 	 * The command dispatcher. | ||||||
| 	 */ | 	 */ | ||||||
| 	protected BungeeBrigadierDispatcher dispatcher = BungeeBrigadierDispatcher.getInstance(); | 	protected final BungeeBrigadierDispatcher dispatcher = BungeeBrigadierDispatcher.getInstance(); | ||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
| 	 * Instanciate this command instance. | 	 * Instantiate this command instance. | ||||||
| 	 */ | 	 */ | ||||||
| 	public BungeeBrigadierCommand() { | 	public BungeeBrigadierCommand() { | ||||||
| 		LiteralCommandNode<CommandSender> commandNode; | 		LiteralCommandNode<CommandSender> commandNode; | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| package fr.pandacube.lib.bungee.commands; | 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 fr.pandacube.lib.commands.BrigadierDispatcher; | ||||||
| import net.kyori.adventure.text.ComponentLike; | import net.kyori.adventure.text.ComponentLike; | ||||||
| import net.md_5.bungee.api.CommandSender; | import net.md_5.bungee.api.CommandSender; | ||||||
| @@ -39,7 +39,7 @@ public class BungeeBrigadierDispatcher extends BrigadierDispatcher<CommandSender | |||||||
| 	 */ | 	 */ | ||||||
| 	public BungeeBrigadierDispatcher(Plugin pl) { | 	public BungeeBrigadierDispatcher(Plugin pl) { | ||||||
| 		if (instance != null) | 		if (instance != null) | ||||||
| 			throw new IllegalStateException("Cannot instanciante more than one BungeeBrigadierDispatcher"); | 			throw new IllegalStateException("Cannot instantiate more than one BungeeBrigadierDispatcher"); | ||||||
| 		instance = this; | 		instance = this; | ||||||
| 		plugin = pl; | 		plugin = pl; | ||||||
| 		ProxyServer.getInstance().getPluginManager().registerListener(plugin, this); | 		ProxyServer.getInstance().getPluginManager().registerListener(plugin, this); | ||||||
| @@ -47,7 +47,7 @@ public class BungeeBrigadierDispatcher extends BrigadierDispatcher<CommandSender | |||||||
|  |  | ||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
| 	 * Called when a player sends a chat message. Used to gets the typed command and execute it. | 	 * Called when a player sends a chat message. Used to get the typed command and execute it. | ||||||
| 	 * @param event the event. | 	 * @param event the event. | ||||||
| 	 */ | 	 */ | ||||||
| 	@EventHandler | 	@EventHandler | ||||||
| @@ -71,6 +71,6 @@ public class BungeeBrigadierDispatcher extends BrigadierDispatcher<CommandSender | |||||||
|  |  | ||||||
| 	@Override | 	@Override | ||||||
| 	protected void sendSenderMessage(CommandSender sender, ComponentLike message) { | 	protected void sendSenderMessage(CommandSender sender, ComponentLike message) { | ||||||
| 		sender.sendMessage(Chat.toBungee(message.asComponent())); | 		sender.sendMessage(ChatBungee.toBungee(message.asComponent())); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,13 +1,12 @@ | |||||||
| package fr.pandacube.lib.bungee.players; | package fr.pandacube.lib.bungee.players; | ||||||
|  |  | ||||||
| import java.util.Locale; | import fr.pandacube.lib.bungee.chat.ChatBungee; | ||||||
| import java.util.UUID; | import fr.pandacube.lib.core.mc_version.ProtocolVersion; | ||||||
|  | import fr.pandacube.lib.players.standalone.AbstractOnlinePlayer; | ||||||
|  | import fr.pandacube.lib.reflect.Reflect; | ||||||
| import io.netty.buffer.ByteBuf; | import io.netty.buffer.ByteBuf; | ||||||
| import io.netty.buffer.ByteBufAllocator; | import io.netty.buffer.ByteBufAllocator; | ||||||
| import net.kyori.adventure.identity.Identified; |  | ||||||
| import net.kyori.adventure.text.Component; | import net.kyori.adventure.text.Component; | ||||||
| import net.kyori.adventure.text.ComponentLike; |  | ||||||
| import net.md_5.bungee.api.ProxyServer; | import net.md_5.bungee.api.ProxyServer; | ||||||
| import net.md_5.bungee.api.SkinConfiguration; | import net.md_5.bungee.api.SkinConfiguration; | ||||||
| import net.md_5.bungee.api.connection.ProxiedPlayer; | import net.md_5.bungee.api.connection.ProxiedPlayer; | ||||||
| @@ -18,10 +17,7 @@ import net.md_5.bungee.protocol.DefinedPacket; | |||||||
| import net.md_5.bungee.protocol.packet.ClientSettings; | import net.md_5.bungee.protocol.packet.ClientSettings; | ||||||
| import net.md_5.bungee.protocol.packet.PluginMessage; | import net.md_5.bungee.protocol.packet.PluginMessage; | ||||||
|  |  | ||||||
| import fr.pandacube.lib.chat.Chat; | import java.util.Locale; | ||||||
| import fr.pandacube.lib.players.standalone.AbstractOnlinePlayer; |  | ||||||
| import fr.pandacube.lib.reflect.Reflect; |  | ||||||
| import fr.pandacube.lib.util.MinecraftVersion; |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Represents any online player on a Bungeecord proxy. |  * Represents any online player on a Bungeecord proxy. | ||||||
| @@ -53,11 +49,11 @@ public interface BungeeOnlinePlayer extends BungeeOffPlayer, AbstractOnlinePlaye | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Gets the minecraft version of this player’s client. |      * Gets the protocol version of this player’s client. | ||||||
|      * @return the minecraft version of this player’s client. |      * @return the protocol version of this player’s client. | ||||||
|      */ |      */ | ||||||
|     default MinecraftVersion getMinecraftVersion() { |     default ProtocolVersion getProtocolVersion() { | ||||||
|         return MinecraftVersion.getVersion(getBungeeProxiedPlayer().getPendingConnection().getVersion()); |         return ProtocolVersion.ofProtocol(getBungeeProxiedPlayer().getPendingConnection().getVersion()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -88,33 +84,13 @@ public interface BungeeOnlinePlayer extends BungeeOffPlayer, AbstractOnlinePlaye | |||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     default void sendMessage(Component message) { |     default void sendMessage(Component message) { | ||||||
|         getBungeeProxiedPlayer().sendMessage(Chat.toBungee(message)); |         getBungeeProxiedPlayer().sendMessage(ChatBungee.toBungee(message)); | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     default void sendMessage(ComponentLike message, UUID sender) { |  | ||||||
|         getBungeeProxiedPlayer().sendMessage(sender, Chat.toBungee(message.asComponent())); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     default void sendMessage(Component message, Identified sender) { |  | ||||||
|         getBungeeProxiedPlayer().sendMessage(sender == null ? null : sender.identity().uuid(), Chat.toBungee(message)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Display the provided message in the player’s chat, if they allows to display CHAT messages. |  | ||||||
|      * @param message the message to send. |  | ||||||
|      * @param sender the player causing the send of this message. Client side filtering may occur. May be null if we |  | ||||||
|      *               don’t want client filtering, but still consider the message as CHAT message. |  | ||||||
|      */ |  | ||||||
|     default void sendMessage(ComponentLike message, ProxiedPlayer sender) { |  | ||||||
|         getBungeeProxiedPlayer().sendMessage(sender == null ? null : sender.getUniqueId(), Chat.toBungee(message.asComponent())); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     default void sendTitle(Component title, Component subtitle, int fadeIn, int stay, int fadeOut) { |     default void sendTitle(Component title, Component subtitle, int fadeIn, int stay, int fadeOut) { | ||||||
|         ProxyServer.getInstance().createTitle() |         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) |                 .fadeIn(fadeIn).stay(stay).fadeOut(fadeOut) | ||||||
|                 .send(getBungeeProxiedPlayer()); |                 .send(getBungeeProxiedPlayer()); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -0,0 +1,45 @@ | |||||||
|  | package fr.pandacube.lib.bungee.util; | ||||||
|  |  | ||||||
|  | import fr.pandacube.lib.util.log.DailyLogRotateFileHandler; | ||||||
|  | import net.md_5.bungee.api.ProxyServer; | ||||||
|  | import net.md_5.bungee.log.ConciseFormatter; | ||||||
|  |  | ||||||
|  | import java.util.logging.Filter; | ||||||
|  | import java.util.logging.Level; | ||||||
|  | import java.util.logging.LogRecord; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * A log rotate that extends the functionalities of {@link DailyLogRotateFileHandler} | ||||||
|  |  * to adapt with bungee specificities. | ||||||
|  |  */ | ||||||
|  | public class BungeeDailyLogRotateFileHandler extends DailyLogRotateFileHandler { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Initialize this file handler. | ||||||
|  |      * @param hideInitialHandlerLogEntries true if we want to hide some InitialHandler log entries | ||||||
|  |      */ | ||||||
|  |     public static void init(boolean hideInitialHandlerLogEntries) { | ||||||
|  |         ProxyServer.getInstance().getLogger().addHandler(new BungeeDailyLogRotateFileHandler(hideInitialHandlerLogEntries)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private BungeeDailyLogRotateFileHandler(boolean hideInitialHandlerLogEntries) { | ||||||
|  |         if (hideInitialHandlerLogEntries) | ||||||
|  |             setFilter(new InitialHandlerLogRemover()); | ||||||
|  |         setFormatter(new ConciseFormatter(false)); | ||||||
|  |         setLevel(Level.parse(System.getProperty("net.md_5.bungee.file-log-level", "INFO"))); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     private class InitialHandlerLogRemover implements Filter { | ||||||
|  |  | ||||||
|  |         @Override | ||||||
|  |         public boolean isLoggable(LogRecord record) { | ||||||
|  |             String formattedRecord = getFormatter().format(record); | ||||||
|  |             return !( | ||||||
|  |                     formattedRecord.contains("<-> InitialHandler has connected") | ||||||
|  |                     || formattedRecord.contains("<-> InitialHandler has pinged") | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,147 +0,0 @@ | |||||||
| package fr.pandacube.lib.bungee.util; |  | ||||||
|  |  | ||||||
| import com.google.common.io.Files; |  | ||||||
| import fr.pandacube.lib.bungee.PandaLibBungee; |  | ||||||
| import net.md_5.bungee.api.ProxyServer; |  | ||||||
| import net.md_5.bungee.log.ConciseFormatter; |  | ||||||
|  |  | ||||||
| import java.io.BufferedWriter; |  | ||||||
| import java.io.File; |  | ||||||
| import java.io.FileOutputStream; |  | ||||||
| import java.io.FileWriter; |  | ||||||
| import java.io.IOException; |  | ||||||
| import java.text.DateFormat; |  | ||||||
| import java.text.SimpleDateFormat; |  | ||||||
| import java.util.Date; |  | ||||||
| import java.util.logging.ErrorManager; |  | ||||||
| import java.util.logging.Filter; |  | ||||||
| import java.util.logging.Handler; |  | ||||||
| import java.util.logging.Level; |  | ||||||
| import java.util.logging.LogRecord; |  | ||||||
| import java.util.zip.GZIPOutputStream; |  | ||||||
|  |  | ||||||
| public class DailyLogRotateFileHandler extends Handler { |  | ||||||
|  |  | ||||||
|     private static final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     public static void init(boolean hideInitialHandlerLogEntries) { |  | ||||||
|         ProxyServer.getInstance().getLogger().addHandler(new DailyLogRotateFileHandler(hideInitialHandlerLogEntries)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     private BufferedWriter currentFile = null; |  | ||||||
|     private String currentFileDate = getCurrentDay(); |  | ||||||
|     private boolean closed = false; |  | ||||||
|  |  | ||||||
|     private DailyLogRotateFileHandler(boolean hideInitialHandlerLogEntries) { |  | ||||||
|         if (hideInitialHandlerLogEntries) |  | ||||||
|             setFilter(new InitialHandlerLogRemover()); |  | ||||||
|         setFormatter(new ConciseFormatter(false)); |  | ||||||
|         setLevel(Level.parse(System.getProperty("net.md_5.bungee.file-log-level", "INFO"))); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public synchronized void close() throws SecurityException { |  | ||||||
|         closed = true; |  | ||||||
|         if (currentFile != null) try { |  | ||||||
|             currentFile.close(); |  | ||||||
|         } catch (IOException ignored) { |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public synchronized void flush() { |  | ||||||
|         if (closed) return; |  | ||||||
|         if (currentFile == null) return; |  | ||||||
|         try { |  | ||||||
|             currentFile.flush(); |  | ||||||
|         } catch (IOException ignored) { |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public synchronized void publish(LogRecord record) { |  | ||||||
|         if (closed || !isLoggable(record)) |  | ||||||
|             return; |  | ||||||
|  |  | ||||||
|         if (currentFile == null || !currentFileDate.equals(getCurrentDay())) |  | ||||||
|             changeFile(); |  | ||||||
|  |  | ||||||
|         if (currentFile == null) |  | ||||||
|             return; |  | ||||||
|  |  | ||||||
|         String formattedMessage; |  | ||||||
|  |  | ||||||
|         try { |  | ||||||
|             formattedMessage = getFormatter().format(record); |  | ||||||
|         } catch (Exception ex) { |  | ||||||
|             reportError(null, ex, ErrorManager.FORMAT_FAILURE); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         try { |  | ||||||
|             currentFile.write(formattedMessage); |  | ||||||
|             currentFile.flush(); |  | ||||||
|         } catch (Exception ex) { |  | ||||||
|             reportError(null, ex, ErrorManager.WRITE_FAILURE); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void changeFile() { |  | ||||||
|         if (currentFile != null) { |  | ||||||
|             try { |  | ||||||
|                 currentFile.flush(); |  | ||||||
|                 currentFile.close(); |  | ||||||
|             } catch (IOException ignored) { |  | ||||||
|             } |  | ||||||
|             File fileNewName = new File("logs/" + currentFileDate + ".log"); |  | ||||||
|             new File("logs/latest.log").renameTo(fileNewName); |  | ||||||
|             ProxyServer.getInstance().getScheduler().runAsync(PandaLibBungee.getPlugin(), () -> compress(fileNewName)); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         currentFileDate = getCurrentDay(); |  | ||||||
|         try { |  | ||||||
|             File logDir = new File("logs"); |  | ||||||
|             logDir.mkdir(); |  | ||||||
|             currentFile = new BufferedWriter(new FileWriter("logs/latest.log", true)); |  | ||||||
|         } catch (SecurityException | IOException e) { |  | ||||||
|             reportError("Erreur lors de l'initialisation d'un fichier log", e, ErrorManager.OPEN_FAILURE); |  | ||||||
|             currentFile = null; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private String getCurrentDay() { |  | ||||||
|         return dateFormat.format(new Date()); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     private void compress(File sourceFile) { |  | ||||||
|         File destFile = new File(sourceFile.getParentFile(), sourceFile.getName() + ".gz"); |  | ||||||
|         if (destFile.exists()) |  | ||||||
|             destFile.delete(); |  | ||||||
|         try (GZIPOutputStream os = new GZIPOutputStream(new FileOutputStream(destFile))) { |  | ||||||
|             Files.copy(sourceFile, os); |  | ||||||
|         } catch (IOException e) { |  | ||||||
|             if (destFile.exists()) |  | ||||||
|                 destFile.delete(); |  | ||||||
|             throw new RuntimeException(e); |  | ||||||
|         } |  | ||||||
|         sourceFile.delete(); |  | ||||||
|  |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     private class InitialHandlerLogRemover implements Filter { |  | ||||||
|  |  | ||||||
|         @Override |  | ||||||
|         public boolean isLoggable(LogRecord record) { |  | ||||||
|             String formattedRecord = getFormatter().format(record); |  | ||||||
|             if (formattedRecord.contains("<-> InitialHandler has connected")) return false; |  | ||||||
|             if (formattedRecord.contains("<-> InitialHandler has pinged")) return false; |  | ||||||
|             return true; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -26,26 +26,35 @@ | |||||||
|         <dependency> |         <dependency> | ||||||
|             <groupId>net.kyori</groupId> |             <groupId>net.kyori</groupId> | ||||||
|             <artifactId>adventure-api</artifactId> |             <artifactId>adventure-api</artifactId> | ||||||
|             <version>4.11.0</version> |             <version>4.15.0</version> | ||||||
|         </dependency> |         </dependency> | ||||||
|         <dependency> |         <dependency> | ||||||
|             <groupId>net.kyori</groupId> |             <groupId>net.kyori</groupId> | ||||||
|             <artifactId>adventure-platform-bungeecord</artifactId> |             <artifactId>adventure-text-serializer-gson</artifactId> | ||||||
|             <version>4.1.1</version> |             <version>4.13.0</version> | ||||||
|  |         </dependency> | ||||||
|  |         <dependency> | ||||||
|  |             <groupId>net.kyori</groupId> | ||||||
|  |             <artifactId>adventure-text-serializer-legacy</artifactId> | ||||||
|  |             <version>4.13.0</version> | ||||||
|         </dependency> |         </dependency> | ||||||
|         <dependency> |         <dependency> | ||||||
|             <groupId>net.kyori</groupId> |             <groupId>net.kyori</groupId> | ||||||
|             <artifactId>adventure-text-serializer-plain</artifactId> |             <artifactId>adventure-text-serializer-plain</artifactId> | ||||||
|             <version>4.11.0</version> |             <version>4.15.0</version> | ||||||
|  |         </dependency> | ||||||
|  |         <dependency> | ||||||
|  |             <groupId>net.kyori</groupId> | ||||||
|  |             <artifactId>adventure-text-minimessage</artifactId> | ||||||
|  |             <version>4.15.0</version> | ||||||
|         </dependency> |         </dependency> | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|         <dependency> |         <dependency> | ||||||
|             <groupId>net.md-5</groupId> |             <groupId>com.google.code.gson</groupId> | ||||||
|             <artifactId>bungeecord-chat</artifactId> |             <artifactId>gson</artifactId> | ||||||
|             <version>${bungeecord.version}</version> |             <version>2.10.1</version> | ||||||
|             <scope>compile</scope> |  | ||||||
|         </dependency> |         </dependency> | ||||||
|     </dependencies> |     </dependencies> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,15 +1,12 @@ | |||||||
| package fr.pandacube.lib.chat; | 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.key.Key; | ||||||
| import net.kyori.adventure.text.Component; | import net.kyori.adventure.text.Component; | ||||||
| import net.kyori.adventure.text.ComponentBuilder; | import net.kyori.adventure.text.ComponentBuilder; | ||||||
| import net.kyori.adventure.text.ComponentLike; | import net.kyori.adventure.text.ComponentLike; | ||||||
| import net.kyori.adventure.text.TextComponent; | 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.ClickEvent; | ||||||
| import net.kyori.adventure.text.event.HoverEvent; | import net.kyori.adventure.text.event.HoverEvent; | ||||||
| import net.kyori.adventure.text.event.HoverEventSource; | import net.kyori.adventure.text.event.HoverEventSource; | ||||||
| @@ -18,11 +15,17 @@ import net.kyori.adventure.text.format.Style; | |||||||
| import net.kyori.adventure.text.format.TextColor; | import net.kyori.adventure.text.format.TextColor; | ||||||
| import net.kyori.adventure.text.format.TextDecoration; | import net.kyori.adventure.text.format.TextDecoration; | ||||||
| import net.kyori.adventure.text.format.TextDecoration.State; | 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.legacy.LegacyComponentSerializer; | ||||||
| import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; | import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; | ||||||
| import net.md_5.bungee.api.ChatColor; | import org.jetbrains.annotations.NotNull; | ||||||
| import net.md_5.bungee.api.chat.BaseComponent; |  | ||||||
|  | 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. |  * A builder for chat components. | ||||||
| @@ -30,10 +33,10 @@ import net.md_5.bungee.api.chat.BaseComponent; | |||||||
|  * Use one of the provided static methods to create a new instance. |  * Use one of the provided static methods to create a new instance. | ||||||
|  * <p> |  * <p> | ||||||
|  * This class implements {@link ComponentLike} and {@link HoverEventSource} so they can be used directly in |  * This class implements {@link ComponentLike} and {@link HoverEventSource} so they can be used directly in | ||||||
|  * Adventure API and its implentation without using the final methods of this builder. |  * Adventure API and its implementation without using the final methods of this builder. | ||||||
|  * <p> |  * <p> | ||||||
|  * The unique possible concrete subclass of this class, {@link FormatableChat}, takes care of the formating of the |  * The unique possible concrete subclass of this class, {@link FormatableChat}, takes care of the formatting of the | ||||||
|  * builded component. The rationale for this design is explained in the documentation of {@link FormatableChat}. |  * built component. The rationale for this design is explained in the documentation of {@link FormatableChat}. | ||||||
|  */ |  */ | ||||||
| public abstract sealed class Chat extends ChatStatic implements HoverEventSource<Component>, ComponentLike { | public abstract sealed class Chat extends ChatStatic implements HoverEventSource<Component>, ComponentLike { | ||||||
|  |  | ||||||
| @@ -60,61 +63,70 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource | |||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Builds the component into Adventure Component instance. |      * Builds the component into Adventure Component instance. | ||||||
|      * @return the {@link Component} builded from this {@link Chat} component. |      * @return the {@link Component} built from this {@link Chat} component. | ||||||
|      */ |      */ | ||||||
|     public Component getAdv() { |     public Component get() { | ||||||
|         return builder.build(); |         return builder.build(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     private static final LegacyComponentSerializer LEGACY_SERIALIZER_BUNGEE_FRIENDLY = LegacyComponentSerializer.builder() | ||||||
|      * Builds the component into BungeeCord {@link BaseComponent} instance. |  | ||||||
|      * @return the {@link BaseComponent} builded from this {@link Chat} component. |  | ||||||
|      */ |  | ||||||
|     public BaseComponent get() { |  | ||||||
|         return toBungee(getAdv()); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Builds the component into BungeeCord {@link BaseComponent} array. |  | ||||||
|      * @return the {@link BaseComponent} array builded from this {@link Chat} component. |  | ||||||
|      */ |  | ||||||
|     public BaseComponent[] getAsArray() { |  | ||||||
|         return toBungeeArray(getAdv()); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private static final LegacyComponentSerializer LEGACY_SERIALIZER_BUNGEE_FIENDLY = LegacyComponentSerializer.builder() |  | ||||||
|             .hexColors() |             .hexColors() | ||||||
|             .useUnusualXRepeatedCharacterHexFormat() |             .useUnusualXRepeatedCharacterHexFormat() | ||||||
|             .build(); |             .build(); | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Converts the builded component into legacy text. |      * Converts the built component into legacy text. | ||||||
|      * @return the legacy text. RGB colors are in BungeeCord format. |      * @return the legacy text. RGB colors are in BungeeCord format. | ||||||
|      */ |      */ | ||||||
|     public String getLegacyText() { |     public String getLegacyText() { | ||||||
|         return LEGACY_SERIALIZER_BUNGEE_FIENDLY.serialize(getAdv()); |         return LEGACY_SERIALIZER_BUNGEE_FRIENDLY.serialize(get()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Converts the builded component into plain text. |      * Converts the built component into plain text. | ||||||
|      * @return the plain text of this component. |      * @return the plain text of this component. | ||||||
|      */ |      */ | ||||||
|     public String getPlainText() { |     public String getPlainText() { | ||||||
|         return PlainTextComponentSerializer.plainText().serializeOr(getAdv(), ""); |         return PlainTextComponentSerializer.plainText().serializeOr(get(), ""); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public HoverEvent<Component> asHoverEvent(UnaryOperator<Component> op) { |     public @NotNull HoverEvent<Component> asHoverEvent(@NotNull UnaryOperator<Component> op) { | ||||||
|         return HoverEvent.showText(op.apply(getAdv())); |         return HoverEvent.showText(op.apply(get())); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Builds the component into Adventure Component instance. |      * Builds the component into Adventure Component instance. | ||||||
|      * @return the {@link Component} builded from this {@link Chat} component. |      * @return the {@link Component} built from this {@link Chat} component. | ||||||
|      */ |      */ | ||||||
|     @Override |     @Override | ||||||
|     public Component asComponent() { |     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()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -154,15 +166,6 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource | |||||||
|         return this; |         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. |      * Appends a component to this component. | ||||||
|      * @param comp the component to append. |      * @param comp the component to append. | ||||||
| @@ -177,15 +180,6 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource | |||||||
|         return then(comp.asComponent()); |         return then(comp.asComponent()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Appends a BungeeCord {@link BaseComponent} array to this component. |  | ||||||
|      * @param comp the components to append. |  | ||||||
|      * @return this. |  | ||||||
|      */ |  | ||||||
|     public Chat then(BaseComponent[] comp) { |  | ||||||
|         return then(toAdventure(comp)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -260,7 +254,7 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource | |||||||
|      * @param comp the component. |      * @param comp the component. | ||||||
|      * @return this. |      * @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. |      * Appends a component consisting of a new line. | ||||||
| @@ -269,12 +263,26 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource | |||||||
|     public Chat thenNewLine() { return then(Component.newline()); } |     public Chat thenNewLine() { return then(Component.newline()); } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Appends a component with the provided legacy text as its content. |      * Appends a component with the provided legacy text as its content, using the section {@code "§"} character. | ||||||
|      * @param legacyText the legacy text. |      * @param legacyText the legacy text that uses the {@code "§"} character. | ||||||
|      * @return this. |      * @return this. | ||||||
|      */ |      */ | ||||||
|     public Chat thenLegacyText(Object legacyText) { return then(legacyText(legacyText)); } |     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. |      * Appends a component with the provided translation key and parameters. | ||||||
|      * @param key the translation key. |      * @param key the translation key. | ||||||
| @@ -284,8 +292,8 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource | |||||||
|     public Chat thenTranslation(String key, Object... with) { return then(translation(key, with)); } |     public Chat thenTranslation(String key, Object... with) { return then(translation(key, with)); } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Appends a component with the provided keybind. |      * Appends a component with the provided keybinding. | ||||||
|      * @param key the keybind to display. |      * @param key the keybinding to display. | ||||||
|      * @return this. |      * @return this. | ||||||
|      */ |      */ | ||||||
|     public Chat thenKeyBind(String key) { return then(keybind(key)); } |     public Chat thenKeyBind(String key) { return then(keybind(key)); } | ||||||
| @@ -443,50 +451,28 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource | |||||||
|  |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Appends a component filling a line of chat (or console) with the configured decoration character and |      * Appends a component filling a chat line with the configured decoration character and | ||||||
|      * color and a left-aligned text. |      * color and a left-aligned text. | ||||||
|      * @param leftText the text aligned to the left. |      * @param leftText the text aligned to the left. | ||||||
|      * @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character |      * @return a new {@link FormatableChat} filling a chat line with the configured decoration character | ||||||
|      *         and color and a left-aligned text. |      *         and color and a left-aligned text. | ||||||
|      */ |      */ | ||||||
|     public Chat thenLeftText(ComponentLike leftText) { return then(leftText(leftText, console)); } |     public Chat thenLeftText(ComponentLike leftText) { return then(leftText(leftText, console)); } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Appends a component filling a line of chat (or console) with the configured decoration character and |      * Appends a component filling a chat line with the configured decoration character and | ||||||
|      * color and a left-aligned text. |  | ||||||
|      * @param leftText the text aligned to the left. |  | ||||||
|      * @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character |  | ||||||
|      *         and color and a left-aligned text. |  | ||||||
|      * @deprecated uses Bungeecord chat API. |  | ||||||
|      */ |  | ||||||
|     @Deprecated |  | ||||||
|     public Chat thenLeftText(BaseComponent leftText) { return thenLeftText(chatComponent(leftText)); } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Appends a component filling a line of chat (or console) with the configured decoration character and |  | ||||||
|      * color and a right-aligned text. |      * color and a right-aligned text. | ||||||
|      * @param rightText the text aligned to the right. |      * @param rightText the text aligned to the right. | ||||||
|      * @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character |      * @return a new {@link FormatableChat} filling a chat line with the configured decoration character | ||||||
|      *         and color and a right-aligned text. |      *         and color and a right-aligned text. | ||||||
|      */ |      */ | ||||||
|     public Chat thenRightText(ComponentLike rightText) { return then(rightText(rightText, console)); } |     public Chat thenRightText(ComponentLike rightText) { return then(rightText(rightText, console)); } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Appends a component filling a line of chat (or console) with the configured decoration character and |      * Appends a component filling a chat line with the configured decoration character and | ||||||
|      * color and a right-aligned text. |  | ||||||
|      * @param rightText the text aligned to the right. |  | ||||||
|      * @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character |  | ||||||
|      *         and color and a right-aligned text. |  | ||||||
|      * @deprecated uses Bungeecord chat API. |  | ||||||
|      */ |  | ||||||
|     @Deprecated |  | ||||||
|     public Chat thenRightText(BaseComponent rightText) { return thenRightText(chatComponent(rightText)); } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Appends a component filling a line of chat (or console) with the configured decoration character and |  | ||||||
|      * color and a centered text. |      * color and a centered text. | ||||||
|      * @param centerText the text aligned to the center. |      * @param centerText the text aligned to the center. | ||||||
|      * @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character |      * @return a new {@link FormatableChat} filling a chat line with the configured decoration character | ||||||
|      *         and color and a centered text. |      *         and color and a centered text. | ||||||
|      */ |      */ | ||||||
|     public Chat thenCenterText(ComponentLike centerText) { |     public Chat thenCenterText(ComponentLike centerText) { | ||||||
| @@ -494,21 +480,8 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Appends a component filling a line of chat (or console) with the configured decoration character and |      * Appends a component filling a chat line with the configured decoration character and color. | ||||||
|      * color and a centered text. |      * @return a new {@link FormatableChat} filling a chat line with a decoration character and color. | ||||||
|      * @param centerText the text aligned to the center. |  | ||||||
|      * @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character |  | ||||||
|      *         and color and a centered text. |  | ||||||
|      * @deprecated uses Bungeecord chat API. |  | ||||||
|      */ |  | ||||||
|     @Deprecated |  | ||||||
|     public Chat thenCenterText(BaseComponent centerText) { |  | ||||||
|         return thenCenterText(chatComponent(centerText)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Appends a component filling a line of chat (or console) with the configured decoration character and color. |  | ||||||
|      * @return a new {@link FormatableChat} filling a line of chat (or console) with a decoration character and color. |  | ||||||
|      */ |      */ | ||||||
|     public Chat thenFilledLine() { return then(filledLine(console)); } |     public Chat thenFilledLine() { return then(filledLine(console)); } | ||||||
|  |  | ||||||
| @@ -534,11 +507,11 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource | |||||||
|      *         .append("!").color(ChatColor.RED) |      *         .append("!").color(ChatColor.RED) | ||||||
|      *         .create(); |      *         .create(); | ||||||
|      * }</pre> |      * }</pre> | ||||||
|      * Here, when you call a formating method (like {@code bold(boolean)} or {@code color(ChatColor)}) after the |      * Here, when you call a formatting method (like {@code bold(boolean)} or {@code color(ChatColor)}) after the | ||||||
|      * {@code append(String)} method, the formating apply to the last sub-component appended. |      * {@code append(String)} method, the formatting apply to the last subcomponent appended. | ||||||
|      * <p> |      * <p> | ||||||
|      * In our design, we want the formating to apply to the currently builded component, not the last appended one. |      * In our design, we want the formatting to apply to the currently built component, not the last appended one. | ||||||
|      * The purpose is to make the component structure clearer and have better control of the formating over the |      * The purpose is to make the component structure clearer and have better control of the formatting over the | ||||||
|      * component hierarchy. |      * component hierarchy. | ||||||
|      * Here is the equivalent of the above code, with the {@link Chat} API: |      * Here is the equivalent of the above code, with the {@link Chat} API: | ||||||
|      * <pre>{@code |      * <pre>{@code | ||||||
| @@ -547,9 +520,9 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource | |||||||
|      *         .thenText("!"); // short for .then(Chat.text("!")) |      *         .thenText("!"); // short for .then(Chat.text("!")) | ||||||
|      *         // the red color for "!" is not needed because the parent component is already red. |      *         // the red color for "!" is not needed because the parent component is already red. | ||||||
|      * }</pre> |      * }</pre> | ||||||
|      * When calling {@link #then(Component) #then(...)} on a {@link FormatableChat}, the method returns itself, casted |      * When calling {@link #then(Component) #then(...)} on a {@link FormatableChat}, the method returns itself, cast | ||||||
|      * to {@link Chat}, to prevent future formating (that the programmer would think it formats the previously appended |      * to {@link Chat}, to prevent future formatting (that the programmer would think it formats the previously appended | ||||||
|      * sub-component). If the formatting of the currently builded component is needed, since {@link Chat} is a sealed |      * 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 FormatableChat}, you can cast the builder, and use the format methods again. | ||||||
|      * <pre>{@code |      * <pre>{@code | ||||||
|      * Chat component = Chat.text("Hello ").red() |      * Chat component = Chat.text("Hello ").red() | ||||||
| @@ -585,12 +558,6 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource | |||||||
|          * @return this. |          * @return this. | ||||||
|          */ |          */ | ||||||
|         public FormatableChat color(TextColor c) { builder.color(c); return this; } |         public FormatableChat color(TextColor c) { builder.color(c); return this; } | ||||||
|         /** |  | ||||||
|          * Sets the color of this component. |  | ||||||
|          * @param c the color. |  | ||||||
|          * @return this. |  | ||||||
|          */ |  | ||||||
|         public FormatableChat color(ChatColor c) { return color(c == null ? null : TextColor.color(c.getColor().getRGB())); } |  | ||||||
|         /** |         /** | ||||||
|          * Sets the color of this component. |          * Sets the color of this component. | ||||||
|          * @param c the color. |          * @param c the color. | ||||||
| @@ -602,7 +569,16 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource | |||||||
|          * @param c the color. |          * @param c the color. | ||||||
|          * @return this. |          * @return this. | ||||||
|          */ |          */ | ||||||
|         public FormatableChat color(String c) { return color(c == null ? null : ChatColor.of(c)); } |         public FormatableChat color(String c) { | ||||||
|  |             if (c == null) | ||||||
|  |                 return color((TextColor) null); | ||||||
|  |             TextColor tc = c.startsWith("#") | ||||||
|  |                     ? TextColor.fromCSSHexString(c) | ||||||
|  |                     : NamedTextColor.NAMES.value(c.toLowerCase(Locale.ROOT)); | ||||||
|  |             if (tc == null) | ||||||
|  |                 throw new IllegalArgumentException("Invalid color string '" + c + "'."); | ||||||
|  |             return color(tc); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |  | ||||||
|         /** |         /** | ||||||
| @@ -880,18 +856,6 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource | |||||||
|          * @return this. |          * @return this. | ||||||
|          */ |          */ | ||||||
|         public FormatableChat hover(ComponentLike v) { return hover(v.asComponent()); } |         public FormatableChat hover(ComponentLike v) { return hover(v.asComponent()); } | ||||||
|         /** |  | ||||||
|          * Configure this component to show the provided component when hovered. |  | ||||||
|          * @param v the component to show. |  | ||||||
|          * @return this. |  | ||||||
|          */ |  | ||||||
|         public FormatableChat hover(BaseComponent v) { return hover(toAdventure(v)); } |  | ||||||
|         /** |  | ||||||
|          * Configure this component to show the provided component when hovered. |  | ||||||
|          * @param v the component to show. |  | ||||||
|          * @return this. |  | ||||||
|          */ |  | ||||||
|         public FormatableChat hover(BaseComponent[] v) { return hover(toAdventure(v)); } |  | ||||||
|         /** |         /** | ||||||
|          * Configure this component to show the provided legacy text when hovered. |          * Configure this component to show the provided legacy text when hovered. | ||||||
|          * @param legacyText the legacy text to show. |          * @param legacyText the legacy text to show. | ||||||
| @@ -919,7 +883,7 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource | |||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public int hashCode() { |     public int hashCode() { | ||||||
|         return getAdv().hashCode(); |         return get().hashCode(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -930,63 +894,44 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource | |||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     /* package */ static ComponentLike filterObjToComponentLike(Object v) { | ||||||
|  |         return switch (v) { | ||||||
|  |             case ComponentLike componentLike -> componentLike; | ||||||
|  |             case null, default -> Component.text(Objects.toString(v)); | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /* package */ static ComponentLike[] filterObjToComponentLike(Object[] values) { |     /* package */ static ComponentLike[] filterObjToComponentLike(Object[] values) { | ||||||
|         if (values == null) |         if (values == null) | ||||||
|             return null; |             return null; | ||||||
|         ComponentLike[] ret = new ComponentLike[values.length]; |         ComponentLike[] ret = new ComponentLike[values.length]; | ||||||
|         for (int i = 0; i < values.length; i++) { |         for (int i = 0; i < values.length; i++) { | ||||||
|             Object v = values[i]; |             ret[i] = filterObjToComponentLike(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)); |  | ||||||
|         } |         } | ||||||
|         return ret; |         return ret; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     /** |     /* package */ static TranslationArgumentLike[] filterObjToTranslationArgumentLike(Object[] values) { | ||||||
|      * Converts the Bungee {@link BaseComponent} array into Adventure {@link Component}. |         if (values == null) | ||||||
|      * @param components the Bungee {@link BaseComponent} array. |             return null; | ||||||
|      * @return a {@link Component}. |         TranslationArgumentLike[] ret = new TranslationArgumentLike[values.length]; | ||||||
|      */ |         for (int i = 0; i < values.length; i++) { | ||||||
|     public static Component toAdventure(BaseComponent[] components) { |             Object v = values[i]; | ||||||
|         return BungeeComponentSerializer.get().deserialize(components); |             if (v instanceof Number n) | ||||||
|     } |                 ret[i] = TranslationArgument.numeric(n); | ||||||
|     /** |             else if (v instanceof Boolean b) | ||||||
|      * Converts the Bungee {@link BaseComponent} into Adventure {@link Component}. |                 ret[i] = TranslationArgument.bool(b); | ||||||
|      * @param component the Bungee {@link BaseComponent}. |             else | ||||||
|      * @return a {@link Component}. |                 ret[i] = TranslationArgument.component(filterObjToComponentLike(values[i])); | ||||||
|      */ |         } | ||||||
|     public static Component toAdventure(BaseComponent component) { |         return ret; | ||||||
|         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(Component component) { |  | ||||||
|         return BungeeComponentSerializer.get().serialize(component); |  | ||||||
|     } |  | ||||||
|     /** |  | ||||||
|      * Converts the Adventure {@link Component} into Bungee {@link BaseComponent}. |  | ||||||
|      * @param component the Adventure {@link Component}. |  | ||||||
|      * @return a {@link BaseComponent}. |  | ||||||
|      */ |  | ||||||
|     public static BaseComponent toBungee(Component component) { |  | ||||||
|         BaseComponent[] arr = toBungeeArray(component); |  | ||||||
|         return arr.length == 1 ? arr[0] : new net.md_5.bungee.api.chat.TextComponent(arr); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Force the italic formating to be set to false if it is not explicitely set in the component. |      * Force the italic formatting to be set to false if it is not explicitly set in the component. | ||||||
|      * This is useful for item lores that defaults to italic in the game UI. |      * This is useful for item lores that defaults to italic in the game UI. | ||||||
|      * @param c the {@link Chat} in which to set the italic property if needed. |      * @param c the {@link Chat} in which to set the italic property if needed. | ||||||
|      * @return the provided {@link Chat} instance. |      * @return the provided {@link Chat} instance. | ||||||
|   | |||||||
| @@ -4,15 +4,30 @@ import java.util.ArrayList; | |||||||
| import java.util.List; | import java.util.List; | ||||||
|  |  | ||||||
| import net.kyori.adventure.text.format.TextColor; | import net.kyori.adventure.text.format.TextColor; | ||||||
|  | import org.jetbrains.annotations.NotNull; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * A custom gradient with a least 2 colors in it. |  * A custom gradient with at least 2 colors in it. | ||||||
|  */ |  */ | ||||||
| public class ChatColorGradient { | public class ChatColorGradient { | ||||||
|     private record GradientColor(float location, TextColor color) { } |  | ||||||
|  |     private record GradientColor( | ||||||
|  |             float location, | ||||||
|  |             TextColor color | ||||||
|  |     ) implements Comparable<GradientColor> { | ||||||
|  |         @Override | ||||||
|  |         public int compareTo(@NotNull ChatColorGradient.GradientColor o) { | ||||||
|  |             return Float.compare(location(), o.location()); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     private final List<GradientColor> colors = new ArrayList<>(); |     private final List<GradientColor> colors = new ArrayList<>(); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Create the custom gradient. | ||||||
|  |      */ | ||||||
|  |     public ChatColorGradient() {} | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Put a specific color at a specific location in the gradient. |      * Put a specific color at a specific location in the gradient. | ||||||
|      * @param gradientLocation the location in the gradient. |      * @param gradientLocation the location in the gradient. | ||||||
| @@ -21,6 +36,7 @@ public class ChatColorGradient { | |||||||
|      */ |      */ | ||||||
|     public synchronized ChatColorGradient add(float gradientLocation, TextColor gradientColor) { |     public synchronized ChatColorGradient add(float gradientLocation, TextColor gradientColor) { | ||||||
|         colors.add(new GradientColor(gradientLocation, gradientColor)); |         colors.add(new GradientColor(gradientLocation, gradientColor)); | ||||||
|  |         colors.sort(null); | ||||||
|         return this; |         return this; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -31,25 +47,26 @@ public class ChatColorGradient { | |||||||
|      */ |      */ | ||||||
|     public synchronized TextColor pickColorAt(float gradientLocation) { |     public synchronized TextColor pickColorAt(float gradientLocation) { | ||||||
|         if (colors.isEmpty()) |         if (colors.isEmpty()) | ||||||
|             throw new IllegalStateException("Must define at least one color in this ChatValueGradient instance."); |             throw new IllegalStateException("Must define at least one color in this ChatColorGradient instance."); | ||||||
|         if (colors.size() == 1) |         if (colors.size() == 1) | ||||||
|             return colors.get(0).color(); |             return colors.getFirst().color(); | ||||||
|  |  | ||||||
|         colors.sort((p1, p2) -> Float.compare(p1.location(), p2.location())); |         int i = 0; | ||||||
|  |         for (; i < colors.size(); i++) { | ||||||
|         if (gradientLocation <= colors.get(0).location()) |             if (gradientLocation <= colors.get(i).location()) | ||||||
|             return colors.get(0).color(); |  | ||||||
|         if (gradientLocation >= colors.get(colors.size() - 1).location()) |  | ||||||
|             return colors.get(colors.size() - 1).color(); |  | ||||||
|  |  | ||||||
|         int p1 = 1; |  | ||||||
|         for (; p1 < colors.size(); p1++) { |  | ||||||
|             if (colors.get(p1).location() >= gradientLocation) |  | ||||||
|                 break; |                 break; | ||||||
|         } |         } | ||||||
|         int p0 = p1 - 1; |  | ||||||
|         float v0 = colors.get(p0).location(), v1 = colors.get(p1).location(); |         if (i == 0) | ||||||
|         TextColor cc0 = colors.get(p0).color(), cc1 = colors.get(p1).color(); |             return colors.get(i).color(); | ||||||
|         return ChatColorUtil.interpolateColor(v0, v1, gradientLocation, cc0, cc1); |         if (i == colors.size()) | ||||||
|  |             return colors.getLast().color(); | ||||||
|  |  | ||||||
|  |         int p = i - 1; | ||||||
|  |         float pLoc = colors.get(p).location(); | ||||||
|  |         float iLoc = colors.get(i).location(); | ||||||
|  |         TextColor pCol = colors.get(p).color(); | ||||||
|  |         TextColor iCol = colors.get(i).color(); | ||||||
|  |         return ChatColorUtil.interpolateColor(pLoc, iLoc, gradientLocation, pCol, iCol); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,20 +1,20 @@ | |||||||
| package fr.pandacube.lib.chat; | 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 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 { | public class ChatColorUtil { | ||||||
|  |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * All characters that represent a colorcode. |      * All characters that represent a color code. | ||||||
|      */ |      */ | ||||||
|     public static final String ALL_COLORS = "0123456789AaBbCcDdEeFf"; |     public static final String ALL_COLORS = "0123456789AaBbCcDdEeFf"; | ||||||
|     /** |     /** | ||||||
| @@ -30,7 +30,7 @@ public class ChatColorUtil { | |||||||
|      * Returns the legacy format needed to reproduce the format at the end of the provided legacy text. |      * Returns the legacy format needed to reproduce the format at the end of the provided legacy text. | ||||||
|      * Supports standard chat colors and formats, BungeeCord Chat rgb format and EssentialsX rgb format. |      * Supports standard chat colors and formats, BungeeCord Chat rgb format and EssentialsX rgb format. | ||||||
|      * The RGB value from EssentialsX format is converted to BungeeCord Chat when included in the returned value. |      * The RGB value from EssentialsX format is converted to BungeeCord Chat when included in the returned value. | ||||||
|      * @param legacyText the legacy formated text. |      * @param legacyText the legacy formatted text. | ||||||
|      * @return the active format at the end of the provided text. |      * @return the active format at the end of the provided text. | ||||||
|      */ |      */ | ||||||
|     public static String getLastColors(String legacyText) { |     public static String getLastColors(String legacyText) { | ||||||
| @@ -38,12 +38,12 @@ public class ChatColorUtil { | |||||||
|         int length = legacyText.length(); |         int length = legacyText.length(); | ||||||
|  |  | ||||||
|         for (int index = length - 2; index >= 0; index--) { |         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 |                 // detection of rgb color §x§0§1§2§3§4§5 | ||||||
|                 String rgb; |                 String rgb; | ||||||
|                 if (index > 11 |                 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' | ||||||
|                         || legacyText.charAt(index - 11) == 'X') |                         || legacyText.charAt(index - 11) == 'X') | ||||||
|                         && HEX_COLOR_PATTERN.matcher(rgb = legacyText.substring(index - 12, index + 2)).matches()) { |                         && HEX_COLOR_PATTERN.matcher(rgb = legacyText.substring(index - 12, index + 2)).matches()) { | ||||||
| @@ -64,7 +64,7 @@ public class ChatColorUtil { | |||||||
|  |  | ||||||
|                 // try detect non-rgb format |                 // try detect non-rgb format | ||||||
|                 char colorChar = legacyText.charAt(index + 1); |                 char colorChar = legacyText.charAt(index + 1); | ||||||
|                 ChatColor legacyColor = getChatColorByChar(colorChar); |                 LegacyChatFormat legacyColor = LegacyChatFormat.of(colorChar); | ||||||
|  |  | ||||||
|                 if (legacyColor != null) { |                 if (legacyColor != null) { | ||||||
|                     result.insert(0, legacyColor); |                     result.insert(0, legacyColor); | ||||||
| @@ -83,15 +83,6 @@ public class ChatColorUtil { | |||||||
|         return result.toString(); |         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 |      * Translate the color code of the provided string, that uses the alt color char, to the {@code §} color code | ||||||
|      * format. |      * format. | ||||||
|      * <p> |      * <p> | ||||||
|      * This method is the improved version of {@link ChatColor#translateAlternateColorCodes(char, String)}, |      * This method is the improved version of Bukkit’s {@code ChatColor.translateAlternateColorCodes(char, String)}, | ||||||
|      * because it takes into account essentials RGB color code, and {@code altColorChar} escaping (by doubling it). |      * 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 |      * 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)}). |      * to component (see {@link Chat#legacyText(Object)}). | ||||||
| @@ -112,7 +103,7 @@ public class ChatColorUtil { | |||||||
|      */ |      */ | ||||||
|     public static String translateAlternateColorCodes(char altColorChar, String textToTranslate) |     public static String translateAlternateColorCodes(char altColorChar, String textToTranslate) | ||||||
|     { |     { | ||||||
|         char colorChar = ChatColor.COLOR_CHAR; |         char colorChar = LegacyChatFormat.COLOR_CHAR; | ||||||
|         StringBuilder acc = new StringBuilder(); |         StringBuilder acc = new StringBuilder(); | ||||||
|         char[] b = textToTranslate.toCharArray(); |         char[] b = textToTranslate.toCharArray(); | ||||||
|         for ( int i = 0; i < b.length; i++ ) |         for ( int i = 0; i < b.length; i++ ) | ||||||
| @@ -180,7 +171,7 @@ public class ChatColorUtil { | |||||||
|      * @return the text fully italic. |      * @return the text fully italic. | ||||||
|      */ |      */ | ||||||
|     public static String forceItalic(String legacyText) { |     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. |      * @return the text fully bold. | ||||||
|      */ |      */ | ||||||
|     public static String forceBold(String legacyText) { |     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. |      * @return the text fully underlined. | ||||||
|      */ |      */ | ||||||
|     public static String forceUnderline(String legacyText) { |     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. |      * @return the text fully stroked through. | ||||||
|      */ |      */ | ||||||
|     public static String forceStrikethrough(String legacyText) { |     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. |      * @return the text fully obfuscated. | ||||||
|      */ |      */ | ||||||
|     public static String forceObfuscated(String legacyText) { |     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 |         return format + legacyText | ||||||
|                 .replace(format.toString(), "") // remove previous tag to make the result cleaner |                 .replace(formatStr, "") // remove previous tag to make the result cleaner | ||||||
|                 .replaceAll("§([a-frA-FR\\d])", "§$1" + format); |                 .replaceAll("§([a-frA-FR\\d])", "§$1" + formatStr); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -243,40 +235,12 @@ public class ChatColorUtil { | |||||||
|      * @return the resulting text. |      * @return the resulting text. | ||||||
|      */ |      */ | ||||||
|     public static String resetToColor(String legacyText, String color) { |     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. |      * Create a color, interpolating between 2 colors. | ||||||
|      * @param v0 the value corresponding to color {@code cc0}. |      * @param v0 the value corresponding to color {@code cc0}. | ||||||
| @@ -293,4 +257,7 @@ public class ChatColorUtil { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     private ChatColorUtil() {} | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -8,6 +8,7 @@ import net.kyori.adventure.text.format.TextColor; | |||||||
| /** | /** | ||||||
|  * Class holding static configuration values for chat component rendering. |  * Class holding static configuration values for chat component rendering. | ||||||
|  */ |  */ | ||||||
|  | @SuppressWarnings("CanBeFinal") | ||||||
| public class ChatConfig { | public class ChatConfig { | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -29,7 +30,7 @@ public class ChatConfig { | |||||||
|     /** |     /** | ||||||
|      * The color used for successful messages. |      * The color used for successful messages. | ||||||
|      */ |      */ | ||||||
|     public static TextColor successColor = PandaTheme.CHAT_GREEN_SATMAX; |     public static TextColor successColor = PandaTheme.CHAT_GREEN_MAX_SAT; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * The color used for error/failure messages. |      * The color used for error/failure messages. | ||||||
| @@ -49,7 +50,7 @@ public class ChatConfig { | |||||||
|     /** |     /** | ||||||
|      * The color used to display data in a message. |      * 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. |      * The color used for displayed URLs and clickable URLs. | ||||||
| @@ -67,14 +68,14 @@ public class ChatConfig { | |||||||
|     public static TextColor highlightedCommandColor = NamedTextColor.WHITE; |     public static TextColor highlightedCommandColor = NamedTextColor.WHITE; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * The color used for broadcasted messages. |      * The color used for broadcast messages. | ||||||
|      * It is often used in combination with {@link #prefix}. |      * It is often used in combination with {@link #prefix}. | ||||||
|      */ |      */ | ||||||
|     public static TextColor broadcastColor = NamedTextColor.YELLOW; |     public static TextColor broadcastColor = NamedTextColor.YELLOW; | ||||||
|      |      | ||||||
|     /** |     /** | ||||||
|      * The prefix used for prefixed messages. |      * The prefix used for prefixed messages. | ||||||
|      * It can be a sylized name of the server, like {@code "[Pandacube] "}. |      * It can be a stylized name of the server, like {@code "[Pandacube] "}. | ||||||
|      * It is often used in combination with {@link #broadcastColor}. |      * It is often used in combination with {@link #broadcastColor}. | ||||||
|      */ |      */ | ||||||
|     public static Supplier<Chat> prefix = PandaTheme::CHAT_MESSAGE_PREFIX; |     public static Supplier<Chat> prefix = PandaTheme::CHAT_MESSAGE_PREFIX; | ||||||
| @@ -86,7 +87,7 @@ public class ChatConfig { | |||||||
|      */ |      */ | ||||||
|     public static int getPrefixWidth(boolean console) { |     public static int getPrefixWidth(boolean console) { | ||||||
|         Chat c; |         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); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -104,7 +105,7 @@ public class ChatConfig { | |||||||
|         public static final TextColor CHAT_GREEN_4 = TextColor.fromHexString("#abe3b0"); // h=126 s=50 l=78 |         public static final TextColor CHAT_GREEN_4 = TextColor.fromHexString("#abe3b0"); // h=126 s=50 l=78 | ||||||
|  |  | ||||||
|         /** Green max saturation color. */ |         /** Green max saturation color. */ | ||||||
|         public static final TextColor CHAT_GREEN_SATMAX = TextColor.fromHexString("#00ff19"); // h=126 s=100 l=50 |         public static final TextColor CHAT_GREEN_MAX_SAT = TextColor.fromHexString("#00ff19"); // h=126 s=100 l=50 | ||||||
|         /** Green 1 saturated color. */ |         /** Green 1 saturated color. */ | ||||||
|         public static final TextColor CHAT_GREEN_1_SAT = TextColor.fromHexString("#20d532"); // h=126 s=50 l=48 |         public static final TextColor CHAT_GREEN_1_SAT = TextColor.fromHexString("#20d532"); // h=126 s=50 l=48 | ||||||
|         /** Green 2 saturated color. */ |         /** Green 2 saturated color. */ | ||||||
| @@ -156,5 +157,9 @@ public class ChatConfig { | |||||||
|                     .thenText("] "); |                     .thenText("] "); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         private PandaTheme() {} | ||||||
|  |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     private ChatConfig() {} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -59,6 +59,7 @@ public class ChatFilledLine implements ComponentLike { | |||||||
|     private boolean decorationBold = false; |     private boolean decorationBold = false; | ||||||
|     private int nbSide = ChatConfig.nbCharMargin; |     private int nbSide = ChatConfig.nbCharMargin; | ||||||
|     private boolean spacesAroundText = false; |     private boolean spacesAroundText = false; | ||||||
|  |     private boolean spacesDecorationRightSide = false; | ||||||
|     private boolean console = false; |     private boolean console = false; | ||||||
|     private Integer maxWidth = null; |     private Integer maxWidth = null; | ||||||
|  |  | ||||||
| @@ -116,6 +117,16 @@ public class ChatFilledLine implements ComponentLike { | |||||||
|         return this; |         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. |      * Configure if the line will be rendered on console or not. | ||||||
|      * @param console true for console, false for game UI. |      * @param console true for console, false for game UI. | ||||||
| @@ -140,7 +151,7 @@ public class ChatFilledLine implements ComponentLike { | |||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Renders this line to a {@link FormatableChat}. |      * Renders this line to a {@link FormatableChat}. | ||||||
|      * @return a new {@link FormatableChat} builded by this {@link ChatFilledLine}. |      * @return a new {@link FormatableChat} built by this {@link ChatFilledLine}. | ||||||
|      */ |      */ | ||||||
|     public FormatableChat toChat() { |     public FormatableChat toChat() { | ||||||
|         int maxWidth = (this.maxWidth != null) |         int maxWidth = (this.maxWidth != null) | ||||||
| @@ -184,7 +195,7 @@ public class ChatFilledLine implements ComponentLike { | |||||||
|         Chat d = Chat.chat() |         Chat d = Chat.chat() | ||||||
|                 .then(Chat.text(ChatUtil.repeatedChar(decorationChar, nbCharLeft)).color(decorationColor).bold(decorationBold)) |                 .then(Chat.text(ChatUtil.repeatedChar(decorationChar, nbCharLeft)).color(decorationColor).bold(decorationBold)) | ||||||
|                 .then(text); |                 .then(text); | ||||||
|         if (decorationChar != ' ') |         if (decorationChar != ' ' || spacesDecorationRightSide) | ||||||
|             d.then(Chat.text(ChatUtil.repeatedChar(decorationChar, nbCharRight)).color(decorationColor).bold(decorationBold)); |             d.then(Chat.text(ChatUtil.repeatedChar(decorationChar, nbCharRight)).color(decorationColor).bold(decorationBold)); | ||||||
|         return (FormatableChat) d; |         return (FormatableChat) d; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| package fr.pandacube.lib.chat; | package fr.pandacube.lib.chat; | ||||||
|  |  | ||||||
| import java.util.Objects; | import fr.pandacube.lib.chat.Chat.FormatableChat; | ||||||
|  |  | ||||||
| import net.kyori.adventure.text.BlockNBTComponent; | import net.kyori.adventure.text.BlockNBTComponent; | ||||||
| import net.kyori.adventure.text.Component; | import net.kyori.adventure.text.Component; | ||||||
| import net.kyori.adventure.text.ComponentBuilder; | 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.event.HoverEventSource; | ||||||
| import net.kyori.adventure.text.format.NamedTextColor; | import net.kyori.adventure.text.format.NamedTextColor; | ||||||
| import net.kyori.adventure.text.format.TextColor; | import net.kyori.adventure.text.format.TextColor; | ||||||
|  | import net.kyori.adventure.text.minimessage.MiniMessage; | ||||||
| import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; | 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. |  * Abstract class holding the publicly accessible methods to create an instance of {@link Chat} component. | ||||||
| @@ -32,15 +31,6 @@ public abstract class ChatStatic { | |||||||
|         return new FormatableChat(componentToBuilder(c)); |         return new FormatableChat(componentToBuilder(c)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Creates a {@link FormatableChat} from the provided Bungee {@link BaseComponent}. |  | ||||||
|      * @param c the {@link BaseComponent}. |  | ||||||
|      * @return a new {@link FormatableChat}. |  | ||||||
|      */ |  | ||||||
|     public static FormatableChat chatComponent(BaseComponent c) { |  | ||||||
|         return new FormatableChat(componentToBuilder(Chat.toAdventure(c))); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Creates a {@link FormatableChat} from the provided {@link ComponentLike}. |      * Creates a {@link FormatableChat} from the provided {@link ComponentLike}. | ||||||
|      * If the provided component is an instance of {@link Chat}, its content will be duplicated, and the provided one |      * If the provided component is an instance of {@link Chat}, its content will be duplicated, and the provided one | ||||||
| @@ -60,15 +50,6 @@ public abstract class ChatStatic { | |||||||
|         return new FormatableChat(Component.text()); |         return new FormatableChat(Component.text()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Creates a {@link FormatableChat} from the provided Bungee {@link BaseComponent BaseComponent[]}. |  | ||||||
|      * @param c the array of {@link BaseComponent}. |  | ||||||
|      * @return a new {@link FormatableChat}. |  | ||||||
|      */ |  | ||||||
|     public static FormatableChat chatComponent(BaseComponent[] c) { |  | ||||||
|         return chatComponent(Chat.toAdventure(c)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -91,18 +72,58 @@ public abstract class ChatStatic { | |||||||
|  |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Creates a {@link FormatableChat} with the provided legacy text as its content. |      * Creates a {@link FormatableChat} with the provided legacy text as its content, using the section {@code "§"} | ||||||
|      * @param legacyText the legacy text to use as the content. |      * character. | ||||||
|  |      * @param legacyText the legacy text to use as the content, that uses the {@code "§"} character. | ||||||
|      * @return a new {@link FormatableChat} with the provided text as its content. |      * @return a new {@link FormatableChat} with the provided text as its content. | ||||||
|      * @throws IllegalArgumentException If the {@code plainText} parameter is instance of {@link Chat} or |      * @throws IllegalArgumentException If the {@code legacyText} parameter is instance of {@link Chat} or | ||||||
|      *         {@link Component}. The caller should use {@link #chatComponent(ComponentLike)} |      *         {@link Component}. The caller should use {@link #chatComponent(ComponentLike)} | ||||||
|      *         instead. |      *         instead. | ||||||
|      */ |      */ | ||||||
|     public static FormatableChat legacyText(Object legacyText) { |     public static FormatableChat legacyText(Object legacyText) { | ||||||
|  |         return legacyText(legacyText, LegacyComponentSerializer.SECTION_CHAR); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Creates a {@link FormatableChat} with the provided legacy text as its content, using the ampersand {@code "&"} | ||||||
|  |      * character. | ||||||
|  |      * @param legacyText the legacy text to use as the content, that uses the {@code "&"} character. | ||||||
|  |      * @return a new {@link FormatableChat} with the provided text as its content. | ||||||
|  |      * @throws IllegalArgumentException If the {@code legacyText} parameter is instance of {@link Chat} or | ||||||
|  |      *         {@link Component}. The caller should use {@link #chatComponent(ComponentLike)} | ||||||
|  |      *         instead. | ||||||
|  |      */ | ||||||
|  |     public static FormatableChat legacyAmpersandText(Object legacyText) { | ||||||
|  |         return legacyText(legacyText, LegacyComponentSerializer.AMPERSAND_CHAR); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Creates a {@link FormatableChat} with the provided legacy text as its content, using the specified | ||||||
|  |      * legacyCharacter. | ||||||
|  |      * @param legacyText the legacy text to use as the content. | ||||||
|  |      * @param legacyCharacter the character used in the provided text to prefix color and format code. | ||||||
|  |      * @return a new {@link FormatableChat} with the provided text as its content. | ||||||
|  |      * @throws IllegalArgumentException If the {@code legacyText} parameter is instance of {@link Chat} or | ||||||
|  |      *         {@link Component}. The caller should use {@link #chatComponent(ComponentLike)} | ||||||
|  |      *         instead. | ||||||
|  |      */ | ||||||
|  |     private static FormatableChat legacyText(Object legacyText, char legacyCharacter) { | ||||||
|         if (legacyText instanceof ComponentLike) { |         if (legacyText instanceof ComponentLike) { | ||||||
|             throw new IllegalArgumentException("Expected any object except instance of " + ComponentLike.class + ". Received " + legacyText + ". Please use ChatStatic.chatComponent(ComponentLike) instead."); |             throw new IllegalArgumentException("Expected any object except instance of " + ComponentLike.class + ". Received " + legacyText + ". Please use ChatStatic.chatComponent(ComponentLike) instead."); | ||||||
|         } |         } | ||||||
|         return chatComponent(LegacyComponentSerializer.legacySection().deserialize(Objects.toString(legacyText))); |         return chatComponent(LegacyComponentSerializer.legacy(legacyCharacter).deserialize(Objects.toString(legacyText))); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Creates a {@link FormatableChat} with the provided MiniMessage text as its content. | ||||||
|  |      * @param miniMessageText the MiniMessage text to use as the content. | ||||||
|  |      * @return a new {@link FormatableChat} with the provided text as its content. | ||||||
|  |      */ | ||||||
|  |     public static FormatableChat miniMessageText(String miniMessageText) { | ||||||
|  |         return chatComponent(MiniMessage.miniMessage().deserialize(miniMessageText)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -207,7 +228,7 @@ public abstract class ChatStatic { | |||||||
|      * @param c the {@link Component}. |      * @param c the {@link Component}. | ||||||
|      * @return a new {@link FormatableChat}. |      * @return a new {@link FormatableChat}. | ||||||
|      */ |      */ | ||||||
|     public static FormatableChat playerNameComponent(Component c) { |     public static FormatableChat playerNameComponent(ComponentLike c) { | ||||||
|         FormatableChat fc = chatComponent(c); |         FormatableChat fc = chatComponent(c); | ||||||
|         fc.builder.colorIfAbsent(NamedTextColor.WHITE); |         fc.builder.colorIfAbsent(NamedTextColor.WHITE); | ||||||
|         return fc; |         return fc; | ||||||
| @@ -223,13 +244,13 @@ public abstract class ChatStatic { | |||||||
|      * @return a new {@link FormatableChat} with the provided translation key and parameters. |      * @return a new {@link FormatableChat} with the provided translation key and parameters. | ||||||
|      */ |      */ | ||||||
|     public static FormatableChat translation(String key, Object... with) { |     public static FormatableChat translation(String key, Object... with) { | ||||||
|         return new FormatableChat(Component.translatable().key(key).args(Chat.filterObjToComponentLike(with))); |         return new FormatableChat(Component.translatable().key(key).arguments(Chat.filterObjToTranslationArgumentLike(with))); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Creates a {@link FormatableChat} with the provided keybind. |      * Creates a {@link FormatableChat} with the provided keybinding. | ||||||
|      * @param key the keybind to display. |      * @param key the keybinding to display. | ||||||
|      * @return a new {@link FormatableChat} with the provided keybind. |      * @return a new {@link FormatableChat} with the provided keybinding. | ||||||
|      */ |      */ | ||||||
|     public static FormatableChat keybind(String key) { |     public static FormatableChat keybind(String key) { | ||||||
|         return new FormatableChat(Component.keybind().keybind(key)); |         return new FormatableChat(Component.keybind().keybind(key)); | ||||||
| @@ -451,12 +472,12 @@ public abstract class ChatStatic { | |||||||
|  |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Creates a {@link FormatableChat} filling a line of chat (or console) with decoration and a left-aligned text. |      * Creates a {@link FormatableChat} filling a chat line with decoration and a left-aligned text. | ||||||
|      * @param text the text aligned to the left. |      * @param text the text aligned to the left. | ||||||
|      * @param decorationChar the character used for decoration around the text. |      * @param decorationChar the character used for decoration around the text. | ||||||
|      * @param decorationColor the color used for the decoration characters. |      * @param decorationColor the color used for the decoration characters. | ||||||
|      * @param console if the line is rendered on console (true) or IG (false). |      * @param console if the line is rendered on console (true) or IG (false). | ||||||
|      * @return a new {@link FormatableChat} filling a line of chat (or console) with decoration and a left-aligned text. |      * @return a new {@link FormatableChat} filling a chat line with decoration and a left-aligned text. | ||||||
|      * @see ChatFilledLine#leftText(ComponentLike) |      * @see ChatFilledLine#leftText(ComponentLike) | ||||||
|      */ |      */ | ||||||
|     public static FormatableChat leftText(ComponentLike text, char decorationChar, TextColor decorationColor, boolean console) { |     public static FormatableChat leftText(ComponentLike text, char decorationChar, TextColor decorationColor, boolean console) { | ||||||
| @@ -464,11 +485,11 @@ public abstract class ChatStatic { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Creates a {@link FormatableChat} filling a line of chat (or console) with the configured decoration character and |      * Creates a {@link FormatableChat} filling a chat line with the configured decoration character and | ||||||
|      * color and a left-aligned text. |      * color and a left-aligned text. | ||||||
|      * @param text the text aligned to the left. |      * @param text the text aligned to the left. | ||||||
|      * @param console if the line is rendered on console (true) or IG (false). |      * @param console if the line is rendered on console (true) or IG (false). | ||||||
|      * @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character |      * @return a new {@link FormatableChat} filling a chat line with the configured decoration character | ||||||
|      *         and color and a left-aligned text. |      *         and color and a left-aligned text. | ||||||
|      * @see ChatFilledLine#leftText(ComponentLike) |      * @see ChatFilledLine#leftText(ComponentLike) | ||||||
|      * @see ChatConfig#decorationChar |      * @see ChatConfig#decorationChar | ||||||
| @@ -479,12 +500,12 @@ public abstract class ChatStatic { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Creates a {@link FormatableChat} filling a line of chat (or console) with decoration and a right-aligned text. |      * Creates a {@link FormatableChat} filling a chat line with decoration and a right-aligned text. | ||||||
|      * @param text the text aligned to the right. |      * @param text the text aligned to the right. | ||||||
|      * @param decorationChar the character used for decoration around the text. |      * @param decorationChar the character used for decoration around the text. | ||||||
|      * @param decorationColor the color used for the decoration characters. |      * @param decorationColor the color used for the decoration characters. | ||||||
|      * @param console if the line is rendered on console (true) or IG (false). |      * @param console if the line is rendered on console (true) or IG (false). | ||||||
|      * @return a new {@link FormatableChat} filling a line of chat (or console) with decoration and a right-aligned |      * @return a new {@link FormatableChat} filling a chat line with decoration and a right-aligned | ||||||
|      *         text. |      *         text. | ||||||
|      * @see ChatFilledLine#rightText(ComponentLike) |      * @see ChatFilledLine#rightText(ComponentLike) | ||||||
|      */ |      */ | ||||||
| @@ -493,11 +514,11 @@ public abstract class ChatStatic { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Creates a {@link FormatableChat} filling a line of chat (or console) with the configured decoration character and |      * Creates a {@link FormatableChat} filling a chat line with the configured decoration character and | ||||||
|      * color and a right-aligned text. |      * color and a right-aligned text. | ||||||
|      * @param text the text aligned to the right. |      * @param text the text aligned to the right. | ||||||
|      * @param console if the line is rendered on console (true) or IG (false). |      * @param console if the line is rendered on console (true) or IG (false). | ||||||
|      * @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character |      * @return a new {@link FormatableChat} filling a chat line with the configured decoration character | ||||||
|      *         and color and a right-aligned text. |      *         and color and a right-aligned text. | ||||||
|      * @see ChatFilledLine#rightText(ComponentLike) |      * @see ChatFilledLine#rightText(ComponentLike) | ||||||
|      * @see ChatConfig#decorationChar |      * @see ChatConfig#decorationChar | ||||||
| @@ -508,12 +529,12 @@ public abstract class ChatStatic { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Creates a {@link FormatableChat} filling a line of chat (or console) with decoration and a centered text. |      * Creates a {@link FormatableChat} filling a chat line with decoration and a centered text. | ||||||
|      * @param text the text aligned to the center. |      * @param text the text aligned to the center. | ||||||
|      * @param decorationChar the character used for decoration around the text. |      * @param decorationChar the character used for decoration around the text. | ||||||
|      * @param decorationColor the color used for the decoration characters. |      * @param decorationColor the color used for the decoration characters. | ||||||
|      * @param console if the line is rendered on console (true) or IG (false). |      * @param console if the line is rendered on console (true) or IG (false). | ||||||
|      * @return a new {@link FormatableChat} filling a line of chat (or console) with decoration and a centered text. |      * @return a new {@link FormatableChat} filling a chat line with decoration and a centered text. | ||||||
|      * @see ChatFilledLine#centerText(ComponentLike) |      * @see ChatFilledLine#centerText(ComponentLike) | ||||||
|      */ |      */ | ||||||
|     public static FormatableChat centerText(ComponentLike text, char decorationChar, TextColor decorationColor, boolean console) { |     public static FormatableChat centerText(ComponentLike text, char decorationChar, TextColor decorationColor, boolean console) { | ||||||
| @@ -521,11 +542,11 @@ public abstract class ChatStatic { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Creates a {@link FormatableChat} filling a line of chat (or console) with the configured decoration character and |      * Creates a {@link FormatableChat} filling a chat line with the configured decoration character and | ||||||
|      * color and a centered text. |      * color and a centered text. | ||||||
|      * @param text the text aligned to the center. |      * @param text the text aligned to the center. | ||||||
|      * @param console if the line is rendered on console (true) or IG (false). |      * @param console if the line is rendered on console (true) or IG (false). | ||||||
|      * @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character |      * @return a new {@link FormatableChat} filling a chat line with the configured decoration character | ||||||
|      *         and color and a centered text. |      *         and color and a centered text. | ||||||
|      * @see ChatFilledLine#centerText(ComponentLike) |      * @see ChatFilledLine#centerText(ComponentLike) | ||||||
|      * @see ChatConfig#decorationChar |      * @see ChatConfig#decorationChar | ||||||
| @@ -536,11 +557,11 @@ public abstract class ChatStatic { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Creates a {@link FormatableChat} filling a line of chat (or console) with a decoration character and color. |      * Creates a {@link FormatableChat} filling a chat line with a decoration character and color. | ||||||
|      * @param decorationChar the character used for decoration. |      * @param decorationChar the character used for decoration. | ||||||
|      * @param decorationColor the color used for the decoration characters. |      * @param decorationColor the color used for the decoration characters. | ||||||
|      * @param console if the line is rendered on console (true) or IG (false). |      * @param console if the line is rendered on console (true) or IG (false). | ||||||
|      * @return a new {@link FormatableChat} filling a line of chat (or console) with a decoration character and color. |      * @return a new {@link FormatableChat} filling a chat line with a decoration character and color. | ||||||
|      * @see ChatFilledLine#filled() |      * @see ChatFilledLine#filled() | ||||||
|      */ |      */ | ||||||
|     public static FormatableChat filledLine(char decorationChar, TextColor decorationColor, boolean console) { |     public static FormatableChat filledLine(char decorationChar, TextColor decorationColor, boolean console) { | ||||||
| @@ -548,10 +569,10 @@ public abstract class ChatStatic { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Creates a {@link FormatableChat} filling a line of chat (or console) with the configured decoration character and |      * Creates a {@link FormatableChat} filling a chat line with the configured decoration character and | ||||||
|      * color. |      * color. | ||||||
|      * @param console if the line is rendered on console (true) or IG (false). |      * @param console if the line is rendered on console (true) or IG (false). | ||||||
|      * @return a new {@link FormatableChat} filling a line of chat (or console) with a decoration character and color. |      * @return a new {@link FormatableChat} filling a chat line with a decoration character and color. | ||||||
|      * @see ChatFilledLine#filled() |      * @see ChatFilledLine#filled() | ||||||
|      * @see ChatConfig#decorationChar |      * @see ChatConfig#decorationChar | ||||||
|      * @see ChatConfig#decorationColor |      * @see ChatConfig#decorationColor | ||||||
| @@ -591,52 +612,39 @@ public abstract class ChatStatic { | |||||||
|  |  | ||||||
|  |  | ||||||
|     private static ComponentBuilder<?, ?> componentToBuilder(Component c) { |     private static ComponentBuilder<?, ?> componentToBuilder(Component c) { | ||||||
|         ComponentBuilder<?, ?> builder; |         ComponentBuilder<?, ?> builder = switch (c) { | ||||||
|         if (c instanceof TextComponent) { |             case TextComponent textComponent -> Component.text() | ||||||
|             builder = Component.text() |                     .content(textComponent.content()); | ||||||
|                     .content(((TextComponent) c).content()); |             case TranslatableComponent translatableComponent -> Component.translatable() | ||||||
|         } |                     .key(translatableComponent.key()).arguments(translatableComponent.arguments()); | ||||||
|         else if (c instanceof TranslatableComponent) { |             case SelectorComponent selectorComponent -> Component.selector() | ||||||
|             builder = Component.translatable() |                     .pattern(selectorComponent.pattern()); | ||||||
|                     .key(((TranslatableComponent) c).key()) |             case ScoreComponent scoreComponent -> Component.score() | ||||||
|                     .args(((TranslatableComponent) c).args()); |                     .name(scoreComponent.name()) | ||||||
|         } |                     .objective(scoreComponent.objective()); | ||||||
|         else if (c instanceof SelectorComponent) { |             case KeybindComponent keybindComponent -> Component.keybind() | ||||||
|             builder = Component.selector() |                     .keybind(keybindComponent.keybind()); | ||||||
|                     .pattern(((SelectorComponent) c).pattern()); |             case BlockNBTComponent blockNBTComponent -> Component.blockNBT() | ||||||
|         } |                     .interpret(blockNBTComponent.interpret()) | ||||||
|         else if (c instanceof ScoreComponent) { |                     .nbtPath(blockNBTComponent.nbtPath()) | ||||||
|             builder = Component.score() |                     .pos(blockNBTComponent.pos()); | ||||||
|                     .name(((ScoreComponent) c).name()) |             case EntityNBTComponent entityNBTComponent -> Component.entityNBT() | ||||||
|                     .objective(((ScoreComponent) c).objective()); |                     .interpret(entityNBTComponent.interpret()) | ||||||
|         } |                     .nbtPath(entityNBTComponent.nbtPath()) | ||||||
|         else if (c instanceof KeybindComponent) { |                     .selector(entityNBTComponent.selector()); | ||||||
|             builder = Component.keybind() |             case StorageNBTComponent storageNBTComponent -> Component.storageNBT() | ||||||
|                     .keybind(((KeybindComponent) c).keybind()); |                     .interpret(storageNBTComponent.interpret()) | ||||||
|         } |                     .nbtPath(storageNBTComponent.nbtPath()) | ||||||
|         else if (c instanceof BlockNBTComponent) { |                     .storage(storageNBTComponent.storage()); | ||||||
|             builder = Component.blockNBT() |             case null, default -> throw new IllegalArgumentException("Unknown component type " + (c == null ? "null" : c.getClass())); | ||||||
|                     .interpret(((BlockNBTComponent) c).interpret()) |         }; | ||||||
|                     .nbtPath(((BlockNBTComponent) c).nbtPath()) |  | ||||||
|                     .pos(((BlockNBTComponent) c).pos()); |  | ||||||
|         } |  | ||||||
|         else if (c instanceof EntityNBTComponent) { |  | ||||||
|             builder = Component.entityNBT() |  | ||||||
|                     .interpret(((EntityNBTComponent) c).interpret()) |  | ||||||
|                     .nbtPath(((EntityNBTComponent) c).nbtPath()) |  | ||||||
|                     .selector(((EntityNBTComponent) c).selector()); |  | ||||||
|         } |  | ||||||
|         else if (c instanceof StorageNBTComponent) { |  | ||||||
|             builder = Component.storageNBT() |  | ||||||
|                     .interpret(((StorageNBTComponent) c).interpret()) |  | ||||||
|                     .nbtPath(((StorageNBTComponent) c).nbtPath()) |  | ||||||
|                     .storage(((StorageNBTComponent) c).storage()); |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             throw new IllegalArgumentException("Unknows component type " + c.getClass()); |  | ||||||
|         } |  | ||||||
|         return builder.style(c.style()).append(c.children()); |         return builder.style(c.style()).append(c.children()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Creates a new {@link ChatStatic} instance. | ||||||
|  |      */ | ||||||
|  |     protected ChatStatic() {} | ||||||
|  |  | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,10 +1,13 @@ | |||||||
| package fr.pandacube.lib.chat; | package fr.pandacube.lib.chat; | ||||||
|  |  | ||||||
|  | import net.kyori.adventure.text.Component; | ||||||
|  | import net.kyori.adventure.text.ComponentLike; | ||||||
|  |  | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.List; | 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 { | public class ChatTreeNode { | ||||||
|  |  | ||||||
| @@ -19,7 +22,7 @@ public class ChatTreeNode { | |||||||
|     /** |     /** | ||||||
|      * The component for the current node. |      * The component for the current node. | ||||||
|      */ |      */ | ||||||
|     public final Chat component; |     public final ComponentLike component; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Children nodes. |      * Children nodes. | ||||||
| @@ -27,10 +30,10 @@ public class ChatTreeNode { | |||||||
|     public final List<ChatTreeNode> children = new ArrayList<>(); |     public final List<ChatTreeNode> children = new ArrayList<>(); | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Construct an new {@link ChatTreeNode}. |      * Construct a new {@link ChatTreeNode}. | ||||||
|      * @param cmp the component for the current node. |      * @param cmp the component for the current node. | ||||||
|      */ |      */ | ||||||
|     public ChatTreeNode(Chat cmp) { |     public ChatTreeNode(ComponentLike cmp) { | ||||||
|         component = cmp; |         component = cmp; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -48,9 +51,9 @@ public class ChatTreeNode { | |||||||
|      * Generate a tree view based on this tree structure. |      * Generate a tree view based on this tree structure. | ||||||
|      * <p> |      * <p> | ||||||
|      * Each element in the returned list represent 1 line of this tree view. |      * Each element in the returned list represent 1 line of this tree view. | ||||||
|      * Thus, the caller may send each line separately or at once depending of the quantity of data. |      * Thus, the caller may send each line separately or at once, depending on the quantity of data. | ||||||
|      * @param console true to render for console, false otherwise. |      * @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) { |     public List<Chat> render(boolean console) { | ||||||
|         List<Chat> ret = new ArrayList<>(); |         List<Chat> ret = new ArrayList<>(); | ||||||
|   | |||||||
| @@ -1,5 +1,17 @@ | |||||||
| package fr.pandacube.lib.chat; | package fr.pandacube.lib.chat; | ||||||
|  |  | ||||||
|  | import fr.pandacube.lib.chat.Chat.FormatableChat; | ||||||
|  | import net.kyori.adventure.text.Component; | ||||||
|  | import net.kyori.adventure.text.ComponentLike; | ||||||
|  | import net.kyori.adventure.text.TextComponent; | ||||||
|  | import net.kyori.adventure.text.TranslatableComponent; | ||||||
|  | import net.kyori.adventure.text.TranslationArgument; | ||||||
|  | import net.kyori.adventure.text.format.NamedTextColor; | ||||||
|  | import net.kyori.adventure.text.format.TextColor; | ||||||
|  | import net.kyori.adventure.text.format.TextDecoration; | ||||||
|  | import net.kyori.adventure.text.format.TextDecoration.State; | ||||||
|  | import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; | ||||||
|  |  | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.Arrays; | import java.util.Arrays; | ||||||
| import java.util.Collections; | import java.util.Collections; | ||||||
| @@ -10,19 +22,10 @@ import java.util.Set; | |||||||
| import java.util.TreeSet; | import java.util.TreeSet; | ||||||
| import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||||
|  |  | ||||||
| import net.kyori.adventure.text.Component; | import static fr.pandacube.lib.chat.ChatStatic.chat; | ||||||
| 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; |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Provides various methods and properties to manipulate text displayed in chat an other parts of the game. |  * Provides various methods and properties to manipulate text displayed in chat and other parts of the game. | ||||||
|  */ |  */ | ||||||
| public class ChatUtil { | public class ChatUtil { | ||||||
|  |  | ||||||
| @@ -48,7 +51,7 @@ public class ChatUtil { | |||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Mapping indicating the text pixel with for specific characters in the default Minecraft font. |      * Mapping indicating the text pixel with for specific characters in the default Minecraft font. | ||||||
|      * If a character doesn’t have a mapping in this map, then its width is {@link #DEFAULT_CHAR_SIZE}. |      * If a character doesn't have a mapping in this map, then its width is {@link #DEFAULT_CHAR_SIZE}. | ||||||
|      */ |      */ | ||||||
|     public static final Map<Character, Integer> CHAR_SIZES; |     public static final Map<Character, Integer> CHAR_SIZES; | ||||||
|     static { |     static { | ||||||
| @@ -112,7 +115,7 @@ public class ChatUtil { | |||||||
|      * @param nbPages the number of pages. |      * @param nbPages the number of pages. | ||||||
|      * @param nbPagesToDisplay the number of pages to display around the first page, the last page and the |      * @param nbPagesToDisplay the number of pages to display around the first page, the last page and the | ||||||
|      *                         {@code currentPage}. |      *                         {@code currentPage}. | ||||||
|      * @return a {@link Chat} containging the created page navigator. |      * @return a {@link Chat} containing the created page navigator. | ||||||
|      */ |      */ | ||||||
|     public static Chat createPagination(String prefix, String cmdFormat, int currentPage, int nbPages, int nbPagesToDisplay) { |     public static Chat createPagination(String prefix, String cmdFormat, int currentPage, int nbPages, int nbPagesToDisplay) { | ||||||
|         Set<Integer> pagesToDisplay = new TreeSet<>(); |         Set<Integer> pagesToDisplay = new TreeSet<>(); | ||||||
| @@ -127,7 +130,7 @@ public class ChatUtil { | |||||||
|                 pagesToDisplay.add(i); |                 pagesToDisplay.add(i); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         Chat d = ChatStatic.chat().thenLegacyText(prefix); |         Chat d = chat().thenLegacyText(prefix); | ||||||
|         boolean first = true; |         boolean first = true; | ||||||
|         int previous = 0; |         int previous = 0; | ||||||
|  |  | ||||||
| @@ -149,11 +152,11 @@ public class ChatUtil { | |||||||
|             else |             else | ||||||
|                 first = false; |                 first = false; | ||||||
|  |  | ||||||
|             FormatableChat pDisp = Chat.clickableCommand(Chat.text(page), String.format(cmdFormat, page), Chat.text("Aller à la page " + page)); |             FormatableChat pDisplay = Chat.clickableCommand(Chat.text(page), String.format(cmdFormat, page), Chat.text("Aller à la page " + page)); | ||||||
|             if (page == currentPage) { |             if (page == currentPage) { | ||||||
|                 pDisp.highlightedCommandColor(); |                 pDisplay.highlightedCommandColor(); | ||||||
|             } |             } | ||||||
|             d.then(pDisp); |             d.then(pDisplay); | ||||||
|  |  | ||||||
|             previous = page; |             previous = page; | ||||||
|         } |         } | ||||||
| @@ -167,6 +170,58 @@ public class ChatUtil { | |||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Do like {@link String#join(CharSequence, Iterable)}, but for components, and the last separator is different from | ||||||
|  |      * the others. It is useful when enumerating things in a sentence, for instance : | ||||||
|  |      * <code>"a thing<u>, </u>a thing<u> and </u>a thing"</code> | ||||||
|  |      * (the coma being the usual separator, and {@code " and "} being the final separator). | ||||||
|  |      * @param regularSeparator the separator used everywhere except between the two last components to join. | ||||||
|  |      * @param finalSeparator the separator used between the two last components to join. | ||||||
|  |      * @param elements the components to join. | ||||||
|  |      * @return a new {@link Chat} instance with all the provided {@code component} joined using the separators. | ||||||
|  |      */ | ||||||
|  |     public static FormatableChat joinGrammatically(ComponentLike regularSeparator, ComponentLike finalSeparator, List<? extends ComponentLike> elements) { | ||||||
|  |         int size = elements == null ? 0 : elements.size(); | ||||||
|  |         int last = size - 1; | ||||||
|  |         return switch (size) { | ||||||
|  |             case 0, 1, 2 -> join(finalSeparator, elements); | ||||||
|  |             default -> (FormatableChat) join(regularSeparator, elements.subList(0, last)) | ||||||
|  |                     .then(finalSeparator) | ||||||
|  |                     .then(elements.get(last)); | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Do like {@link String#join(CharSequence, Iterable)}, but for components. | ||||||
|  |      * @param separator the separator used everywhere except between the two last components to join. | ||||||
|  |      * @param elements the components to join. | ||||||
|  |      * @return a new {@link Chat} instance with all the provided {@code component} joined using the separators. | ||||||
|  |      */ | ||||||
|  |     public static FormatableChat join(ComponentLike separator, Iterable<? extends ComponentLike> elements) { | ||||||
|  |         FormatableChat c = chat(); | ||||||
|  |         if (elements == null) | ||||||
|  |             return c; | ||||||
|  |         boolean first = true; | ||||||
|  |         for (ComponentLike el : elements) { | ||||||
|  |             if (!first) { | ||||||
|  |                 c.then(separator); | ||||||
|  |             } | ||||||
|  |             c.then(el); | ||||||
|  |             first = false; | ||||||
|  |         } | ||||||
|  |         return c; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -210,8 +265,8 @@ public class ChatUtil { | |||||||
|             count += strWidth(((TextComponent)component).content(), console, actuallyBold); |             count += strWidth(((TextComponent)component).content(), console, actuallyBold); | ||||||
|         } |         } | ||||||
|         else if (component instanceof TranslatableComponent) { |         else if (component instanceof TranslatableComponent) { | ||||||
|             for (Component c : ((TranslatableComponent)component).args()) |             for (TranslationArgument c : ((TranslatableComponent)component).arguments()) | ||||||
|                 count += componentWidth(c, console, actuallyBold); |                 count += componentWidth(c.asComponent(), console, actuallyBold); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         for (Component c : component.children()) |         for (Component c : component.children()) | ||||||
| @@ -258,7 +313,7 @@ public class ChatUtil { | |||||||
|  |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Wraps the provided text in multiple lines, taking into account the legacy formating. |      * Wraps the provided text in multiple lines, taking into account the legacy formatting. | ||||||
|      * <p> |      * <p> | ||||||
|      * This method only takes into account IG text width. Use a regular text-wrapper for console instead. |      * This method only takes into account IG text width. Use a regular text-wrapper for console instead. | ||||||
|      * @param legacyText the text to wrap. |      * @param legacyText the text to wrap. | ||||||
| @@ -272,7 +327,7 @@ public class ChatUtil { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Wraps the provided text in multiple lines, taking into account the legacy formating. |      * Wraps the provided text in multiple lines, taking into account the legacy formatting. | ||||||
|      * <p> |      * <p> | ||||||
|      * This method only takes into account IG text width. Use a regular text-wrapper for console instead. |      * This method only takes into account IG text width. Use a regular text-wrapper for console instead. | ||||||
|      * @param legacyText the text to wrap. |      * @param legacyText the text to wrap. | ||||||
| @@ -295,7 +350,7 @@ public class ChatUtil { | |||||||
|  |  | ||||||
|         do { |         do { | ||||||
|             char c = legacyText.charAt(index); |             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); |                 currentWord.append(c); | ||||||
|                 c = legacyText.charAt(++index); |                 c = legacyText.charAt(++index); | ||||||
|                 currentWord.append(c); |                 currentWord.append(c); | ||||||
| @@ -369,7 +424,7 @@ public class ChatUtil { | |||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Try to render a matrix of {@link Chat} components into a table in the chat or console. |      * Try to render a matrix of {@link Chat} components into a table in the chat or console. | ||||||
|      * @param data the component, in the form of {@link List} of {@link List} of {@link Chat}. The englobing list holds |      * @param data the component, in the form of {@link List} of {@link List} of {@link Chat}. The parent list holds | ||||||
|      *             the table lines (line 0 being the top line). Each sublist holds the cells content (element 0 is the |      *             the table lines (line 0 being the top line). Each sublist holds the cells content (element 0 is the | ||||||
|      *             leftText one). The row lengths can be different. |      *             leftText one). The row lengths can be different. | ||||||
|      * @param space a spacer to put between columns. |      * @param space a spacer to put between columns. | ||||||
| @@ -377,12 +432,12 @@ public class ChatUtil { | |||||||
|      *                alignment, much harder). |      *                alignment, much harder). | ||||||
|      * @return a List containing each rendered line of the table. |      * @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()); |         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()); |             List<Component> compRow = new ArrayList<>(row.size()); | ||||||
|             for (Chat c : row) { |             for (ComponentLike c : row) { | ||||||
|                 compRow.add(c.getAdv()); |                 compRow.add(c.asComponent()); | ||||||
|             } |             } | ||||||
|             compRows.add(compRow); |             compRows.add(compRow); | ||||||
|         } |         } | ||||||
| @@ -392,7 +447,7 @@ public class ChatUtil { | |||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Try to render a matrix of {@link Component} components into a table in the chat or console. |      * Try to render a matrix of {@link Component} components into a table in the chat or console. | ||||||
|      * @param data the component, in the form of {@link List} of {@link List} of {@link Component}. The englobing list holds |      * @param data the component, in the form of {@link List} of {@link List} of {@link Component}. The parent list holds | ||||||
|      *             the table lines (line 0 being the top line). Each sublist holds the cells content (element 0 is the |      *             the table lines (line 0 being the top line). Each sublist holds the cells content (element 0 is the | ||||||
|      *             leftText one). The row lengths can be different. |      *             leftText one). The row lengths can be different. | ||||||
|      * @param space a spacer to put between columns. |      * @param space a spacer to put between columns. | ||||||
| @@ -416,7 +471,7 @@ public class ChatUtil { | |||||||
|         // create the lines with appropriate spacing |         // create the lines with appropriate spacing | ||||||
|         List<Component> spacedRows = new ArrayList<>(data.size()); |         List<Component> spacedRows = new ArrayList<>(data.size()); | ||||||
|         for (List<Component> row : data) { |         for (List<Component> row : data) { | ||||||
|             Chat spacedRow = Chat.chat(); |             Chat spacedRow = chat(); | ||||||
|             for (int i = 0; i < row.size() - 1; i++) { |             for (int i = 0; i < row.size() - 1; i++) { | ||||||
|                 int w = componentWidth(row.get(i), console); |                 int w = componentWidth(row.get(i), console); | ||||||
|                 int padding = nbPixelPerColumn.get(i) - w; |                 int padding = nbPixelPerColumn.get(i) - w; | ||||||
| @@ -425,8 +480,8 @@ public class ChatUtil { | |||||||
|                 spacedRow.thenText(space); |                 spacedRow.thenText(space); | ||||||
|             } |             } | ||||||
|             if (!row.isEmpty()) |             if (!row.isEmpty()) | ||||||
|                 spacedRow.then(row.get(row.size() - 1)); |                 spacedRow.then(row.getLast()); | ||||||
|             spacedRows.add(spacedRow.getAdv()); |             spacedRows.add(spacedRow.get()); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return spacedRows; |         return spacedRows; | ||||||
| @@ -448,14 +503,14 @@ public class ChatUtil { | |||||||
|      */ |      */ | ||||||
|     public static Component customWidthSpace(int width, boolean console) { |     public static Component customWidthSpace(int width, boolean console) { | ||||||
|         if (console) |         if (console) | ||||||
|             return Chat.text(" ".repeat(width)).getAdv(); |             return Chat.text(" ".repeat(width)).get(); | ||||||
|         return switch (width) { |         return switch (width) { | ||||||
|             case 0, 1 -> Component.empty(); |             case 0, 1 -> Component.empty(); | ||||||
|             case 2 -> Chat.text(".").black().getAdv(); |             case 2 -> Chat.text(".").black().get(); | ||||||
|             case 3 -> Chat.text("`").black().getAdv(); |             case 3 -> Chat.text("`").black().get(); | ||||||
|             case 6 -> Chat.text(". ").black().getAdv(); |             case 6 -> Chat.text(". ").black().get(); | ||||||
|             case 7 -> Chat.text("` ").black().getAdv(); |             case 7 -> Chat.text("` ").black().get(); | ||||||
|             case 11 -> Chat.text("`  ").black().getAdv(); |             case 11 -> Chat.text("`  ").black().get(); | ||||||
|             default -> { |             default -> { | ||||||
|                 int nbSpace = width / 4; |                 int nbSpace = width / 4; | ||||||
|                 int nbBold = width % 4; |                 int nbBold = width % 4; | ||||||
| @@ -464,13 +519,13 @@ public class ChatUtil { | |||||||
|                     if (nbBold > 0) { |                     if (nbBold > 0) { | ||||||
|                         yield Chat.text(" ".repeat(nbNotBold)).bold(false) |                         yield Chat.text(" ".repeat(nbNotBold)).bold(false) | ||||||
|                                 .then(Chat.text(" ".repeat(nbBold)).bold(true)) |                                 .then(Chat.text(" ".repeat(nbBold)).bold(true)) | ||||||
|                                 .getAdv(); |                                 .get(); | ||||||
|                     } |                     } | ||||||
|                     else |                     else | ||||||
|                         yield Chat.text(" ".repeat(nbNotBold)).bold(false).getAdv(); |                         yield Chat.text(" ".repeat(nbNotBold)).bold(false).get(); | ||||||
|                 } |                 } | ||||||
|                 else if (nbBold > 0) { |                 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 + ")"); |                 throw new IllegalStateException("Should not be here (width=" + width + "; nbSpace=" + nbSpace + "; nbBold=" + nbBold + "; nbNotBold=" + nbNotBold + ")"); | ||||||
|             } |             } | ||||||
| @@ -505,9 +560,9 @@ public class ChatUtil { | |||||||
|     private static final char PROGRESS_BAR_FULL_CHAR = '|'; |     private static final char PROGRESS_BAR_FULL_CHAR = '|'; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Generate a (eventually multi-part) progress bar using text. |      * Generate a (eventually multipart) progress bar using text. | ||||||
|      * @param values the values to render in the progress bar. |      * @param values the values to render in the progress bar. | ||||||
|      * @param colors the colors attributed to each values. |      * @param colors the colors attributed to each value. | ||||||
|      * @param total the total value of the progress bar. |      * @param total the total value of the progress bar. | ||||||
|      * @param width the width in which the progress bar should fit (in pixel for IG, in character count for console) |      * @param width the width in which the progress bar should fit (in pixel for IG, in character count for console) | ||||||
|      * @param console true if the progress bar is intended to be displayed on the console, false if it’s in game chat. |      * @param console true if the progress bar is intended to be displayed on the console, false if it’s in game chat. | ||||||
| @@ -602,5 +657,6 @@ public class ChatUtil { | |||||||
|         return str; |         return str; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     private ChatUtil() {} | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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)); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -15,42 +15,36 @@ | |||||||
|     <packaging>jar</packaging> |     <packaging>jar</packaging> | ||||||
|      |      | ||||||
| 	<repositories> | 	<repositories> | ||||||
| 		<repository> |  | ||||||
| 			<id>minecraft-libraries</id> |  | ||||||
| 			<name>Minecraft Libraries</name> |  | ||||||
| 			<url>https://libraries.minecraft.net</url> |  | ||||||
| 		</repository> |  | ||||||
|         <repository> |         <repository> | ||||||
|             <id>bungeecord-repo</id> |             <id>bungeecord-repo</id> | ||||||
|             <url>https://oss.sonatype.org/content/repositories/snapshots</url> |             <url>https://oss.sonatype.org/content/repositories/snapshots</url> | ||||||
|         </repository> |         </repository> | ||||||
|     </repositories> |     </repositories> | ||||||
|  |  | ||||||
|   <dependencies> |     <dependencies> | ||||||
|       <dependency> |  | ||||||
|           <groupId>fr.pandacube.lib</groupId> |  | ||||||
|           <artifactId>pandalib-core</artifactId> |  | ||||||
|           <version>${project.version}</version> |  | ||||||
|       </dependency> |  | ||||||
|       <dependency> |  | ||||||
|           <groupId>fr.pandacube.lib</groupId> |  | ||||||
|           <artifactId>pandalib-reflect</artifactId> |  | ||||||
|           <version>${project.version}</version> |  | ||||||
|       </dependency> |  | ||||||
|       <dependency> |  | ||||||
|           <groupId>fr.pandacube.lib</groupId> |  | ||||||
|           <artifactId>pandalib-commands</artifactId> |  | ||||||
|           <version>${project.version}</version> |  | ||||||
|       </dependency> |  | ||||||
|  |  | ||||||
|         <dependency> |         <dependency> | ||||||
|             <groupId>net.md-5</groupId> |             <groupId>fr.pandacube.lib</groupId> | ||||||
|             <artifactId>bungeecord-log</artifactId> |             <artifactId>pandalib-core</artifactId> | ||||||
|             <version>${bungeecord.version}</version> |             <version>${project.version}</version> | ||||||
|  |         </dependency> | ||||||
|  |         <dependency> | ||||||
|  |             <groupId>fr.pandacube.lib</groupId> | ||||||
|  |             <artifactId>pandalib-reflect</artifactId> | ||||||
|  |             <version>${project.version}</version> | ||||||
|  |         </dependency> | ||||||
|  |         <dependency> | ||||||
|  |             <groupId>fr.pandacube.lib</groupId> | ||||||
|  |             <artifactId>pandalib-commands</artifactId> | ||||||
|  |             <version>${project.version}</version> | ||||||
|  |         </dependency> | ||||||
|  |         <dependency> | ||||||
|  |             <groupId>fr.pandacube.lib</groupId> | ||||||
|  |             <artifactId>pandalib-config</artifactId> | ||||||
|  |             <version>${project.version}</version> | ||||||
|         </dependency> |         </dependency> | ||||||
|         <dependency> |         <dependency> | ||||||
|             <groupId>net.md-5</groupId> |             <groupId>net.md-5</groupId> | ||||||
|             <artifactId>bungeecord-config</artifactId> |             <artifactId>bungeecord-log</artifactId> | ||||||
|             <version>${bungeecord.version}</version> |             <version>${bungeecord.version}</version> | ||||||
|         </dependency> |         </dependency> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -8,10 +8,10 @@ import fr.pandacube.lib.cli.log.CLILogger; | |||||||
| import jline.console.ConsoleReader; | import jline.console.ConsoleReader; | ||||||
| import org.fusesource.jansi.AnsiConsole; | import org.fusesource.jansi.AnsiConsole; | ||||||
|  |  | ||||||
| import fr.pandacube.lib.util.Log; | import fr.pandacube.lib.util.log.Log; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Class to hangle general standard IO operation for a CLI application. It uses Jline’s {@link ConsoleReader} for the |  * Class to handle general standard IO operation for a CLI application. It uses Jline’s {@link ConsoleReader} for the | ||||||
|  * console rendering, a JUL {@link Logger} for logging, and Brigadier to handle commands. |  * console rendering, a JUL {@link Logger} for logging, and Brigadier to handle commands. | ||||||
|  */ |  */ | ||||||
| public class CLI extends Thread { | public class CLI extends Thread { | ||||||
| @@ -33,7 +33,7 @@ public class CLI extends Thread { | |||||||
| 		reader.setPrompt(">"); | 		reader.setPrompt(">"); | ||||||
| 		reader.addCompleter(CLIBrigadierDispatcher.instance); | 		reader.addCompleter(CLIBrigadierDispatcher.instance); | ||||||
|  |  | ||||||
| 		// configuration du formatteur pour le logger | 		// configure logger's formatter | ||||||
| 		System.setProperty("net.md_5.bungee.log-date-format", "yyyy-MM-dd HH:mm:ss"); | 		System.setProperty("net.md_5.bungee.log-date-format", "yyyy-MM-dd HH:mm:ss"); | ||||||
| 		logger = CLILogger.getLogger(this); | 		logger = CLILogger.getLogger(this); | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -2,7 +2,8 @@ package fr.pandacube.lib.cli; | |||||||
|  |  | ||||||
| import fr.pandacube.lib.cli.commands.CommandAdmin; | import fr.pandacube.lib.cli.commands.CommandAdmin; | ||||||
| import fr.pandacube.lib.cli.commands.CommandStop; | 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.concurrent.atomic.AtomicBoolean; | ||||||
| import java.util.logging.Logger; | import java.util.logging.Logger; | ||||||
| @@ -14,17 +15,23 @@ public abstract class CLIApplication { | |||||||
|  |  | ||||||
|     private static CLIApplication instance; |     private static CLIApplication instance; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns the current application instance. | ||||||
|  |      * @return the current application instance. | ||||||
|  |      */ | ||||||
|     public static CLIApplication getInstance() { |     public static CLIApplication getInstance() { | ||||||
|         return instance; |         return instance; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The instance of {@link CLI} for this application. | ||||||
|  |      */ | ||||||
|  |  | ||||||
|     public final CLI cli; |     public final CLI cli; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Creates a new application instance. | ||||||
|  |      */ | ||||||
|     protected CLIApplication() { |     protected CLIApplication() { | ||||||
|         instance = this; |         instance = this; | ||||||
|         CLI tmpCLI = null; |         CLI tmpCLI = null; | ||||||
| @@ -46,7 +53,7 @@ public abstract class CLIApplication { | |||||||
|             new CommandAdmin(); |             new CommandAdmin(); | ||||||
|             new CommandStop(); |             new CommandStop(); | ||||||
|  |  | ||||||
|             Runtime.getRuntime().addShutdownHook(new Thread(this::end)); |             Runtime.getRuntime().addShutdownHook(shutdownThread); | ||||||
|  |  | ||||||
|             cli.start(); // actually starts the CLI thread |             cli.start(); // actually starts the CLI thread | ||||||
|  |  | ||||||
| @@ -56,47 +63,74 @@ public abstract class CLIApplication { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns the application's {@link Logger}. | ||||||
|  |      * @return the application's {@link Logger}. | ||||||
|  |      */ | ||||||
|     public Logger getLogger() { |     public Logger getLogger() { | ||||||
|         return cli.getLogger(); |         return cli.getLogger(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     private final Object stopLock = new Object(); |     private final Thread shutdownThread = new Thread(this::stop); | ||||||
|  |  | ||||||
|     private final AtomicBoolean stopping = new AtomicBoolean(false); |     private final AtomicBoolean stopping = new AtomicBoolean(false); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Stops this application. | ||||||
|  |      */ | ||||||
|     public final void stop() { |     public final void stop() { | ||||||
|         synchronized (stopLock) { |         synchronized (stopping) { | ||||||
|             synchronized (stopping) { |             if (stopping.get()) | ||||||
|                 if (stopping.get()) |                 return; | ||||||
|                     return; |             stopping.set(true); | ||||||
|                 stopping.set(true); |  | ||||||
|             } |  | ||||||
|             Log.info("Stopping " + getName() + " version " + getClass().getPackage().getImplementationVersion()); |  | ||||||
|             try { |  | ||||||
|                 end(); |  | ||||||
|             } catch (Throwable t) { |  | ||||||
|                 Log.severe("Error stopping application " + getName() + " version " + getClass().getPackage().getImplementationVersion(), t); |  | ||||||
|             } finally { |  | ||||||
|                 Log.info("Bye bye."); |  | ||||||
|                 System.exit(0); |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|  |         Log.info("Stopping " + getName() + " version " + getClass().getPackage().getImplementationVersion()); | ||||||
|  |         try { | ||||||
|  |             end(); | ||||||
|  |         } catch (Throwable t) { | ||||||
|  |             Log.severe("Error stopping application " + getName() + " version " + getClass().getPackage().getImplementationVersion(), t); | ||||||
|  |         } finally { | ||||||
|  |             Log.info("Bye bye."); | ||||||
|  |  | ||||||
|  |             CLILogger.actuallyResetLogManager(); | ||||||
|  |             if (!Thread.currentThread().equals(shutdownThread)) | ||||||
|  |                 System.exit(0); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Tells if this application is currently stopping, that is the {@link #stop()} method has been called. | ||||||
|  |      * @return true if the application is stopping, false otherwise. | ||||||
|  |      */ | ||||||
|     public boolean isStopping() { |     public boolean isStopping() { | ||||||
|         return stopping.get(); |         return stopping.get(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Gets the name of this application. | ||||||
|  |      * @return the name of this application. | ||||||
|  |      */ | ||||||
|     public abstract String getName(); |     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; |     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(); |     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(); |     protected abstract void end(); | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ import java.util.function.Predicate; | |||||||
| public abstract class CLIBrigadierCommand extends BrigadierCommand<CLICommandSender> { | public abstract class CLIBrigadierCommand extends BrigadierCommand<CLICommandSender> { | ||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
| 	 * Instanciate this command instance. | 	 * Instantiate this command instance. | ||||||
| 	 */ | 	 */ | ||||||
| 	public CLIBrigadierCommand() { | 	public CLIBrigadierCommand() { | ||||||
| 		LiteralCommandNode<CLICommandSender> commandNode = buildCommand().build(); | 		LiteralCommandNode<CLICommandSender> commandNode = buildCommand().build(); | ||||||
|   | |||||||
| @@ -25,9 +25,12 @@ public class CLIBrigadierDispatcher extends BrigadierDispatcher<CLICommandSender | |||||||
| 	public static final CLICommandSender CLI_CONSOLE_COMMAND_SENDER = new CLIConsoleCommandSender(); | 	public static final CLICommandSender CLI_CONSOLE_COMMAND_SENDER = new CLIConsoleCommandSender(); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 	private CLIBrigadierDispatcher() {} | ||||||
|  |  | ||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
| 	 * Executes the provided command as the console. | 	 * Executes the provided command as the console. | ||||||
| 	 * @param commandWithoutSlash the command, without the eventual slash at the begining. | 	 * @param commandWithoutSlash the command, without the eventual slash at the beginning. | ||||||
| 	 * @return the value returned by the executed command. | 	 * @return the value returned by the executed command. | ||||||
| 	 */ | 	 */ | ||||||
| 	public int execute(String commandWithoutSlash) { | 	public int execute(String commandWithoutSlash) { | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ import net.kyori.adventure.audience.Audience; | |||||||
| import net.kyori.adventure.audience.MessageType; | import net.kyori.adventure.audience.MessageType; | ||||||
| import net.kyori.adventure.identity.Identity; | import net.kyori.adventure.identity.Identity; | ||||||
| import net.kyori.adventure.text.Component; | import net.kyori.adventure.text.Component; | ||||||
|  | import org.jetbrains.annotations.NotNull; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * A command sender. |  * A command sender. | ||||||
| @@ -41,5 +42,5 @@ public interface CLICommandSender extends Audience { | |||||||
|     void sendMessage(String message); |     void sendMessage(String message); | ||||||
|  |  | ||||||
|     @Override // force implementation of super-interface default method |     @Override // force implementation of super-interface default method | ||||||
|     void sendMessage(Identity source, Component message, MessageType type); |     void sendMessage(@NotNull Identity source, @NotNull Component message, @NotNull MessageType type); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,15 +1,22 @@ | |||||||
| package fr.pandacube.lib.cli.commands; | package fr.pandacube.lib.cli.commands; | ||||||
|  |  | ||||||
| import fr.pandacube.lib.chat.Chat; | 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.audience.MessageType; | ||||||
| import net.kyori.adventure.identity.Identity; | import net.kyori.adventure.identity.Identity; | ||||||
| import net.kyori.adventure.text.Component; | import net.kyori.adventure.text.Component; | ||||||
|  | import org.jetbrains.annotations.NotNull; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * The console command sender. |  * The console command sender. | ||||||
|  */ |  */ | ||||||
| public class CLIConsoleCommandSender implements CLICommandSender { | public class CLIConsoleCommandSender implements CLICommandSender { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Creates a new console command sender. | ||||||
|  |      */ | ||||||
|  |     protected CLIConsoleCommandSender() {} | ||||||
|  |  | ||||||
|     public String getName() { |     public String getName() { | ||||||
|         return "Console"; |         return "Console"; | ||||||
|     } |     } | ||||||
| @@ -31,7 +38,7 @@ public class CLIConsoleCommandSender implements CLICommandSender { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void sendMessage(Identity source, Component message, MessageType type) { |     public void sendMessage(@NotNull Identity source, @NotNull Component message, @NotNull MessageType type) { | ||||||
|         sendMessage(Chat.chatComponent(message).getLegacyText()); |         sendMessage(Chat.chatComponent(message).getLegacyText()); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -17,8 +17,8 @@ import fr.pandacube.lib.chat.Chat; | |||||||
| import fr.pandacube.lib.chat.Chat.FormatableChat; | import fr.pandacube.lib.chat.Chat.FormatableChat; | ||||||
| import fr.pandacube.lib.chat.ChatTreeNode; | import fr.pandacube.lib.chat.ChatTreeNode; | ||||||
| import fr.pandacube.lib.cli.CLIApplication; | import fr.pandacube.lib.cli.CLIApplication; | ||||||
| import fr.pandacube.lib.util.Log; | import fr.pandacube.lib.util.log.Log; | ||||||
| import net.md_5.bungee.api.chat.BaseComponent; | import net.kyori.adventure.text.Component; | ||||||
|  |  | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.Arrays; | 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.successText; | ||||||
| import static fr.pandacube.lib.chat.ChatStatic.text; | import static fr.pandacube.lib.chat.ChatStatic.text; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * The {@code admin} command for a {@link CLIApplication}. | ||||||
|  |  */ | ||||||
| public class CommandAdmin extends CLIBrigadierCommand { | public class CommandAdmin extends CLIBrigadierCommand { | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * Initializes the admin command. | ||||||
|  | 	 */ | ||||||
|  | 	public CommandAdmin() {} | ||||||
|  |  | ||||||
| 	@Override | 	@Override | ||||||
| 	protected LiteralArgumentBuilder<CLICommandSender> buildCommand() { | 	protected LiteralArgumentBuilder<CLICommandSender> buildCommand() { | ||||||
| 		return literal("admin") | 		return literal("admin") | ||||||
| @@ -170,13 +178,13 @@ public class CommandAdmin extends CLIBrigadierCommand { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  |  | ||||||
| 		ChatTreeNode dispTree = new ChatTreeNode(d); | 		ChatTreeNode displayTree = new ChatTreeNode(d); | ||||||
| 		 | 		 | ||||||
| 		for (DisplayCommandNode child : displayNode.children) { | 		for (DisplayCommandNode child : displayNode.children) { | ||||||
| 			dispTree.addChild(buildDisplayTree(child, sender)); | 			displayTree.addChild(buildDisplayTree(child, sender)); | ||||||
| 		} | 		} | ||||||
| 		 | 		 | ||||||
| 		return dispTree; | 		return displayTree; | ||||||
| 		 | 		 | ||||||
| 	} | 	} | ||||||
| 	 | 	 | ||||||
| @@ -184,7 +192,7 @@ 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) | 		if (node == null) | ||||||
| 			throw new IllegalArgumentException("node must not be null"); | 			throw new IllegalArgumentException("node must not be null"); | ||||||
| 		FormatableChat d; | 		FormatableChat d; | ||||||
| @@ -257,8 +265,8 @@ public class CommandAdmin extends CLIBrigadierCommand { | |||||||
| 	 | 	 | ||||||
| 	 | 	 | ||||||
| 	private static class DisplayCommandNode { | 	private static class DisplayCommandNode { | ||||||
| 		List<CommandNode<CLICommandSender>> nodes = new ArrayList<>(); | 		final List<CommandNode<CLICommandSender>> nodes = new ArrayList<>(); | ||||||
| 		List<DisplayCommandNode> children = new ArrayList<>(); | 		final List<DisplayCommandNode> children = new ArrayList<>(); | ||||||
| 		 | 		 | ||||||
| 		void addInline(CommandNode<CLICommandSender> node) { | 		void addInline(CommandNode<CLICommandSender> node) { | ||||||
| 			nodes.add(node); | 			nodes.add(node); | ||||||
|   | |||||||
| @@ -5,10 +5,15 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder; | |||||||
| import fr.pandacube.lib.cli.CLIApplication; | 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 { | public class CommandStop extends CLIBrigadierCommand { | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * Initializes the admin command. | ||||||
|  | 	 */ | ||||||
|  | 	public CommandStop() {} | ||||||
|  |  | ||||||
| 	@Override | 	@Override | ||||||
| 	protected LiteralArgumentBuilder<CLICommandSender> buildCommand() { | 	protected LiteralArgumentBuilder<CLICommandSender> buildCommand() { | ||||||
| 		return literal("stop") | 		return literal("stop") | ||||||
|   | |||||||
| @@ -1,8 +1,10 @@ | |||||||
| package fr.pandacube.lib.cli.log; | package fr.pandacube.lib.cli.log; | ||||||
|  |  | ||||||
| import fr.pandacube.lib.cli.CLI; | 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.ThrowableUtil; | ||||||
|  | import fr.pandacube.lib.util.log.DailyLogRotateFileHandler; | ||||||
|  | import fr.pandacube.lib.util.log.Log; | ||||||
| import net.md_5.bungee.log.ColouredWriter; | import net.md_5.bungee.log.ColouredWriter; | ||||||
| import net.md_5.bungee.log.ConciseFormatter; | import net.md_5.bungee.log.ConciseFormatter; | ||||||
|  |  | ||||||
| @@ -12,6 +14,7 @@ import java.io.PrintStream; | |||||||
| import java.util.Scanner; | import java.util.Scanner; | ||||||
| import java.util.logging.Handler; | import java.util.logging.Handler; | ||||||
| import java.util.logging.Level; | import java.util.logging.Level; | ||||||
|  | import java.util.logging.LogManager; | ||||||
| import java.util.logging.Logger; | import java.util.logging.Logger; | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -19,12 +22,35 @@ import java.util.logging.Logger; | |||||||
|  */ |  */ | ||||||
| public class CLILogger { | public class CLILogger { | ||||||
|  |  | ||||||
|  | 	static { | ||||||
|  | 		System.setProperty("java.util.logging.manager", ShutdownHookDelayerLogManager.class.getName()); | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	private static Logger logger = null; | 	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. | 	 * Initialize and return the logger for this application. | ||||||
| 	 * @param cli the CLI instance to use | 	 * @param cli the CLI instance to use | ||||||
| 	 * @return the logger of this application. | 	 * @return the logger for this application. | ||||||
| 	 */ | 	 */ | ||||||
| 	public static synchronized Logger getLogger(CLI cli) { | 	public static synchronized Logger getLogger(CLI cli) { | ||||||
| 		if (logger == null) { | 		if (logger == null) { | ||||||
| @@ -45,6 +71,8 @@ public class CLILogger { | |||||||
| 	        System.setOut(newRedirector(logger, Level.INFO)); | 	        System.setOut(newRedirector(logger, Level.INFO)); | ||||||
|  |  | ||||||
| 			Log.setLogger(logger); | 			Log.setLogger(logger); | ||||||
|  |  | ||||||
|  | 			Thread.setDefaultUncaughtExceptionHandler((t, e) -> Log.severe("Uncaught Exception in thread " + t.getName(), e)); | ||||||
| 		} | 		} | ||||||
| 		return logger; | 		return logger; | ||||||
| 	} | 	} | ||||||
| @@ -69,5 +97,7 @@ public class CLILogger { | |||||||
| 		t.start(); | 		t.start(); | ||||||
| 		return ps; | 		return ps; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	private CLILogger() {} | ||||||
| 	 | 	 | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,45 @@ | |||||||
|  | package fr.pandacube.lib.commands; | ||||||
|  |  | ||||||
|  | import java.util.logging.Logger; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Throw an instance of this exception to indicate to the plugin command handler that the user has missused the command. | ||||||
|  |  * The message, if provided, must indicate the reason of the mussusage of the command. It will be displayed on the | ||||||
|  |  * screen with eventual indications of how to use the command (help command for example). | ||||||
|  |  * If a {@link Throwable} cause is provided, it will be relayed to the plugin {@link Logger}. | ||||||
|  |  *  | ||||||
|  |  */ | ||||||
|  | public class BadCommandUsage extends RuntimeException { | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * Constructs a new runtime exception with no message or cause. | ||||||
|  | 	 */ | ||||||
|  | 	public BadCommandUsage() { | ||||||
|  | 		super(); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * Constructs a new runtime exception with the specified cause. | ||||||
|  | 	 * @param cause the cause. | ||||||
|  | 	 */ | ||||||
|  | 	public BadCommandUsage(Throwable cause) { | ||||||
|  | 		super(cause); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * Constructs a new runtime exception with the specified message. | ||||||
|  | 	 * @param message the message. | ||||||
|  | 	 */ | ||||||
|  | 	public BadCommandUsage(String message) { | ||||||
|  | 		super(message); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * Constructs a new runtime exception with the specified message and cause. | ||||||
|  | 	 * @param message the message. | ||||||
|  | 	 * @param cause the cause. | ||||||
|  | 	 */ | ||||||
|  | 	public BadCommandUsage(String message, Throwable cause) { | ||||||
|  | 		super(message, cause); | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -10,9 +10,10 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException; | |||||||
| import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; | import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; | ||||||
| import com.mojang.brigadier.suggestion.SuggestionProvider; | import com.mojang.brigadier.suggestion.SuggestionProvider; | ||||||
| import com.mojang.brigadier.tree.LiteralCommandNode; | 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.Arrays; | ||||||
|  | import java.util.List; | ||||||
| import java.util.function.Function; | import java.util.function.Function; | ||||||
| import java.util.function.Predicate; | import java.util.function.Predicate; | ||||||
|  |  | ||||||
| @@ -24,12 +25,17 @@ import java.util.function.Predicate; | |||||||
|  */ |  */ | ||||||
| public abstract class BrigadierCommand<S> { | public abstract class BrigadierCommand<S> { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Creates a Brigadier command. | ||||||
|  |      */ | ||||||
|  |     public BrigadierCommand() {} | ||||||
|  |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Returns a builder for this command. |      * Returns a builder for this command. | ||||||
|      * Concrete class should include any element in the builder that is needed to build the command (sub-commands and |      * Concrete class should include any element in the builder that is needed to build the command (sub-commands and | ||||||
|      * arguments, requirements, redirection, ...). |      * arguments, requirements, redirection, ...). | ||||||
|      * If any of the sub-commands and arguments needs to know the {@link LiteralCommandNode} builded from the returned |      * If any of the sub-commands and arguments needs to know the {@link LiteralCommandNode} built from the returned | ||||||
|      * {@link LiteralArgumentBuilder}, this can be done by overriding {@link #postBuildCommand(LiteralCommandNode)}. |      * {@link LiteralArgumentBuilder}, this can be done by overriding {@link #postBuildCommand(LiteralCommandNode)}. | ||||||
|      * @return a builder for this command. |      * @return a builder for this command. | ||||||
|      */ |      */ | ||||||
| @@ -37,16 +43,16 @@ public abstract class BrigadierCommand<S> { | |||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Method to override if the reference to the command node has to be known when building the subcommands. |      * Method to override if the reference to the command node has to be known when building the subcommands. | ||||||
|      * @param commandNode the command node builded from {@link #buildCommand()}. |      * @param commandNode the command node built from {@link #buildCommand()}. | ||||||
|      */ |      */ | ||||||
|     protected void postBuildCommand(LiteralCommandNode<S> commandNode) { |     protected void postBuildCommand(LiteralCommandNode<S> commandNode) { | ||||||
|         // default implementation does nothing. |         // default implementation does nothing. | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Method to override if this command have any aliases. |      * Method to override if this command has any aliases. | ||||||
|      * @return an array of string corresponding to the aliases. This must not include the orignal command name (that |      * @return an array of string corresponding to the aliases. This must not include the orignal command name (that | ||||||
|      * is the name of the literal command node builded from {@link #buildCommand()}). |      * is the name of the literal command node built from {@link #buildCommand()}). | ||||||
|      */ |      */ | ||||||
|     protected String[] getAliases() { |     protected String[] getAliases() { | ||||||
|         return new String[0]; |         return new String[0]; | ||||||
| @@ -235,9 +241,12 @@ public abstract class BrigadierCommand<S> { | |||||||
|                 args = Arrays.copyOf(args, args.length + 1); |                 args = Arrays.copyOf(args, args.length + 1); | ||||||
|                 args[args.length - 1] = message.substring(tokenStartPos); |                 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 (s != null) |                 if (wrappedResult != null) { | ||||||
|                         builder.suggest(s); |                     for (String s : wrappedResult) { | ||||||
|  |                         if (s != null) | ||||||
|  |                             builder.suggest(s); | ||||||
|  |                     } | ||||||
|                 } |                 } | ||||||
|             } catch (Throwable e) { |             } catch (Throwable e) { | ||||||
|                 Log.severe("Error while tab-completing '" + message + "' for " + sender, e); |                 Log.severe("Error while tab-completing '" + message + "' for " + sender, e); | ||||||
|   | |||||||
| @@ -6,14 +6,14 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException; | |||||||
| import com.mojang.brigadier.suggestion.Suggestions; | import com.mojang.brigadier.suggestion.Suggestions; | ||||||
| import com.mojang.brigadier.tree.LiteralCommandNode; | import com.mojang.brigadier.tree.LiteralCommandNode; | ||||||
| import fr.pandacube.lib.chat.Chat; | 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 net.kyori.adventure.text.ComponentLike; | ||||||
|  |  | ||||||
| import java.util.concurrent.CompletableFuture; | import java.util.concurrent.CompletableFuture; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Abstract class that holds a Brigadier {@link CommandDispatcher} instance. |  * Abstract class that holds a Brigadier {@link CommandDispatcher} instance. | ||||||
|  * Subclasses contains logic to integrate this commands dispatcher into their environment (like Bungee or CLI app). |  * Subclasses contain logic to integrate this commands dispatcher into their environment (like Bungee or CLI app). | ||||||
|  * @param <S> the command source (or command sender) type. |  * @param <S> the command source (or command sender) type. | ||||||
|  */ |  */ | ||||||
| public abstract class BrigadierDispatcher<S> { | public abstract class BrigadierDispatcher<S> { | ||||||
| @@ -21,6 +21,11 @@ public abstract class BrigadierDispatcher<S> { | |||||||
|  |  | ||||||
|     private final CommandDispatcher<S> dispatcher = new CommandDispatcher<>(); |     private final CommandDispatcher<S> dispatcher = new CommandDispatcher<>(); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Creates a new Dispatcher instance. | ||||||
|  |      */ | ||||||
|  |     public BrigadierDispatcher() {} | ||||||
|  |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Registers the provided command node into this dispatcher. |      * Registers the provided command node into this dispatcher. | ||||||
| @@ -43,7 +48,7 @@ public abstract class BrigadierDispatcher<S> { | |||||||
|     /** |     /** | ||||||
|      * Executes the provided command as the provided sender. |      * Executes the provided command as the provided sender. | ||||||
|      * @param sender the command sender. |      * @param sender the command sender. | ||||||
|      * @param commandWithoutSlash the command, without the eventual slash at the begining. |      * @param commandWithoutSlash the command, without the eventual slash at the beginning. | ||||||
|      * @return the value returned by the executed command. |      * @return the value returned by the executed command. | ||||||
|      */ |      */ | ||||||
|     public int execute(S sender, String commandWithoutSlash) { |     public int execute(S sender, String commandWithoutSlash) { | ||||||
|   | |||||||
| @@ -18,7 +18,7 @@ import java.util.List; | |||||||
| import java.util.concurrent.CompletableFuture; | import java.util.concurrent.CompletableFuture; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Utility methods to replace some functionalities of Brigadier, especialy suggestion sorting that we don’t like. |  * Utility methods to replace some functionalities of Brigadier, especially suggestion sorting that we don’t like. | ||||||
|  */ |  */ | ||||||
| public class BrigadierSuggestionsUtil { | public class BrigadierSuggestionsUtil { | ||||||
|  |  | ||||||
| @@ -140,4 +140,8 @@ public class BrigadierSuggestionsUtil { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     private BrigadierSuggestionsUtil() {} | ||||||
|  |  | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ import java.util.stream.LongStream; | |||||||
| import java.util.stream.Stream; | import java.util.stream.Stream; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Functionnal interface providing suggestions for an argument of a command. |  * Functional interface providing suggestions for an argument of a command. | ||||||
|  * @param <S> the type of the command sender. |  * @param <S> the type of the command sender. | ||||||
|  */ |  */ | ||||||
| @FunctionalInterface | @FunctionalInterface | ||||||
| @@ -66,7 +66,7 @@ public interface SuggestionsSupplier<S> { | |||||||
| 	 * Filter the provided {@link Stream} of string according to the provided token, using the filter returned by {@link #filter(String)}, | 	 * Filter the provided {@link Stream} of string according to the provided token, using the filter returned by {@link #filter(String)}, | ||||||
| 	 * then returns the strings collected into a {@link List}. | 	 * then returns the strings collected into a {@link List}. | ||||||
| 	 * <p> | 	 * <p> | ||||||
| 	 * This methods consume the provided stream, so will not be usable anymore. | 	 * This method consume the provided stream, so will not be usable anymore. | ||||||
| 	 * @param stream the stream to filter and collet. | 	 * @param stream the stream to filter and collet. | ||||||
| 	 * @param token the token to consider for filtering. | 	 * @param token the token to consider for filtering. | ||||||
| 	 * @return the stream, filtered and collected into a {@link List}. | 	 * @return the stream, filtered and collected into a {@link List}. | ||||||
| @@ -505,7 +505,7 @@ public interface SuggestionsSupplier<S> { | |||||||
| 	/** | 	/** | ||||||
| 	 * Creates a new {@link SuggestionsSupplier} containing all the suggestions of this instance, | 	 * Creates a new {@link SuggestionsSupplier} containing all the suggestions of this instance, | ||||||
| 	 * but if this list is still empty, returns the suggestions from the provided one. | 	 * but if this list is still empty, returns the suggestions from the provided one. | ||||||
| 	 * @param other another {@link SuggestionsSupplier} to fallback to. | 	 * @param other another {@link SuggestionsSupplier} to fall back to. | ||||||
| 	 * @return a new {@link SuggestionsSupplier}. | 	 * @return a new {@link SuggestionsSupplier}. | ||||||
| 	 */ | 	 */ | ||||||
| 	default SuggestionsSupplier<S> orIfEmpty(SuggestionsSupplier<S> other) { | 	default SuggestionsSupplier<S> orIfEmpty(SuggestionsSupplier<S> other) { | ||||||
|   | |||||||
| @@ -10,20 +10,24 @@ | |||||||
|     </parent> |     </parent> | ||||||
|     <modelVersion>4.0.0</modelVersion> |     <modelVersion>4.0.0</modelVersion> | ||||||
| 
 | 
 | ||||||
|     <artifactId>pandalib-net</artifactId> |     <artifactId>pandalib-config</artifactId> | ||||||
|     <packaging>jar</packaging> |     <packaging>jar</packaging> | ||||||
| 
 | 
 | ||||||
|  |     <repositories> | ||||||
|  |         <repository> | ||||||
|  |             <id>bungeecord-repo</id> | ||||||
|  |             <url>https://oss.sonatype.org/content/repositories/snapshots</url> | ||||||
|  |         </repository> | ||||||
|  |     </repositories> | ||||||
|  | 
 | ||||||
|     <dependencies> |     <dependencies> | ||||||
|  | 
 | ||||||
|         <dependency> |         <dependency> | ||||||
|             <groupId>fr.pandacube.lib</groupId> |             <groupId>net.md-5</groupId> | ||||||
|             <artifactId>pandalib-util</artifactId> |             <artifactId>bungeecord-config</artifactId> | ||||||
|             <version>${project.version}</version> |             <version>${bungeecord.version}</version> | ||||||
|         </dependency> |  | ||||||
|         <dependency> |  | ||||||
|             <groupId>com.google.guava</groupId> |  | ||||||
|             <artifactId>guava</artifactId> |  | ||||||
|             <version>31.0.1-jre</version> |  | ||||||
|         </dependency> |         </dependency> | ||||||
|  | 
 | ||||||
|     </dependencies> |     </dependencies> | ||||||
| 
 | 
 | ||||||
| </project> | </project> | ||||||
| @@ -1,23 +1,19 @@ | |||||||
| package fr.pandacube.lib.core.config; | package fr.pandacube.lib.config; | ||||||
| 
 | 
 | ||||||
| import java.io.BufferedReader; | import java.io.BufferedReader; | ||||||
| import java.io.File; | import java.io.File; | ||||||
| import java.io.FileReader; | import java.io.FileReader; | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.Arrays; |  | ||||||
| import java.util.List; | import java.util.List; | ||||||
| 
 | 
 | ||||||
| import fr.pandacube.lib.chat.ChatColorUtil; |  | ||||||
| import fr.pandacube.lib.util.Log; |  | ||||||
| 
 |  | ||||||
| /** | /** | ||||||
|  * Class that loads a specific config file or directory. |  * Class that loads a specific config file or directory. | ||||||
|  */ |  */ | ||||||
| public abstract class AbstractConfig { | public abstract class AbstractConfig { | ||||||
| 	 | 	 | ||||||
| 	/** | 	/** | ||||||
| 	 * The {@link File} corresponging to this config file or directory. | 	 * The {@link File} corresponding to this config file or directory. | ||||||
| 	 */ | 	 */ | ||||||
| 	protected final File configFile; | 	protected final File configFile; | ||||||
| 	 | 	 | ||||||
| @@ -57,7 +53,7 @@ public abstract class AbstractConfig { | |||||||
| 		while ((line = reader.readLine()) != null) { | 		while ((line = reader.readLine()) != null) { | ||||||
| 			String trimmedLine = line.trim(); | 			String trimmedLine = line.trim(); | ||||||
| 			 | 			 | ||||||
| 			if (ignoreEmpty && trimmedLine.equals("")) | 			if (ignoreEmpty && trimmedLine.isEmpty()) | ||||||
| 				continue; | 				continue; | ||||||
| 			 | 			 | ||||||
| 			if (ignoreHashtagComment && trimmedLine.startsWith("#")) | 			if (ignoreHashtagComment && trimmedLine.startsWith("#")) | ||||||
| @@ -94,7 +90,8 @@ public abstract class AbstractConfig { | |||||||
| 	 * @return the list of files in the config directory, or null if this config is not a directory. | 	 * @return the list of files in the config directory, or null if this config is not a directory. | ||||||
| 	 */ | 	 */ | ||||||
| 	protected List<File> getFileList() { | 	protected List<File> getFileList() { | ||||||
| 		return configFile.isDirectory() ? Arrays.asList(configFile.listFiles()) : null; | 		File[] arr = configFile.listFiles(); | ||||||
|  | 		return arr != null ? List.of(arr) : null; | ||||||
| 	} | 	} | ||||||
| 	 | 	 | ||||||
| 	 | 	 | ||||||
| @@ -105,7 +102,7 @@ public abstract class AbstractConfig { | |||||||
| 	 * Splits the provided string into a list of permission nodes. | 	 * Splits the provided string into a list of permission nodes. | ||||||
| 	 * The permission nodes must be separated by {@code ";"}. | 	 * The permission nodes must be separated by {@code ";"}. | ||||||
| 	 * @param perms one or more permissions nodes, separated by {@code ";"}. | 	 * @param perms one or more permissions nodes, separated by {@code ";"}. | ||||||
| 	 * @return {@code null} if the parameter is null or is equal to {@code "*"}, or the string splitted using {@code ";"}. | 	 * @return {@code null} if the parameter is null or is equal to {@code "*"}, or the string split using {@code ";"}. | ||||||
| 	 */ | 	 */ | ||||||
| 	public static List<String> splitPermissionsString(String perms) { | 	public static List<String> splitPermissionsString(String perms) { | ||||||
| 		if (perms == null || perms.equals("*")) | 		if (perms == null || perms.equals("*")) | ||||||
| @@ -114,25 +111,6 @@ public abstract class AbstractConfig { | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 	/** |  | ||||||
| 	 * Utility method to that translate the {@code '&'} formated string to the legacy format. |  | ||||||
| 	 * @param string the string to convert. |  | ||||||
| 	 * @return a legacy formated string (using {@code '§'}). |  | ||||||
| 	 */ |  | ||||||
| 	public static String getTranslatedColorCode(String string) { |  | ||||||
| 		return ChatColorUtil.translateAlternateColorCodes('&', string); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 	/** |  | ||||||
| 	 * Logs the message as a warning into console, prefixed with the context of this config. |  | ||||||
| 	 * @param message the message to log. |  | ||||||
| 	 */ |  | ||||||
| 	protected void warning(String message) { |  | ||||||
| 		Log.warning("Error in configuration '"+configFile.getName()+"': " + message); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 	/** | 	/** | ||||||
| 	 * The type of config. | 	 * The type of config. | ||||||
| 	 */ | 	 */ | ||||||
| @@ -1,17 +1,17 @@ | |||||||
| package fr.pandacube.lib.core.config; | package fr.pandacube.lib.config; | ||||||
| 
 | 
 | ||||||
| import java.io.File; | import java.io.File; | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * An abstract manager for a set of configuration files and folders. |  * An abstract manager for a set of configuration files and folders. | ||||||
|  * Its uses is to manage the loading/reloading of the configuration of a plugin. |  * It's uses to manage the loading/reloading of the configuration of a plugin. | ||||||
|  */ |  */ | ||||||
| public abstract class AbstractConfigManager { | public abstract class AbstractConfigManager { | ||||||
| 
 | 
 | ||||||
| 	/** | 	/** | ||||||
| 	 * The global configuration directory. | 	 * The global configuration directory. | ||||||
| 	 * May be the one provided by the environmenet API (like Plugin.getPluginFolder() in Bukkit). | 	 * It may be the one provided by the environment API (like Plugin.getPluginFolder() in Bukkit). | ||||||
| 	 */ | 	 */ | ||||||
| 	protected final File configDir; | 	protected final File configDir; | ||||||
| 
 | 
 | ||||||
| @@ -37,6 +37,12 @@ | |||||||
|             <version>${project.version}</version> |             <version>${project.version}</version> | ||||||
|         </dependency> |         </dependency> | ||||||
|  |  | ||||||
|  |         <dependency> | ||||||
|  |             <groupId>com.google.guava</groupId> | ||||||
|  |             <artifactId>guava</artifactId> | ||||||
|  |             <version>${guava.version}</version> | ||||||
|  |         </dependency> | ||||||
|  |  | ||||||
|         <!-- Cron expression interpreter --> |         <!-- Cron expression interpreter --> | ||||||
|         <dependency> |         <dependency> | ||||||
|             <groupId>ch.eitchnet</groupId> |             <groupId>ch.eitchnet</groupId> | ||||||
| @@ -50,7 +56,7 @@ | |||||||
|             <plugin> |             <plugin> | ||||||
|                 <groupId>org.apache.maven.plugins</groupId> |                 <groupId>org.apache.maven.plugins</groupId> | ||||||
|                 <artifactId>maven-shade-plugin</artifactId> |                 <artifactId>maven-shade-plugin</artifactId> | ||||||
|                 <version>3.3.0</version> |                 <version>3.5.2</version> | ||||||
|                 <executions> |                 <executions> | ||||||
|                     <execution> |                     <execution> | ||||||
|                         <phase>package</phase> |                         <phase>package</phase> | ||||||
| @@ -85,6 +91,28 @@ | |||||||
|                     </execution> |                     </execution> | ||||||
|                 </executions> |                 </executions> | ||||||
|             </plugin> |             </plugin> | ||||||
|  |  | ||||||
|  |             <plugin> | ||||||
|  |                 <groupId>com.googlecode.maven-download-plugin</groupId> | ||||||
|  |                 <artifactId>download-maven-plugin</artifactId> | ||||||
|  |                 <version>1.7.0</version> | ||||||
|  |                 <executions> | ||||||
|  |                     <execution> | ||||||
|  |                         <id>mcversion-download</id> | ||||||
|  |                         <phase>compile</phase> | ||||||
|  |                         <goals> | ||||||
|  |                             <goal>wget</goal> | ||||||
|  |                         </goals> | ||||||
|  |                     </execution> | ||||||
|  |                 </executions> | ||||||
|  |                 <configuration> | ||||||
|  |                     <url>https://api.pandacube.fr/rest/mcversion</url> | ||||||
|  |                     <outputDirectory>${project.basedir}/src/main/resources/fr/pandacube/lib/core/mc_version</outputDirectory> | ||||||
|  |                     <outputFileName>mcversion.json</outputFileName> | ||||||
|  |                     <skipCache>true</skipCache> | ||||||
|  |                     <overwrite>true</overwrite> | ||||||
|  |                 </configuration> | ||||||
|  |             </plugin> | ||||||
|         </plugins> |         </plugins> | ||||||
|     </build> |     </build> | ||||||
| </project> | </project> | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| package fr.pandacube.lib.core.backup; | package fr.pandacube.lib.core.backup; | ||||||
|  |  | ||||||
| import fr.pandacube.lib.chat.Chat; | import fr.pandacube.lib.chat.Chat; | ||||||
| import fr.pandacube.lib.util.Log; | import fr.pandacube.lib.chat.LegacyChatFormat; | ||||||
| import net.md_5.bungee.api.ChatColor; | import fr.pandacube.lib.util.log.Log; | ||||||
|  |  | ||||||
| import java.io.File; | import java.io.File; | ||||||
| import java.time.LocalDateTime; | import java.time.LocalDateTime; | ||||||
| @@ -18,7 +18,7 @@ import static fr.pandacube.lib.chat.ChatStatic.text; | |||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Cleanup a backup directory (i.e. removes old backup archives). |  * Cleanup a backup directory (i.e. removes old backup archives). | ||||||
|  * It is possible to combine differents instances to affect which archive to keep or delete. |  * It is possible to combine different instances to affect which archive to keep or delete. | ||||||
|  */ |  */ | ||||||
| public abstract class BackupCleaner implements UnaryOperator<TreeSet<LocalDateTime>> { | public abstract class BackupCleaner implements UnaryOperator<TreeSet<LocalDateTime>> { | ||||||
|  |  | ||||||
| @@ -48,7 +48,7 @@ public abstract class BackupCleaner implements UnaryOperator<TreeSet<LocalDateTi | |||||||
|      * formula <code><i>YEAR</i> * (12 / <i>n</i>) + <i>MONTH</i> / <i>n</i></code>. It then keeps the first archive |      * formula <code><i>YEAR</i> * (12 / <i>n</i>) + <i>MONTH</i> / <i>n</i></code>. It then keeps the first archive | ||||||
|      * found in each section. |      * found in each section. | ||||||
|      * |      * | ||||||
|      * @param n the interval in month between each kept archives. Must be a dividor of 12 (1, 2, 3, 4, 6 or 12). |      * @param n the interval in month between each kept archives. Must be a divider of 12 (1, 2, 3, 4, 6 or 12). | ||||||
|      * @return a {@link BackupCleaner} that keeps one archive every n month. |      * @return a {@link BackupCleaner} that keeps one archive every n month. | ||||||
|      */ |      */ | ||||||
|     public static BackupCleaner KEEPING_1_EVERY_N_MONTH(int n) { |     public static BackupCleaner KEEPING_1_EVERY_N_MONTH(int n) { | ||||||
| @@ -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 |      * Creates a new {@link BackupCleaner} that keeps the archives kept by this {@link BackupCleaner} or by the provided | ||||||
| @@ -94,20 +99,22 @@ public abstract class BackupCleaner implements UnaryOperator<TreeSet<LocalDateTi | |||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Performs the cleanup operation on the provided directory. |      * Performs the cleanup operation on the provided directory. | ||||||
|      * @param archiveDir the backup directory to cleanup. |      * @param archiveDir the backup directory to clean up. | ||||||
|      * @param compressDisplayName the displayname of the backup process that manages the backup directory. Used for logs. |      * @param compressDisplayName the display name of the backup process that manages the backup directory. Used for logs. | ||||||
|      */ |      */ | ||||||
|     public void cleanupArchives(File archiveDir, String compressDisplayName) { |     public void cleanupArchives(File archiveDir, String compressDisplayName) { | ||||||
|         String[] files = archiveDir.list(); |         String[] files = archiveDir.list(); | ||||||
|  |         if (files == null) | ||||||
|  |             return; | ||||||
|  |  | ||||||
|         Log.info("[Backup] Cleaning up backup directory " + ChatColor.GRAY + compressDisplayName + ChatColor.RESET + "..."); |         Log.info("[Backup] Cleaning up backup directory " + LegacyChatFormat.GRAY + compressDisplayName + LegacyChatFormat.RESET + "..."); | ||||||
|  |  | ||||||
|         TreeMap<LocalDateTime, File> datedFiles = new TreeMap<>(); |         TreeMap<LocalDateTime, File> datedFiles = new TreeMap<>(); | ||||||
|  |  | ||||||
|         for (String filename : files) { |         for (String filename : files) { | ||||||
|             File file = new File(archiveDir, filename); |             File file = new File(archiveDir, filename); | ||||||
|             if (!filename.matches("\\d{8}-\\d{6}\\.zip")) { |             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; |                 continue; | ||||||
|             } |             } | ||||||
|  |  | ||||||
| @@ -116,7 +123,7 @@ public abstract class BackupCleaner implements UnaryOperator<TreeSet<LocalDateTi | |||||||
|             try { |             try { | ||||||
|                 ldt = LocalDateTime.parse(dateTimeStr, BackupProcess.dateFileNameFormatter); |                 ldt = LocalDateTime.parse(dateTimeStr, BackupProcess.dateFileNameFormatter); | ||||||
|             } catch (DateTimeParseException e) { |             } 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; |                 continue; | ||||||
|             } |             } | ||||||
|  |  | ||||||
| @@ -149,7 +156,7 @@ public abstract class BackupCleaner implements UnaryOperator<TreeSet<LocalDateTi | |||||||
|         if (testOnly || oneDeleted) |         if (testOnly || oneDeleted) | ||||||
|             Log.warning(c.getLegacyText()); |             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."); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| package fr.pandacube.lib.core.backup; | 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.io.File; | ||||||
| import java.time.ZonedDateTime; | import java.time.ZonedDateTime; | ||||||
| @@ -33,11 +33,14 @@ public class BackupManager extends TimerTask { | |||||||
|     private final Timer schedulerTimer = new Timer(); |     private final Timer schedulerTimer = new Timer(); | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Instanciate a new backup manager. |      * Instantiate a new backup manager. | ||||||
|      * @param backupDirectory the root backup directory. |      * @param backupDirectory the root backup directory. | ||||||
|      */ |      */ | ||||||
|     public BackupManager(File backupDirectory) { |     public BackupManager(File backupDirectory) { | ||||||
|         this.backupDirectory = backupDirectory; |         this.backupDirectory = backupDirectory; | ||||||
|  |         if (!backupDirectory.exists()) { | ||||||
|  |             backupDirectory.mkdirs(); | ||||||
|  |         } | ||||||
|         persist = new Persist(this); |         persist = new Persist(this); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -63,7 +66,13 @@ public class BackupManager extends TimerTask { | |||||||
|         return backupDirectory; |         return backupDirectory; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Tells if a backup is currently running. | ||||||
|  |      * @return true if a backup is running, false otherwise. | ||||||
|  |      */ | ||||||
|  |     public synchronized boolean isBackupRunning() { | ||||||
|  |         return runningBackup.get() != null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     public synchronized void run() { |     public synchronized void run() { | ||||||
| @@ -87,11 +96,12 @@ public class BackupManager extends TimerTask { | |||||||
|      * Disables this backup manager, canceling scheduled backups. |      * Disables this backup manager, canceling scheduled backups. | ||||||
|      * It will wait for a currently running backup to finish before returning. |      * It will wait for a currently running backup to finish before returning. | ||||||
|      */ |      */ | ||||||
|  |     @SuppressWarnings("BusyWait") | ||||||
|     public synchronized void onDisable() { |     public synchronized void onDisable() { | ||||||
|  |  | ||||||
|         schedulerTimer.cancel(); |         schedulerTimer.cancel(); | ||||||
|  |  | ||||||
|         if (runningBackup.get() != null) { |         if (isBackupRunning()) { | ||||||
|             Log.warning("[Backup] Waiting after the end of a backup..."); |             Log.warning("[Backup] Waiting after the end of a backup..."); | ||||||
|             BackupProcess tmp; |             BackupProcess tmp; | ||||||
|             while ((tmp = runningBackup.get()) != null) { |             while ((tmp = runningBackup.get()) != null) { | ||||||
|   | |||||||
| @@ -1,10 +1,10 @@ | |||||||
| package fr.pandacube.lib.core.backup; | package fr.pandacube.lib.core.backup; | ||||||
|  |  | ||||||
| import fc.cron.CronExpression; | import fc.cron.CronExpression; | ||||||
|  | import fr.pandacube.lib.chat.LegacyChatFormat; | ||||||
| import fr.pandacube.lib.core.cron.CronScheduler; | import fr.pandacube.lib.core.cron.CronScheduler; | ||||||
| import fr.pandacube.lib.util.FileUtils; | import fr.pandacube.lib.util.FileUtils; | ||||||
| import fr.pandacube.lib.util.Log; | import fr.pandacube.lib.util.log.Log; | ||||||
| import net.md_5.bungee.api.ChatColor; |  | ||||||
|  |  | ||||||
| import java.io.File; | import java.io.File; | ||||||
| import java.text.DateFormat; | import java.text.DateFormat; | ||||||
| @@ -35,12 +35,12 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab | |||||||
|  |  | ||||||
|  |  | ||||||
|     private boolean enabled = true; |     private boolean enabled = true; | ||||||
|     private String scheduling = "0 2 * * *"; // cron format, here is everyday at 2am |     private String scheduling = "0 2 * * *"; // cron format, here is every day at 2am | ||||||
|     private BackupCleaner backupCleaner = null; |     private BackupCleaner backupCleaner = null; | ||||||
|     private List<String> ignoreList = new ArrayList<>(); |     private List<String> ignoreList = new ArrayList<>(); | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Instanciates a new backup process. |      * Instantiates a new backup process. | ||||||
|      * @param bm the associated backup manager. |      * @param bm the associated backup manager. | ||||||
|      * @param n the process identifier. |      * @param n the process identifier. | ||||||
|      */ |      */ | ||||||
| @@ -66,9 +66,9 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Gets the displayname of this process. |      * Gets the display name of this process. | ||||||
|      * Default implementation returns {@link #getIdentifier()}. |      * Default implementation returns {@link #getIdentifier()}. | ||||||
|      * @return the displayname of this process. |      * @return the display name of this process. | ||||||
|      */ |      */ | ||||||
|     protected String getDisplayName() { |     protected String getDisplayName() { | ||||||
|         return getIdentifier(); |         return getIdentifier(); | ||||||
| @@ -105,8 +105,8 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Gets the source directory to backup. |      * Gets the source directory to back up. | ||||||
|      * @return the source directory to backup. |      * @return the source directory to back up. | ||||||
|      */ |      */ | ||||||
|     public abstract File getSourceDir(); |     public abstract File getSourceDir(); | ||||||
|  |  | ||||||
| @@ -123,7 +123,7 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab | |||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Called when the backup ends. |      * Called when the backup ends. | ||||||
|      * @param success true if the backup ended successfuly. |      * @param success true if the backup ended successfully. | ||||||
|      */ |      */ | ||||||
|     protected abstract void onBackupEnd(boolean success); |     protected abstract void onBackupEnd(boolean success); | ||||||
|  |  | ||||||
| @@ -209,7 +209,7 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab | |||||||
|             File sourceDir = getSourceDir(); |             File sourceDir = getSourceDir(); | ||||||
|  |  | ||||||
|             if (!sourceDir.exists()) { |             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; |                 return; | ||||||
|             } |             } | ||||||
|  |  | ||||||
| @@ -219,7 +219,7 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab | |||||||
|             onBackupStart(); |             onBackupStart(); | ||||||
|  |  | ||||||
|             new Thread(() -> { |             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); |                 compressor = new ZipCompressor(sourceDir, target, 9, filter); | ||||||
|  |  | ||||||
| @@ -229,7 +229,7 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab | |||||||
|  |  | ||||||
|                     success = true; |                     success = true; | ||||||
|  |  | ||||||
|                     Log.info("[Backup] Finished for " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET); |                     Log.info("[Backup] Finished for " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET); | ||||||
|  |  | ||||||
|                     try { |                     try { | ||||||
|                         BackupCleaner cleaner = getBackupCleaner(); |                         BackupCleaner cleaner = getBackupCleaner(); | ||||||
| @@ -267,7 +267,7 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab | |||||||
|      * Logs the scheduling status of this backup process. |      * Logs the scheduling status of this backup process. | ||||||
|      */ |      */ | ||||||
|     public void displayNextSchedule() { |     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()))); |                 + 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() { |     public void logProgress() { | ||||||
|         if (compressor == null) |         if (compressor == null) | ||||||
|             return; |             return; | ||||||
|         Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + ": " + compressor.getState().getLegacyText()); |         Log.info("[Backup] " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + ": " + compressor.getState().getLegacyText()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ package fr.pandacube.lib.core.backup; | |||||||
| import com.google.gson.JsonParseException; | import com.google.gson.JsonParseException; | ||||||
| import com.google.gson.reflect.TypeToken; | import com.google.gson.reflect.TypeToken; | ||||||
| import fr.pandacube.lib.core.json.Json; | 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.File; | ||||||
| import java.io.FileReader; | import java.io.FileReader; | ||||||
| @@ -26,7 +26,7 @@ public class Persist { | |||||||
| 	// private final Set<String> dirtyWorldsSave = new HashSet<>(); | 	// private final Set<String> dirtyWorldsSave = new HashSet<>(); | ||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
| 	 * Creates a new instance, immediatly loading the data from the file if it exists, or creating an empty one if not. | 	 * Creates a new instance, immediately loading the data from the file if it exists, or creating an empty one if not. | ||||||
| 	 * @param bm the associated backup manager. | 	 * @param bm the associated backup manager. | ||||||
| 	 */ | 	 */ | ||||||
| 	public Persist(BackupManager bm) { | 	public Persist(BackupManager bm) { | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| package fr.pandacube.lib.core.backup; | package fr.pandacube.lib.core.backup; | ||||||
|  |  | ||||||
| import com.google.common.io.Files; | import com.google.common.io.Files; | ||||||
| import fr.pandacube.lib.util.Log; | import fr.pandacube.lib.chat.LegacyChatFormat; | ||||||
| import net.md_5.bungee.api.ChatColor; | import fr.pandacube.lib.util.log.Log; | ||||||
|  |  | ||||||
| import java.io.File; | import java.io.File; | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| @@ -24,7 +24,8 @@ public class RotatedLogsBackupProcess extends BackupProcess { | |||||||
|      * @param inNewThread tells if this process should be run in a separate thread (true) or in the same thread handling |      * @param inNewThread tells if this process should be run in a separate thread (true) or in the same thread handling | ||||||
|      *                    the backup manager (false). |      *                    the backup manager (false). | ||||||
|      * @param sourceLogDir the directory where the rotated log files are stored, usually {@code ./logs/}. |      * @param sourceLogDir the directory where the rotated log files are stored, usually {@code ./logs/}. | ||||||
|      * @param logFileRegexPattern the pattern to match the rotated log files (usually dated log files, excuding the current log file). |      * @param logFileRegexPattern the pattern to match the rotated log files (usually dated log files, excluding the | ||||||
|  |      *                            current log file). | ||||||
|      */ |      */ | ||||||
|     public RotatedLogsBackupProcess(BackupManager bm, boolean inNewThread, File sourceLogDir, String logFileRegexPattern) { |     public RotatedLogsBackupProcess(BackupManager bm, boolean inNewThread, File sourceLogDir, String logFileRegexPattern) { | ||||||
|         super(bm, "logs"); |         super(bm, "logs"); | ||||||
| @@ -52,7 +53,7 @@ public class RotatedLogsBackupProcess extends BackupProcess { | |||||||
|         if (!getSourceDir().isDirectory()) |         if (!getSourceDir().isDirectory()) | ||||||
|             return; |             return; | ||||||
|  |  | ||||||
|         Log.info("[Backup] Starting for " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " ..."); |         Log.info("[Backup] Starting for " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + " ..."); | ||||||
|  |  | ||||||
|         try { |         try { | ||||||
|             // wait a little after the log message above, in case the log file rotation has to be performed. |             // wait a little after the log message above, in case the log file rotation has to be performed. | ||||||
| @@ -81,9 +82,9 @@ public class RotatedLogsBackupProcess extends BackupProcess { | |||||||
|  |  | ||||||
|             success = true; |             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) { |         } 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 { |         } finally { | ||||||
|             onBackupEnd(success); |             onBackupEnd(success); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ import java.io.File; | |||||||
| import java.io.FileOutputStream; | import java.io.FileOutputStream; | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.nio.file.Files; | import java.nio.file.Files; | ||||||
|  | import java.nio.file.NoSuchFileException; | ||||||
| import java.nio.file.attribute.BasicFileAttributes; | import java.nio.file.attribute.BasicFileAttributes; | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| @@ -117,7 +118,11 @@ public class ZipCompressor { | |||||||
| 			} | 			} | ||||||
| 			 | 			 | ||||||
| 			for (Entry entry : entriesToCompress) { | 			for (Entry entry : entriesToCompress) { | ||||||
| 				entry.zip(); | 				try { | ||||||
|  | 					entry.zip(); | ||||||
|  | 				} catch (NoSuchFileException ignored) { | ||||||
|  | 					// file has been deleted since | ||||||
|  | 				} | ||||||
| 			} | 			} | ||||||
| 			 | 			 | ||||||
| 			synchronized (stateLock) { | 			synchronized (stateLock) { | ||||||
| @@ -158,8 +163,8 @@ public class ZipCompressor { | |||||||
| 	} | 	} | ||||||
| 	 | 	 | ||||||
| 	private class Entry { | 	private class Entry { | ||||||
| 		File file; | 		final File file; | ||||||
| 		String entry; | 		final String entry; | ||||||
| 		Entry(File f, String e) { | 		Entry(File f, String e) { | ||||||
| 			file = f; | 			file = f; | ||||||
| 			entry = e; | 			entry = e; | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ import com.google.gson.JsonParseException; | |||||||
| import com.google.gson.reflect.TypeToken; | import com.google.gson.reflect.TypeToken; | ||||||
| import fc.cron.CronExpression; | import fc.cron.CronExpression; | ||||||
| import fr.pandacube.lib.core.json.Json; | 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.File; | ||||||
| import java.io.FileReader; | import java.io.FileReader; | ||||||
| @@ -54,10 +54,10 @@ public class CronScheduler { | |||||||
|                 long now = System.currentTimeMillis(); |                 long now = System.currentTimeMillis(); | ||||||
|  |  | ||||||
|                 if (!tasks.isEmpty()) { |                 if (!tasks.isEmpty()) { | ||||||
|                     CronTask next = tasks.get(0); |                     CronTask next = tasks.getFirst(); | ||||||
|                     if (next.nextRun <= now) { |                     if (next.nextRun <= now) { | ||||||
|                         next.runAsync(); |                         next.runAsync(); | ||||||
|                         setLastRun(next.taskId, next.nextRun); |                         setLastRun(next.taskId, now); | ||||||
|                         onTaskUpdate(false); |                         onTaskUpdate(false); | ||||||
|                         continue; |                         continue; | ||||||
|                     } |                     } | ||||||
| @@ -102,7 +102,7 @@ public class CronScheduler { | |||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Cancel a scheduled task. |      * Cancel a scheduled task. | ||||||
|      * Will not stop a current execution of the task. If the task does not exists, it will not do anything. |      * Will not stop a current execution of the task. If the task does not exist, it will not do anything. | ||||||
|      * @param taskId the id of the task to cancel. |      * @param taskId the id of the task to cancel. | ||||||
|      */ |      */ | ||||||
|     public static void unSchedule(String taskId) { |     public static void unSchedule(String taskId) { | ||||||
| @@ -224,5 +224,6 @@ public class CronScheduler { | |||||||
|                 .toEpochMilli(); |                 .toEpochMilli(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     private CronScheduler() {} | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,44 +2,94 @@ package fr.pandacube.lib.core.json; | |||||||
|  |  | ||||||
| import com.google.gson.Gson; | import com.google.gson.Gson; | ||||||
| import com.google.gson.GsonBuilder; | import com.google.gson.GsonBuilder; | ||||||
|  | import com.google.gson.JsonParseException; | ||||||
|  | import com.google.gson.ToNumberStrategy; | ||||||
| import com.google.gson.TypeAdapter; | import com.google.gson.TypeAdapter; | ||||||
| import com.google.gson.TypeAdapterFactory; | import com.google.gson.TypeAdapterFactory; | ||||||
| import com.google.gson.reflect.TypeToken; | import com.google.gson.reflect.TypeToken; | ||||||
| import fr.pandacube.lib.util.Log; | 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.ArrayList; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.function.Function; | import java.util.function.Function; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Provides pre-instanciated {@link Gson} instances, all with support for Java records and additionnal |  * Provides pre-instanced {@link Gson} objects, all with support for Java records and additional | ||||||
|  * {@link TypeAdapterFactory} provided with {@link #registerTypeAdapterFactory(TypeAdapterFactory)}. |  * {@link TypeAdapterFactory} provided with {@link #registerTypeAdapterFactory(TypeAdapterFactory)}. | ||||||
|  */ |  */ | ||||||
| public class Json { | public class Json { | ||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
| 	 * {@link Gson} instance with {@link GsonBuilder#setLenient()} and support for Java records and additionnal | 	 * Makes Gson deserialize numbers to Number subclasses the same way SnakeYAML does | ||||||
|  | 	 */ | ||||||
|  | 	private static final ToNumberStrategy YAML_EQUIVALENT_NUMBER_STRATEGY = in -> { | ||||||
|  | 		String value = in.nextString(); | ||||||
|  |  | ||||||
|  | 		// YAML uses Regex to resolve values as INT or FLOAT (see org.yaml.snakeyaml.resolver.Resolver), trying FLOAT first. | ||||||
|  | 		// We see in the regex that FLOAT MUST have a "." in the string, but INT must not, so we try that. | ||||||
|  | 		boolean isFloat = value.contains("."); | ||||||
|  |  | ||||||
|  | 		if (isFloat) { | ||||||
|  | 			// if float, will only parse to Double | ||||||
|  | 			//   (see org.yaml.snakeyaml.constructor.SafeConstructor.ConstructYamlFloat) | ||||||
|  | 			try { | ||||||
|  | 				Double d = Double.valueOf(value); | ||||||
|  | 				if ((d.isInfinite() || d.isNaN()) && !in.isLenient()) { | ||||||
|  | 					throw new MalformedJsonException("JSON forbids NaN and infinities: " + d + "; at path " + in.getPreviousPath()); | ||||||
|  | 				} | ||||||
|  | 				return d; | ||||||
|  | 			} catch (NumberFormatException e) { | ||||||
|  | 				throw new JsonParseException("Cannot parse " + value + "; at path " + in.getPreviousPath(), e); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		else { | ||||||
|  | 			// if integer, will try to parse int, then long, then BigDecimal | ||||||
|  | 			//   (see org.yaml.snakeyaml.constructor.SafeConstructor.ConstructYamlInt | ||||||
|  | 			//    then org.yaml.snakeyaml.constructor.SafeConstructor.createNumber) | ||||||
|  | 			try { | ||||||
|  | 				return Integer.valueOf(value); | ||||||
|  | 			} catch (NumberFormatException e) { | ||||||
|  | 				try { | ||||||
|  | 					return Long.valueOf(value); | ||||||
|  | 				} catch (NumberFormatException e2) { | ||||||
|  | 					try { | ||||||
|  | 						return new BigInteger(value); | ||||||
|  | 					} catch (NumberFormatException e3) { | ||||||
|  | 						throw new JsonParseException("Cannot parse " + value + "; at path " + in.getPreviousPath(), e3); | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	}; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * {@link Gson} instance with {@link GsonBuilder#setLenient()} and support for Java records and additional | ||||||
| 	 * {@link TypeAdapterFactory} provided with {@link #registerTypeAdapterFactory(TypeAdapterFactory)}. | 	 * {@link TypeAdapterFactory} provided with {@link #registerTypeAdapterFactory(TypeAdapterFactory)}. | ||||||
| 	 */ | 	 */ | ||||||
| 	public static final Gson gson = build(Function.identity()); | 	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 GsonBuilder#setLenient()}, {@link GsonBuilder#setPrettyPrinting()} and support | ||||||
| 	 * for Java records and additionnal {@link TypeAdapterFactory} provided with | 	 * for Java records and additional {@link TypeAdapterFactory} provided with | ||||||
| 	 * {@link #registerTypeAdapterFactory(TypeAdapterFactory)}. | 	 * {@link #registerTypeAdapterFactory(TypeAdapterFactory)}. | ||||||
| 	 */ | 	 */ | ||||||
| 	public static final Gson gsonPrettyPrinting = build(GsonBuilder::setPrettyPrinting); | 	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 GsonBuilder#setLenient()}, {@link GsonBuilder#serializeNulls()} and support for | ||||||
| 	 * Java records and additionnal {@link TypeAdapterFactory} provided with | 	 * Java records and additional {@link TypeAdapterFactory} provided with | ||||||
| 	 * {@link #registerTypeAdapterFactory(TypeAdapterFactory)}. | 	 * {@link #registerTypeAdapterFactory(TypeAdapterFactory)}. | ||||||
| 	 */ | 	 */ | ||||||
| 	public static final Gson gsonSerializeNulls = build(GsonBuilder::serializeNulls); | 	public static final Gson gsonSerializeNulls = build(GsonBuilder::serializeNulls); | ||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
| 	 * {@link Gson} instance with {@link GsonBuilder#setLenient()}, {@link GsonBuilder#serializeNulls()}, | 	 * {@link Gson} instance with {@link GsonBuilder#setLenient()}, {@link GsonBuilder#serializeNulls()}, | ||||||
| 	 * {@link GsonBuilder#setPrettyPrinting()} and support for Java records and additionnal {@link TypeAdapterFactory} | 	 * {@link GsonBuilder#setPrettyPrinting()} and support for Java records and additional {@link TypeAdapterFactory} | ||||||
| 	 * provided with {@link #registerTypeAdapterFactory(TypeAdapterFactory)}. | 	 * provided with {@link #registerTypeAdapterFactory(TypeAdapterFactory)}. | ||||||
| 	 */ | 	 */ | ||||||
| 	public static final Gson gsonSerializeNullsPrettyPrinting = build(b -> b.serializeNulls().setPrettyPrinting()); | 	public static final Gson gsonSerializeNullsPrettyPrinting = build(b -> b.serializeNulls().setPrettyPrinting()); | ||||||
| @@ -53,6 +103,8 @@ public class Json { | |||||||
| 	private static Gson build(Function<GsonBuilder, GsonBuilder> builderModifier) { | 	private static Gson build(Function<GsonBuilder, GsonBuilder> builderModifier) { | ||||||
| 		GsonBuilder base = new GsonBuilder() | 		GsonBuilder base = new GsonBuilder() | ||||||
| 				.registerTypeAdapterFactory(new CustomAdapterFactory()) | 				.registerTypeAdapterFactory(new CustomAdapterFactory()) | ||||||
|  | 				.disableHtmlEscaping() | ||||||
|  | 				.setObjectToNumberStrategy(YAML_EQUIVALENT_NUMBER_STRATEGY) | ||||||
| 				.setLenient(); | 				.setLenient(); | ||||||
| 		return builderModifier.apply(base).create(); | 		return builderModifier.apply(base).create(); | ||||||
| 	} | 	} | ||||||
| @@ -90,25 +142,27 @@ public class Json { | |||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| 	private static boolean hasGsonNativeRecordSupport() { |  | ||||||
| 		try { |  | ||||||
| 			com.google.gson.internal.reflect.ReflectionHelper.class.getDeclaredField("RECORD_HELPER"); |  | ||||||
| 			return true; |  | ||||||
| 		} catch (NoClassDefFoundError e) { |  | ||||||
| 			Log.warning("Unable to check Gson supporting records. Assuming it does not. " + e); |  | ||||||
| 			return false; |  | ||||||
| 		} catch (NoSuchFieldException e) { |  | ||||||
| 			return false; |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| 	static { | 	static { | ||||||
| 		if (!hasGsonNativeRecordSupport()) |  | ||||||
| 			registerTypeAdapterFactory(RecordTypeAdapter.FACTORY); |  | ||||||
| 		registerTypeAdapterFactory(StackTraceElementAdapter.FACTORY); | 		registerTypeAdapterFactory(StackTraceElementAdapter.FACTORY); | ||||||
| 		registerTypeAdapterFactory(ThrowableAdapter.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() {} | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,88 +0,0 @@ | |||||||
| package fr.pandacube.lib.core.json; |  | ||||||
|  |  | ||||||
| import com.google.gson.Gson; |  | ||||||
| import com.google.gson.TypeAdapter; |  | ||||||
| import com.google.gson.TypeAdapterFactory; |  | ||||||
| import com.google.gson.reflect.TypeToken; |  | ||||||
| import com.google.gson.stream.JsonReader; |  | ||||||
| import com.google.gson.stream.JsonToken; |  | ||||||
| import com.google.gson.stream.JsonWriter; |  | ||||||
|  |  | ||||||
| import java.io.IOException; |  | ||||||
| import java.lang.reflect.Constructor; |  | ||||||
| import java.lang.reflect.InvocationTargetException; |  | ||||||
| import java.lang.reflect.RecordComponent; |  | ||||||
| import java.util.HashMap; |  | ||||||
| import java.util.Map; |  | ||||||
|  |  | ||||||
| // from https://github.com/google/gson/issues/1794#issuecomment-812964421 |  | ||||||
| /* package */ class RecordTypeAdapter<T> extends TypeAdapter<T> { |  | ||||||
|  |  | ||||||
|     public static final TypeAdapterFactory FACTORY = new TypeAdapterFactory() { |  | ||||||
|         @Override |  | ||||||
|         public <TT> TypeAdapter<TT> create(Gson gson, TypeToken<TT> type) { |  | ||||||
|             @SuppressWarnings("unchecked") |  | ||||||
|             Class<TT> clazz = (Class<TT>) type.getRawType(); |  | ||||||
|             if (!clazz.isRecord() || clazz == Record.class) { |  | ||||||
|                 return null; |  | ||||||
|             } |  | ||||||
|             return new RecordTypeAdapter<>(gson, this, type); |  | ||||||
|         } |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     private final Gson gson; |  | ||||||
|     private final TypeAdapterFactory factory; |  | ||||||
|     private final TypeToken<T> type; |  | ||||||
|  |  | ||||||
|     public RecordTypeAdapter(Gson gson, TypeAdapterFactory factory, TypeToken<T> type) { |  | ||||||
|         this.gson = gson; |  | ||||||
|         this.factory = factory; |  | ||||||
|         this.type = type; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public void write(JsonWriter out, T value) throws IOException { |  | ||||||
|         gson.getDelegateAdapter(factory, type).write(out, value); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Override |  | ||||||
|     public T read(JsonReader reader) throws IOException { |  | ||||||
|         if (reader.peek() == JsonToken.NULL) { |  | ||||||
|             reader.nextNull(); |  | ||||||
|             return null; |  | ||||||
|         } else { |  | ||||||
|             @SuppressWarnings("unchecked") |  | ||||||
|             Class<T> clazz = (Class<T>) type.getRawType(); |  | ||||||
|  |  | ||||||
|             RecordComponent[] recordComponents = clazz.getRecordComponents(); |  | ||||||
|             Map<String, TypeToken<?>> typeMap = new HashMap<>(); |  | ||||||
|             for (RecordComponent recordComponent : recordComponents) { |  | ||||||
|                 typeMap.put(recordComponent.getName(), TypeToken.get(recordComponent.getGenericType())); |  | ||||||
|             } |  | ||||||
|             var argsMap = new HashMap<String, Object>(); |  | ||||||
|             reader.beginObject(); |  | ||||||
|             while (reader.hasNext()) { |  | ||||||
|                 String name = reader.nextName(); |  | ||||||
|                 argsMap.put(name, gson.getAdapter(typeMap.get(name)).read(reader)); |  | ||||||
|             } |  | ||||||
|             reader.endObject(); |  | ||||||
|  |  | ||||||
|             var argTypes = new Class<?>[recordComponents.length]; |  | ||||||
|             var args = new Object[recordComponents.length]; |  | ||||||
|             for (int i = 0; i < recordComponents.length; i++) { |  | ||||||
|                 argTypes[i] = recordComponents[i].getType(); |  | ||||||
|                 args[i] = argsMap.get(recordComponents[i].getName()); |  | ||||||
|             } |  | ||||||
|             Constructor<T> constructor; |  | ||||||
|             try { |  | ||||||
|                 constructor = clazz.getDeclaredConstructor(argTypes); |  | ||||||
|                 constructor.setAccessible(true); |  | ||||||
|                 return constructor.newInstance(args); |  | ||||||
|             } catch (NoSuchMethodException | InstantiationException | SecurityException | IllegalAccessException |  | ||||||
|                      | IllegalArgumentException | InvocationTargetException e) { |  | ||||||
|                 throw new RuntimeException(e); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -10,7 +10,7 @@ import com.google.gson.JsonSerializationContext; | |||||||
| import com.google.gson.JsonSerializer; | import com.google.gson.JsonSerializer; | ||||||
| import com.google.gson.TypeAdapterFactory; | import com.google.gson.TypeAdapterFactory; | ||||||
| import com.google.gson.internal.bind.TreeTypeAdapter; | 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 fr.pandacube.lib.util.ThrowableUtil; | ||||||
|  |  | ||||||
| import java.lang.reflect.Constructor; | 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()); |     /* package */ static final TypeAdapterFactory FACTORY = TreeTypeAdapter.newTypeHierarchyFactory(Throwable.class, new ThrowableAdapter()); | ||||||
|  |  | ||||||
|  |     private ThrowableAdapter() {} | ||||||
|  |  | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public Throwable deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { |     public Throwable deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { | ||||||
| @@ -43,7 +45,7 @@ public class ThrowableAdapter implements JsonSerializer<Throwable>, JsonDeserial | |||||||
|         // handle types |         // handle types | ||||||
|         Throwable t = null; |         Throwable t = null; | ||||||
|         if (obj.has("types") && obj.get("types").isJsonArray()) { |         if (obj.has("types") && obj.get("types").isJsonArray()) { | ||||||
|             t = instanciate(obj.getAsJsonArray("types"), message, cause); |             t = instantiate(obj.getAsJsonArray("types"), message, cause); | ||||||
|         } |         } | ||||||
|         if (t == null) { |         if (t == null) { | ||||||
|             t = new Throwable(message, cause); |             t = new Throwable(message, cause); | ||||||
| @@ -53,8 +55,8 @@ public class ThrowableAdapter implements JsonSerializer<Throwable>, JsonDeserial | |||||||
|         JsonArray suppressed = obj.has("suppressed") && !obj.get("suppressed").isJsonNull() |         JsonArray suppressed = obj.has("suppressed") && !obj.get("suppressed").isJsonNull() | ||||||
|                 ? obj.get("suppressed").getAsJsonArray() : null; |                 ? obj.get("suppressed").getAsJsonArray() : null; | ||||||
|         if (suppressed != null) { |         if (suppressed != null) { | ||||||
|             for (JsonElement jsonel : suppressed) { |             for (JsonElement jsonEl : suppressed) { | ||||||
|                 t.addSuppressed(context.deserialize(jsonel, Throwable.class)); |                 t.addSuppressed(context.deserialize(jsonEl, Throwable.class)); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -63,8 +65,8 @@ public class ThrowableAdapter implements JsonSerializer<Throwable>, JsonDeserial | |||||||
|                 ? obj.get("stacktrace").getAsJsonArray() : null; |                 ? obj.get("stacktrace").getAsJsonArray() : null; | ||||||
|         if (stacktrace != null) { |         if (stacktrace != null) { | ||||||
|             List<StackTraceElement> els = new ArrayList<>(); |             List<StackTraceElement> els = new ArrayList<>(); | ||||||
|             for (JsonElement jsonel : stacktrace) { |             for (JsonElement jsonEl : stacktrace) { | ||||||
|                 els.add(context.deserialize(jsonel, StackTraceElement.class)); |                 els.add(context.deserialize(jsonEl, StackTraceElement.class)); | ||||||
|             } |             } | ||||||
|             t.setStackTrace(els.toArray(new StackTraceElement[0])); |             t.setStackTrace(els.toArray(new StackTraceElement[0])); | ||||||
|         } |         } | ||||||
| @@ -159,7 +161,7 @@ public class ThrowableAdapter implements JsonSerializer<Throwable>, JsonDeserial | |||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     private Throwable instanciate(JsonArray types, String message, Throwable cause) { |     private Throwable instantiate(JsonArray types, String message, Throwable cause) { | ||||||
|         Throwable t = null; |         Throwable t = null; | ||||||
|         for (JsonElement clNameEl : types) { |         for (JsonElement clNameEl : types) { | ||||||
|             String clName = clNameEl.getAsString(); |             String clName = clNameEl.getAsString(); | ||||||
| @@ -196,7 +198,7 @@ public class ThrowableAdapter implements JsonSerializer<Throwable>, JsonDeserial | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         /** |         /** | ||||||
|          * Utiliy method to use on {@link Throwable} class that only have a message (no cause) constructor. |          * Utility method to use on {@link Throwable} class that only have a message (no cause) constructor. | ||||||
|          * @param constructorWithMessage function that will construct a new throwable, with prefilled message. |          * @param constructorWithMessage function that will construct a new throwable, with prefilled message. | ||||||
|          * @return a function that will construct a throwable using the provided function, then will try to init the cause of the throwable. |          * @return a function that will construct a throwable using the provided function, then will try to init the cause of the throwable. | ||||||
|          * @param <T> the type of the constructed {@link Throwable}. |          * @param <T> the type of the constructed {@link Throwable}. | ||||||
|   | |||||||
| @@ -15,8 +15,8 @@ public class TypeConverter { | |||||||
| 	/** | 	/** | ||||||
| 	 * Converts the provided object to an {@link Integer}. | 	 * Converts the provided object to an {@link Integer}. | ||||||
| 	 * @param o the object to convert. | 	 * @param o the object to convert. | ||||||
| 	 * @return a the object converted to an {@link Integer}. | 	 * @return the object converted to an {@link Integer}. | ||||||
| 	 * @throws ConvertionException is a conversion error occurs. | 	 * @throws ConversionException is a conversion error occurs. | ||||||
| 	 */ | 	 */ | ||||||
| 	public static Integer toInteger(Object o) { | 	public static Integer toInteger(Object o) { | ||||||
| 		if (o == null) { | 		if (o == null) { | ||||||
| @@ -27,7 +27,7 @@ public class TypeConverter { | |||||||
| 			try { | 			try { | ||||||
| 				return ((JsonElement)o).getAsInt(); | 				return ((JsonElement)o).getAsInt(); | ||||||
| 			} catch(UnsupportedOperationException e) { | 			} catch(UnsupportedOperationException e) { | ||||||
| 				throw new ConvertionException(e); | 				throw new ConversionException(e); | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		 | 		 | ||||||
| @@ -38,34 +38,34 @@ public class TypeConverter { | |||||||
| 			try { | 			try { | ||||||
| 			return Integer.parseInt((String)o); | 			return Integer.parseInt((String)o); | ||||||
| 			} catch (NumberFormatException e) { | 			} catch (NumberFormatException e) { | ||||||
| 				throw new ConvertionException(e); | 				throw new ConversionException(e); | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		if (o instanceof Boolean) { | 		if (o instanceof Boolean) { | ||||||
| 			return ((Boolean)o) ? 1 : 0; | 			return ((Boolean)o) ? 1 : 0; | ||||||
| 		} | 		} | ||||||
| 		 | 		 | ||||||
| 		throw new ConvertionException("No integer convertion available for an instance of "+o.getClass()); | 		throw new ConversionException("No integer conversion available for an instance of "+o.getClass()); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
| 	 * Converts the provided object to a primitive int. | 	 * Converts the provided object to a primitive int. | ||||||
| 	 * @param o the object to convert. | 	 * @param o the object to convert. | ||||||
| 	 * @return a the object converted to a primitive int. | 	 * @return the object converted to a primitive int. | ||||||
| 	 * @throws ConvertionException is a conversion error occurs. | 	 * @throws ConversionException is a conversion error occurs. | ||||||
| 	 */ | 	 */ | ||||||
| 	public static int toPrimInt(Object o) { | 	public static int toPrimInt(Object o) { | ||||||
| 		Integer val = toInteger(o); | 		Integer val = toInteger(o); | ||||||
| 		if (val == null) | 		if (val == null) | ||||||
| 			throw new ConvertionException("null values can't be converted to primitive int"); | 			throw new ConversionException("null values can't be converted to primitive int"); | ||||||
| 		return val; | 		return val; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
| 	 * Converts the provided object to a {@link Double}. | 	 * Converts the provided object to a {@link Double}. | ||||||
| 	 * @param o the object to convert. | 	 * @param o the object to convert. | ||||||
| 	 * @return a the object converted to a {@link Double}. | 	 * @return the object converted to a {@link Double}. | ||||||
| 	 * @throws ConvertionException is a conversion error occurs. | 	 * @throws ConversionException is a conversion error occurs. | ||||||
| 	 */ | 	 */ | ||||||
| 	public static Double toDouble(Object o) { | 	public static Double toDouble(Object o) { | ||||||
| 		if (o == null) { | 		if (o == null) { | ||||||
| @@ -76,7 +76,7 @@ public class TypeConverter { | |||||||
| 			try { | 			try { | ||||||
| 			return ((JsonElement)o).getAsDouble(); | 			return ((JsonElement)o).getAsDouble(); | ||||||
| 			} catch(UnsupportedOperationException e) { | 			} catch(UnsupportedOperationException e) { | ||||||
| 				throw new ConvertionException(e); | 				throw new ConversionException(e); | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @@ -87,35 +87,35 @@ public class TypeConverter { | |||||||
| 			try { | 			try { | ||||||
| 			return Double.parseDouble((String)o); | 			return Double.parseDouble((String)o); | ||||||
| 			} catch (NumberFormatException e) { | 			} catch (NumberFormatException e) { | ||||||
| 				throw new ConvertionException(e); | 				throw new ConversionException(e); | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		if (o instanceof Boolean) { | 		if (o instanceof Boolean) { | ||||||
| 			return ((Boolean)o) ? 1d : 0d; | 			return ((Boolean)o) ? 1d : 0d; | ||||||
| 		} | 		} | ||||||
| 		 | 		 | ||||||
| 		throw new ConvertionException("No double convertion available for an instance of "+o.getClass()); | 		throw new ConversionException("No double conversion available for an instance of "+o.getClass()); | ||||||
| 		 | 		 | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
| 	 * Converts the provided object to a primitive double. | 	 * Converts the provided object to a primitive double. | ||||||
| 	 * @param o the object to convert. | 	 * @param o the object to convert. | ||||||
| 	 * @return a the object converted to a primitive double. | 	 * @return the object converted to a primitive double. | ||||||
| 	 * @throws ConvertionException is a conversion error occurs. | 	 * @throws ConversionException is a conversion error occurs. | ||||||
| 	 */ | 	 */ | ||||||
| 	public static double toPrimDouble(Object o) { | 	public static double toPrimDouble(Object o) { | ||||||
| 		Double val = toDouble(o); | 		Double val = toDouble(o); | ||||||
| 		if (val == null) | 		if (val == null) | ||||||
| 			throw new ConvertionException("null values can't converted to primitive int"); | 			throw new ConversionException("null values can't converted to primitive int"); | ||||||
| 		return val; | 		return val; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
| 	 * Converts the provided object to a {@link String}. | 	 * Converts the provided object to a {@link String}. | ||||||
| 	 * @param o the object to convert. | 	 * @param o the object to convert. | ||||||
| 	 * @return a the object converted to a {@link String}. | 	 * @return the object converted to a {@link String}. | ||||||
| 	 * @throws ConvertionException is a conversion error occurs. | 	 * @throws ConversionException is a conversion error occurs. | ||||||
| 	 */ | 	 */ | ||||||
| 	public static String toString(Object o) { | 	public static String toString(Object o) { | ||||||
| 		if (o == null) { | 		if (o == null) { | ||||||
| @@ -126,7 +126,7 @@ public class TypeConverter { | |||||||
| 			try { | 			try { | ||||||
| 				return ((JsonElement)o).getAsString(); | 				return ((JsonElement)o).getAsString(); | ||||||
| 			} catch(UnsupportedOperationException e) { | 			} catch(UnsupportedOperationException e) { | ||||||
| 				throw new ConvertionException(e); | 				throw new ConversionException(e); | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		 | 		 | ||||||
| @@ -134,7 +134,7 @@ public class TypeConverter { | |||||||
| 			return o.toString(); | 			return o.toString(); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		throw new ConvertionException("No string convertion available for an instance of "+o.getClass()); | 		throw new ConversionException("No string conversion available for an instance of "+o.getClass()); | ||||||
| 		 | 		 | ||||||
| 	} | 	} | ||||||
| 	 | 	 | ||||||
| @@ -144,8 +144,8 @@ public class TypeConverter { | |||||||
| 	 * @param mapIntKeys if the String key representing an int should be duplicated as integer type, | 	 * @param mapIntKeys if the String key representing an int should be duplicated as integer type, | ||||||
| 	 * which map to the same value as the original String key. For example, if a key is "12" and map | 	 * which map to the same value as the original String key. For example, if a key is "12" and map | ||||||
| 	 * to the object <i>o</i>, an integer key 12 will be added and map to the same object <i>o</i>. | 	 * to the object <i>o</i>, an integer key 12 will be added and map to the same object <i>o</i>. | ||||||
| 	 * @return a the object converted to a {@link Map}. | 	 * @return the object converted to a {@link Map}. | ||||||
| 	 * @throws ConvertionException is a conversion error occurs. | 	 * @throws ConversionException is a conversion error occurs. | ||||||
| 	 */ | 	 */ | ||||||
| 	@SuppressWarnings("unchecked") | 	@SuppressWarnings("unchecked") | ||||||
| 	public static Map<Object, Object> toMap(Object o, boolean mapIntKeys) { | 	public static Map<Object, Object> toMap(Object o, boolean mapIntKeys) { | ||||||
| @@ -186,15 +186,15 @@ public class TypeConverter { | |||||||
| 			return map; | 			return map; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		throw new ConvertionException("No Map convertion available for an instance of "+o.getClass()); | 		throw new ConversionException("No Map conversion available for an instance of "+o.getClass()); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
| 	 * Converts the provided object to a {@link List}. | 	 * Converts the provided object to a {@link List}. | ||||||
| 	 * @param o the object to convert. | 	 * @param o the object to convert. | ||||||
| 	 * @return a the object converted to a {@link List}. | 	 * @return the object converted to a {@link List}. | ||||||
| 	 * @throws ConvertionException is a conversion error occurs. | 	 * @throws ConversionException is a conversion error occurs. | ||||||
| 	 */ | 	 */ | ||||||
| 	@SuppressWarnings("unchecked") | 	@SuppressWarnings("unchecked") | ||||||
| 	public static List<Object> toList(Object o) { | 	public static List<Object> toList(Object o) { | ||||||
| @@ -217,7 +217,7 @@ public class TypeConverter { | |||||||
| 		} | 		} | ||||||
| 		 | 		 | ||||||
|  |  | ||||||
| 		throw new ConvertionException("No Map convertion available for an instance of "+o.getClass()); | 		throw new ConversionException("No Map conversion available for an instance of "+o.getClass()); | ||||||
| 		 | 		 | ||||||
| 		 | 		 | ||||||
| 		 | 		 | ||||||
| @@ -225,17 +225,20 @@ public class TypeConverter { | |||||||
|  |  | ||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
| 	 * Thrown when a convertion error occurs. | 	 * Thrown when a conversion error occurs. | ||||||
| 	 */ | 	 */ | ||||||
| 	public static class ConvertionException extends RuntimeException { | 	public static class ConversionException extends RuntimeException { | ||||||
| 		 | 		 | ||||||
| 		private ConvertionException(String m) { | 		private ConversionException(String m) { | ||||||
| 			super(m); | 			super(m); | ||||||
| 		} | 		} | ||||||
| 		private ConvertionException(Throwable t) { | 		private ConversionException(Throwable t) { | ||||||
| 			super(t); | 			super(t); | ||||||
| 		} | 		} | ||||||
| 		 | 		 | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 	private TypeConverter() {} | ||||||
| 	 | 	 | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,85 @@ | |||||||
|  | package fr.pandacube.lib.core.mc_version; | ||||||
|  |  | ||||||
|  | import com.google.gson.JsonDeserializationContext; | ||||||
|  | import com.google.gson.JsonDeserializer; | ||||||
|  | import com.google.gson.JsonElement; | ||||||
|  | import com.google.gson.JsonObject; | ||||||
|  | import com.google.gson.JsonParseException; | ||||||
|  | import com.google.gson.JsonSerializationContext; | ||||||
|  | import com.google.gson.JsonSerializer; | ||||||
|  | import com.google.gson.TypeAdapterFactory; | ||||||
|  | import com.google.gson.internal.bind.TreeTypeAdapter; | ||||||
|  | import com.google.gson.reflect.TypeToken; | ||||||
|  |  | ||||||
|  | import java.lang.reflect.Type; | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.Map; | ||||||
|  | import java.util.TreeMap; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Record holding the data for {@link ProtocolVersion}, to facilitate serializing and deserializing. | ||||||
|  |  * @param protocolOfVersion mapping from a version string to the corresponding protocol version number. | ||||||
|  |  * @param versionsOfProtocol mapping from a protocol version number to a list of the supported MC versions. | ||||||
|  |  */ | ||||||
|  | public record MinecraftVersionList( | ||||||
|  |         Map<String, Integer> protocolOfVersion, | ||||||
|  |         Map<Integer, List<String>> versionsOfProtocol | ||||||
|  | ) { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Creates an empty {@link MinecraftVersionList}. | ||||||
|  |      */ | ||||||
|  |     public MinecraftVersionList() { | ||||||
|  |         this(new TreeMap<>(MinecraftVersionUtil::compareVersions), new TreeMap<>()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Adds a new pair of version string and protocol version number. | ||||||
|  |      * @param versionId the version string (e.g. "1.19.4"). | ||||||
|  |      * @param protocolVersion the protocol version number. | ||||||
|  |      */ | ||||||
|  |     public void add(String versionId, int protocolVersion) { | ||||||
|  |         protocolOfVersion.put(versionId, protocolVersion); | ||||||
|  |         List<String> versions = versionsOfProtocol.computeIfAbsent(protocolVersion, p -> new ArrayList<>()); | ||||||
|  |         versions.add(versionId); | ||||||
|  |         versions.sort(MinecraftVersionUtil::compareVersions); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Gson Adapter that ensure the data in {@link MinecraftVersionList} is sorted correctly when deserializing. | ||||||
|  |      */ | ||||||
|  |     public static class MinecraftVersionListAdapter implements JsonSerializer<MinecraftVersionList>, JsonDeserializer<MinecraftVersionList> { | ||||||
|  |         /** | ||||||
|  |          * Gson adapter factory for {@link MinecraftVersionList}. | ||||||
|  |          */ | ||||||
|  |         public static final TypeAdapterFactory FACTORY = TreeTypeAdapter.newTypeHierarchyFactory(MinecraftVersionList.class, new MinecraftVersionListAdapter()); | ||||||
|  |  | ||||||
|  |         private static final TypeToken<Map<String, Integer>> MAP_STR_INT_TYPE = new TypeToken<>() { }; | ||||||
|  |         private static final TypeToken<Map<Integer, List<String>>> MAP_INT_LIST_STRING_TYPE = new TypeToken<>() { }; | ||||||
|  |  | ||||||
|  |         private MinecraftVersionListAdapter() {} | ||||||
|  |  | ||||||
|  |         @Override | ||||||
|  |         public MinecraftVersionList deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { | ||||||
|  |             if (!(json instanceof JsonObject jsonObj)) | ||||||
|  |                 throw new JsonParseException("Expected JsonObject, got " + json.getClass().getSimpleName() + "."); | ||||||
|  |             MinecraftVersionList mvList = new MinecraftVersionList(); | ||||||
|  |             mvList.protocolOfVersion.putAll(context.deserialize(jsonObj.get("protocolOfVersion"), MAP_STR_INT_TYPE.getType())); | ||||||
|  |             mvList.versionsOfProtocol.putAll(context.deserialize(jsonObj.get("versionsOfProtocol"), MAP_INT_LIST_STRING_TYPE.getType())); | ||||||
|  |             for (List<String> versionLists : mvList.versionsOfProtocol.values()) { | ||||||
|  |                 versionLists.sort(MinecraftVersionUtil::compareVersions); | ||||||
|  |             } | ||||||
|  |             return mvList; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         @Override | ||||||
|  |         public JsonElement serialize(MinecraftVersionList src, Type typeOfSrc, JsonSerializationContext context) { | ||||||
|  |             JsonObject obj = new JsonObject(); | ||||||
|  |             obj.add("protocolOfVersion", context.serialize(src.protocolOfVersion)); | ||||||
|  |             obj.add("versionsOfProtocol", context.serialize(src.versionsOfProtocol)); | ||||||
|  |             return obj; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,141 @@ | |||||||
|  | package fr.pandacube.lib.core.mc_version; | ||||||
|  |  | ||||||
|  | import fr.pandacube.lib.util.StringUtil; | ||||||
|  |  | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.Arrays; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.Set; | ||||||
|  | import java.util.TreeSet; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Utility class to manipulate {@link String}s representing Minecraft versions. | ||||||
|  |  */ | ||||||
|  | public class MinecraftVersionUtil { | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Compare two Minecraft version strings. It uses the rules of semantic | ||||||
|  |      * versioning to compare the versions. | ||||||
|  |      * @param v1 the first version to compare. | ||||||
|  |      * @param v2 the second version to compare. | ||||||
|  |      * @return 0 if they are equal, <0 if v1<v2 and vice-versa. | ||||||
|  |      */ | ||||||
|  |     public static int compareVersions(String v1, String v2) { | ||||||
|  |         int[] v1Int = decomposedVersion(v1); | ||||||
|  |         int[] v2Int = decomposedVersion(v2); | ||||||
|  |  | ||||||
|  |         for (int i = 0; i < Math.min(v1Int.length, v2Int.length); i++) { | ||||||
|  |             int cmp = Integer.compare(v1Int[i], v2Int[i]); | ||||||
|  |             if (cmp != 0) | ||||||
|  |                 return cmp; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return Integer.compare(v1Int.length, v2Int.length); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Decompose a version string into a series of integers. | ||||||
|  |      * @param v a string representation of a version (eg. 1.19.1). | ||||||
|  |      * @return an array of int representing the provided version (eg. [1, 19, 1]). | ||||||
|  |      */ | ||||||
|  |     public static int[] decomposedVersion(String v) { | ||||||
|  |         try { | ||||||
|  |             return Arrays.stream(v.split("\\.")).mapToInt(Integer::parseInt).toArray(); | ||||||
|  |         } catch (NumberFormatException e) { | ||||||
|  |             throw new IllegalArgumentException("Invalid version format: '" + v + "'.", e); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Tells if the two provided Minecraft versions are consecutive. | ||||||
|  |      * <p> | ||||||
|  |      * Two versions are consecutive if (considering {@code 1.X[.Y]}): | ||||||
|  |      * <ul> | ||||||
|  |      *     <li>They are part of the same main version (X value)</li> | ||||||
|  |      *     <li>v1 has no Y value, and v2 has Y = 1 (eg. 1.19 and 1.19.1) OR | ||||||
|  |      *         both v1 and v2 has a Y value and those values are consecutive. | ||||||
|  |      *     </li> | ||||||
|  |      * </ul> | ||||||
|  |      * @param v1 the first version. | ||||||
|  |      * @param v2 the second version. | ||||||
|  |      * @return thue if the second version is consecutive to the first one. | ||||||
|  |      */ | ||||||
|  |     public static boolean areConsecutive(String v1, String v2) { | ||||||
|  |         int[] v1Int = decomposedVersion(v1); | ||||||
|  |         int[] v2Int = decomposedVersion(v2); | ||||||
|  |  | ||||||
|  |         if (v1Int.length == v2Int.length) { | ||||||
|  |             for (int i = 0; i < v1Int.length - 1; i++) { | ||||||
|  |                 if (v1Int[i] != v2Int[i]) | ||||||
|  |                     return false; | ||||||
|  |             } | ||||||
|  |             return v1Int[v1Int.length - 1] + 1 == v2Int[v2Int.length - 1]; | ||||||
|  |         } | ||||||
|  |         else if (v1Int.length == v2Int.length - 1) { | ||||||
|  |             for (int i = 0; i < v1Int.length; i++) { | ||||||
|  |                 if (v1Int[i] != v2Int[i]) | ||||||
|  |                     return false; | ||||||
|  |             } | ||||||
|  |             return v2Int[v2Int.length - 1] == 1; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Generate a string representation of the provided list of version, with | ||||||
|  |      * merged consecutive versions and using | ||||||
|  |      * {@link StringUtil#joinGrammatically(CharSequence, CharSequence, List)}. | ||||||
|  |      * | ||||||
|  |      * @param versions the minecraft versions list to use. | ||||||
|  |      * @param finalWordSeparator the word separator between the two last versions in the returned string, like "and", | ||||||
|  |      *                           "or" or any other word of any language. The spaces before and after are already | ||||||
|  |      *                           concatenated. | ||||||
|  |      * @return a string representation of the provided list of version. | ||||||
|  |      */ | ||||||
|  |     public static String toString(List<String> versions, String finalWordSeparator) { | ||||||
|  |         if (versions.isEmpty()) | ||||||
|  |             return ""; | ||||||
|  |         // put them in order and remove duplicates | ||||||
|  |         versions = new ArrayList<>(toOrderedSet(versions)); | ||||||
|  |         List<String> keptVersions = new ArrayList<>(versions.size()); | ||||||
|  |  | ||||||
|  |         for (int i = 0, firstConsecutive = 0; i < versions.size(); i++) { | ||||||
|  |             if (i == versions.size() - 1 || !areConsecutive(versions.get(i), versions.get(i + 1))) { | ||||||
|  |                 if (firstConsecutive == i) { | ||||||
|  |                     keptVersions.add(versions.get(i)); | ||||||
|  |                     firstConsecutive++; | ||||||
|  |                 } | ||||||
|  |                 else { | ||||||
|  |                     // merge | ||||||
|  |                     if (i - firstConsecutive > 1) | ||||||
|  |                         keptVersions.add(versions.get(firstConsecutive) + "-" + versions.get(i)); | ||||||
|  |                     else { | ||||||
|  |                         keptVersions.add(versions.get(firstConsecutive)); | ||||||
|  |                         keptVersions.add(versions.get(i)); | ||||||
|  |                     } | ||||||
|  |                     firstConsecutive = i + 1; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return StringUtil.joinGrammatically(", ", " " + finalWordSeparator + " ", keptVersions); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     private static Set<String> toOrderedSet(List<String> versions) { | ||||||
|  |         Set<String> set = new TreeSet<>(MinecraftVersionUtil::compareVersions); | ||||||
|  |         set.addAll(versions); | ||||||
|  |         return set; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     private MinecraftVersionUtil() {} | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -0,0 +1,249 @@ | |||||||
|  | package fr.pandacube.lib.core.mc_version; | ||||||
|  |  | ||||||
|  | import fr.pandacube.lib.core.json.Json; | ||||||
|  | import fr.pandacube.lib.util.log.Log; | ||||||
|  | import fr.pandacube.lib.util.StringUtil; | ||||||
|  | import org.jetbrains.annotations.NotNull; | ||||||
|  |  | ||||||
|  | import java.io.InputStream; | ||||||
|  | import java.io.InputStreamReader; | ||||||
|  | import java.net.URI; | ||||||
|  | import java.net.http.HttpClient; | ||||||
|  | import java.net.http.HttpRequest; | ||||||
|  | import java.net.http.HttpResponse; | ||||||
|  | import java.net.http.HttpResponse.BodyHandlers; | ||||||
|  | import java.time.Duration; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.concurrent.atomic.AtomicReference; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Class handling a relationship table of known Minecraft version and their | ||||||
|  |  * corresponding protocol version. | ||||||
|  |  * <p> | ||||||
|  |  * The data if fetch updated data from an external API on startup. If it fails, | ||||||
|  |  * it uses the data stored in the current package at build time. | ||||||
|  |  * <p> | ||||||
|  |  * The public static methods are used to fetch an instance of {@link ProtocolVersion} | ||||||
|  |  * based on the provided protocol version (e.g. 763) or Minecraft version (e.g. "1.20.1"). | ||||||
|  |  * An instance of this class provides information related to a protocol version | ||||||
|  |  * (the protocol version number and all the corresponding Minecraft versions). | ||||||
|  |  */ | ||||||
|  | public class ProtocolVersion implements Comparable<ProtocolVersion> { | ||||||
|  |  | ||||||
|  |     private static final String ONLINE_DATA_URL = "https://api.pandacube.fr/rest/mcversion"; | ||||||
|  |  | ||||||
|  |     private static final AtomicReference<MinecraftVersionList> versionList = new AtomicReference<>(); | ||||||
|  |  | ||||||
|  |     private static void initIfNecessary() { | ||||||
|  |         synchronized (versionList) { | ||||||
|  |             if (versionList.get() == null) { | ||||||
|  |                 init(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Replace the currently used data cache by a new source. | ||||||
|  |      * <p> | ||||||
|  |      * <b>Note: </b>this method is not meant to be used by the final user of | ||||||
|  |      * this class. Use it only if you have a better data source. | ||||||
|  |      * @param data the data to use instead of the provided (external API or packaged file) | ||||||
|  |      */ | ||||||
|  |     public static void setRawData(MinecraftVersionList data) { | ||||||
|  |         versionList.set(data); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Gets the raw data used internally by this class. | ||||||
|  |      * <p> | ||||||
|  |      * <b>Note: </b>this method is not meant to be used by the final user of | ||||||
|  |      * this class. Use it only if you know what you do. | ||||||
|  |      * @return the current instance of {@link MinecraftVersionUtil} uses | ||||||
|  |      *         internally by this class. | ||||||
|  |      */ | ||||||
|  |     public static MinecraftVersionList getRawData() { | ||||||
|  |         initIfNecessary(); | ||||||
|  |         return versionList.get(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private static void init() { | ||||||
|  |         // try online source first | ||||||
|  |         try { | ||||||
|  |             HttpResponse<String> response = HttpClient.newBuilder() | ||||||
|  |                     .connectTimeout(Duration.ofSeconds(5)) | ||||||
|  |                     .build() | ||||||
|  |                     .send(HttpRequest.newBuilder(URI.create(ONLINE_DATA_URL)).build(), | ||||||
|  |                             BodyHandlers.ofString() | ||||||
|  |                     ); | ||||||
|  |             if (response.statusCode() == 200) { | ||||||
|  |                 MinecraftVersionList data = Json.gson.fromJson(response.body(), MinecraftVersionList.class); | ||||||
|  |                 versionList.set(data); | ||||||
|  |             } | ||||||
|  |         } catch (Exception e) { | ||||||
|  |             Log.warning(e); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (versionList.get() != null) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         Log.warning("Unable to get minecraft version data from API. Using local data instead."); | ||||||
|  |         // try local source | ||||||
|  |         try (InputStream is = ProtocolVersion.class.getResourceAsStream("mcversion.json")) { | ||||||
|  |             if (is != null) { | ||||||
|  |                 try (InputStreamReader isr = new InputStreamReader(is)) { | ||||||
|  |                     MinecraftVersionList data = Json.gson.fromJson(isr, MinecraftVersionList.class); | ||||||
|  |                     versionList.set(data); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } catch (Exception e) { | ||||||
|  |             Log.warning(e); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (versionList.get() != null) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         Log.severe("Unable to get Minecraft versions data from classpath. Using empty data instead."); | ||||||
|  |  | ||||||
|  |         versionList.set(new MinecraftVersionList()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     private static int getPVNOfVersion(String version) { | ||||||
|  |         initIfNecessary(); | ||||||
|  |         Integer v = versionList.get().protocolOfVersion().get(version); | ||||||
|  |         return v == null ? -1 : v; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private static List<String> getVersionsOfPVN(int pvn) { | ||||||
|  |         initIfNecessary(); | ||||||
|  |         return versionList.get().versionsOfProtocol().get(pvn); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Gets the {@link ProtocolVersion} associated with the provided Minecraft version. | ||||||
|  |      * @param version The Minecraft version, in the format "X.X[.X]" (eg. "1.17" or "1.8.8"). | ||||||
|  |      * @return an instance of {@link ProtocolVersion}. | ||||||
|  |      */ | ||||||
|  |     public static ProtocolVersion ofVersion(String version) { | ||||||
|  |         int pvn = getPVNOfVersion(version); | ||||||
|  |         if (pvn == -1) | ||||||
|  |             return null; | ||||||
|  |         List<String> versions = getVersionsOfPVN(pvn); | ||||||
|  |         if (versions == null) { | ||||||
|  |             versions = List.of(version); | ||||||
|  |         } | ||||||
|  |         return new ProtocolVersion(pvn, List.copyOf(versions)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Gets the {@link ProtocolVersion} associated with the provided protocol version number. | ||||||
|  |      * @param pvn The protocol version number. | ||||||
|  |      * @return an instance of {@link ProtocolVersion}. | ||||||
|  |      */ | ||||||
|  |     public static ProtocolVersion ofProtocol(int pvn) { | ||||||
|  |         List<String> versions = getVersionsOfPVN(pvn); | ||||||
|  |         if (versions == null) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |         return new ProtocolVersion(pvn, List.copyOf(versions)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns all the {@link ProtocolVersion} currently known by this class. | ||||||
|  |      * @return all the {@link ProtocolVersion} currently known by this class. | ||||||
|  |      */ | ||||||
|  |     public static List<ProtocolVersion> allKnownProtocolVersions() { | ||||||
|  |         return versionList.get().versionsOfProtocol().entrySet().stream() | ||||||
|  |                 .filter(e -> e.getValue() != null && !e.getValue().isEmpty()) | ||||||
|  |                 .map(e -> new ProtocolVersion(e.getKey(), List.copyOf(e.getValue()))) | ||||||
|  |                 .toList(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Generate a string representation of the provided list of version, using | ||||||
|  |      * {@link StringUtil#joinGrammatically(CharSequence, CharSequence, List)}. | ||||||
|  |      * | ||||||
|  |      * @param versions the minecraft versions to list | ||||||
|  |      * @param finalWordSeparator the word separator between the two last versions in the returned string, like "and", | ||||||
|  |      *                           "or" or any other word of any language. The spaces before and after are already | ||||||
|  |      *                           concatenated. | ||||||
|  |      * @return a string representation of the provided list of version. | ||||||
|  |      */ | ||||||
|  |     public static String displayOptimizedListOfVersions(List<ProtocolVersion> versions, String finalWordSeparator) { | ||||||
|  |         return MinecraftVersionUtil.toString(versions.stream().flatMap(pv -> pv.versions.stream()).toList(), finalWordSeparator); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The protocol version number. | ||||||
|  |      */ | ||||||
|  |     public final int protocolVersionNumber; | ||||||
|  |     /** | ||||||
|  |      * All Minecraft version supported by this protocol version number. | ||||||
|  |      */ | ||||||
|  |     public final List<String> versions; | ||||||
|  |  | ||||||
|  |     private ProtocolVersion(int protocolVersionNumber, List<String> versions) { | ||||||
|  |         this.protocolVersionNumber = protocolVersionNumber; | ||||||
|  |         this.versions = versions; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public String toString() { | ||||||
|  |         return "ProtocolVersion{protocol=" + protocolVersionNumber + ", toString(\"and\")=" + toString("and") + "}"; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns a string representation of all the Minecraft version of this enum value, using | ||||||
|  |      * {@link StringUtil#joinGrammatically(CharSequence, CharSequence, List)}. | ||||||
|  |      * | ||||||
|  |      * @param finalWordSeparator the word separator between the two last versions in the returned string, like "and", | ||||||
|  |      *                           "or" or any other word of any language. The spaces before and after are already | ||||||
|  |      *                           concatenated. | ||||||
|  |      * @return a string representation of this {@link ProtocolVersion}. | ||||||
|  |      */ | ||||||
|  |     public String toString(String finalWordSeparator) { | ||||||
|  |         return displayOptimizedListOfVersions(List.of(this), finalWordSeparator); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Gets the first (earliest) Minecraft version that supports this protocol version. | ||||||
|  |      * @return the first (earliest) Minecraft version that supports this protocol version. | ||||||
|  |      */ | ||||||
|  |     public String getFirstVersion() { | ||||||
|  |         return versions.get(0); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Gets the last (latest) Minecraft version that supports this protocol version. | ||||||
|  |      * @return the last (latest) Minecraft version that supports this protocol version. | ||||||
|  |      */ | ||||||
|  |     public String getLastVersion() { | ||||||
|  |         return versions.get(versions.size() - 1); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public boolean equals(Object o) { | ||||||
|  |         return o instanceof ProtocolVersion pv && protocolVersionNumber == pv.protocolVersionNumber; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public int hashCode() { | ||||||
|  |         return protocolVersionNumber; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public int compareTo(@NotNull ProtocolVersion o) { | ||||||
|  |         return Integer.compare(protocolVersionNumber, o.protocolVersionNumber); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -2,7 +2,7 @@ package fr.pandacube.lib.core.search; | |||||||
|  |  | ||||||
| import com.google.common.cache.Cache; | import com.google.common.cache.Cache; | ||||||
| import com.google.common.cache.CacheBuilder; | 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.ArrayList; | ||||||
| import java.util.HashMap; | import java.util.HashMap; | ||||||
| @@ -15,7 +15,7 @@ import java.util.concurrent.ExecutionException; | |||||||
| import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Utility class to manage searching among a set of {@link SearchResult} instances, using case insensitive keywords. |  * Utility class to manage searching among a set of {@link SearchResult} instances, using case-insensitive keywords. | ||||||
|  * The search engine is responsible for storing a database of entries ({@link SearchResult}) that can be searched using |  * The search engine is responsible for storing a database of entries ({@link SearchResult}) that can be searched using | ||||||
|  * keywords. This class provides methods to returns a list of results for provided keywords, a list of keyword |  * keywords. This class provides methods to returns a list of results for provided keywords, a list of keyword | ||||||
|  * suggestions based on pre-typed keywords. |  * suggestions based on pre-typed keywords. | ||||||
|   | |||||||
| @@ -0,0 +1,238 @@ | |||||||
|  | { | ||||||
|  |   "protocolOfVersion": { | ||||||
|  |     "1.7.2": 4, | ||||||
|  |     "1.7.3": 4, | ||||||
|  |     "1.7.4": 4, | ||||||
|  |     "1.7.5": 4, | ||||||
|  |     "1.7.6": 5, | ||||||
|  |     "1.7.7": 5, | ||||||
|  |     "1.7.8": 5, | ||||||
|  |     "1.7.9": 5, | ||||||
|  |     "1.7.10": 5, | ||||||
|  |     "1.8": 47, | ||||||
|  |     "1.8.1": 47, | ||||||
|  |     "1.8.2": 47, | ||||||
|  |     "1.8.3": 47, | ||||||
|  |     "1.8.4": 47, | ||||||
|  |     "1.8.5": 47, | ||||||
|  |     "1.8.6": 47, | ||||||
|  |     "1.8.7": 47, | ||||||
|  |     "1.8.8": 47, | ||||||
|  |     "1.8.9": 47, | ||||||
|  |     "1.9": 107, | ||||||
|  |     "1.9.1": 108, | ||||||
|  |     "1.9.2": 109, | ||||||
|  |     "1.9.3": 110, | ||||||
|  |     "1.9.4": 110, | ||||||
|  |     "1.10": 210, | ||||||
|  |     "1.10.1": 210, | ||||||
|  |     "1.10.2": 210, | ||||||
|  |     "1.11": 315, | ||||||
|  |     "1.11.1": 316, | ||||||
|  |     "1.11.2": 316, | ||||||
|  |     "1.12": 335, | ||||||
|  |     "1.12.1": 338, | ||||||
|  |     "1.12.2": 340, | ||||||
|  |     "1.13": 393, | ||||||
|  |     "1.13.1": 401, | ||||||
|  |     "1.13.2": 404, | ||||||
|  |     "1.14": 477, | ||||||
|  |     "1.14.1": 480, | ||||||
|  |     "1.14.2": 485, | ||||||
|  |     "1.14.3": 490, | ||||||
|  |     "1.14.4": 498, | ||||||
|  |     "1.15": 573, | ||||||
|  |     "1.15.1": 575, | ||||||
|  |     "1.15.2": 578, | ||||||
|  |     "1.16": 735, | ||||||
|  |     "1.16.1": 736, | ||||||
|  |     "1.16.2": 751, | ||||||
|  |     "1.16.3": 753, | ||||||
|  |     "1.16.4": 754, | ||||||
|  |     "1.16.5": 754, | ||||||
|  |     "1.17": 755, | ||||||
|  |     "1.17.1": 756, | ||||||
|  |     "1.18": 757, | ||||||
|  |     "1.18.1": 757, | ||||||
|  |     "1.18.2": 758, | ||||||
|  |     "1.19": 759, | ||||||
|  |     "1.19.1": 760, | ||||||
|  |     "1.19.2": 760, | ||||||
|  |     "1.19.3": 761, | ||||||
|  |     "1.19.4": 762, | ||||||
|  |     "1.20": 763, | ||||||
|  |     "1.20.1": 763, | ||||||
|  |     "1.20.2": 764, | ||||||
|  |     "1.20.3": 765, | ||||||
|  |     "1.20.4": 765, | ||||||
|  |     "1.20.5": 766, | ||||||
|  |     "1.20.6": 766, | ||||||
|  |     "1.21": 767, | ||||||
|  |     "1.21.1": 767, | ||||||
|  |     "1.21.2": 768, | ||||||
|  |     "1.21.3": 768, | ||||||
|  |     "1.21.4": 769 | ||||||
|  |   }, | ||||||
|  |   "versionsOfProtocol": { | ||||||
|  |     "4": [ | ||||||
|  |       "1.7.2", | ||||||
|  |       "1.7.3", | ||||||
|  |       "1.7.4", | ||||||
|  |       "1.7.5" | ||||||
|  |     ], | ||||||
|  |     "5": [ | ||||||
|  |       "1.7.6", | ||||||
|  |       "1.7.7", | ||||||
|  |       "1.7.8", | ||||||
|  |       "1.7.9", | ||||||
|  |       "1.7.10" | ||||||
|  |     ], | ||||||
|  |     "47": [ | ||||||
|  |       "1.8", | ||||||
|  |       "1.8.1", | ||||||
|  |       "1.8.2", | ||||||
|  |       "1.8.3", | ||||||
|  |       "1.8.4", | ||||||
|  |       "1.8.5", | ||||||
|  |       "1.8.6", | ||||||
|  |       "1.8.7", | ||||||
|  |       "1.8.8", | ||||||
|  |       "1.8.9" | ||||||
|  |     ], | ||||||
|  |     "107": [ | ||||||
|  |       "1.9" | ||||||
|  |     ], | ||||||
|  |     "108": [ | ||||||
|  |       "1.9.1" | ||||||
|  |     ], | ||||||
|  |     "109": [ | ||||||
|  |       "1.9.2" | ||||||
|  |     ], | ||||||
|  |     "110": [ | ||||||
|  |       "1.9.3", | ||||||
|  |       "1.9.4" | ||||||
|  |     ], | ||||||
|  |     "210": [ | ||||||
|  |       "1.10", | ||||||
|  |       "1.10.1", | ||||||
|  |       "1.10.2" | ||||||
|  |     ], | ||||||
|  |     "315": [ | ||||||
|  |       "1.11" | ||||||
|  |     ], | ||||||
|  |     "316": [ | ||||||
|  |       "1.11.1", | ||||||
|  |       "1.11.2" | ||||||
|  |     ], | ||||||
|  |     "335": [ | ||||||
|  |       "1.12" | ||||||
|  |     ], | ||||||
|  |     "338": [ | ||||||
|  |       "1.12.1" | ||||||
|  |     ], | ||||||
|  |     "340": [ | ||||||
|  |       "1.12.2" | ||||||
|  |     ], | ||||||
|  |     "393": [ | ||||||
|  |       "1.13" | ||||||
|  |     ], | ||||||
|  |     "401": [ | ||||||
|  |       "1.13.1" | ||||||
|  |     ], | ||||||
|  |     "404": [ | ||||||
|  |       "1.13.2" | ||||||
|  |     ], | ||||||
|  |     "477": [ | ||||||
|  |       "1.14" | ||||||
|  |     ], | ||||||
|  |     "480": [ | ||||||
|  |       "1.14.1" | ||||||
|  |     ], | ||||||
|  |     "485": [ | ||||||
|  |       "1.14.2" | ||||||
|  |     ], | ||||||
|  |     "490": [ | ||||||
|  |       "1.14.3" | ||||||
|  |     ], | ||||||
|  |     "498": [ | ||||||
|  |       "1.14.4" | ||||||
|  |     ], | ||||||
|  |     "573": [ | ||||||
|  |       "1.15" | ||||||
|  |     ], | ||||||
|  |     "575": [ | ||||||
|  |       "1.15.1" | ||||||
|  |     ], | ||||||
|  |     "578": [ | ||||||
|  |       "1.15.2" | ||||||
|  |     ], | ||||||
|  |     "735": [ | ||||||
|  |       "1.16" | ||||||
|  |     ], | ||||||
|  |     "736": [ | ||||||
|  |       "1.16.1" | ||||||
|  |     ], | ||||||
|  |     "751": [ | ||||||
|  |       "1.16.2" | ||||||
|  |     ], | ||||||
|  |     "753": [ | ||||||
|  |       "1.16.3" | ||||||
|  |     ], | ||||||
|  |     "754": [ | ||||||
|  |       "1.16.4", | ||||||
|  |       "1.16.5" | ||||||
|  |     ], | ||||||
|  |     "755": [ | ||||||
|  |       "1.17" | ||||||
|  |     ], | ||||||
|  |     "756": [ | ||||||
|  |       "1.17.1" | ||||||
|  |     ], | ||||||
|  |     "757": [ | ||||||
|  |       "1.18", | ||||||
|  |       "1.18.1" | ||||||
|  |     ], | ||||||
|  |     "758": [ | ||||||
|  |       "1.18.2" | ||||||
|  |     ], | ||||||
|  |     "759": [ | ||||||
|  |       "1.19" | ||||||
|  |     ], | ||||||
|  |     "760": [ | ||||||
|  |       "1.19.1", | ||||||
|  |       "1.19.2" | ||||||
|  |     ], | ||||||
|  |     "761": [ | ||||||
|  |       "1.19.3" | ||||||
|  |     ], | ||||||
|  |     "762": [ | ||||||
|  |       "1.19.4" | ||||||
|  |     ], | ||||||
|  |     "763": [ | ||||||
|  |       "1.20", | ||||||
|  |       "1.20.1" | ||||||
|  |     ], | ||||||
|  |     "764": [ | ||||||
|  |       "1.20.2" | ||||||
|  |     ], | ||||||
|  |     "765": [ | ||||||
|  |       "1.20.3", | ||||||
|  |       "1.20.4" | ||||||
|  |     ], | ||||||
|  |     "766": [ | ||||||
|  |       "1.20.5", | ||||||
|  |       "1.20.6" | ||||||
|  |     ], | ||||||
|  |     "767": [ | ||||||
|  |       "1.21", | ||||||
|  |       "1.21.1" | ||||||
|  |     ], | ||||||
|  |     "768": [ | ||||||
|  |       "1.21.2", | ||||||
|  |       "1.21.3" | ||||||
|  |     ], | ||||||
|  |     "769": [ | ||||||
|  |       "1.21.4" | ||||||
|  |     ] | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -27,7 +27,7 @@ | |||||||
|         <dependency> |         <dependency> | ||||||
|             <groupId>org.apache.commons</groupId> |             <groupId>org.apache.commons</groupId> | ||||||
|             <artifactId>commons-dbcp2</artifactId> |             <artifactId>commons-dbcp2</artifactId> | ||||||
|             <version>2.9.0</version> |             <version>2.12.0</version> | ||||||
|         </dependency> |         </dependency> | ||||||
|     </dependencies> |     </dependencies> | ||||||
|  |  | ||||||
| @@ -36,7 +36,7 @@ | |||||||
|             <plugin> |             <plugin> | ||||||
|                 <groupId>org.apache.maven.plugins</groupId> |                 <groupId>org.apache.maven.plugins</groupId> | ||||||
|                 <artifactId>maven-shade-plugin</artifactId> |                 <artifactId>maven-shade-plugin</artifactId> | ||||||
|                 <version>3.3.0</version> |                 <version>3.5.2</version> | ||||||
|                 <executions> |                 <executions> | ||||||
|                     <execution> |                     <execution> | ||||||
|                         <phase>package</phase> |                         <phase>package</phase> | ||||||
| @@ -56,29 +56,32 @@ | |||||||
|                                     <artifact>org.apache.commons:commons-dbcp2</artifact> |                                     <artifact>org.apache.commons:commons-dbcp2</artifact> | ||||||
|                                     <excludes> |                                     <excludes> | ||||||
|                                         <exclude>META-INF/MANIFEST.MF</exclude> |                                         <exclude>META-INF/MANIFEST.MF</exclude> | ||||||
|  |                                         <exclude>META-INF/versions/9/**</exclude> | ||||||
|                                     </excludes> |                                     </excludes> | ||||||
|                                 </filter> |                                 </filter> | ||||||
|                                 <filter> |                                 <filter> | ||||||
|                                     <artifact>org.apache.commons:commons-pool2</artifact> |                                     <artifact>org.apache.commons:commons-pool2</artifact> | ||||||
|                                     <excludes> |                                     <excludes> | ||||||
|                                         <exclude>META-INF/MANIFEST.MF</exclude> |                                         <exclude>META-INF/MANIFEST.MF</exclude> | ||||||
|  |                                         <exclude>META-INF/versions/9/**</exclude> | ||||||
|                                     </excludes> |                                     </excludes> | ||||||
|                                 </filter> |                                 </filter> | ||||||
|                                 <filter> |                                 <filter> | ||||||
|                                     <artifact>commons-logging:commons-logging</artifact> |                                     <artifact>commons-logging:commons-logging</artifact> | ||||||
|                                     <excludes> |                                     <excludes> | ||||||
|                                         <exclude>META-INF/MANIFEST.MF</exclude> |                                         <exclude>META-INF/MANIFEST.MF</exclude> | ||||||
|  |                                         <exclude>META-INF/versions/9/**</exclude> | ||||||
|                                     </excludes> |                                     </excludes> | ||||||
|                                 </filter> |                                 </filter> | ||||||
|                             </filters> |                             </filters> | ||||||
|                             <relocations> |                             <relocations> | ||||||
|                                 <relocation> |                                 <relocation> | ||||||
|                                     <pattern>org.apache.commons</pattern> |                                     <pattern>org.apache.commons.dbcp2</pattern> | ||||||
|                                     <shadedPattern>fr.pandacube.lib.db.shaded.commons</shadedPattern> |                                     <shadedPattern>fr.pandacube.lib.db.shaded.commons.dbcp2</shadedPattern> | ||||||
|                                 </relocation> |                                 </relocation> | ||||||
|                                 <relocation> |                                 <relocation> | ||||||
|                                     <pattern>org.apache.commons</pattern> |                                     <pattern>org.apache.commons.pool2</pattern> | ||||||
|                                     <shadedPattern>fr.pandacube.lib.db.shaded.commons</shadedPattern> |                                     <shadedPattern>fr.pandacube.lib.db.shaded.commons.pool2</shadedPattern> | ||||||
|                                 </relocation> |                                 </relocation> | ||||||
|                             </relocations> |                             </relocations> | ||||||
|                             <transformers> |                             <transformers> | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ import java.util.Objects; | |||||||
| import java.util.function.Consumer; | import java.util.function.Consumer; | ||||||
|  |  | ||||||
| import fr.pandacube.lib.reflect.Reflect; | 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. |  * Static class to handle most of the database operations. | ||||||
| @@ -52,10 +52,10 @@ public final class DB { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Initialialize the table represented by the provided class. |      * Initialize the table represented by the provided class. | ||||||
|      * @param elemClass the class representing a table. |      * @param elemClass the class representing a table. | ||||||
|      * @param <E> the type representing the table. |      * @param <E> the type representing the table. | ||||||
|      * @throws DBInitTableException if the table failed to initialized. |      * @throws DBInitTableException if the table failed to initialize. | ||||||
|      */ |      */ | ||||||
|     public static synchronized <E extends SQLElement<E>> void initTable(Class<E> elemClass) throws DBInitTableException { |     public static synchronized <E extends SQLElement<E>> void initTable(Class<E> elemClass) throws DBInitTableException { | ||||||
|         if (connection == null) { |         if (connection == null) { | ||||||
| @@ -111,7 +111,7 @@ public final class DB { | |||||||
|      * @param elemClass the class representing a table. |      * @param elemClass the class representing a table. | ||||||
|      * @return a table name. |      * @return a table name. | ||||||
|      * @param <E> the type representing the table. |      * @param <E> the type representing the table. | ||||||
|      * @throws DBInitTableException if the provided table had to be initialized and it failed to do so. |      * @throws DBInitTableException if the provided table had to be initialized and failed to do so. | ||||||
|      */ |      */ | ||||||
|     public static <E extends SQLElement<E>> String getTableName(Class<E> elemClass) throws DBInitTableException { |     public static <E extends SQLElement<E>> String getTableName(Class<E> elemClass) throws DBInitTableException { | ||||||
|         initTable(elemClass); |         initTable(elemClass); | ||||||
| @@ -130,7 +130,7 @@ public final class DB { | |||||||
|      * @param elemClass the class representing a table. |      * @param elemClass the class representing a table. | ||||||
|      * @return the {@code id} field of the provided table. |      * @return the {@code id} field of the provided table. | ||||||
|      * @param <E> the type representing the table. |      * @param <E> the type representing the table. | ||||||
|      * @throws DBInitTableException if the provided table had to be initialized and it failed to do so. |      * @throws DBInitTableException if the provided table had to be initialized and failed to do so. | ||||||
|      */ |      */ | ||||||
|     @SuppressWarnings("unchecked") |     @SuppressWarnings("unchecked") | ||||||
|     public static <E extends SQLElement<E>> SQLField<E, Integer> getSQLIdField(Class<E> elemClass) throws DBInitTableException { |     public static <E extends SQLElement<E>> SQLField<E, Integer> getSQLIdField(Class<E> elemClass) throws DBInitTableException { | ||||||
| @@ -226,8 +226,8 @@ public final class DB { | |||||||
|      * @throws DBException if an error occurs when interacting with the database. |      * @throws DBException if an error occurs when interacting with the database. | ||||||
|      */ |      */ | ||||||
|     public static <E extends SQLElement<E>> E getFirst(Class<E> elemClass, SQLWhere<E> where, SQLOrderBy<E> orderBy, Integer offset) throws DBException { |     public static <E extends SQLElement<E>> E getFirst(Class<E> elemClass, SQLWhere<E> where, SQLOrderBy<E> orderBy, Integer offset) throws DBException { | ||||||
|         SQLElementList<E> elts = getAll(elemClass, where, orderBy, 1, offset); |         SQLElementList<E> elements = getAll(elemClass, where, orderBy, 1, offset); | ||||||
|         return (elts.size() == 0) ? null : elts.get(0); |         return (elements.size() == 0) ? null : elements.get(0); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -294,15 +294,15 @@ public final class DB { | |||||||
|      * @throws DBException if an error occurs when interacting with the database. |      * @throws DBException if an error occurs when interacting with the database. | ||||||
|      */ |      */ | ||||||
|     public static <E extends SQLElement<E>> SQLElementList<E> getAll(Class<E> elemClass, SQLWhere<E> where, SQLOrderBy<E> orderBy, Integer limit, Integer offset) throws DBException { |     public static <E extends SQLElement<E>> SQLElementList<E> getAll(Class<E> elemClass, SQLWhere<E> where, SQLOrderBy<E> orderBy, Integer limit, Integer offset) throws DBException { | ||||||
|         SQLElementList<E> elmts = new SQLElementList<>(); |         SQLElementList<E> elements = new SQLElementList<>(); | ||||||
|         forEach(elemClass, where, orderBy, limit, offset, elmts::add); |         forEach(elemClass, where, orderBy, limit, offset, elements::add); | ||||||
|         return elmts; |         return elements; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Iterate through all the entries from the provided table. |      * Iterate through all the entries from the provided table. | ||||||
|      * @param elemClass the class representing a table. |      * @param elemClass the class representing a table. | ||||||
|      * @param action the action to perform on each entries. |      * @param action the action to perform on each entry. | ||||||
|      * @param <E> the type representing the table. |      * @param <E> the type representing the table. | ||||||
|      * @throws DBException if an error occurs when interacting with the database. |      * @throws DBException if an error occurs when interacting with the database. | ||||||
|      */ |      */ | ||||||
| @@ -314,7 +314,7 @@ public final class DB { | |||||||
|      * Iterate through the entries from the provided table, using the provided {@code WHERE} clause. |      * Iterate through the entries from the provided table, using the provided {@code WHERE} clause. | ||||||
|      * @param elemClass the class representing a table. |      * @param elemClass the class representing a table. | ||||||
|      * @param where the {@code WHERE} clause of the query. |      * @param where the {@code WHERE} clause of the query. | ||||||
|      * @param action the action to perform on each entries. |      * @param action the action to perform on each entry. | ||||||
|      * @param <E> the type representing the table. |      * @param <E> the type representing the table. | ||||||
|      * @throws DBException if an error occurs when interacting with the database. |      * @throws DBException if an error occurs when interacting with the database. | ||||||
|      */ |      */ | ||||||
| @@ -328,7 +328,7 @@ public final class DB { | |||||||
|      * @param elemClass the class representing a table. |      * @param elemClass the class representing a table. | ||||||
|      * @param where the {@code WHERE} clause of the query. |      * @param where the {@code WHERE} clause of the query. | ||||||
|      * @param orderBy the {@code ORDER BY} clause of the query. |      * @param orderBy the {@code ORDER BY} clause of the query. | ||||||
|      * @param action the action to perform on each entries. |      * @param action the action to perform on each entry. | ||||||
|      * @param <E> the type representing the table. |      * @param <E> the type representing the table. | ||||||
|      * @throws DBException if an error occurs when interacting with the database. |      * @throws DBException if an error occurs when interacting with the database. | ||||||
|      */ |      */ | ||||||
| @@ -343,7 +343,7 @@ public final class DB { | |||||||
|      * @param where the {@code WHERE} clause of the query. |      * @param where the {@code WHERE} clause of the query. | ||||||
|      * @param orderBy the {@code ORDER BY} clause of the query. |      * @param orderBy the {@code ORDER BY} clause of the query. | ||||||
|      * @param limit the {@code LIMIT} clause of the query. |      * @param limit the {@code LIMIT} clause of the query. | ||||||
|      * @param action the action to perform on each entries. |      * @param action the action to perform on each entry. | ||||||
|      * @param <E> the type representing the table. |      * @param <E> the type representing the table. | ||||||
|      * @throws DBException if an error occurs when interacting with the database. |      * @throws DBException if an error occurs when interacting with the database. | ||||||
|      */ |      */ | ||||||
| @@ -359,7 +359,7 @@ public final class DB { | |||||||
|      * @param orderBy the {@code ORDER BY} clause of the query. |      * @param orderBy the {@code ORDER BY} clause of the query. | ||||||
|      * @param limit the {@code LIMIT} clause of the query. |      * @param limit the {@code LIMIT} clause of the query. | ||||||
|      * @param offset the {@code OFFSET} clause of the query. |      * @param offset the {@code OFFSET} clause of the query. | ||||||
|      * @param action the action to perform on each entries. |      * @param action the action to perform on each entry. | ||||||
|      * @param <E> the type representing the table. |      * @param <E> the type representing the table. | ||||||
|      * @throws DBException if an error occurs when interacting with the database. |      * @throws DBException if an error occurs when interacting with the database. | ||||||
|      */ |      */ | ||||||
| @@ -577,7 +577,7 @@ public final class DB { | |||||||
|     @SuppressWarnings("unchecked") |     @SuppressWarnings("unchecked") | ||||||
|     private static <E extends SQLElement<E>> E getElementInstance(ResultSet set, Class<E> elemClass) throws DBException { |     private static <E extends SQLElement<E>> E getElementInstance(ResultSet set, Class<E> elemClass) throws DBException { | ||||||
|         try { |         try { | ||||||
|             E instance = Reflect.ofClass(elemClass).constructor(int.class).instanciate(set.getInt("id")); |             E instance = Reflect.ofClass(elemClass).constructor(int.class).instantiate(set.getInt("id")); | ||||||
|  |  | ||||||
|             int fieldCount = set.getMetaData().getColumnCount(); |             int fieldCount = set.getMetaData().getColumnCount(); | ||||||
|  |  | ||||||
| @@ -623,7 +623,7 @@ public final class DB { | |||||||
|  |  | ||||||
|             return instance; |             return instance; | ||||||
|         } catch (ReflectiveOperationException | IllegalArgumentException | SecurityException | SQLException e) { |         } catch (ReflectiveOperationException | IllegalArgumentException | SecurityException | SQLException e) { | ||||||
|             throw new DBException("Can't instanciate " + elemClass.getName(), e); |             throw new DBException("Can't instantiate " + elemClass.getName(), e); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -23,7 +23,8 @@ public class DBConnection { | |||||||
|     public DBConnection(String host, int port, String dbname, String login, String password) { |     public DBConnection(String host, int port, String dbname, String login, String password) { | ||||||
|         this("jdbc:mysql://" + host + ":" + port + "/" + dbname |         this("jdbc:mysql://" + host + ":" + port + "/" + dbname | ||||||
|                         + "?useUnicode=true" |                         + "?useUnicode=true" | ||||||
|                         + "&useSSL=false" |                         + "&sslMode=DISABLED" | ||||||
|  |                         + "&allowPublicKeyRetrieval=true" | ||||||
|                         + "&characterEncoding=utf8" |                         + "&characterEncoding=utf8" | ||||||
|                         + "&characterSetResults=utf8" |                         + "&characterSetResults=utf8" | ||||||
|                         + "&character_set_server=utf8mb4" |                         + "&character_set_server=utf8mb4" | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| package fr.pandacube.lib.db; | package fr.pandacube.lib.db; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Exception thrown when something bad happends when using the {@link DB} API. |  * Exception thrown when something bad happens when using the {@link DB} API. | ||||||
|  */ |  */ | ||||||
| public class DBException extends Exception { | public class DBException extends Exception { | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| package fr.pandacube.lib.db; | package fr.pandacube.lib.db; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Exception thrown when something bad happends when initializing a new table using {@link DB#initTable(Class)}. |  * Exception thrown when something bad happens when initializing a new table using {@link DB#initTable(Class)}. | ||||||
|  */ |  */ | ||||||
| public class DBInitTableException extends DBException { | public class DBInitTableException extends DBException { | ||||||
|  |  | ||||||
|   | |||||||
| @@ -26,7 +26,7 @@ import java.util.UUID; | |||||||
| import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||||
|  |  | ||||||
| import fr.pandacube.lib.util.EnumUtil; | 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. |  * Represents an entry in a SQL table. Each subclass is for a specific table. | ||||||
| @@ -119,7 +119,7 @@ public abstract class SQLElement<E extends SQLElement<E>> { | |||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Gets the name of the table in the database, without the prefix defined by {@link DB#init(DBConnection, String)}. |      * Gets the name of the table in the database, without the prefix defined by {@link DB#init(DBConnection, String)}. | ||||||
|      * @return The unprefixed name of the table in the database. |      * @return The non-prefixed name of the table in the database. | ||||||
|      */ |      */ | ||||||
|     protected abstract String tableName(); |     protected abstract String tableName(); | ||||||
|  |  | ||||||
| @@ -133,7 +133,7 @@ public abstract class SQLElement<E extends SQLElement<E>> { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Fills the values of this entry that are known to be nullable or have a default value. |      * Fills the entries values that are known to be nullable or have a default value. | ||||||
|      */ |      */ | ||||||
|     @SuppressWarnings("unchecked") |     @SuppressWarnings("unchecked") | ||||||
|     private void initDefaultValues() { |     private void initDefaultValues() { | ||||||
| @@ -193,7 +193,7 @@ public abstract class SQLElement<E extends SQLElement<E>> { | |||||||
|     /** |     /** | ||||||
|      * Sets a value in this entry. |      * Sets a value in this entry. | ||||||
|      * <p> |      * <p> | ||||||
|      * This is not good practice to set the {@code id} field of any entry, because it’s an unique auto-incremented |      * This is not good practice to set the {@code id} field of any entry, because it’s a unique auto-incremented | ||||||
|      * value. Use {@link #save()} and {@link #delete()} to set or unset the {@code id} instead, in consistence with the |      * value. Use {@link #save()} and {@link #delete()} to set or unset the {@code id} instead, in consistence with the | ||||||
|      * database. |      * database. | ||||||
|      * @param field the field to set. |      * @param field the field to set. | ||||||
| @@ -241,25 +241,32 @@ public abstract class SQLElement<E extends SQLElement<E>> { | |||||||
|      * Gets the value of the provided field in this entry. |      * Gets the value of the provided field in this entry. | ||||||
|      * @param field the field to get the value from. |      * @param field the field to get the value from. | ||||||
|      * @return the value of the provided field in this entry. |      * @return the value of the provided field in this entry. | ||||||
|  |      * @throws IllegalArgumentException if the provided field is null or not from the table represented by this class. | ||||||
|  |      * @throws IllegalStateException if the field is not nullable and there is no value set | ||||||
|      * @param <T> the Java type of the field. |      * @param <T> the Java type of the field. | ||||||
|      */ |      */ | ||||||
|     public <T> T get(SQLField<E, T> field) { |     public <T> T get(SQLField<E, T> field) { | ||||||
|         if (field == null) throw new IllegalArgumentException("field can't be null"); |         if (field == null) | ||||||
|  |             throw new IllegalArgumentException("field can't be null"); | ||||||
|  |         if (!fields.containsKey(field.getName()) || !fields.get(field.getName()).equals(field)) | ||||||
|  |             throw new IllegalArgumentException("The provided field " + field + " is not from this table " + getClass().getName()); | ||||||
|         if (values.containsKey(field)) { |         if (values.containsKey(field)) { | ||||||
|             @SuppressWarnings("unchecked") |             @SuppressWarnings("unchecked") | ||||||
|             T val = (T) values.get(field); |             T val = (T) values.get(field); | ||||||
|             return val; |             return val; | ||||||
|         } |         } | ||||||
|         throw new IllegalArgumentException("The field '" + field.getName() + "' in this instance of " + getClass().getName() |         if (field.nullable) | ||||||
|                 + " does not exist or is not set"); |             return null; | ||||||
|  |         throw new IllegalStateException("The non-nullable field '" + field.getName() + "' in this instance of " + getClass().getName() | ||||||
|  |                 + " is not set"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Gets the foreign table entry targeted by the provided foreignkey of this table. |      * Gets the foreign table entry targeted by the provided foreign key of this table. | ||||||
|      * @param field a foreignkey of this table. |      * @param field a foreign key of this table. | ||||||
|      * @param <T> the type of the foreignkey field. |      * @param <T> the type of the foreign key field. | ||||||
|      * @param <P> the targeted foreign table type. |      * @param <P> the targeted foreign table type. | ||||||
|      * @return the foreign table entry targeted by the provided foreignkey of this table. |      * @return the foreign table entry targeted by the provided foreign key of this table. | ||||||
|      * @throws DBException if an error occurs when interacting with the database. |      * @throws DBException if an error occurs when interacting with the database. | ||||||
|      */ |      */ | ||||||
|     public <T, P extends SQLElement<P>> P getReferencedEntry(SQLFKField<E, T, P> field) throws DBException { |     public <T, P extends SQLElement<P>> P getReferencedEntry(SQLFKField<E, T, P> field) throws DBException { | ||||||
| @@ -271,11 +278,11 @@ public abstract class SQLElement<E extends SQLElement<E>> { | |||||||
|     /** |     /** | ||||||
|      * Gets the original table entry which the provided foreign key is targeting this entry, and following the provided |      * Gets the original table entry which the provided foreign key is targeting this entry, and following the provided | ||||||
|      * {@code ORDER BY}, {@code LIMIT} and {@code OFFSET} clauses. |      * {@code ORDER BY}, {@code LIMIT} and {@code OFFSET} clauses. | ||||||
|      * @param field a foreignkey in the original table. |      * @param field a foreign key in the original table. | ||||||
|      * @param orderBy the {@code ORDER BY} clause of the query. |      * @param orderBy the {@code ORDER BY} clause of the query. | ||||||
|      * @param limit the {@code LIMIT} clause of the query. |      * @param limit the {@code LIMIT} clause of the query. | ||||||
|      * @param offset the {@code OFFSET} clause of the query. |      * @param offset the {@code OFFSET} clause of the query. | ||||||
|      * @param <T> the type of the foreignkey field. |      * @param <T> the type of the foreign key field. | ||||||
|      * @param <F> the table class of the foreign key that reference a field of this entry. |      * @param <F> the table class of the foreign key that reference a field of this entry. | ||||||
|      * @return the original table entry which the provided foreign key is targeting this entry. |      * @return the original table entry which the provided foreign key is targeting this entry. | ||||||
|      * @throws DBException if an error occurs when interacting with the database. |      * @throws DBException if an error occurs when interacting with the database. | ||||||
| @@ -314,7 +321,7 @@ public abstract class SQLElement<E extends SQLElement<E>> { | |||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Saves this entry into the database, either by updating the already existing entry in it, or by creating a new |      * Saves this entry into the database, either by updating the already existing entry in it, or by creating a new | ||||||
|      * entry if it doesn’t exist yet. |      * entry if it doesn't exist yet. | ||||||
|      * @return this. |      * @return this. | ||||||
|      * @throws DBException if an error occurs when interacting with the database. |      * @throws DBException if an error occurs when interacting with the database. | ||||||
|      */ |      */ | ||||||
| @@ -354,7 +361,7 @@ public abstract class SQLElement<E extends SQLElement<E>> { | |||||||
|                     first = false; |                     first = false; | ||||||
|                     concatValues.append(" ? "); |                     concatValues.append(" ? "); | ||||||
|                     concatFields.append("`").append(entry.getKey().getName()).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(); |                 try (Connection c = db.getConnection(); | ||||||
| @@ -382,20 +389,6 @@ public abstract class SQLElement<E extends SQLElement<E>> { | |||||||
|         return (E) this; |         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. |      * Tells if this entry is currently stored in DB or not. | ||||||
|      * @return true if this entry is currently stored in DB, or false otherwise. |      * @return true if this entry is currently stored in DB, or false otherwise. | ||||||
| @@ -474,14 +467,14 @@ public abstract class SQLElement<E extends SQLElement<E>> { | |||||||
|      * Creates a new SQL field. |      * Creates a new SQL field. | ||||||
|      * @param type the type of the field. |      * @param type the type of the field. | ||||||
|      * @param nullable true if nullable, false if {@code NOT NULL}. |      * @param nullable true if nullable, false if {@code NOT NULL}. | ||||||
|      * @param autoIncr if {@code AUTO_INCREMENT}. |      * @param autoIncrement if {@code AUTO_INCREMENT}. | ||||||
|      * @param deflt a default value for this field. A null value indicate that this has no default value. |      * @param deflt a default value for this field. A null value indicate that this has no default value. | ||||||
|      * @return the new SQL field. |      * @return the new SQL field. | ||||||
|      * @param <E> the table type. |      * @param <E> the table type. | ||||||
|      * @param <T> the Java type of this field. |      * @param <T> the Java type of this field. | ||||||
|      */ |      */ | ||||||
|     protected static <E extends SQLElement<E>, T> SQLField<E, T> field(SQLType<T> type, boolean nullable, boolean autoIncr, T deflt) { |     protected static <E extends SQLElement<E>, T> SQLField<E, T> field(SQLType<T> type, boolean nullable, boolean autoIncrement, T deflt) { | ||||||
|         return new SQLField<>(type, nullable, autoIncr, deflt); |         return new SQLField<>(type, nullable, autoIncrement, deflt); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -500,13 +493,13 @@ public abstract class SQLElement<E extends SQLElement<E>> { | |||||||
|      * Creates a new SQL field. |      * Creates a new SQL field. | ||||||
|      * @param type the type of the field. |      * @param type the type of the field. | ||||||
|      * @param nullable true if nullable, false if {@code NOT NULL}. |      * @param nullable true if nullable, false if {@code NOT NULL}. | ||||||
|      * @param autoIncr if {@code AUTO_INCREMENT}. |      * @param autoIncrement if {@code AUTO_INCREMENT}. | ||||||
|      * @return the new SQL field. |      * @return the new SQL field. | ||||||
|      * @param <E> the table type. |      * @param <E> the table type. | ||||||
|      * @param <T> the Java type of this field. |      * @param <T> the Java type of this field. | ||||||
|      */ |      */ | ||||||
|     protected static <E extends SQLElement<E>, T> SQLField<E, T> field(SQLType<T> type, boolean nullable, boolean autoIncr) { |     protected static <E extends SQLElement<E>, T> SQLField<E, T> field(SQLType<T> type, boolean nullable, boolean autoIncrement) { | ||||||
|         return new SQLField<>(type, nullable, autoIncr); |         return new SQLField<>(type, nullable, autoIncrement); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|   | |||||||
| @@ -16,6 +16,11 @@ import java.util.stream.Collectors; | |||||||
|  */ |  */ | ||||||
| public class SQLElementList<E extends SQLElement<E>> extends ArrayList<E> { | 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)}. |      * Stores all the values modified by {@link #setCommon(SQLField, Object)}. | ||||||
|      */ |      */ | ||||||
| @@ -46,7 +51,7 @@ public class SQLElementList<E extends SQLElement<E>> extends ArrayList<E> { | |||||||
|             E emptyElement = elemClass.getConstructor().newInstance(); |             E emptyElement = elemClass.getConstructor().newInstance(); | ||||||
|             emptyElement.set(field, value, false); |             emptyElement.set(field, value, false); | ||||||
|         } catch (Exception e) { |         } catch (Exception e) { | ||||||
|             throw new IllegalArgumentException("Illegal field or value or can't instanciante an empty instance of " |             throw new IllegalArgumentException("Illegal field or value or can't instantiate an empty instance of " | ||||||
|                     + elemClass.getName() + ". (the instance is only created to test validity of field and value)", e); |                     + elemClass.getName() + ". (the instance is only created to test validity of field and value)", e); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -83,7 +88,7 @@ public class SQLElementList<E extends SQLElement<E>> extends ArrayList<E> { | |||||||
|  |  | ||||||
|     @SuppressWarnings("unchecked") |     @SuppressWarnings("unchecked") | ||||||
|     private void applyNewValuesToElements(List<E> storedEl) { |     private void applyNewValuesToElements(List<E> storedEl) { | ||||||
|         // applique les valeurs dans chaques objets de la liste |         // applique les valeurs dans chaque objet de la liste | ||||||
|         for (E el : storedEl) { |         for (E el : storedEl) { | ||||||
|             for (@SuppressWarnings("rawtypes") SQLField entry : modifiedValues.keySet()) { |             for (@SuppressWarnings("rawtypes") SQLField entry : modifiedValues.keySet()) { | ||||||
|                 if (!el.isModified(entry)) { |                 if (!el.isModified(entry)) { | ||||||
| @@ -100,7 +105,7 @@ public class SQLElementList<E extends SQLElement<E>> extends ArrayList<E> { | |||||||
|     /** |     /** | ||||||
|      * Removes all the entries of this list from the database. |      * Removes all the entries of this list from the database. | ||||||
|      * This method has the same effect as calling the {@link SQLElement#delete()} method individually on each element, |      * This method has the same effect as calling the {@link SQLElement#delete()} method individually on each element, | ||||||
|      * but with only one SQL query to delete all of the entries. |      * but with only one SQL query to delete all the entries. | ||||||
|      * <p> |      * <p> | ||||||
|      * If you intend to remove the entries from the database just after fetching them, call directly the |      * If you intend to remove the entries from the database just after fetching them, call directly the | ||||||
|      * {@link DB#delete(Class, SQLWhere)} method instead. |      * {@link DB#delete(Class, SQLWhere)} method instead. | ||||||
| @@ -124,9 +129,9 @@ public class SQLElementList<E extends SQLElement<E>> extends ArrayList<E> { | |||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Get all the entries targeted by the foreign key of all the entries in this list. |      * Get all the entries targeted by the foreign key of all the entries in this list. | ||||||
|      * @param foreignKey a foreignkey of this table. |      * @param foreignKey a foreign key of this table. | ||||||
|      * @param orderBy the {@code ORDER BY} clause of the query. |      * @param orderBy the {@code ORDER BY} clause of the query. | ||||||
|      * @return a list of foreign table entries targeted by the provided foreignkey of this table. |      * @return a list of foreign table entries targeted by the provided foreign key of this table. | ||||||
|      * @param <T> the field’s Java type. |      * @param <T> the field’s Java type. | ||||||
|      * @param <P> the target table type. |      * @param <P> the target table type. | ||||||
|      * @throws DBException if an error occurs when interacting with the database. |      * @throws DBException if an error occurs when interacting with the database. | ||||||
| @@ -144,7 +149,7 @@ public class SQLElementList<E extends SQLElement<E>> extends ArrayList<E> { | |||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Get all the entries targeted by the foreign key of all the entries in this list, mapped from the foreign key value. |      * Get all the entries targeted by the foreign key of all the entries in this list, mapped from the foreign key value. | ||||||
|      * @param foreignKey a foreignkey of this table. |      * @param foreignKey a foreign key of this table. | ||||||
|      * @return a map of the foreign key values, mapped to the foreign table’s entries. |      * @return a map of the foreign key values, mapped to the foreign table’s entries. | ||||||
|      * @param <T> the field’s Java type. |      * @param <T> the field’s Java type. | ||||||
|      * @param <P> the target table type. |      * @param <P> the target table type. | ||||||
| @@ -163,11 +168,11 @@ public class SQLElementList<E extends SQLElement<E>> extends ArrayList<E> { | |||||||
|     /** |     /** | ||||||
|      * Gets all the original table’s entries which the provided foreign key is targeting the entries of this list, and |      * Gets all the original table’s entries which the provided foreign key is targeting the entries of this list, and | ||||||
|      * following the provided {@code ORDER BY}, {@code LIMIT} and {@code OFFSET} clauses. |      * following the provided {@code ORDER BY}, {@code LIMIT} and {@code OFFSET} clauses. | ||||||
|      * @param foreignKey a foreignkey in the original table. |      * @param foreignKey a foreign key in the original table. | ||||||
|      * @param orderBy the {@code ORDER BY} clause of the query. |      * @param orderBy the {@code ORDER BY} clause of the query. | ||||||
|      * @param limit the {@code LIMIT} clause of the query. |      * @param limit the {@code LIMIT} clause of the query. | ||||||
|      * @param offset the {@code OFFSET} clause of the query. |      * @param offset the {@code OFFSET} clause of the query. | ||||||
|      * @param <T> the type of the foreignkey field. |      * @param <T> the type of the foreign key field. | ||||||
|      * @param <F> the table class of the foreign key that reference a field of this entry. |      * @param <F> the table class of the foreign key that reference a field of this entry. | ||||||
|      * @return the original table’s entries which the provided foreign key is targeting the entries of this list. |      * @return the original table’s entries which the provided foreign key is targeting the entries of this list. | ||||||
|      * @throws DBException if an error occurs when interacting with the database. |      * @throws DBException if an error occurs when interacting with the database. | ||||||
| @@ -187,11 +192,11 @@ public class SQLElementList<E extends SQLElement<E>> extends ArrayList<E> { | |||||||
|      * Gets all the original table’s entries which the provided foreign key is targeting the entries of this list, |      * Gets all the original table’s entries which the provided foreign key is targeting the entries of this list, | ||||||
|      * following the provided {@code ORDER BY}, {@code LIMIT} and {@code OFFSET} clauses, and mapped from the foreign |      * following the provided {@code ORDER BY}, {@code LIMIT} and {@code OFFSET} clauses, and mapped from the foreign | ||||||
|      * key value. |      * key value. | ||||||
|      * @param foreignKey a foreignkey in the original table. |      * @param foreignKey a foreign key in the original table. | ||||||
|      * @param orderBy the {@code ORDER BY} clause of the query. |      * @param orderBy the {@code ORDER BY} clause of the query. | ||||||
|      * @param limit the {@code LIMIT} clause of the query. |      * @param limit the {@code LIMIT} clause of the query. | ||||||
|      * @param offset the {@code OFFSET} clause of the query. |      * @param offset the {@code OFFSET} clause of the query. | ||||||
|      * @param <T> the type of the foreignkey field. |      * @param <T> the type of the foreign key field. | ||||||
|      * @param <F> the table class of the foreign key that reference a field of this entry. |      * @param <F> the table class of the foreign key that reference a field of this entry. | ||||||
|      * @return a map of the foreign key values, mapped to the orignal table’s entries. |      * @return a map of the foreign key values, mapped to the orignal table’s entries. | ||||||
|      * @throws DBException if an error occurs when interacting with the database. |      * @throws DBException if an error occurs when interacting with the database. | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| package fr.pandacube.lib.db; | 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. |  * A foreign key field in a SQL table. | ||||||
| @@ -28,7 +28,7 @@ public class SQLFKField<F extends SQLElement<F>, T, P extends SQLElement<P>> ext | |||||||
|             SQLField<F, Integer> f = DB.getSQLIdField(fkEl); |             SQLField<F, Integer> f = DB.getSQLIdField(fkEl); | ||||||
|             return new SQLFKField<>(f.type, nul, deflt, fkEl, f); |             return new SQLFKField<>(f.type, nul, deflt, fkEl, f); | ||||||
|         } catch (DBInitTableException e) { |         } catch (DBInitTableException e) { | ||||||
|             Log.severe("Can't create Foreign key Field targetting id field of '"+fkEl+"'", e); |             Log.severe("Can't create Foreign key Field targeting id field of '"+fkEl+"'", e); | ||||||
|             return null; |             return null; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -51,7 +51,7 @@ public class SQLFKField<F extends SQLElement<F>, T, P extends SQLElement<P>> ext | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (fkF.getSQLElementType() == null) |         if (fkF.getSQLElementType() == null) | ||||||
|             throw new RuntimeException("Can't initialize foreign key. The primary key in the table " + fkEl.getName() + " is not properly initialized and can't be targetted by a forein key"); |             throw new RuntimeException("Can't initialize foreign key. The primary key in the table " + fkEl.getName() + " is not properly initialized and can't be targeted by a foreign key"); | ||||||
|         sqlPrimaryKeyField = fkF; |         sqlPrimaryKeyField = fkF; | ||||||
|         sqlForeignKeyElemClass = fkEl; |         sqlForeignKeyElemClass = fkEl; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -24,10 +24,10 @@ public class SQLField<E extends SQLElement<E>, T> { | |||||||
|     /* package */ final boolean autoIncrement; |     /* package */ final boolean autoIncrement; | ||||||
|     /* package */ final T defaultValue; |     /* package */ final T defaultValue; | ||||||
|  |  | ||||||
|     /* package */ SQLField(SQLType<T> type, boolean nullable, boolean autoIncr, T deflt) { |     /* package */ SQLField(SQLType<T> type, boolean nullable, boolean autoIncrement, T deflt) { | ||||||
|         this.type = type; |         this.type = type; | ||||||
|         this.nullable = nullable; |         this.nullable = nullable; | ||||||
|         autoIncrement = autoIncr; |         this.autoIncrement = autoIncrement; | ||||||
|         defaultValue = deflt; |         defaultValue = deflt; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -35,8 +35,8 @@ public class SQLField<E extends SQLElement<E>, T> { | |||||||
|         this(type, nullable, false, null); |         this(type, nullable, false, null); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /* package */ SQLField(SQLType<T> type, boolean nullable, boolean autoIncr) { |     /* package */ SQLField(SQLType<T> type, boolean nullable, boolean autoIncrement) { | ||||||
|         this(type, nullable, autoIncr, null); |         this(type, nullable, autoIncrement, null); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /* package */ SQLField(SQLType<T> type, boolean nullable, T deflt) { |     /* package */ SQLField(SQLType<T> type, boolean nullable, T deflt) { | ||||||
| @@ -133,7 +133,7 @@ public class SQLField<E extends SQLElement<E>, T> { | |||||||
|     /** |     /** | ||||||
|      * Create a SQL {@code WHERE} expression comparing this field with the provided value using the {@code =} operator. |      * Create a SQL {@code WHERE} expression comparing this field with the provided value using the {@code =} operator. | ||||||
|      * @param r the value to compare with. |      * @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) { |     public fr.pandacube.lib.db.SQLWhere<E> eq(T r) { | ||||||
|         return comp(SQLComparator.EQ, 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. |      * Create a SQL {@code WHERE} expression comparing this field with the provided value using the {@code >=} operator. | ||||||
|      * @param r the value to compare with. |      * @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) { |     public fr.pandacube.lib.db.SQLWhere<E> geq(T r) { | ||||||
|         return comp(SQLComparator.GEQ, 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. |      * Create a SQL {@code WHERE} expression comparing this field with the provided value using the {@code >} operator. | ||||||
|      * @param r the value to compare with. |      * @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) { |     public fr.pandacube.lib.db.SQLWhere<E> gt(T r) { | ||||||
|         return comp(SQLComparator.GT, 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. |      * Create a SQL {@code WHERE} expression comparing this field with the provided value using the {@code <=} operator. | ||||||
|      * @param r the value to compare with. |      * @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) { |     public fr.pandacube.lib.db.SQLWhere<E> leq(T r) { | ||||||
|         return comp(SQLComparator.LEQ, 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. |      * Create a SQL {@code WHERE} expression comparing this field with the provided value using the {@code <} operator. | ||||||
|      * @param r the value to compare with. |      * @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) { |     public fr.pandacube.lib.db.SQLWhere<E> lt(T r) { | ||||||
|         return comp(SQLComparator.LT, 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. |      * Create a SQL {@code WHERE} expression comparing this field with the provided value using the {@code !=} operator. | ||||||
|      * @param r the value to compare with. |      * @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) { |     public fr.pandacube.lib.db.SQLWhere<E> neq(T r) { | ||||||
|         return comp(SQLComparator.NEQ, 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} |      * Create a SQL {@code WHERE} expression comparing this field with the provided value using the {@code LIKE} | ||||||
|      * keyword. |      * keyword. | ||||||
|      * @param like the value to compare with. |      * @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) { |     public fr.pandacube.lib.db.SQLWhere<E> like(String like) { | ||||||
|         return new SQLWhereLike<>(this, 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 |      * Create a SQL {@code WHERE} expression testing the presence of this field in the provided collection of value | ||||||
|      * using the {@code IN} keyword. |      * using the {@code IN} keyword. | ||||||
|      * @param v the value to compare with. |      * @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) { |     public fr.pandacube.lib.db.SQLWhere<E> in(Collection<T> v) { | ||||||
|         return new SQLWhereIn<>(this, 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. |      * 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() { |     public fr.pandacube.lib.db.SQLWhere<E> isNull() { | ||||||
|         return new SQLWhereNull<>(this, true); |         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} |      * Create a SQL {@code WHERE} expression testing the non-nullity of this field using the {@code IS NOT NULL} | ||||||
|      * keyword. |      * keyword. | ||||||
|      * @return a SQL {@code WHERE} expression. |      * @return a new SQL {@code WHERE} expression. | ||||||
|      */ |      */ | ||||||
|     public fr.pandacube.lib.db.SQLWhere<E> isNotNull() { |     public fr.pandacube.lib.db.SQLWhere<E> isNotNull() { | ||||||
|         return new SQLWhereNull<>(this, false); |         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; | ||||||
|  |     } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ import java.util.HashMap; | |||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
|  |  | ||||||
| import fr.pandacube.lib.util.Log; | import fr.pandacube.lib.util.log.Log; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Builder for a SQL {@code UPDATE} query. |  * Builder for a SQL {@code UPDATE} query. | ||||||
| @@ -74,7 +74,7 @@ public class SQLUpdateBuilder<E extends SQLElement<E>> { | |||||||
|             if (!first) |             if (!first) | ||||||
|                 sql.append(", "); |                 sql.append(", "); | ||||||
|             sql.append("`").append(entry.getKey().getName()).append("` = ? "); |             sql.append("`").append(entry.getKey().getName()).append("` = ? "); | ||||||
|             SQLElement.addValueToSQLObjectList(params, entry.getKey(), entry.getValue()); |             params.add(entry.getKey().fromJavaTypeToJDBCType(entry.getValue())); | ||||||
|             first = false; |             first = false; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,15 +3,21 @@ package fr.pandacube.lib.db; | |||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.Collection; | import java.util.Collection; | ||||||
| import java.util.List; | 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. |  * @param <E> the table type. | ||||||
|  */ |  */ | ||||||
| public abstract class SQLWhere<E extends SQLElement<E>> { | public abstract class SQLWhere<E extends SQLElement<E>> { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Creates a SQL WHERE expression. | ||||||
|  |      */ | ||||||
|  |     protected SQLWhere() {} | ||||||
|  |  | ||||||
|     /* package */ abstract ParameterizedSQLString toSQL() throws DBException; |     /* package */ abstract ParameterizedSQLString toSQL() throws DBException; | ||||||
|  |  | ||||||
|     @Override |     @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 |      * Create a SQL {@code WHERE} expression that is true when this expression {@code AND} the provided expression is | ||||||
|      * true. |      * true. | ||||||
|      * @param other the other expression. |      * @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) { |     public SQLWhere<E> and(SQLWhere<E> other) { | ||||||
|         return SQLWhere.<E>and().and(this).and(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 |      * Create a SQL {@code WHERE} expression that is true when this expression {@code OR} the provided expression is | ||||||
|      * true. |      * true. | ||||||
|      * @param other the other expression. |      * @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) { |     public SQLWhere<E> or(SQLWhere<E> other) { | ||||||
|         return SQLWhere.<E>or().or(this).or(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. |      * 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. |      * @param <E> the table type. | ||||||
|      */ |      */ | ||||||
|     public static <E extends SQLElement<E>> SQLWhereAndBuilder<E> and() { |     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. |      * 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. |      * @param <E> the table type. | ||||||
|      */ |      */ | ||||||
|     public static <E extends SQLElement<E>> SQLWhereOrBuilder<E> or() { |     public static <E extends SQLElement<E>> SQLWhereOrBuilder<E> or() { | ||||||
|         return new SQLWhereOrBuilder<>(); |         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. |      * 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 |         @Override | ||||||
|         /* package */ ParameterizedSQLString toSQL() throws DBException { |         /* package */ ParameterizedSQLString toSQL() throws DBException { | ||||||
|             List<Object> params = new ArrayList<>(); |             return new ParameterizedSQLString("`" + left.getName() + "` " + comp.sql + " ? ", | ||||||
|             SQLElement.addValueToSQLObjectList(params, left, right); |                     List.of(left.fromJavaTypeToJDBCType(right))); | ||||||
|             return new ParameterizedSQLString("`" + left.getName() + "` " + comp.sql + " ? ", params); |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         /* package */ enum SQLComparator { |         /* 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 String leftExpression; | ||||||
|         private final Collection<?> values; |         private final List<Object> leftExpressionParameters; | ||||||
|  |         protected Collection<?> collectionIn; | ||||||
|  |  | ||||||
|         /* package */ <T> SQLWhereIn(SQLField<E, T> f, Collection<T> v) { |         /* package */ <T> SQLWhereInCustom(String leftExpr, List<Object> leftExprParams, Collection<T> collectionIn) { | ||||||
|             if (f == null || v == null) |             if (leftExpr == null) | ||||||
|                 throw new IllegalArgumentException("All arguments for SQLWhereIn constructor can't be null"); |                 throw new IllegalArgumentException("leftExpr can't be null"); | ||||||
|             field = f; |             if (leftExprParams == null) | ||||||
|             values = v; |                 leftExprParams = List.of(); | ||||||
|  |             if (collectionIn == null) | ||||||
|  |                 collectionIn = List.of(); | ||||||
|  |             leftExpression = leftExpr; | ||||||
|  |             leftExpressionParameters = leftExprParams; | ||||||
|  |             this.collectionIn = collectionIn; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         @Override |         @Override | ||||||
|         /* package */ ParameterizedSQLString toSQL() throws DBException { |         /* package */ ParameterizedSQLString toSQL() throws DBException { | ||||||
|             List<Object> params = new ArrayList<>(); |             List<Object> params = new ArrayList<>(); | ||||||
|  |  | ||||||
|             if (values.isEmpty()) |             if (collectionIn.isEmpty()) | ||||||
|                 return new ParameterizedSQLString(" 1=0 ", params); |                 return new ParameterizedSQLString(" 1=0 ", params); | ||||||
|  |  | ||||||
|             for (Object v : values) |             params.addAll(leftExpressionParameters); | ||||||
|                 SQLElement.addValueToSQLObjectList(params, field, v); |             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++) |             for (int i = 0; i < questions.length; i++) | ||||||
|                 questions[i] = i % 2 == 0 ? '?' : ','; |                 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> { |     /* 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); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|   | |||||||
| @@ -1,12 +0,0 @@ | |||||||
| # pandalib-net |  | ||||||
|  |  | ||||||
| A TCP network library that uses the standard Java socket API, to ease the ommunication between the different processes |  | ||||||
| running the server network Pandacube. |  | ||||||
|  |  | ||||||
| It’s still in development (actually not touched since years), and it’s supposed to be a replacement for the old |  | ||||||
| `pandalib-netapi`. This module is then marked as Beta using the Google Guava annotation. |  | ||||||
|  |  | ||||||
| - Packet based communication |  | ||||||
| - Supports Request/Answer packets |  | ||||||
| - Uses binary packet id and data |  | ||||||
| * Input streams are handled in separate Threads |  | ||||||
| @@ -1,65 +0,0 @@ | |||||||
| package fr.pandacube.lib.net; |  | ||||||
|  |  | ||||||
| import java.util.Arrays; |  | ||||||
|  |  | ||||||
| import com.google.common.annotations.Beta; |  | ||||||
|  |  | ||||||
| @Beta |  | ||||||
| public class Array8Bit { |  | ||||||
| 	 |  | ||||||
| 	public static final int BIT_COUNT = Byte.SIZE; |  | ||||||
| 	 |  | ||||||
| 	private boolean[] values = new boolean[BIT_COUNT]; |  | ||||||
| 	 |  | ||||||
| 	public Array8Bit(byte b) { |  | ||||||
| 		fromByte(b); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * @param bits (index 0 is the lowest significant bit) |  | ||||||
| 	 */ |  | ||||||
| 	public Array8Bit(boolean[] bits) { |  | ||||||
| 		if (bits == null || bits.length != BIT_COUNT) |  | ||||||
| 			throw new IllegalArgumentException("bits is null or bits.length != "+BIT_COUNT); |  | ||||||
| 		values = Arrays.copyOf(bits, BIT_COUNT); |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	/** |  | ||||||
| 	 * i = 0 is the lowest significant bit |  | ||||||
| 	 */ |  | ||||||
| 	public boolean getBit(int i) { |  | ||||||
| 		return values[i]; |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	/** |  | ||||||
| 	 * i = 0 is the lowest significant bit |  | ||||||
| 	 */ |  | ||||||
| 	public void setBit(int i, boolean b) { |  | ||||||
| 		values[i] = b; |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	public void fromByte(byte b) { |  | ||||||
| 		int mask = 1; |  | ||||||
| 		for (int i = 0; i < BIT_COUNT; i++) { |  | ||||||
| 			values[i] = (b & mask) != 0; |  | ||||||
| 			mask <<= 1; |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	public byte toByte() { |  | ||||||
| 		byte b = 0; |  | ||||||
| 		for (int i=BIT_COUNT-1; i>=0; i--) { |  | ||||||
| 			b <<= 1; |  | ||||||
| 			if (values[i]) b |= 1; |  | ||||||
| 		} |  | ||||||
| 		return b; |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| } |  | ||||||
| @@ -1,275 +0,0 @@ | |||||||
| package fr.pandacube.lib.net; |  | ||||||
|  |  | ||||||
| import java.nio.charset.Charset; |  | ||||||
| import java.nio.charset.StandardCharsets; |  | ||||||
| import java.util.ArrayList; |  | ||||||
| import java.util.Arrays; |  | ||||||
| import java.util.List; |  | ||||||
|  |  | ||||||
| import com.google.common.annotations.Beta; |  | ||||||
|  |  | ||||||
| @Beta |  | ||||||
| public final class ByteBuffer implements Cloneable { |  | ||||||
|  |  | ||||||
| 	public static final Charset NETWORK_CHARSET = StandardCharsets.UTF_8; |  | ||||||
|  |  | ||||||
| 	private java.nio.ByteBuffer buff; |  | ||||||
|  |  | ||||||
| 	public ByteBuffer() { |  | ||||||
| 		this(16); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	public ByteBuffer(int initSize) { |  | ||||||
| 		buff = java.nio.ByteBuffer.allocate(initSize); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * Create a ByteBuffer that is initially <b>backed</b> by the provided byte array. |  | ||||||
| 	 * The position of this buffer will be 0. |  | ||||||
| 	 * If this ByteBuffer needs a biffer array, the provided array is replaced by a new one, |  | ||||||
| 	 * making the provided array not related to this ByteBuffer anymore. |  | ||||||
| 	 * @param data array of byte that serve as a backend for this ByteBuffer. |  | ||||||
| 	 */ |  | ||||||
| 	public ByteBuffer(byte[] data) { |  | ||||||
| 		buff = java.nio.ByteBuffer.wrap(data); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	private void askForBufferExtension(int needed) { |  | ||||||
| 		while (buff.remaining() < needed) { |  | ||||||
| 			java.nio.ByteBuffer newBuff = java.nio.ByteBuffer.wrap(Arrays.copyOf(buff.array(), buff.array().length * 2)); |  | ||||||
| 			newBuff.position(buff.position()); |  | ||||||
| 			buff = newBuff; |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * This clone method also clone the underlying array. |  | ||||||
| 	 */ |  | ||||||
| 	@SuppressWarnings("MethodDoesntCallSuperMethod") |  | ||||||
| 	@Override |  | ||||||
| 	public ByteBuffer clone() { |  | ||||||
| 		return new ByteBuffer(Arrays.copyOf(buff.array(), buff.array().length)); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * @see java.nio.ByteBuffer#get() |  | ||||||
| 	 */ |  | ||||||
| 	public byte getByte() { |  | ||||||
| 		return buff.get(); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * @see java.nio.ByteBuffer#get(byte[]) |  | ||||||
| 	 */ |  | ||||||
| 	public byte[] getByteArray(byte[] b) { |  | ||||||
| 		buff.get(b); |  | ||||||
| 		return b; |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	/** |  | ||||||
| 	 * Return the next byte array wich is preceded with his size as integer, |  | ||||||
| 	 * or null if the founded size is negative. |  | ||||||
| 	 */ |  | ||||||
| 	public byte[] getSizedByteArray() { |  | ||||||
| 		int size = getInt(); |  | ||||||
| 		if (size < 0) return null; |  | ||||||
| 		return getByteArray(new byte[size]); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * @see java.nio.ByteBuffer#getChar() |  | ||||||
| 	 */ |  | ||||||
| 	public char getChar() { |  | ||||||
| 		return buff.getChar(); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * @see java.nio.ByteBuffer#getShort() |  | ||||||
| 	 */ |  | ||||||
| 	public short getShort() { |  | ||||||
| 		return buff.getShort(); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * @see java.nio.ByteBuffer#getInt() |  | ||||||
| 	 */ |  | ||||||
| 	public int getInt() { |  | ||||||
| 		return buff.getInt(); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * @see java.nio.ByteBuffer#getLong() |  | ||||||
| 	 */ |  | ||||||
| 	public long getLong() { |  | ||||||
| 		return buff.getLong(); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * @see java.nio.ByteBuffer#getFloat() |  | ||||||
| 	 */ |  | ||||||
| 	public float getFloat() { |  | ||||||
| 		return buff.getFloat(); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * @see java.nio.ByteBuffer#getDouble() |  | ||||||
| 	 */ |  | ||||||
| 	public double getDouble() { |  | ||||||
| 		return buff.getDouble(); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * @see java.nio.ByteBuffer#put(byte) |  | ||||||
| 	 */ |  | ||||||
| 	public ByteBuffer putByte(byte b) { |  | ||||||
| 		askForBufferExtension(Byte.BYTES); |  | ||||||
| 		buff.put(b); |  | ||||||
| 		return this; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * @see java.nio.ByteBuffer#put(byte[]) |  | ||||||
| 	 */ |  | ||||||
| 	public ByteBuffer putByteArray(byte[] b) { |  | ||||||
| 		askForBufferExtension(b.length * Byte.BYTES); |  | ||||||
| 		buff.put(b); |  | ||||||
| 		return this; |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	public ByteBuffer putSizedByteArray(byte[] b) { |  | ||||||
| 		if (b == null) { |  | ||||||
| 			return putInt(-1); |  | ||||||
| 		} |  | ||||||
| 		putInt(b.length); |  | ||||||
| 		return putByteArray(b); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * @see java.nio.ByteBuffer#putChar(char) |  | ||||||
| 	 */ |  | ||||||
| 	public ByteBuffer putChar(char value) { |  | ||||||
| 		askForBufferExtension(Character.BYTES); |  | ||||||
| 		buff.putChar(value); |  | ||||||
| 		return this; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * @see java.nio.ByteBuffer#putShort(short) |  | ||||||
| 	 */ |  | ||||||
| 	public ByteBuffer putShort(short value) { |  | ||||||
| 		askForBufferExtension(Short.BYTES); |  | ||||||
| 		buff.putShort(value); |  | ||||||
| 		return this; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * @see java.nio.ByteBuffer#putInt(int) |  | ||||||
| 	 */ |  | ||||||
| 	public ByteBuffer putInt(int value) { |  | ||||||
| 		askForBufferExtension(Integer.BYTES); |  | ||||||
| 		buff.putInt(value); |  | ||||||
| 		return this; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * @see java.nio.ByteBuffer#putLong(long) |  | ||||||
| 	 */ |  | ||||||
| 	public ByteBuffer putLong(long value) { |  | ||||||
| 		askForBufferExtension(Long.BYTES); |  | ||||||
| 		buff.putLong(value); |  | ||||||
| 		return this; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * @see java.nio.ByteBuffer#putFloat(float) |  | ||||||
| 	 */ |  | ||||||
| 	public ByteBuffer putFloat(float value) { |  | ||||||
| 		askForBufferExtension(Float.BYTES); |  | ||||||
| 		buff.putFloat(value); |  | ||||||
| 		return this; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * @see java.nio.ByteBuffer#putDouble(double) |  | ||||||
| 	 */ |  | ||||||
| 	public ByteBuffer putDouble(double value) { |  | ||||||
| 		askForBufferExtension(Double.BYTES); |  | ||||||
| 		buff.putDouble(value); |  | ||||||
| 		return this; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * @see java.nio.ByteBuffer#position() |  | ||||||
| 	 */ |  | ||||||
| 	public int getPosition() { |  | ||||||
| 		return buff.position(); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * @see java.nio.ByteBuffer#position(int) |  | ||||||
| 	 */ |  | ||||||
| 	public void setPosition(int p) { |  | ||||||
| 		buff.position(p); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * @see java.nio.ByteBuffer#capacity() |  | ||||||
| 	 */ |  | ||||||
| 	public int capacity() { |  | ||||||
| 		return buff.capacity(); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 *  |  | ||||||
| 	 * @param s null String are supported |  | ||||||
| 	 */ |  | ||||||
| 	public ByteBuffer putString(String s) { |  | ||||||
| 		if (s == null) { |  | ||||||
| 			return putInt(-1); |  | ||||||
| 		} |  | ||||||
| 		return putSizedByteArray(s.getBytes(NETWORK_CHARSET)); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * returned string can be null |  | ||||||
| 	 */ |  | ||||||
| 	public String getString() { |  | ||||||
| 		byte[] binaryString = getSizedByteArray(); |  | ||||||
| 		return (binaryString == null) ? null : new String(binaryString, NETWORK_CHARSET); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 *  |  | ||||||
| 	 * @param list The list can be null, and any String can be null too. |  | ||||||
| 	 */ |  | ||||||
| 	public ByteBuffer putListOfString(List<String> list) { |  | ||||||
| 		if (list == null) { |  | ||||||
| 			return putInt(-1); |  | ||||||
| 		} |  | ||||||
| 		putInt(list.size()); |  | ||||||
| 		for (String str : list) |  | ||||||
| 			putString(str); |  | ||||||
| 		return this; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * @return a List of String. The list can be null, and any element can be null too. |  | ||||||
| 	 */ |  | ||||||
| 	public List<String> getListOfString() { |  | ||||||
| 		int size = getInt(); |  | ||||||
| 		if (size < 0) |  | ||||||
| 			return null; |  | ||||||
| 		List<String> list = new ArrayList<>(); |  | ||||||
| 		for (int i = 0; i < size; i++) |  | ||||||
| 			list.add(getString()); |  | ||||||
| 		return list; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * @see java.nio.ByteBuffer#array() |  | ||||||
| 	 */ |  | ||||||
| 	public byte[] array() { |  | ||||||
| 		return buff.array(); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| } |  | ||||||
| @@ -1,60 +0,0 @@ | |||||||
| package fr.pandacube.lib.net; |  | ||||||
|  |  | ||||||
| import java.util.Arrays; |  | ||||||
|  |  | ||||||
| import com.google.common.annotations.Beta; |  | ||||||
|  |  | ||||||
| @Beta |  | ||||||
| public class PPacket { |  | ||||||
| 	public final String name; |  | ||||||
| 	/* package */ int id; |  | ||||||
| 	public final byte[] content; |  | ||||||
| 	 |  | ||||||
| 	/** |  | ||||||
| 	 * Construct a new PPacket based on the content of the provided buffer before his position. |  | ||||||
| 	 * @param n the name of the packet. |  | ||||||
| 	 * @param buff the buffer where the data comes from. Only the content before {@link ByteBuffer#getPosition()} is copied. |  | ||||||
| 	 */ |  | ||||||
| 	public PPacket(String n, ByteBuffer buff) { |  | ||||||
| 		this(n, Arrays.copyOf(buff.array(), buff.getPosition())); |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	public PPacket(String n, byte[] c) { |  | ||||||
| 		name = n; |  | ||||||
| 		content = c; |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	/* package */ PPacket(String n, int i, byte[] c) { |  | ||||||
| 		this(n, c); |  | ||||||
| 		id = i; |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	public ByteBuffer getContentAsBuffer() { |  | ||||||
| 		return new ByteBuffer(content); |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	public static PPacket buildSingleStringContentPacket(String name, String content) { |  | ||||||
| 		return new PPacket(name, new ByteBuffer().putString(content)); |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	/* package */ static PPacket buildLoginPacket(String password) { |  | ||||||
| 		return buildSingleStringContentPacket("login", password); |  | ||||||
| 	} |  | ||||||
| 	/* package */ static PPacket buildBadFormatPacket(String message) { |  | ||||||
| 		return buildSingleStringContentPacket("bad_format", message); |  | ||||||
| 	} |  | ||||||
| 	/* package */ static PPacket buildLoginBadPacket() { |  | ||||||
| 		return new PPacket("login_bad", new byte[0]); |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @@ -1,47 +0,0 @@ | |||||||
| package fr.pandacube.lib.net; |  | ||||||
|  |  | ||||||
| import java.util.Arrays; |  | ||||||
|  |  | ||||||
| import com.google.common.annotations.Beta; |  | ||||||
|  |  | ||||||
| @Beta |  | ||||||
| public class PPacketAnswer extends PPacket { |  | ||||||
| 	/* package */ final int answer; |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * Construct a new PPacketAnswer based on the content of the provided buffer before his position. |  | ||||||
| 	 * @param n the name of the packet. |  | ||||||
| 	 * @param buff the buffer where the data comes from. Only the content before {@link ByteBuffer#getPosition()} is copied. |  | ||||||
| 	 */ |  | ||||||
| 	public PPacketAnswer(PPacket answered, String n, ByteBuffer buff) { |  | ||||||
| 		this(answered, n, Arrays.copyOf(buff.array(), buff.getPosition())); |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	public PPacketAnswer(PPacket answered, String n, byte[] c) { |  | ||||||
| 		super(n, c); |  | ||||||
| 		answer = answered.id; |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	/* package */ PPacketAnswer(String n, int i, int a, byte[] c) { |  | ||||||
| 		super(n, i, c); |  | ||||||
| 		answer = a; |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
|  |  | ||||||
| 	 |  | ||||||
| 	public static PPacketAnswer buildSingleStringContentPacketAnswer(PPacket answered, String name, String content) { |  | ||||||
| 		ByteBuffer pwBuff = new ByteBuffer().putString(content); |  | ||||||
| 		return new PPacketAnswer(answered, name, Arrays.copyOf(pwBuff.array(), pwBuff.getPosition())); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	/* package */ static PPacketAnswer buildLoginOkPacket(PPacket loginPacket) { |  | ||||||
| 		return new PPacketAnswer(loginPacket, "login_ok", new byte[0]); |  | ||||||
| 	} |  | ||||||
| 	/* package */ static PPacketAnswer buildExceptionPacket(PPacket answered, String message) { |  | ||||||
| 		return buildSingleStringContentPacketAnswer(answered, "exception", message); |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @@ -1,16 +0,0 @@ | |||||||
| package fr.pandacube.lib.net; |  | ||||||
|  |  | ||||||
| import com.google.common.annotations.Beta; |  | ||||||
|  |  | ||||||
| @Beta |  | ||||||
| @FunctionalInterface |  | ||||||
| public interface PPacketListener<P extends PPacket> { |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * Called when we receive a packet (except responses) |  | ||||||
| 	 * @param connection the connection from where the packet comes |  | ||||||
| 	 * @param packet the received packet |  | ||||||
| 	 */ |  | ||||||
| 	void onPacketReceive(PSocket connection, P packet); |  | ||||||
|  |  | ||||||
| } |  | ||||||
| @@ -1,157 +0,0 @@ | |||||||
| package fr.pandacube.lib.net; |  | ||||||
|  |  | ||||||
| import java.io.Closeable; |  | ||||||
| import java.io.IOException; |  | ||||||
| import java.net.InetSocketAddress; |  | ||||||
| import java.net.ServerSocket; |  | ||||||
| import java.net.Socket; |  | ||||||
| import java.net.SocketException; |  | ||||||
| import java.util.ArrayList; |  | ||||||
| import java.util.Collections; |  | ||||||
| import java.util.List; |  | ||||||
| import java.util.concurrent.atomic.AtomicBoolean; |  | ||||||
| import java.util.concurrent.atomic.AtomicInteger; |  | ||||||
|  |  | ||||||
| import com.google.common.annotations.Beta; |  | ||||||
|  |  | ||||||
| import fr.pandacube.lib.util.Log; |  | ||||||
|  |  | ||||||
| @Beta |  | ||||||
| public class PServer extends Thread implements Closeable { |  | ||||||
| 	private static final AtomicInteger connectionCounterId = new AtomicInteger(0); |  | ||||||
|  |  | ||||||
| 	private final int port; |  | ||||||
| 	private ServerSocket socket; |  | ||||||
| 	private final String socketName; |  | ||||||
|  |  | ||||||
| 	private final List<TCPServerClientConnection> clients = Collections.synchronizedList(new ArrayList<>()); |  | ||||||
|  |  | ||||||
| 	private final AtomicBoolean isClosed = new AtomicBoolean(false); |  | ||||||
| 	 |  | ||||||
|  |  | ||||||
| 	private final List<PPacketListener<PPacket>> globalPacketListeners = Collections.synchronizedList(new ArrayList<>()); |  | ||||||
| 	private final List<PSocketConnectionListener> clientConnectionListeners = Collections.synchronizedList(new ArrayList<>()); |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	private final String password; |  | ||||||
|  |  | ||||||
| 	public PServer(int port, String sckName, String password) { |  | ||||||
| 		super("PServer " + sckName); |  | ||||||
| 		setDaemon(true); |  | ||||||
| 		if (port <= 0 || port > 65535) throw new IllegalArgumentException("le numéro de port est invalide"); |  | ||||||
| 		socketName = sckName; |  | ||||||
| 		this.port = port; |  | ||||||
| 		this.password = password; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	@Override |  | ||||||
| 	public void run() { |  | ||||||
|  |  | ||||||
| 		try { |  | ||||||
| 			 |  | ||||||
| 			socket = new ServerSocket(); |  | ||||||
| 			socket.setReceiveBufferSize(PSocket.NETWORK_TCP_BUFFER_SIZE); |  | ||||||
| 			socket.setPerformancePreferences(0, 1, 0); |  | ||||||
| 			socket.bind(new InetSocketAddress(port)); |  | ||||||
| 			 |  | ||||||
| 			while (true) { |  | ||||||
| 				Socket socketClient = socket.accept(); |  | ||||||
| 				socketClient.setSendBufferSize(PSocket.NETWORK_TCP_BUFFER_SIZE); |  | ||||||
| 				socketClient.setSoTimeout(PSocket.NETWORK_TIMEOUT); |  | ||||||
|  |  | ||||||
| 				TCPServerClientConnection co = new TCPServerClientConnection(socketClient, |  | ||||||
| 						connectionCounterId.getAndIncrement()); |  | ||||||
| 				co.start(); |  | ||||||
| 			} |  | ||||||
| 		} catch (SocketException ignored) { |  | ||||||
| 		} catch (Exception e) { |  | ||||||
| 			Log.warning("Plus aucune connexion ne peux être acceptée", e); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
|  |  | ||||||
| 	 |  | ||||||
| 	public void addPacketListener(PPacketListener<PPacket> l) { |  | ||||||
| 		globalPacketListeners.add(l); |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	public boolean removePacketListener(PPacketListener<PPacket> l) { |  | ||||||
| 		return globalPacketListeners.remove(l); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	public void addConnectionListener(PSocketConnectionListener l) { |  | ||||||
| 		clientConnectionListeners.add(l); |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	public void removeConnectionListener(PSocketConnectionListener l) { |  | ||||||
| 		clientConnectionListeners.remove(l); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	protected class TCPServerClientConnection extends PSocket { |  | ||||||
| 		 |  | ||||||
| 		boolean loggedIn; |  | ||||||
|  |  | ||||||
| 		private TCPServerClientConnection(Socket s, int coId) { |  | ||||||
| 			super(s, "Conn#" + coId + " via PServer " + socketName, password); |  | ||||||
| 			addConnectionListener(new PSocketConnectionListener() { |  | ||||||
| 				@Override |  | ||||||
| 				public void onDisconnect(PSocket connection) { |  | ||||||
| 					try { |  | ||||||
| 						clientConnectionListeners.forEach(l -> l.onDisconnect(connection)); |  | ||||||
| 					} finally { |  | ||||||
| 						clients.remove((TCPServerClientConnection)connection); |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 				@Override |  | ||||||
| 				public void onConnect(PSocket connection) { |  | ||||||
| 					clients.add((TCPServerClientConnection)connection); |  | ||||||
| 					clientConnectionListeners.forEach(l -> l.onConnect(connection)); |  | ||||||
| 				} |  | ||||||
| 			}); |  | ||||||
| 			addPacketListener((conn, packet) -> |  | ||||||
| 					globalPacketListeners.forEach(l -> { |  | ||||||
| 						try { |  | ||||||
| 							l.onPacketReceive(conn, packet); |  | ||||||
| 						} catch (Exception e) { |  | ||||||
| 							Log.severe("Exception while calling PPacketListener.onPacketReceive().", e); |  | ||||||
| 							sendSilently(PPacketAnswer.buildExceptionPacket(packet, e.toString())); |  | ||||||
| 						} |  | ||||||
| 					}) |  | ||||||
| 			); |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	@Override |  | ||||||
| 	public void close() { |  | ||||||
| 		try { |  | ||||||
| 			if (isClosed.get()) return; |  | ||||||
| 			isClosed.set(true); |  | ||||||
|  |  | ||||||
| 			clients.forEach(PSocket::close); |  | ||||||
|  |  | ||||||
| 			socket.close(); |  | ||||||
| 		} catch (IOException ignored) {} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	public boolean isClosed() { |  | ||||||
| 		return isClosed.get() || socket.isClosed(); |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	public List<PSocket> getClients() { |  | ||||||
| 		synchronized (clients) { |  | ||||||
| 			return new ArrayList<>(clients); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
|  |  | ||||||
| 	 |  | ||||||
| 	@Override |  | ||||||
| 	public String toString() { |  | ||||||
| 		return this.getClass().getName() + "{thread=" + getName() + ", socket=" + socket + "}"; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| } |  | ||||||
| @@ -1,350 +0,0 @@ | |||||||
| package fr.pandacube.lib.net; |  | ||||||
|  |  | ||||||
| import java.io.BufferedOutputStream; |  | ||||||
| import java.io.Closeable; |  | ||||||
| import java.io.DataInputStream; |  | ||||||
| import java.io.DataOutputStream; |  | ||||||
| import java.io.IOException; |  | ||||||
| import java.net.ServerSocket; |  | ||||||
| import java.net.Socket; |  | ||||||
| import java.net.SocketAddress; |  | ||||||
| import java.util.ArrayList; |  | ||||||
| import java.util.Collections; |  | ||||||
| import java.util.HashMap; |  | ||||||
| import java.util.List; |  | ||||||
| import java.util.Map; |  | ||||||
| import java.util.Objects; |  | ||||||
| import java.util.concurrent.atomic.AtomicBoolean; |  | ||||||
|  |  | ||||||
| import com.google.common.annotations.Beta; |  | ||||||
|  |  | ||||||
| import fr.pandacube.lib.util.Log; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * A wrapper for a {@link Socket}. The connection must point to a software using {@link PServer} |  | ||||||
|  * as wrapper for the target {@link ServerSocket}. |  | ||||||
|  * <br> |  | ||||||
|  * This class provides a simple way to exchange data between client and server : |  | ||||||
|  * <ul> |  | ||||||
|  * <li>Maintained connection with the server</li> |  | ||||||
|  * <li>Login with a password (send in the first packet)</li> |  | ||||||
|  * <li>Binary packet id</li> |  | ||||||
|  * <li>Binary data</li> |  | ||||||
|  * <li>Input stream in a separate Thread</li> |  | ||||||
|  * </ul> |  | ||||||
|  *  |  | ||||||
|  */ |  | ||||||
| @Beta |  | ||||||
| public class PSocket extends Thread implements Closeable { |  | ||||||
| 	 |  | ||||||
| 	public static final int NETWORK_TCP_BUFFER_SIZE = 1024 * 1024; |  | ||||||
|  |  | ||||||
| 	public static final int NETWORK_TIMEOUT = 0; // no timeout (milli-seconds) |  | ||||||
|  |  | ||||||
| 	private boolean server = false; |  | ||||||
| 	private Socket socket; |  | ||||||
| 	private final SocketAddress addr; |  | ||||||
| 	private DataInputStream in; |  | ||||||
| 	private DataOutputStream out; |  | ||||||
| 	private final Object outSynchronizer = new Object(); |  | ||||||
| 	private String password; |  | ||||||
| 	 |  | ||||||
| 	private final AtomicBoolean isClosed = new AtomicBoolean(false); |  | ||||||
|  |  | ||||||
| 	private final List<PPacketListener<PPacket>> packetListeners = Collections.synchronizedList(new ArrayList<>()); |  | ||||||
| 	private final List<PSocketConnectionListener> connectionListeners = Collections.synchronizedList(new ArrayList<>()); |  | ||||||
| 	private final Map<Integer, PPacketListener<PPacketAnswer>> answersCallbacks = Collections.synchronizedMap(new HashMap<>()); |  | ||||||
| 	 |  | ||||||
| 	private int nextSendId = 0; |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * Create a new PSocket that will connect to the specified SocketAddress. |  | ||||||
| 	 * @param a The target server to connect to |  | ||||||
| 	 * @param connName the name of the connection, used to name the Thread used to receive the packet. |  | ||||||
| 	 * @param pass the password to send to the server. |  | ||||||
| 	 */ |  | ||||||
| 	public PSocket(SocketAddress a, String connName, String pass) { |  | ||||||
| 		super("PSocket " + connName); |  | ||||||
| 		setDaemon(true); |  | ||||||
| 		if (a == null) throw new IllegalArgumentException("les arguments ne peuvent pas être null"); |  | ||||||
| 		addr = a; |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	/* package */ PSocket(Socket s, String connName, String pass) { |  | ||||||
| 		this(s.getRemoteSocketAddress(), connName, pass); |  | ||||||
| 		socket = s; |  | ||||||
| 		server = true; |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
|  |  | ||||||
| 	@Override |  | ||||||
| 	public void run() { |  | ||||||
|  |  | ||||||
| 		try { |  | ||||||
| 			if (socket == null) { |  | ||||||
| 				socket = new Socket(); |  | ||||||
| 				socket.setReceiveBufferSize(NETWORK_TCP_BUFFER_SIZE); |  | ||||||
| 				socket.setSendBufferSize(NETWORK_TCP_BUFFER_SIZE); |  | ||||||
| 				 |  | ||||||
| 				socket.setSoTimeout(10000); // initial timeout before login |  | ||||||
| 			 |  | ||||||
| 				socket.connect(addr); |  | ||||||
| 				 |  | ||||||
| 				in = new DataInputStream(socket.getInputStream()); |  | ||||||
| 				out = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream())); |  | ||||||
| 			} |  | ||||||
| 			 |  | ||||||
| 			// password check |  | ||||||
| 			if (server) { |  | ||||||
| 				PPacket packet = readPacket(); |  | ||||||
| 				if (packet == null || packet instanceof PPacketAnswer || !"login".equals(packet.name)) { |  | ||||||
| 					send(PPacket.buildLoginBadPacket()); |  | ||||||
| 					close(); |  | ||||||
| 					return; |  | ||||||
| 				} |  | ||||||
| 				try { |  | ||||||
| 					String receivedPassword = new ByteBuffer(packet.content).getString(); |  | ||||||
| 					if (!Objects.equals(receivedPassword, password)) { |  | ||||||
| 						send(PPacket.buildLoginBadPacket()); |  | ||||||
| 						close(); |  | ||||||
| 						return; |  | ||||||
| 					} |  | ||||||
| 				} catch(Exception e) { |  | ||||||
| 					send(PPacket.buildLoginBadPacket()); |  | ||||||
| 					close(); |  | ||||||
| 					return; |  | ||||||
| 				} |  | ||||||
| 				send(PPacketAnswer.buildLoginOkPacket(packet)); |  | ||||||
| 				// login ok at this point |  | ||||||
| 			} |  | ||||||
| 			else { |  | ||||||
| 				send(PPacket.buildLoginPacket(password)); |  | ||||||
| 				PPacket packet = readPacket(); |  | ||||||
| 				if (packet == null) { |  | ||||||
| 					Log.severe("bad packet received from server. Disconnecting."); |  | ||||||
| 					close(); |  | ||||||
| 					return; |  | ||||||
| 				} |  | ||||||
| 				if (packet.name.equals("login_bad")) { |  | ||||||
| 					Log.severe("Wrong password to connect to server. Disconnecting."); |  | ||||||
| 					close(); |  | ||||||
| 					return; |  | ||||||
| 				} |  | ||||||
| 				if (!packet.name.equals("login_ok")) { |  | ||||||
| 					Log.severe("Unexpected packet from server. Disconnecting."); |  | ||||||
| 					close(); |  | ||||||
| 					return; |  | ||||||
| 				} |  | ||||||
| 				// login ok at this point |  | ||||||
| 			} |  | ||||||
| 			password = null; |  | ||||||
|  |  | ||||||
| 			socket.setSoTimeout(NETWORK_TIMEOUT); |  | ||||||
| 			 |  | ||||||
| 			Log.info(getName() + " connected."); |  | ||||||
| 			 |  | ||||||
| 			connectionListeners.forEach(l -> { |  | ||||||
| 				try { |  | ||||||
| 					l.onConnect(this); |  | ||||||
| 				} catch (Exception e) { |  | ||||||
| 					Log.severe("Exception while calling PSocketConnectionListener.onConnect().", e); |  | ||||||
| 				} |  | ||||||
| 			}); |  | ||||||
| 			 |  | ||||||
| 			while (!socket.isClosed()) { |  | ||||||
| 				PPacket packet = readPacket(); |  | ||||||
| 				 |  | ||||||
| 				if (packet == null) { |  | ||||||
| 					send(PPacket.buildBadFormatPacket("Bad format for the last packet received. Closing connection.")); |  | ||||||
| 					break; |  | ||||||
| 				} |  | ||||||
|  |  | ||||||
| 				if (packet instanceof PPacketAnswer) { |  | ||||||
| 					try { |  | ||||||
| 						answersCallbacks.remove(((PPacketAnswer)packet).answer).onPacketReceive(this, (PPacketAnswer)packet); |  | ||||||
| 					} catch (Exception e) { |  | ||||||
| 						Log.severe("Exception while calling PPacketListener.onPacketReceive().", e); |  | ||||||
| 						send(PPacketAnswer.buildExceptionPacket(packet, e.toString())); |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 				else { |  | ||||||
| 					packetListeners.forEach(l -> { |  | ||||||
| 						try { |  | ||||||
| 							l.onPacketReceive(this, packet); |  | ||||||
| 						} catch (Exception e) { |  | ||||||
| 							Log.severe("Exception while calling PPacketListener.onPacketReceive().", e); |  | ||||||
| 							sendSilently(PPacketAnswer.buildExceptionPacket(packet, e.toString())); |  | ||||||
| 						} |  | ||||||
| 					}); |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 		} catch (Exception e) { |  | ||||||
| 			Log.severe(e); |  | ||||||
| 		} |  | ||||||
| 		close(); |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	/** |  | ||||||
| 	 * Return the packet read in the socket, or null if the packet is in a bad format. |  | ||||||
| 	 * @return the packet |  | ||||||
| 	 *  |  | ||||||
| 	 */ |  | ||||||
| 	private PPacket readPacket() throws IOException { |  | ||||||
| 		byte nSize = in.readByte(); |  | ||||||
| 		if (nSize == 0) { |  | ||||||
| 			return null; |  | ||||||
| 		} |  | ||||||
| 		boolean answer = nSize < 0; |  | ||||||
| 		if (answer) |  | ||||||
| 			nSize *= -1; |  | ||||||
| 		 |  | ||||||
| 		 |  | ||||||
| 		byte[] nBytes = new byte[nSize]; |  | ||||||
| 		in.readFully(nBytes); |  | ||||||
| 		String name = new String(nBytes, ByteBuffer.NETWORK_CHARSET); |  | ||||||
| 		 |  | ||||||
| 		 |  | ||||||
| 		int packetId = in.readInt(); |  | ||||||
| 		 |  | ||||||
| 		 |  | ||||||
| 		int answerId = (answer) ? in.readInt() : -1; |  | ||||||
| 		 |  | ||||||
| 		 |  | ||||||
| 		int cSize = in.readInt(); |  | ||||||
| 		if (cSize < 0 || cSize > 0xFFFFFF) { // can't be more that 16 MiB |  | ||||||
| 			return null; |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		 |  | ||||||
| 		byte[] content = new byte[cSize]; |  | ||||||
| 		in.readFully(content); |  | ||||||
| 		 |  | ||||||
| 		return answer ? new PPacketAnswer(name, packetId, answerId, content) : new PPacket(name, packetId, content); |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	/** |  | ||||||
| 	 * Send the provided packet, without waiting for an answer. |  | ||||||
| 	 */ |  | ||||||
| 	public void send(PPacket packet) throws IOException { |  | ||||||
| 		if (packet == null) |  | ||||||
| 			throw new IllegalArgumentException("packet can't be null"); |  | ||||||
| 		if (packet.name == null) |  | ||||||
| 			throw new IllegalArgumentException("packet.name can't be null"); |  | ||||||
| 		if (packet.content == null) |  | ||||||
| 			throw new IllegalArgumentException("packet.content can't be null"); |  | ||||||
| 		 |  | ||||||
| 		byte[] nameBytes = packet.name.getBytes(ByteBuffer.NETWORK_CHARSET); |  | ||||||
| 		if (nameBytes.length > 127) |  | ||||||
| 			throw new IllegalArgumentException("packet.name must take fewer than 128 bytes when converted to UTF-8"); |  | ||||||
| 		byte nameSize = (byte)nameBytes.length; |  | ||||||
| 		 |  | ||||||
| 		boolean answer = packet instanceof PPacketAnswer; |  | ||||||
| 		 |  | ||||||
| 		if (answer) nameSize *= -1; |  | ||||||
| 			 |  | ||||||
| 		synchronized (outSynchronizer) { |  | ||||||
| 			int packetId = nextSendId++; |  | ||||||
| 			 |  | ||||||
| 			packet.id = packetId; |  | ||||||
| 			 |  | ||||||
| 			out.write(new byte[] {nameSize}); |  | ||||||
| 			out.write(nameBytes); |  | ||||||
| 			out.write(packetId); |  | ||||||
| 			if (answer) |  | ||||||
| 				out.write(((PPacketAnswer)packet).answer); |  | ||||||
| 			out.write(packet.content.length); |  | ||||||
| 			out.write(packet.content); |  | ||||||
| 			out.flush(); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	public void sendSilently(PPacket packet) { |  | ||||||
| 		try { |  | ||||||
| 			send(packet); |  | ||||||
| 		} catch (IOException ignored) {} |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	public void send(PPacket packet, PPacketListener<PPacketAnswer> answerCallback) throws IOException { |  | ||||||
| 		synchronized (answersCallbacks) { |  | ||||||
| 			/* |  | ||||||
| 			 * This synch block ensure that the callback will be put in the listeners Map before |  | ||||||
| 			 * we receve the answer (in case this is really really fast) |  | ||||||
| 			 */ |  | ||||||
| 			send(packet); |  | ||||||
| 			answersCallbacks.put(packet.id, answerCallback); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	 |  | ||||||
| 	public void addPacketListener(PPacketListener<PPacket> l) { |  | ||||||
| 		packetListeners.add(l); |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	public boolean removePacketListener(PPacketListener<PPacket> l) { |  | ||||||
| 		return packetListeners.remove(l); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	 |  | ||||||
| 	public void addConnectionListener(PSocketConnectionListener l) { |  | ||||||
| 		connectionListeners.add(l); |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	public void removeConnectionListener(PSocketConnectionListener l) { |  | ||||||
| 		connectionListeners.remove(l); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	@Override |  | ||||||
| 	public void close() { |  | ||||||
| 		try { |  | ||||||
| 			synchronized (outSynchronizer) { |  | ||||||
| 				if (isClosed.get()) return; |  | ||||||
|  |  | ||||||
| 				Log.info(getName() + " closing..."); |  | ||||||
|  |  | ||||||
| 				connectionListeners.forEach(l -> { |  | ||||||
| 					try { |  | ||||||
| 						l.onDisconnect(this); |  | ||||||
| 					} catch (Exception e) { |  | ||||||
| 						Log.severe("Exception while calling PSocketConnectionListener.onDisconnect().", e); |  | ||||||
| 					} |  | ||||||
| 				}); |  | ||||||
| 				 |  | ||||||
| 				socket.close(); |  | ||||||
| 				isClosed.set(true); |  | ||||||
| 			} |  | ||||||
| 		} catch (IOException e) { |  | ||||||
| 			Log.warning(e); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	public SocketAddress getRemoteAddress() { |  | ||||||
| 		return addr; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	public boolean isClosed() { |  | ||||||
| 		return isClosed.get() || socket.isClosed(); |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	@Override |  | ||||||
| 	public String toString() { |  | ||||||
| 		return this.getClass().getName() + "{thread=" + getName() + ", socket=" + socket + "}"; |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| } |  | ||||||
| @@ -1,20 +0,0 @@ | |||||||
| package fr.pandacube.lib.net; |  | ||||||
|  |  | ||||||
| import com.google.common.annotations.Beta; |  | ||||||
|  |  | ||||||
| @Beta |  | ||||||
| public interface PSocketConnectionListener { |  | ||||||
|  |  | ||||||
| 	/** |  | ||||||
| 	 * Called when a socket is connected |  | ||||||
| 	 * @param connection the connection |  | ||||||
| 	 */ |  | ||||||
| 	void onConnect(PSocket connection); |  | ||||||
| 	 |  | ||||||
| 	/** |  | ||||||
| 	 * Called just before a socket is disconnected |  | ||||||
| 	 * @param connection the connection |  | ||||||
| 	 */ |  | ||||||
| 	void onDisconnect(PSocket connection); |  | ||||||
|  |  | ||||||
| } |  | ||||||
| @@ -21,4 +21,8 @@ | |||||||
|         </dependency> |         </dependency> | ||||||
|     </dependencies> |     </dependencies> | ||||||
|  |  | ||||||
|  |     <properties> | ||||||
|  |         <maven.javadoc.skip>true</maven.javadoc.skip> | ||||||
|  |     </properties> | ||||||
|  |  | ||||||
| </project> | </project> | ||||||
| @@ -18,7 +18,7 @@ public class ResponseAnalyser { | |||||||
| 		if (socket == null || socket.isClosed() || socket.isInputShutdown()) | 		if (socket == null || socket.isClosed() || socket.isInputShutdown()) | ||||||
| 			throw new IllegalArgumentException("le socket doit être non null et doit être ouvert sur le flux d'entrée"); | 			throw new IllegalArgumentException("le socket doit être non null et doit être ouvert sur le flux d'entrée"); | ||||||
|  |  | ||||||
| 		// on lis la réponse | 		// on lit la réponse | ||||||
| 		BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); | 		BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); | ||||||
|  |  | ||||||
| 		String line; | 		String line; | ||||||
|   | |||||||
| @@ -5,15 +5,15 @@ import java.io.PrintStream; | |||||||
| import java.net.InetAddress; | import java.net.InetAddress; | ||||||
| import java.net.Socket; | import java.net.Socket; | ||||||
|  |  | ||||||
| import fr.pandacube.lib.util.Log; | import fr.pandacube.lib.util.log.Log; | ||||||
|  |  | ||||||
| public abstract class AbstractRequestExecutor { | public abstract class AbstractRequestExecutor { | ||||||
|  |  | ||||||
| 	public final String command; | 	public final String command; | ||||||
|  |  | ||||||
| 	public AbstractRequestExecutor(String cmd, NetworkAPIListener napiListener) { | 	public AbstractRequestExecutor(String cmd, NetworkAPIListener nAPIListener) { | ||||||
| 		command = cmd.toLowerCase(); | 		command = cmd.toLowerCase(); | ||||||
| 		napiListener.registerRequestExecutor(command, this); | 		nAPIListener.registerRequestExecutor(command, this); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	public void execute(String data, Socket socket) throws IOException { | 	public void execute(String data, Socket socket) throws IOException { | ||||||
| @@ -34,9 +34,8 @@ public abstract class AbstractRequestExecutor { | |||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
| 	 * | 	 * | ||||||
| 	 * @param data La représentation sous forme de String des données envoyés | 	 * @param data The String representation of the request data. | ||||||
| 	 *        dans la requête | 	 * @return The response to send back to the client. | ||||||
| 	 * @return La réponse à retourner au client |  | ||||||
| 	 */ | 	 */ | ||||||
| 	protected abstract Response run(InetAddress source, String data); | 	protected abstract Response run(InetAddress source, String data); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| package fr.pandacube.lib.netapi.server; | 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.io.IOException; | ||||||
| import java.net.InetAddress; | import java.net.InetAddress; | ||||||
| @@ -18,7 +18,7 @@ public class NetworkAPIListener implements Runnable { | |||||||
| 	private final String name; | 	private final String name; | ||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
| 	 * Instencie le côté serveur du NetworkAPI. | 	 * Instancie le côté serveur du NetworkAPI. | ||||||
| 	 * | 	 * | ||||||
| 	 * @param n nom du networkAPI (permet l'identification dans les logs) | 	 * @param n nom du networkAPI (permet l'identification dans les logs) | ||||||
| 	 * @param p le port d'écoute | 	 * @param p le port d'écoute | ||||||
| @@ -29,7 +29,7 @@ public class NetworkAPIListener implements Runnable { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
| 	 * Instencie le côté serveur du NetworkAPI. | 	 * Instancie le côté serveur du NetworkAPI. | ||||||
| 	 * | 	 * | ||||||
| 	 * @param n nom du networkAPI (permet l'identification dans les logs) | 	 * @param n nom du networkAPI (permet l'identification dans les logs) | ||||||
| 	 * @param p le port d'écoute | 	 * @param p le port d'écoute | ||||||
| @@ -56,7 +56,6 @@ public class NetworkAPIListener implements Runnable { | |||||||
| 		Log.info("NetworkAPI '" + name + "' à l'écoute sur le socket " + serverSocket.getLocalSocketAddress()); | 		Log.info("NetworkAPI '" + name + "' à l'écoute sur le socket " + serverSocket.getLocalSocketAddress()); | ||||||
|  |  | ||||||
| 		try { | 		try { | ||||||
| 			// réception des connexion client |  | ||||||
| 			while (!serverSocket.isClosed()) { | 			while (!serverSocket.isClosed()) { | ||||||
| 				Thread t = new Thread(new PacketExecutor(serverSocket.accept(), this)); | 				Thread t = new Thread(new PacketExecutor(serverSocket.accept(), this)); | ||||||
| 				t.setDaemon(true); | 				t.setDaemon(true); | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ import java.io.PrintStream; | |||||||
| import java.net.Socket; | import java.net.Socket; | ||||||
|  |  | ||||||
| import fr.pandacube.lib.netapi.server.RequestAnalyser.BadRequestException; | 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 |  * Prends en charge un socket client et le transmet au gestionnaire de paquet | ||||||
| @@ -20,9 +20,9 @@ public class PacketExecutor implements Runnable { | |||||||
| 	private final Socket socket; | 	private final Socket socket; | ||||||
| 	private final NetworkAPIListener networkAPIListener; | 	private final NetworkAPIListener networkAPIListener; | ||||||
|  |  | ||||||
| 	public PacketExecutor(Socket s, NetworkAPIListener napiListener) { | 	public PacketExecutor(Socket s, NetworkAPIListener nAPIListener) { | ||||||
| 		socket = s; | 		socket = s; | ||||||
| 		networkAPIListener = napiListener; | 		networkAPIListener = nAPIListener; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	@Override | 	@Override | ||||||
|   | |||||||
| @@ -10,23 +10,23 @@ public class RequestAnalyser { | |||||||
| 	public final String command; | 	public final String command; | ||||||
| 	public final String data; | 	public final String data; | ||||||
|  |  | ||||||
| 	public RequestAnalyser(Socket socket, NetworkAPIListener napiListener) throws IOException, BadRequestException { | 	public RequestAnalyser(Socket socket, NetworkAPIListener nAPIListener) throws IOException, BadRequestException { | ||||||
| 		if (socket == null || socket.isClosed() || socket.isInputShutdown() || napiListener == null) | 		if (socket == null || socket.isClosed() || socket.isInputShutdown() || nAPIListener == null) | ||||||
| 			throw new IllegalArgumentException( | 			throw new IllegalArgumentException( | ||||||
| 					"le socket doit être non null et doit être ouvert sur le flux d'entrée et napiListener ne doit pas être null"); | 					"le socket doit être non null et doit être ouvert sur le flux d'entrée et nAPIListener ne doit pas être null"); | ||||||
|  |  | ||||||
| 		// on lis la réponse | 		// on lit la réponse | ||||||
| 		BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); | 		BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); | ||||||
|  |  | ||||||
| 		String line; | 		String line; | ||||||
|  |  | ||||||
| 		// lecture de la première ligne | 		// lecture de la première ligne | ||||||
| 		line = in.readLine(); | 		line = in.readLine(); | ||||||
| 		if (line == null || !line.equals(napiListener.pass)) throw new BadRequestException("wrong_password"); | 		if (line == null || !line.equals(nAPIListener.pass)) throw new BadRequestException("wrong_password"); | ||||||
|  |  | ||||||
| 		// lecture de la deuxième ligne | 		// lecture de la deuxième ligne | ||||||
| 		line = in.readLine(); | 		line = in.readLine(); | ||||||
| 		if (line == null || napiListener.getRequestExecutor(line) == null) | 		if (line == null || nAPIListener.getRequestExecutor(line) == null) | ||||||
| 			throw new BadRequestException("command_not_exists"); | 			throw new BadRequestException("command_not_exists"); | ||||||
| 		command = line; | 		command = line; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ | |||||||
|     <repositories> |     <repositories> | ||||||
|         <repository> |         <repository> | ||||||
|             <id>papermc</id> |             <id>papermc</id> | ||||||
|             <url>https://papermc.io/repo/repository/maven-public/</url> |             <url>https://repo.papermc.io/repository/maven-public/</url> | ||||||
|         </repository> |         </repository> | ||||||
|  |  | ||||||
|         <!-- WorldEdit --> |         <!-- WorldEdit --> | ||||||
| @@ -25,7 +25,7 @@ | |||||||
|             <url>https://maven.enginehub.org/repo/</url> |             <url>https://maven.enginehub.org/repo/</url> | ||||||
|         </repository> |         </repository> | ||||||
|  |  | ||||||
|         <!--  Vault and maybe other dependecies --> |         <!--  Vault and maybe other dependencies --> | ||||||
|         <repository> |         <repository> | ||||||
|             <id>jitpack.io</id> |             <id>jitpack.io</id> | ||||||
|             <url>https://jitpack.io</url> |             <url>https://jitpack.io</url> | ||||||
| @@ -33,12 +33,12 @@ | |||||||
|     </repositories> |     </repositories> | ||||||
|  |  | ||||||
|     <dependencies> |     <dependencies> | ||||||
|         <dependency> |         <!-- <dependency> | ||||||
|             <groupId>fr.pandacube.lib</groupId> |             <groupId>fr.pandacube.lib</groupId> | ||||||
|             <artifactId>pandalib-players-permissible</artifactId> |             <artifactId>pandalib-players-permissible</artifactId> | ||||||
|             <version>${project.version}</version> |             <version>${project.version}</version> | ||||||
|             <scope>provided</scope> |             <scope>provided</scope> | ||||||
|         </dependency> |         </dependency> --> | ||||||
|         <dependency> |         <dependency> | ||||||
|             <groupId>fr.pandacube.lib</groupId> |             <groupId>fr.pandacube.lib</groupId> | ||||||
|             <artifactId>pandalib-permissions</artifactId> |             <artifactId>pandalib-permissions</artifactId> | ||||||
| @@ -77,7 +77,7 @@ | |||||||
|         <dependency> |         <dependency> | ||||||
|             <groupId>com.sk89q.worldedit</groupId> |             <groupId>com.sk89q.worldedit</groupId> | ||||||
|             <artifactId>worldedit-bukkit</artifactId> |             <artifactId>worldedit-bukkit</artifactId> | ||||||
|             <version>7.2.9</version> |             <version>7.2.19</version> | ||||||
|             <scope>provided</scope> |             <scope>provided</scope> | ||||||
|             <exclusions> |             <exclusions> | ||||||
|                 <exclusion> |                 <exclusion> | ||||||
|   | |||||||
| @@ -23,11 +23,11 @@ import org.bukkit.permissions.ServerOperator; | |||||||
| import org.bukkit.plugin.java.JavaPlugin; | import org.bukkit.plugin.java.JavaPlugin; | ||||||
|  |  | ||||||
| import fr.pandacube.lib.permissions.Permissions; | 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. |  * Class that integrates the {@code pandalib-permissions} system into a Bukkit/Spigot/Paper instance. | ||||||
|  * The integration is made when calling {@link #init(JavaPlugin, String)}. |  * The integration is made when calling {@link #onLoad(JavaPlugin, String)} and {@link #onEnable()}. | ||||||
|  * The permission system must be initialized first, using {@link Permissions#init(Function)}. |  * The permission system must be initialized first, using {@link Permissions#init(Function)}. | ||||||
|  * Don’t forget that the permission system also needs a connection to a database, so don’t forget to call |  * Don’t forget that the permission system also needs a connection to a database, so don’t forget to call | ||||||
|  * {@link DB#init(DBConnection, String)} with the appropriate parameters before anything. |  * {@link DB#init(DBConnection, String)} with the appropriate parameters before anything. | ||||||
| @@ -38,18 +38,26 @@ public class PandalibPaperPermissions implements Listener { | |||||||
| 	/* package */ static String serverName; | 	/* package */ static String serverName; | ||||||
| 	/* package */ static final Map<String, String> permissionMap = new HashMap<>(); | 	/* package */ static final Map<String, String> permissionMap = new HashMap<>(); | ||||||
|  |  | ||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
| 	 * Integrates the {@code pandalib-permissions} system into the Bukkit server. | 	 * Integrates the {@code pandalib-permissions} system into the Bukkit server, during the loading phase of the plugin. | ||||||
| 	 * @param plugin a Bukkit plugin. | 	 * @param plugin a Bukkit plugin. | ||||||
| 	 * @param serverName the name of the current server, used to fetch server specific permissions. Cannot be null. | 	 * @param serverName the name of the current server, used to fetch server specific permissions. Cannot be null. | ||||||
| 	 *                   If this server in not in a multi-server configuration, use a dummy server name, like | 	 *                   If this server in not in a multiserver configuration, use a dummy server name, like | ||||||
| 	 *                   {@code ""} (empty string). | 	 *                   {@code ""} (empty string). | ||||||
| 	 */ | 	 */ | ||||||
| 	public static void init(JavaPlugin plugin, String serverName) { | 	public static void onLoad(JavaPlugin plugin, String serverName) { | ||||||
| 		PandalibPaperPermissions.plugin = plugin; | 		PandalibPaperPermissions.plugin = plugin; | ||||||
| 		PandalibPaperPermissions.serverName = serverName; | 		PandalibPaperPermissions.serverName = serverName; | ||||||
|  | 		PermissionsInjectorVault.onLoad(); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * Integrates the {@code pandalib-permissions} system into the Bukkit server, during the enabling phase of the plugin. | ||||||
|  | 	 */ | ||||||
|  | 	public static void onEnable() { | ||||||
| 		PermissionsInjectorBukkit.inject(Bukkit.getConsoleSender()); | 		PermissionsInjectorBukkit.inject(Bukkit.getConsoleSender()); | ||||||
| 		PermissionsInjectorVault.inject(); | 		PermissionsInjectorVault.onEnable(); | ||||||
| 		PermissionsInjectorWEPIF.inject(); | 		PermissionsInjectorWEPIF.inject(); | ||||||
|  |  | ||||||
| 		Bukkit.getPluginManager().registerEvents(new PandalibPaperPermissions(), plugin); | 		Bukkit.getPluginManager().registerEvents(new PandalibPaperPermissions(), plugin); | ||||||
| @@ -74,6 +82,12 @@ public class PandalibPaperPermissions implements Listener { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * Creates a {@link PandalibPaperPermissions} instance. | ||||||
|  | 	 */ | ||||||
|  | 	private PandalibPaperPermissions() {} | ||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
| 	 * Player login event handler. | 	 * Player login event handler. | ||||||
| 	 * @param event the event. | 	 * @param event the event. | ||||||
|   | |||||||
| @@ -1,5 +1,23 @@ | |||||||
| package fr.pandacube.lib.paper.permissions; | package fr.pandacube.lib.paper.permissions; | ||||||
|  |  | ||||||
|  | import com.google.common.cache.Cache; | ||||||
|  | import com.google.common.cache.CacheBuilder; | ||||||
|  | import com.google.common.cache.CacheLoader; | ||||||
|  | import com.google.common.cache.LoadingCache; | ||||||
|  | import fr.pandacube.lib.permissions.Permissions; | ||||||
|  | import fr.pandacube.lib.reflect.Reflect; | ||||||
|  | import fr.pandacube.lib.util.log.Log; | ||||||
|  | import org.bukkit.command.CommandSender; | ||||||
|  | import org.bukkit.command.ConsoleCommandSender; | ||||||
|  | import org.bukkit.entity.Player; | ||||||
|  | import org.bukkit.permissions.Permissible; | ||||||
|  | import org.bukkit.permissions.PermissibleBase; | ||||||
|  | import org.bukkit.permissions.Permission; | ||||||
|  | import org.bukkit.permissions.PermissionAttachment; | ||||||
|  | import org.bukkit.permissions.PermissionAttachmentInfo; | ||||||
|  | import org.bukkit.plugin.Plugin; | ||||||
|  | import org.jetbrains.annotations.NotNull; | ||||||
|  |  | ||||||
| import java.lang.reflect.Field; | import java.lang.reflect.Field; | ||||||
| import java.util.HashMap; | import java.util.HashMap; | ||||||
| import java.util.LinkedHashSet; | import java.util.LinkedHashSet; | ||||||
| @@ -11,24 +29,6 @@ import java.util.concurrent.TimeUnit; | |||||||
| import java.util.function.Function; | import java.util.function.Function; | ||||||
| import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||||
|  |  | ||||||
| import com.google.common.cache.Cache; |  | ||||||
| import com.google.common.cache.CacheBuilder; |  | ||||||
| import com.google.common.cache.CacheLoader; |  | ||||||
| import com.google.common.cache.LoadingCache; |  | ||||||
| import org.bukkit.command.CommandSender; |  | ||||||
| import org.bukkit.command.ConsoleCommandSender; |  | ||||||
| import org.bukkit.entity.Player; |  | ||||||
| import org.bukkit.permissions.Permissible; |  | ||||||
| import org.bukkit.permissions.PermissibleBase; |  | ||||||
| import org.bukkit.permissions.Permission; |  | ||||||
| import org.bukkit.permissions.PermissionAttachment; |  | ||||||
| import org.bukkit.permissions.PermissionAttachmentInfo; |  | ||||||
| import org.bukkit.plugin.Plugin; |  | ||||||
|  |  | ||||||
| import fr.pandacube.lib.permissions.Permissions; |  | ||||||
| import fr.pandacube.lib.reflect.Reflect; |  | ||||||
| import fr.pandacube.lib.util.Log; |  | ||||||
|  |  | ||||||
| /* package */ class PermissionsInjectorBukkit | /* package */ class PermissionsInjectorBukkit | ||||||
| { | { | ||||||
| 	 | 	 | ||||||
| @@ -58,26 +58,24 @@ import fr.pandacube.lib.util.Log; | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private static void setPermissible(CommandSender sender, Permissible newpermissible) |     private static void setPermissible(CommandSender sender, Permissible newPermissible) | ||||||
|     { |     { | ||||||
|         try { |         try { | ||||||
|             Field perm = getPermField(sender); |             Field perm = getPermField(sender); | ||||||
|             if (perm == null) |             if (perm == null) | ||||||
|                 return; |                 return; | ||||||
|             perm.setAccessible(true); |             perm.setAccessible(true); | ||||||
|             perm.set(sender, newpermissible); |             perm.set(sender, newPermissible); | ||||||
|         } |         } | ||||||
|         catch (Exception e) { |         catch (Exception e) { | ||||||
|             Log.severe(e); |             throw new RuntimeException(e); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /* package */ static Permissible getPermissible(CommandSender sender) |     /* package */ static Permissible getPermissible(CommandSender sender) | ||||||
|     { |     { | ||||||
|         Field perm = getPermField(sender); |  | ||||||
|         if (perm == null) |  | ||||||
|             return null; |  | ||||||
|         try { |         try { | ||||||
|  |             Field perm = getPermField(sender); | ||||||
|             perm.setAccessible(true); |             perm.setAccessible(true); | ||||||
|             Permissible p = (Permissible) perm.get(sender); |             Permissible p = (Permissible) perm.get(sender); | ||||||
|             if (p == null) { |             if (p == null) { | ||||||
| @@ -86,26 +84,19 @@ import fr.pandacube.lib.util.Log; | |||||||
|             return p; |             return p; | ||||||
|         } |         } | ||||||
|         catch (Exception e) { |         catch (Exception e) { | ||||||
|             Log.severe(e); |             throw new RuntimeException(e); | ||||||
|         } |         } | ||||||
|         return null; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private static Field getPermField(CommandSender sender) |     private static Field getPermField(CommandSender sender) throws NoSuchFieldException { | ||||||
|     { |  | ||||||
|     	if (sender == null) { |     	if (sender == null) { | ||||||
|     		throw new IllegalArgumentException("sender cannot be null"); |     		throw new IllegalArgumentException("sender cannot be null"); | ||||||
|     	} |     	} | ||||||
|         try { |         if (sender instanceof Player || sender instanceof ConsoleCommandSender) | ||||||
|             if (sender instanceof Player || sender instanceof ConsoleCommandSender) |             return Reflect.ofClassOfInstance(sender).field("perm").get(); | ||||||
|                 return Reflect.ofClassOfInstance(sender).field("perm").get(); |         else | ||||||
|             else |             throw new IllegalArgumentException("Unsupported type for sender: " + sender.getClass()); | ||||||
|             	throw new IllegalArgumentException("Unsupported type for sender: " + sender.getClass()); |  | ||||||
|         } |  | ||||||
|         catch (Exception e) { |  | ||||||
|             Log.severe(e); |  | ||||||
|         } |  | ||||||
|         return null; |  | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     /* package */ static class PandaPermissible extends PermissibleBase |     /* package */ static class PandaPermissible extends PermissibleBase | ||||||
| @@ -118,7 +109,7 @@ import fr.pandacube.lib.util.Log; | |||||||
|  |  | ||||||
|         @SuppressWarnings("UnusedAssignment") |         @SuppressWarnings("UnusedAssignment") | ||||||
|         private boolean init = false; |         private boolean init = false; | ||||||
|         /* assigment to false is necessary because of super class constructor calling the method recalculatePermission() |         /* assignment to false is necessary because of super class constructor calling the method recalculatePermission() | ||||||
|          * and we don’t want that. |          * and we don’t want that. | ||||||
|          */ |          */ | ||||||
|  |  | ||||||
| @@ -143,7 +134,7 @@ import fr.pandacube.lib.util.Log; | |||||||
|         public boolean hasPermission(String permission) |         public boolean hasPermission(String permission) | ||||||
|         { |         { | ||||||
|         	/* |         	/* | ||||||
|         	 * WARNING: don’t call PermissibleOnlinePlayer#hasPermission(String) here or it will result on a stack overflow |         	 * WARNING: don’t call PermissibleOnlinePlayer#hasPermission(String) here, or it will result on a stack overflow | ||||||
|         	 */ |         	 */ | ||||||
|         	 |         	 | ||||||
|         	if (permission.toLowerCase().startsWith("minecraft.command.")) |         	if (permission.toLowerCase().startsWith("minecraft.command.")) | ||||||
| @@ -180,7 +171,7 @@ import fr.pandacube.lib.util.Log; | |||||||
|         	if (res != null) |         	if (res != null) | ||||||
|         		return res; |         		return res; | ||||||
|  |  | ||||||
|         	return oldPermissible.hasPermission(permission); // doesn’t need to manage negative permission (should not happend) |         	return oldPermissible.hasPermission(permission); // doesn't need to manage negative permission (should not happen) | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         @Override |         @Override | ||||||
| @@ -214,36 +205,34 @@ import fr.pandacube.lib.util.Log; | |||||||
|     			.build(); |     			.build(); | ||||||
|  |  | ||||||
|         @Override |         @Override | ||||||
|         public Set<PermissionAttachmentInfo> getEffectivePermissions() |         public @NotNull Set<PermissionAttachmentInfo> getEffectivePermissions() | ||||||
|         { |         { | ||||||
|         	// PlotSquared uses this method to optimize permission range (plots.limit.10 for example) |         	// PlotSquared uses this method to optimize permission range (plots.limit.10 for example) | ||||||
|         	// MobArena uses this method when a player leave the arena |         	// MobArena uses this method when a player leave the arena | ||||||
|         	// LibsDisguises uses this method (and only this one) to parse all the permissions |         	// LibsDisguises uses this method (and only this one) to parse all the permissions | ||||||
|         	 |  | ||||||
|         	//Log.warning("There is a plugin calling CommandSender#getEffectivePermissions(). See the stacktrace to understand the reason for that.", new Throwable()); |  | ||||||
|  |  | ||||||
|         	String world = null; |  | ||||||
|     		if (sender instanceof Player player) { |     		if (sender instanceof Player player) { | ||||||
|                 world = player.getWorld().getName(); |                 String world = player.getWorld().getName(); | ||||||
|  |                 try { | ||||||
|  |                     return effectivePermissionsListCache.get(world, () -> { | ||||||
|  |                         // first get the superperms effective permissions (that take isOp into account) | ||||||
|  |                         Map<String, PermissionAttachmentInfo> perms = oldPermissible.getEffectivePermissions().stream() | ||||||
|  |                                 .collect(Collectors.toMap(PermissionAttachmentInfo::getPermission, Function.identity())); | ||||||
|  |  | ||||||
|  |                         // then override them with the permissions from our permission system (that has priority, and take current world into account) | ||||||
|  |                         for (Map.Entry<String, Boolean> permE : getEffectivePermissionsOnServerInWorld().entrySet()) { | ||||||
|  |                             perms.put(permE.getKey(), new PermissionAttachmentInfo(this, permE.getKey(), null, permE.getValue())); | ||||||
|  |                         } | ||||||
|  |  | ||||||
|  |                         return new LinkedHashSet<>(perms.values()); | ||||||
|  |                     }); | ||||||
|  |                 } catch (ExecutionException e) { | ||||||
|  |                     Log.severe(e); | ||||||
|  |                 } | ||||||
|     		} |     		} | ||||||
|         	 |         	 | ||||||
|     		try { |  | ||||||
| 				return effectivePermissionsListCache.get(world, () -> { |             return oldPermissible.getEffectivePermissions(); | ||||||
| 					// first get the superperms effective permissions (taht take isOp into accound) |  | ||||||
| 					Map<String, PermissionAttachmentInfo> perms = oldPermissible.getEffectivePermissions().stream() |  | ||||||
| 							.collect(Collectors.toMap(PermissionAttachmentInfo::getPermission, Function.identity())); |  | ||||||
| 					 |  | ||||||
| 					// then override them with the permissions from our permission system (that has priority, and take current world into account) |  | ||||||
| 					for (Map.Entry<String, Boolean> permE : getEffectivePermissionsOnServerInWorld().entrySet()) { |  | ||||||
| 						perms.put(permE.getKey(), new PermissionAttachmentInfo(this, permE.getKey(), null, permE.getValue())); |  | ||||||
| 					} |  | ||||||
| 					 |  | ||||||
| 				    return new LinkedHashSet<>(perms.values()); |  | ||||||
| 				}); |  | ||||||
| 			} catch (ExecutionException e) { |  | ||||||
| 				Log.severe(e); |  | ||||||
| 				return oldPermissible.getEffectivePermissions(); |  | ||||||
| 			} |  | ||||||
|         	 |         	 | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -260,7 +249,7 @@ import fr.pandacube.lib.util.Log; | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         @Override |         @Override | ||||||
|         public boolean isPermissionSet(String permission) |         public boolean isPermissionSet(@NotNull String permission) | ||||||
|         { |         { | ||||||
|         	Boolean res = hasPermissionOnServerInWorld(permission); |         	Boolean res = hasPermissionOnServerInWorld(permission); | ||||||
|         	if (res != null) |         	if (res != null) | ||||||
| @@ -278,31 +267,31 @@ import fr.pandacube.lib.util.Log; | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         @Override |         @Override | ||||||
|         public PermissionAttachment addAttachment(Plugin plugin) |         public @NotNull PermissionAttachment addAttachment(@NotNull Plugin plugin) | ||||||
|         { |         { | ||||||
|             return oldPermissible.addAttachment(plugin); |             return oldPermissible.addAttachment(plugin); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         @Override |         @Override | ||||||
|         public PermissionAttachment addAttachment(Plugin plugin, int ticks) |         public PermissionAttachment addAttachment(@NotNull Plugin plugin, int ticks) | ||||||
|         { |         { | ||||||
|             return oldPermissible.addAttachment(plugin, ticks); |             return oldPermissible.addAttachment(plugin, ticks); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         @Override |         @Override | ||||||
|         public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value) |         public @NotNull PermissionAttachment addAttachment(@NotNull Plugin plugin, @NotNull String name, boolean value) | ||||||
|         { |         { | ||||||
|             return oldPermissible.addAttachment(plugin, name, value); |             return oldPermissible.addAttachment(plugin, name, value); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         @Override |         @Override | ||||||
|         public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value, int ticks) |         public PermissionAttachment addAttachment(@NotNull Plugin plugin, @NotNull String name, boolean value, int ticks) | ||||||
|         { |         { | ||||||
|             return oldPermissible.addAttachment(plugin, name, value, ticks); |             return oldPermissible.addAttachment(plugin, name, value, ticks); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         @Override |         @Override | ||||||
|         public void removeAttachment(PermissionAttachment attachment) |         public void removeAttachment(@NotNull PermissionAttachment attachment) | ||||||
|         { |         { | ||||||
|             oldPermissible.removeAttachment(attachment); |             oldPermissible.removeAttachment(attachment); | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -2,7 +2,9 @@ package fr.pandacube.lib.paper.permissions; | |||||||
|  |  | ||||||
| import fr.pandacube.lib.permissions.PermGroup; | import fr.pandacube.lib.permissions.PermGroup; | ||||||
| import fr.pandacube.lib.permissions.Permissions; | 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.Bukkit; | ||||||
| import org.bukkit.OfflinePlayer; | import org.bukkit.OfflinePlayer; | ||||||
| import org.bukkit.plugin.ServicePriority; | import org.bukkit.plugin.ServicePriority; | ||||||
| @@ -10,28 +12,55 @@ import org.bukkit.plugin.ServicePriority; | |||||||
| import java.util.List; | import java.util.List; | ||||||
|  |  | ||||||
| /* package */ class PermissionsInjectorVault { | /* package */ class PermissionsInjectorVault { | ||||||
|  |  | ||||||
|  | 	private static final ServicePriority servicePriority = ServicePriority.Highest; | ||||||
| 	 | 	 | ||||||
| 	public static PandaVaultPermission permInstance; | 	public static PandaVaultPermission permInstance; | ||||||
| 	 |  | ||||||
| 	public static void inject() { | 	/** | ||||||
|  | 	 * Vault injection needs to happen as soon as possible so other plugins detects it when they load. | ||||||
|  | 	 */ | ||||||
|  | 	public static void onLoad() { | ||||||
| 		try { | 		try { | ||||||
| 			permInstance = new PandaVaultPermission(); | 			permInstance = new PandaVaultPermission(); | ||||||
| 			PandaVaultChat chat = new PandaVaultChat(permInstance); | 			PandaVaultChat chat = new PandaVaultChat(permInstance); | ||||||
| 			Bukkit.getServicesManager().register(net.milkbowl.vault.permission.Permission.class, permInstance, | 			Bukkit.getServicesManager().register(Permission.class, permInstance, | ||||||
| 					PandalibPaperPermissions.plugin, ServicePriority.High); | 					PandalibPaperPermissions.plugin, servicePriority); | ||||||
| 			Bukkit.getServicesManager().register(net.milkbowl.vault.chat.Chat.class, chat, | 			Bukkit.getServicesManager().register(Chat.class, chat, | ||||||
| 					PandalibPaperPermissions.plugin, ServicePriority.High); | 					PandalibPaperPermissions.plugin, servicePriority); | ||||||
| 			Log.info("Providing permissions and chat prefix/suffix through Vault API."); | 			Log.info("Providing permissions and chat prefix/suffix through Vault API."); | ||||||
| 		} catch (NoClassDefFoundError e) { | 		} catch (NoClassDefFoundError e) { | ||||||
| 			Log.warning("Vault plugin not detected. Not using it to provide permissions and prefix/suffix." + e.getMessage()); | 			Log.warning("Vault plugin not detected. Not using it to provide permissions and prefix/suffix." + e.getMessage()); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | 	 | ||||||
|  | 	public static void onEnable() { | ||||||
|  | 		Bukkit.getScheduler().runTaskLater(PandalibPaperPermissions.plugin, | ||||||
|  | 				PermissionsInjectorVault::checkServicesRegistration, 1); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 	private static void checkServicesRegistration() { | ||||||
|  | 		Permission permService = Bukkit.getServicesManager().load(Permission.class); | ||||||
|  | 		if (!(permService instanceof PandaVaultPermission)) { | ||||||
|  | 			Log.severe("Check for Vault Permission service failed. " | ||||||
|  | 					+ (permService == null ? "Service manager returned null." | ||||||
|  | 					: ("Returned service is " + permService.getName() + " (" + permService.getClass().getName() + ")."))); | ||||||
|  |  | ||||||
|  | 		} | ||||||
|  | 		Chat chatService = Bukkit.getServicesManager().load(Chat.class); | ||||||
|  | 		if (!(chatService instanceof PandaVaultChat)) { | ||||||
|  | 			Log.severe("Check for Vault Chat service failed. " | ||||||
|  | 					+ (chatService == null ? "Service manager returned null." | ||||||
|  | 					: ("Returned service is " + chatService.getName() + " (" + chatService.getClass().getName() + ")."))); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| 	/* package */ static class PandaVaultPermission extends net.milkbowl.vault.permission.Permission { | 	/* package */ static class PandaVaultPermission extends Permission { | ||||||
| 		 | 		 | ||||||
| 		private PandaVaultPermission() { } | 		private PandaVaultPermission() { } | ||||||
|  |  | ||||||
| @@ -45,6 +74,11 @@ import java.util.List; | |||||||
| 			return PandalibPaperPermissions.plugin != null && PandalibPaperPermissions.plugin.isEnabled(); | 			return PandalibPaperPermissions.plugin != null && PandalibPaperPermissions.plugin.isEnabled(); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		private void checkEnabled() { | ||||||
|  | 			if (!isEnabled()) | ||||||
|  | 				throw new IllegalStateException("Cannot provide permission service because plugin is disabled."); | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		@Override | 		@Override | ||||||
| 		public boolean hasSuperPermsCompat() { | 		public boolean hasSuperPermsCompat() { | ||||||
| 			return true; | 			return true; | ||||||
| @@ -58,6 +92,7 @@ import java.util.List; | |||||||
| 		 | 		 | ||||||
| 		@Override | 		@Override | ||||||
| 		public boolean playerHas(String world, OfflinePlayer player, String permission) { | 		public boolean playerHas(String world, OfflinePlayer player, String permission) { | ||||||
|  | 			checkEnabled(); | ||||||
| 			Boolean res = Permissions.getPlayer(player.getUniqueId()).hasPermission(permission, PandalibPaperPermissions.serverName, world); | 			Boolean res = Permissions.getPlayer(player.getUniqueId()).hasPermission(permission, PandalibPaperPermissions.serverName, world); | ||||||
| 			if (res != null) | 			if (res != null) | ||||||
| 				return res; | 				return res; | ||||||
| @@ -77,8 +112,10 @@ import java.util.List; | |||||||
|  |  | ||||||
| 		@Override | 		@Override | ||||||
| 		public boolean playerAdd(String world, OfflinePlayer player, String permission) { | 		public boolean playerAdd(String world, OfflinePlayer player, String permission) { | ||||||
|  | 			checkEnabled(); | ||||||
| 			String server = PandalibPaperPermissions.serverName; | 			String server = PandalibPaperPermissions.serverName; | ||||||
| 			Permissions.getPlayer(player.getUniqueId()).addSelfPermission(permission, server, world); | 			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."); | 			Log.info("A plugin added permission " + permission + " (server=" + server + ",world=" + world + ") to player " + player.getName() + " through Vault."); | ||||||
| 			return true; | 			return true; | ||||||
| 		} | 		} | ||||||
| @@ -91,14 +128,17 @@ import java.util.List; | |||||||
|  |  | ||||||
| 		@Override | 		@Override | ||||||
| 		public boolean playerRemove(String world, OfflinePlayer player, String permission) { | 		public boolean playerRemove(String world, OfflinePlayer player, String permission) { | ||||||
|  | 			checkEnabled(); | ||||||
| 			String server = PandalibPaperPermissions.serverName; | 			String server = PandalibPaperPermissions.serverName; | ||||||
| 			Permissions.getPlayer(player.getUniqueId()).removeSelfPermission(permission, server, world); | 			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."); | 			Log.info("A plugin removed permission " + permission + " (server=" + server + ",world=" + world + ") to player " + player.getName() + " through Vault."); | ||||||
| 			return true; | 			return true; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		@Override | 		@Override | ||||||
| 		public boolean groupHas(String world, String group, String permission) { | 		public boolean groupHas(String world, String group, String permission) { | ||||||
|  | 			checkEnabled(); | ||||||
| 			Boolean res = Permissions.getGroup(group).hasPermission(permission, PandalibPaperPermissions.serverName, world); | 			Boolean res = Permissions.getGroup(group).hasPermission(permission, PandalibPaperPermissions.serverName, world); | ||||||
| 			if (res != null) | 			if (res != null) | ||||||
| 				return res; | 				return res; | ||||||
| @@ -112,14 +152,14 @@ import java.util.List; | |||||||
|  |  | ||||||
| 		@Override | 		@Override | ||||||
| 		public boolean groupAdd(String world, String group, String permission) { | 		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.")); | 					+ " through Vault but Pandalib does not support it.")); | ||||||
| 			return false; | 			return false; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		@Override | 		@Override | ||||||
| 		public boolean groupRemove(String world, String group, String permission) { | 		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.")); | 					+ " through Vault but Pandalib does not support it.")); | ||||||
| 			return false; | 			return false; | ||||||
| 		} | 		} | ||||||
| @@ -132,13 +172,14 @@ import java.util.List; | |||||||
| 		 | 		 | ||||||
| 		@Override | 		@Override | ||||||
| 		public boolean playerInGroup(String world, OfflinePlayer player, String group) { | 		public boolean playerInGroup(String world, OfflinePlayer player, String group) { | ||||||
|  | 			checkEnabled(); | ||||||
| 			return Permissions.getPlayer(player.getUniqueId()).isInGroup(group); | 			return Permissions.getPlayer(player.getUniqueId()).isInGroup(group); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		@Deprecated | 		@Deprecated | ||||||
| 		@Override | 		@Override | ||||||
| 		public boolean playerAddGroup(String world, String player, String group) { | 		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.")); | 					+ " through Vault but Pandalib does not support it.")); | ||||||
| 			return false; | 			return false; | ||||||
| 		} | 		} | ||||||
| @@ -146,7 +187,7 @@ import java.util.List; | |||||||
| 		@Deprecated | 		@Deprecated | ||||||
| 		@Override | 		@Override | ||||||
| 		public boolean playerRemoveGroup(String world, String player, String group) { | 		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.")); | 					+ " through Vault but Pandalib does not support it.")); | ||||||
| 			return false; | 			return false; | ||||||
| 		} | 		} | ||||||
| @@ -159,6 +200,7 @@ import java.util.List; | |||||||
| 		 | 		 | ||||||
| 		@Override | 		@Override | ||||||
| 		public String[] getPlayerGroups(String world, OfflinePlayer player) { | 		public String[] getPlayerGroups(String world, OfflinePlayer player) { | ||||||
|  | 			checkEnabled(); | ||||||
| 			List<String> groups = Permissions.getPlayer(player.getUniqueId()).getGroupsString(); | 			List<String> groups = Permissions.getPlayer(player.getUniqueId()).getGroupsString(); | ||||||
| 			return groups.toArray(new String[0]); | 			return groups.toArray(new String[0]); | ||||||
| 		} | 		} | ||||||
| @@ -171,12 +213,14 @@ import java.util.List; | |||||||
| 		 | 		 | ||||||
| 		@Override | 		@Override | ||||||
| 		public String getPrimaryGroup(String world, OfflinePlayer player) { | 		public String getPrimaryGroup(String world, OfflinePlayer player) { | ||||||
|  | 			checkEnabled(); | ||||||
| 			return Permissions.getPlayer(player.getUniqueId()).getGroupsString().stream() | 			return Permissions.getPlayer(player.getUniqueId()).getGroupsString().stream() | ||||||
| 					.findFirst().orElse(null); | 					.findFirst().orElse(null); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		@Override | 		@Override | ||||||
| 		public String[] getGroups() { | 		public String[] getGroups() { | ||||||
|  | 			checkEnabled(); | ||||||
| 			return Permissions.getGroups().stream() | 			return Permissions.getGroups().stream() | ||||||
| 					.map(PermGroup::getName).toArray(String[]::new); | 					.map(PermGroup::getName).toArray(String[]::new); | ||||||
| 		} | 		} | ||||||
| @@ -189,9 +233,9 @@ import java.util.List; | |||||||
| 	} | 	} | ||||||
| 	 | 	 | ||||||
| 	 | 	 | ||||||
| 	private static class PandaVaultChat extends net.milkbowl.vault.chat.Chat { | 	private static class PandaVaultChat extends Chat { | ||||||
|  |  | ||||||
| 		public PandaVaultChat(net.milkbowl.vault.permission.Permission perms) { | 		public PandaVaultChat(Permission perms) { | ||||||
| 			super(perms); | 			super(perms); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @@ -205,6 +249,11 @@ import java.util.List; | |||||||
| 			return PandalibPaperPermissions.plugin != null && PandalibPaperPermissions.plugin.isEnabled(); | 			return PandalibPaperPermissions.plugin != null && PandalibPaperPermissions.plugin.isEnabled(); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		private void checkEnabled() { | ||||||
|  | 			if (!isEnabled()) | ||||||
|  | 				throw new IllegalStateException("Cannot provide permission service because plugin is disabled."); | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		@Deprecated | 		@Deprecated | ||||||
| 		@Override | 		@Override | ||||||
| 		public String getPlayerPrefix(String world, String player) { | 		public String getPlayerPrefix(String world, String player) { | ||||||
| @@ -213,6 +262,7 @@ import java.util.List; | |||||||
| 		 | 		 | ||||||
| 		@Override | 		@Override | ||||||
| 		public String getPlayerPrefix(String world, OfflinePlayer player) { | 		public String getPlayerPrefix(String world, OfflinePlayer player) { | ||||||
|  | 			checkEnabled(); | ||||||
| 			return Permissions.getPlayer(player.getUniqueId()).getPrefix(); | 			return Permissions.getPlayer(player.getUniqueId()).getPrefix(); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @@ -224,16 +274,19 @@ import java.util.List; | |||||||
| 		 | 		 | ||||||
| 		@Override | 		@Override | ||||||
| 		public String getPlayerSuffix(String world, OfflinePlayer player) { | 		public String getPlayerSuffix(String world, OfflinePlayer player) { | ||||||
|  | 			checkEnabled(); | ||||||
| 			return Permissions.getPlayer(player.getUniqueId()).getSuffix(); | 			return Permissions.getPlayer(player.getUniqueId()).getSuffix(); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		@Override | 		@Override | ||||||
| 		public String getGroupPrefix(String world, String group) { | 		public String getGroupPrefix(String world, String group) { | ||||||
|  | 			checkEnabled(); | ||||||
| 			return Permissions.getGroup(group).getPrefix(); | 			return Permissions.getGroup(group).getPrefix(); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		@Override | 		@Override | ||||||
| 		public String getGroupSuffix(String world, String group) { | 		public String getGroupSuffix(String world, String group) { | ||||||
|  | 			checkEnabled(); | ||||||
| 			return Permissions.getGroup(group).getSuffix(); | 			return Permissions.getGroup(group).getSuffix(); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ import org.bukkit.plugin.ServicePriority; | |||||||
|  |  | ||||||
| import fr.pandacube.lib.permissions.PermPlayer; | import fr.pandacube.lib.permissions.PermPlayer; | ||||||
| import fr.pandacube.lib.permissions.Permissions; | import fr.pandacube.lib.permissions.Permissions; | ||||||
| import fr.pandacube.lib.util.Log; | import fr.pandacube.lib.util.log.Log; | ||||||
|  |  | ||||||
| /* package */ class PermissionsInjectorWEPIF { | /* package */ class PermissionsInjectorWEPIF { | ||||||
| 	 | 	 | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ | |||||||
|     <repositories> |     <repositories> | ||||||
|         <repository> |         <repository> | ||||||
|             <id>papermc</id> |             <id>papermc</id> | ||||||
|             <url>https://papermc.io/repo/repository/maven-public/</url> |             <url>https://repo.papermc.io/repository/maven-public/</url> | ||||||
|         </repository> |         </repository> | ||||||
|         <repository> |         <repository> | ||||||
|             <id>fabricmc</id> |             <id>fabricmc</id> | ||||||
| @@ -71,6 +71,12 @@ | |||||||
|             <version>${project.version}</version> |             <version>${project.version}</version> | ||||||
|         </dependency> |         </dependency> | ||||||
|  |  | ||||||
|  |         <dependency> | ||||||
|  |             <groupId>fr.pandacube.lib</groupId> | ||||||
|  |             <artifactId>pandalib-bungee-chat</artifactId> | ||||||
|  |             <version>${project.version}</version> | ||||||
|  |         </dependency> | ||||||
|  |  | ||||||
|         <dependency> |         <dependency> | ||||||
|             <groupId>fr.pandacube.lib</groupId> |             <groupId>fr.pandacube.lib</groupId> | ||||||
|             <artifactId>pandalib-paper-permissions</artifactId> |             <artifactId>pandalib-paper-permissions</artifactId> | ||||||
| @@ -84,19 +90,6 @@ | |||||||
|             <artifactId>paper-api</artifactId> |             <artifactId>paper-api</artifactId> | ||||||
|             <version>${paper.version}-SNAPSHOT</version> |             <version>${paper.version}-SNAPSHOT</version> | ||||||
|         </dependency> |         </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> |     </dependencies> | ||||||
|  |  | ||||||
|     <build> |     <build> | ||||||
|   | |||||||
| @@ -1,29 +1,51 @@ | |||||||
| package fr.pandacube.lib.paper; | package fr.pandacube.lib.paper; | ||||||
|  |  | ||||||
|  | import fr.pandacube.lib.paper.event.ServerStopEvent; | ||||||
| import fr.pandacube.lib.paper.json.PaperJson; | import fr.pandacube.lib.paper.json.PaperJson; | ||||||
| import fr.pandacube.lib.paper.modules.PerformanceAnalysisManager; | import fr.pandacube.lib.paper.modules.PerformanceAnalysisManager; | ||||||
| import org.bukkit.plugin.Plugin; | import org.bukkit.plugin.Plugin; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Main class for pandalib-paper. | ||||||
|  |  */ | ||||||
| public class PandaLibPaper { | public class PandaLibPaper { | ||||||
| 	 | 	 | ||||||
| 	private static Plugin plugin; | 	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) { | 	public static void onLoad(Plugin plugin) { | ||||||
| 		PandaLibPaper.plugin = plugin; | 		PandaLibPaper.plugin = plugin; | ||||||
| 		PaperJson.init(); | 		PaperJson.init(); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * Method to call in plugin's {@link Plugin#onEnable()} method. | ||||||
|  | 	 */ | ||||||
| 	public static void onEnable() { | 	public static void onEnable() { | ||||||
| 		PerformanceAnalysisManager.getInstance(); // initialize | 		PerformanceAnalysisManager.getInstance(); // initialize | ||||||
|  | 		ServerStopEvent.init(); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * Method to call in plugin's {@link Plugin#onDisable()} method. | ||||||
|  | 	 */ | ||||||
| 	public static void disable() { | 	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() { | 	public static Plugin getPlugin() { | ||||||
| 		return plugin; | 		return plugin; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	private PandaLibPaper() {} | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,13 +6,65 @@ import java.io.File; | |||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.List; | import java.util.List; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * A basic class holding configuration for {@link PaperBackupManager}. | ||||||
|  |  */ | ||||||
|  | @SuppressWarnings("CanBeFinal") | ||||||
| public class PaperBackupConfig { | 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; |     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; |     public boolean workdirBackupEnabled = true; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Set to true to enable the backup of logs. | ||||||
|  |      * Defaults to true. | ||||||
|  |      */ | ||||||
|     public boolean logsBackupEnabled = true; |     public boolean logsBackupEnabled = true; | ||||||
|     public String scheduling = "0 2 * * *"; // cron format, here is everyday at 2am |  | ||||||
|  |     /** | ||||||
|  |      * The cron-formatted scheduling of the worlds and workdir backups. | ||||||
|  |      * The default value is {@code "0 2 * * *"}, that is every day at 2am. | ||||||
|  |      */ | ||||||
|  |     public String scheduling = "0 2 * * *"; // cron format, here is every day at 2am | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The backup target directory. | ||||||
|  |      * Must be set (defaults to null). | ||||||
|  |      */ | ||||||
|     public File backupDirectory = null; |     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)); |     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)); |     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<>(); |     public List<String> workdirIgnoreList = new ArrayList<>(); | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -21,13 +21,21 @@ import java.util.HashMap; | |||||||
| import java.util.HashSet; | import java.util.HashSet; | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
| import java.util.Set; | import java.util.Set; | ||||||
|  | import java.util.concurrent.CancellationException; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * The backup manager for Paper servers. | ||||||
|  |  */ | ||||||
| public class PaperBackupManager extends BackupManager implements Listener { | public class PaperBackupManager extends BackupManager implements Listener { | ||||||
|  |  | ||||||
| 	private final Map<String, PaperWorldProcess> compressWorlds = new HashMap<>(); | 	private final Map<String, PaperWorldProcess> compressWorlds = new HashMap<>(); | ||||||
|  |  | ||||||
| 	PaperBackupConfig config; | 	PaperBackupConfig config; | ||||||
| 	 |  | ||||||
|  | 	/** | ||||||
|  | 	 * Instantiate a new backup manager. | ||||||
|  | 	 * @param config the configuration of the backups. | ||||||
|  | 	 */ | ||||||
| 	public PaperBackupManager(PaperBackupConfig config) { | 	public PaperBackupManager(PaperBackupConfig config) { | ||||||
| 		super(config.backupDirectory); | 		super(config.backupDirectory); | ||||||
| 		setConfig(config); | 		setConfig(config); | ||||||
| @@ -48,13 +56,17 @@ public class PaperBackupManager extends BackupManager implements Listener { | |||||||
| 		super.addProcess(process); | 		super.addProcess(process); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * Updates the backups config | ||||||
|  | 	 * @param config the new config. | ||||||
|  | 	 */ | ||||||
| 	public void setConfig(PaperBackupConfig config) { | 	public void setConfig(PaperBackupConfig config) { | ||||||
| 		this.config = config; | 		this.config = config; | ||||||
| 		backupQueue.forEach(this::updateProcessConfig); | 		backupQueue.forEach(this::updateProcessConfig); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  |  | ||||||
| 	public void updateProcessConfig(BackupProcess process) { | 	private void updateProcessConfig(BackupProcess process) { | ||||||
| 		if (process instanceof PaperWorkdirProcess) { | 		if (process instanceof PaperWorkdirProcess) { | ||||||
| 			process.setEnabled(config.workdirBackupEnabled); | 			process.setEnabled(config.workdirBackupEnabled); | ||||||
| 			process.setBackupCleaner(config.workdirBackupCleaner); | 			process.setBackupCleaner(config.workdirBackupCleaner); | ||||||
| @@ -76,7 +88,9 @@ public class PaperBackupManager extends BackupManager implements Listener { | |||||||
| 	public void run() { | 	public void run() { | ||||||
| 		try { | 		try { | ||||||
| 			SchedulerUtil.runOnServerThreadAndWait(super::run); | 			SchedulerUtil.runOnServerThreadAndWait(super::run); | ||||||
| 		} catch (Exception e) { | 		} catch (CancellationException ignored) { | ||||||
|  |  | ||||||
|  |         } catch (Exception e) { | ||||||
| 			throw new RuntimeException(e); | 			throw new RuntimeException(e); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| @@ -116,12 +130,12 @@ public class PaperBackupManager extends BackupManager implements Listener { | |||||||
| 	private final Set<String> dirtyForSave = new HashSet<>(); | 	private final Set<String> dirtyForSave = new HashSet<>(); | ||||||
|  |  | ||||||
| 	@EventHandler(priority = EventPriority.MONITOR) | 	@EventHandler(priority = EventPriority.MONITOR) | ||||||
| 	public void onWorldLoad(WorldLoadEvent event) { | 	void onWorldLoad(WorldLoadEvent event) { | ||||||
| 		initWorldProcess(event.getWorld().getName()); | 		initWorldProcess(event.getWorld().getName()); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	@EventHandler(priority = EventPriority.MONITOR) | 	@EventHandler(priority = EventPriority.MONITOR) | ||||||
| 	public void onWorldSave(WorldSaveEvent event) { | 	void onWorldSave(WorldSaveEvent event) { | ||||||
| 		if (event.getWorld().getLoadedChunks().length > 0 | 		if (event.getWorld().getLoadedChunks().length > 0 | ||||||
| 				|| dirtyForSave.contains(event.getWorld().getName())) { | 				|| dirtyForSave.contains(event.getWorld().getName())) { | ||||||
| 			compressWorlds.get(event.getWorld().getName()).setDirtyAfterSave(); | 			compressWorlds.get(event.getWorld().getName()).setDirtyAfterSave(); | ||||||
| @@ -134,18 +148,18 @@ public class PaperBackupManager extends BackupManager implements Listener { | |||||||
|  |  | ||||||
|  |  | ||||||
| 	@EventHandler(priority = EventPriority.MONITOR) | 	@EventHandler(priority = EventPriority.MONITOR) | ||||||
| 	public void onPlayerChangeWorldEvent(PlayerChangedWorldEvent event) { | 	void onPlayerChangeWorldEvent(PlayerChangedWorldEvent event) { | ||||||
| 		dirtyForSave.add(event.getFrom().getName()); | 		dirtyForSave.add(event.getFrom().getName()); | ||||||
| 		dirtyForSave.add(event.getPlayer().getWorld().getName()); | 		dirtyForSave.add(event.getPlayer().getWorld().getName()); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	@EventHandler(priority = EventPriority.MONITOR) | 	@EventHandler(priority = EventPriority.MONITOR) | ||||||
| 	public void onPlayerJoin(PlayerJoinEvent event) { | 	void onPlayerJoin(PlayerJoinEvent event) { | ||||||
| 		dirtyForSave.add(event.getPlayer().getWorld().getName()); | 		dirtyForSave.add(event.getPlayer().getWorld().getName()); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	@EventHandler(priority = EventPriority.MONITOR) | 	@EventHandler(priority = EventPriority.MONITOR) | ||||||
| 	public void onPlayerQuit(PlayerQuitEvent event) { | 	void onPlayerQuit(PlayerQuitEvent event) { | ||||||
| 		dirtyForSave.add(event.getPlayer().getWorld().getName()); | 		dirtyForSave.add(event.getPlayer().getWorld().getName()); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -10,11 +10,19 @@ import net.kyori.adventure.bossbar.BossBar.Color; | |||||||
| import net.kyori.adventure.bossbar.BossBar.Overlay; | import net.kyori.adventure.bossbar.BossBar.Overlay; | ||||||
| import org.bukkit.Bukkit; | import org.bukkit.Bukkit; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * A backup process with specific logic around Paper server. | ||||||
|  |  */ | ||||||
| public abstract class PaperBackupProcess extends BackupProcess { | public abstract class PaperBackupProcess extends BackupProcess { | ||||||
|  |  | ||||||
|  |  | ||||||
| 	private BossBar bossBar; | 	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) { | 	protected PaperBackupProcess(PaperBackupManager bm, String id) { | ||||||
| 		super(bm, id); | 		super(bm, id); | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -1,31 +1,31 @@ | |||||||
| package fr.pandacube.lib.paper.backup; | package fr.pandacube.lib.paper.backup; | ||||||
|  |  | ||||||
| import fr.pandacube.lib.util.Log; |  | ||||||
|  |  | ||||||
| import java.io.File; | import java.io.File; | ||||||
| import java.text.DateFormat; |  | ||||||
| import java.util.Date; |  | ||||||
| import java.util.function.BiPredicate; | import java.util.function.BiPredicate; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * A backup process with specific logic around Paper server working directory. | ||||||
|  |  */ | ||||||
| public class PaperWorkdirProcess extends PaperBackupProcess { | 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) { | 	protected PaperWorkdirProcess(PaperBackupManager bm) { | ||||||
| 		super(bm, "workdir"); | 		super(bm, "workdir"); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  |  | ||||||
| 	public BiPredicate<File, String> getFilenameFilter() { | 	public BiPredicate<File, String> getFilenameFilter() { | ||||||
| 		return new BiPredicate<File, String>() { | 		return (file, path) -> { | ||||||
| 			@Override | 			if (file.isDirectory() && new File(file, "level.dat").exists()) | ||||||
| 			public boolean test(File file, String path) { | 				return false; | ||||||
| 				if (file.isDirectory() && new File(file, "level.dat").exists()) | 			if (new File(getSourceDir(), "logs").equals(file)) | ||||||
| 					return false; | 				return false; | ||||||
| 				if (new File(getSourceDir(), "logs").equals(file)) | 			if (file.isFile() && file.getName().endsWith(".lck")) | ||||||
| 					return false; | 				return false; | ||||||
| 				if (file.isFile() && file.getName().endsWith(".lck")) | 			return PaperWorkdirProcess.super.getFilenameFilter().test(file, path); | ||||||
| 					return false; |  | ||||||
| 				return PaperWorkdirProcess.super.getFilenameFilter().test(file, path); |  | ||||||
| 			} |  | ||||||
| 		}; | 		}; | ||||||
| 	} | 	} | ||||||
| 	 | 	 | ||||||
|   | |||||||
| @@ -1,25 +1,32 @@ | |||||||
| package fr.pandacube.lib.paper.backup; | package fr.pandacube.lib.paper.backup; | ||||||
|  |  | ||||||
|  | import fr.pandacube.lib.chat.LegacyChatFormat; | ||||||
| import fr.pandacube.lib.paper.scheduler.SchedulerUtil; | import fr.pandacube.lib.paper.scheduler.SchedulerUtil; | ||||||
| import fr.pandacube.lib.paper.util.WorldUtil; | import fr.pandacube.lib.paper.world.WorldUtil; | ||||||
| import fr.pandacube.lib.util.Log; | import fr.pandacube.lib.util.log.Log; | ||||||
| import net.md_5.bungee.api.ChatColor; |  | ||||||
| import org.bukkit.Bukkit; | import org.bukkit.Bukkit; | ||||||
| import org.bukkit.World; | import org.bukkit.World; | ||||||
|  |  | ||||||
| import java.io.File; | import java.io.File; | ||||||
| import java.text.DateFormat; | import java.text.DateFormat; | ||||||
| import java.util.Date; | import java.util.Date; | ||||||
| import java.util.function.BiPredicate; |  | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * A backup process with specific logic around Paper server world. | ||||||
|  |  */ | ||||||
| public class PaperWorldProcess extends PaperBackupProcess { | public class PaperWorldProcess extends PaperBackupProcess { | ||||||
| 	private final String worldName; | 	private final String worldName; | ||||||
| 	 | 	 | ||||||
| 	private boolean autoSave = true;  | 	private boolean autoSave = true; | ||||||
| 	 |  | ||||||
| 	protected PaperWorldProcess(PaperBackupManager bm, final String n) { | 	/** | ||||||
| 		super(bm, "worlds/" + n); | 	 * Instantiates a new backup process for a world. | ||||||
| 		worldName = n; | 	 * @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() { | 	private World getWorld() { | ||||||
| @@ -63,11 +70,11 @@ public class PaperWorldProcess extends PaperBackupProcess { | |||||||
|  |  | ||||||
| 	public void displayNextSchedule() { | 	public void displayNextSchedule() { | ||||||
| 		if (hasNextScheduled()) { | 		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()))); | 					+ DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(new Date(getNext()))); | ||||||
| 		} | 		} | ||||||
| 		else { | 		else { | ||||||
| 			Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " is clean. Next backup not scheduled."); | 			Log.info("[Backup] " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + " is clean. Next backup not scheduled."); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -81,7 +88,7 @@ public class PaperWorldProcess extends PaperBackupProcess { | |||||||
| 	public void setDirtyAfterSave() { | 	public void setDirtyAfterSave() { | ||||||
| 		if (!isDirty()) { // don't set dirty if it is already | 		if (!isDirty()) { // don't set dirty if it is already | ||||||
| 			setDirtySinceNow(); | 			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) | 					+ DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG) | ||||||
| 					.format(new Date(getNext())) | 					.format(new Date(getNext())) | ||||||
| 			); | 			); | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| package fr.pandacube.lib.paper.commands; | package fr.pandacube.lib.paper.commands; | ||||||
|  |  | ||||||
| import com.destroystokyo.paper.brigadier.BukkitBrigadierCommandSource; |  | ||||||
| import com.mojang.brigadier.CommandDispatcher; | import com.mojang.brigadier.CommandDispatcher; | ||||||
| import com.mojang.brigadier.arguments.ArgumentType; | import com.mojang.brigadier.arguments.ArgumentType; | ||||||
|  | import com.mojang.brigadier.builder.LiteralArgumentBuilder; | ||||||
| import com.mojang.brigadier.context.CommandContext; | import com.mojang.brigadier.context.CommandContext; | ||||||
| import com.mojang.brigadier.exceptions.CommandSyntaxException; | import com.mojang.brigadier.exceptions.CommandSyntaxException; | ||||||
| import com.mojang.brigadier.suggestion.SuggestionProvider; | import com.mojang.brigadier.suggestion.SuggestionProvider; | ||||||
| @@ -10,132 +10,153 @@ import com.mojang.brigadier.tree.CommandNode; | |||||||
| import com.mojang.brigadier.tree.LiteralCommandNode; | import com.mojang.brigadier.tree.LiteralCommandNode; | ||||||
| import com.mojang.brigadier.tree.RootCommandNode; | import com.mojang.brigadier.tree.RootCommandNode; | ||||||
| import fr.pandacube.lib.chat.Chat; | import fr.pandacube.lib.chat.Chat; | ||||||
|  | import fr.pandacube.lib.commands.BadCommandUsage; | ||||||
| import fr.pandacube.lib.commands.BrigadierCommand; | import fr.pandacube.lib.commands.BrigadierCommand; | ||||||
| import fr.pandacube.lib.commands.SuggestionsSupplier; | import fr.pandacube.lib.commands.SuggestionsSupplier; | ||||||
| import fr.pandacube.lib.paper.permissions.PandalibPaperPermissions; | import fr.pandacube.lib.paper.PandaLibPaper; | ||||||
| import fr.pandacube.lib.paper.reflect.PandalibPaperReflect; |  | ||||||
| import fr.pandacube.lib.paper.reflect.wrapper.craftbukkit.CraftServer; |  | ||||||
| import fr.pandacube.lib.paper.reflect.wrapper.craftbukkit.CraftVector; | 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.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.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.commands.Vec3Argument; | ||||||
| import fr.pandacube.lib.paper.reflect.wrapper.minecraft.core.BlockPos; | import fr.pandacube.lib.paper.reflect.wrapper.paper.commands.BukkitCommandNode; | ||||||
| import fr.pandacube.lib.paper.reflect.wrapper.minecraft.server.ServerPlayer; | import fr.pandacube.lib.paper.reflect.wrapper.paper.commands.PluginCommandNode; | ||||||
| import fr.pandacube.lib.paper.reflect.wrapper.paper.PaperAdventure; |  | ||||||
| import fr.pandacube.lib.players.standalone.AbstractOffPlayer; | import fr.pandacube.lib.players.standalone.AbstractOffPlayer; | ||||||
| import fr.pandacube.lib.players.standalone.AbstractOnlinePlayer; | import fr.pandacube.lib.players.standalone.AbstractOnlinePlayer; | ||||||
| import fr.pandacube.lib.players.standalone.AbstractPlayerManager; | import fr.pandacube.lib.players.standalone.AbstractPlayerManager; | ||||||
| import fr.pandacube.lib.reflect.wrapper.ReflectWrapper; | import fr.pandacube.lib.reflect.Reflect; | ||||||
| import fr.pandacube.lib.util.Log; | import fr.pandacube.lib.reflect.ReflectClass; | ||||||
| import net.kyori.adventure.text.Component; | 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.Bukkit; | ||||||
| import org.bukkit.World; | import org.bukkit.World; | ||||||
| import org.bukkit.command.Command; | import org.bukkit.command.Command; | ||||||
| import org.bukkit.command.CommandMap; |  | ||||||
| import org.bukkit.command.CommandSender; | import org.bukkit.command.CommandSender; | ||||||
| import org.bukkit.command.ConsoleCommandSender; | import org.bukkit.command.ConsoleCommandSender; | ||||||
| import org.bukkit.command.PluginCommand; | import org.bukkit.command.PluginCommand; | ||||||
| import org.bukkit.command.defaults.BukkitCommand; |  | ||||||
| import org.bukkit.entity.Entity; |  | ||||||
| import org.bukkit.entity.Player; | import org.bukkit.entity.Player; | ||||||
| import org.bukkit.event.EventHandler; |  | ||||||
| import org.bukkit.event.Listener; | 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.plugin.Plugin; | ||||||
| import org.bukkit.util.BlockVector; |  | ||||||
| import org.bukkit.util.Vector; | import org.bukkit.util.Vector; | ||||||
|  |  | ||||||
| import java.util.ArrayList; | import java.lang.reflect.InvocationTargetException; | ||||||
| import java.util.HashSet; | import java.util.HashSet; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Set; | import java.util.Set; | ||||||
| import java.util.function.Predicate; | import java.util.function.Predicate; | ||||||
| import java.util.stream.Stream; | import java.util.stream.Stream; | ||||||
|  |  | ||||||
|  | import static fr.pandacube.lib.reflect.wrapper.ReflectWrapper.unwrap; | ||||||
|  | import static fr.pandacube.lib.reflect.wrapper.ReflectWrapper.wrap; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Abstract class to hold a command to be integrated into a Paper server vanilla command dispatcher. |  * 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 CommandDispatcher<CommandSourceStack> vanillaPaperDispatcher = null; | ||||||
|     private static final CommandDispatcher<BukkitBrigadierCommandSource> nmsDispatcher; |  | ||||||
|  |  | ||||||
|     static { |     /** | ||||||
|         PandalibPaperReflect.init(); |      * Gets the Brigadier dispatcher provided by paper API during {@link LifecycleEvents#COMMANDS}. | ||||||
|         vanillaCommandDispatcher = ReflectWrapper.wrapTyped(Bukkit.getServer(), CraftServer.class) |      * <p> | ||||||
|                 .getServer() |      * This Dispatcher is not the vanilla one. Instead, Paper implementation wraps the vanilla one to handle proper registration | ||||||
|                 .vanillaCommandDispatcher(); |      * of commands from plugins. | ||||||
|         nmsDispatcher = vanillaCommandDispatcher.dispatcher(); |      * @return the Brigadier dispatcher. | ||||||
|  |      */ | ||||||
|  |     public static CommandDispatcher<CommandSourceStack> getVanillaPaperDispatcher() { | ||||||
|  |         return vanillaPaperDispatcher; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Removes a plugin command that overrides a vanilla command, so the vanilla command functionnalities are fully |      * Gets the root node of the dispatcher from {@link #getVanillaPaperDispatcher()}. | ||||||
|      * restored (so, not only the usage, but also the suggestions and the command structure sent to the client). |      * @return the root node, or null if {@link #getVanillaPaperDispatcher()} is also null. | ||||||
|      * @param name the name of the command to restore. |  | ||||||
|      */ |      */ | ||||||
|     public static void restoreVanillaCommand(String name) { |     public static RootCommandNode<CommandSourceStack> getRootNode() { | ||||||
|         CommandMap bukkitCmdMap = Bukkit.getCommandMap(); |         return vanillaPaperDispatcher == null ? null : vanillaPaperDispatcher.getRoot(); | ||||||
|         Command bukkitCommand = bukkitCmdMap.getCommand(name); |     } | ||||||
|         if (bukkitCommand != null) { |  | ||||||
|             if (VanillaCommandWrapper.REFLECT.get().isInstance(bukkitCommand)) { |  | ||||||
|                 //Log.info("Command /" + name + " is already a vanilla command."); |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
|             Log.info("Removing Bukkit command /" + name + " (" + getCommandIdentity(bukkitCommand) + ")"); |  | ||||||
|             bukkitCmdMap.getKnownCommands().remove(name.toLowerCase(java.util.Locale.ENGLISH)); |  | ||||||
|             bukkitCommand.unregister(bukkitCmdMap); |  | ||||||
|  |  | ||||||
|             LiteralCommandNode<BukkitBrigadierCommandSource> node = (LiteralCommandNode<BukkitBrigadierCommandSource>) getRootNode().getChild(name); |     private static void updateVanillaPaperDispatcher(CommandDispatcher<CommandSourceStack> newDispatcher) { | ||||||
|             Command newCommand = new VanillaCommandWrapper(vanillaCommandDispatcher, node).__getRuntimeInstance(); |         if (vanillaPaperDispatcher == null || newDispatcher != vanillaPaperDispatcher) { | ||||||
|             bukkitCmdMap.getKnownCommands().put(name.toLowerCase(), newCommand); |             vanillaPaperDispatcher = newDispatcher; | ||||||
|             newCommand.register(bukkitCmdMap); |  | ||||||
|  |             // vanillaPaperDispatcher.getRoot() is not the real root but a wrapped root. Trying to map the fake root with the real one to trick the Paper/Brigadier (un)wrapper | ||||||
|  |             RootCommandNode<CommandSourceStack> wrappedRoot = vanillaPaperDispatcher.getRoot(); | ||||||
|  |             ReflectClass<?> apiMirrorRootNodeClass = Reflect.ofClassOfInstance(wrappedRoot); | ||||||
|  |             try { | ||||||
|  |                 RootCommandNode<?> unwrappedRoot = ((CommandDispatcher<?>) apiMirrorRootNodeClass.method("getDispatcher").invoke(wrappedRoot)).getRoot(); | ||||||
|  |  | ||||||
|  |                 Reflect.ofClass(CommandNode.class).field("unwrappedCached").setValue(wrappedRoot, unwrappedRoot); | ||||||
|  |                 Reflect.ofClass(CommandNode.class).field("wrappedCached").setValue(unwrappedRoot, wrappedRoot); | ||||||
|  |  | ||||||
|  |             } catch (InvocationTargetException|IllegalAccessException|NoSuchMethodException|NoSuchFieldException e) { | ||||||
|  |                 Log.severe("Unable to trick the Paper/Brigadier unwrapper to properly handle commands redirecting to root command node.", e); | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Returns the vanilla instance of the Brigadier dispatcher. |  | ||||||
|      * @return the vanilla instance of the Brigadier dispatcher. |  | ||||||
|      */ |  | ||||||
|     public static CommandDispatcher<BukkitBrigadierCommandSource> getNMSDispatcher() { |  | ||||||
|         return nmsDispatcher; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Returns the root command node of the Brigadier dispatcher. |      * Removes a plugin command that overrides a vanilla command, so the vanilla command functionalities are fully | ||||||
|      * @return the root command node of the Brigadier dispatcher. |      * restored (so, not only the usage, but also the suggestions and the command structure sent to the client). | ||||||
|  |      * @param name the name of the command to restore. | ||||||
|      */ |      */ | ||||||
|     protected static RootCommandNode<BukkitBrigadierCommandSource> getRootNode() { |     public static void restoreVanillaCommand(String name) { | ||||||
|         return nmsDispatcher.getRoot(); |  | ||||||
|  |         PandaLibPaper.getPlugin().getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, | ||||||
|  |                 event -> updateVanillaPaperDispatcher(event.registrar().getDispatcher())); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |         Bukkit.getServer().getScheduler().runTask(PandaLibPaper.getPlugin(), () -> { | ||||||
|  |             if (vanillaPaperDispatcher == null) | ||||||
|  |                 return; | ||||||
|  |  | ||||||
|  |             CommandNode<CommandSourceStack> targetCommand = vanillaPaperDispatcher.getRoot().getChild("minecraft:" + name); | ||||||
|  |             if (targetCommand == null) { | ||||||
|  |                 Log.warning("There is no vanilla command '" + name + "' to restore."); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             CommandNode<CommandSourceStack> eventuallyBadCommandToReplace = vanillaPaperDispatcher.getRoot().getChild(name); | ||||||
|  |             Boolean isPluginCommand = isPluginCommand(eventuallyBadCommandToReplace); | ||||||
|  |             if (isPluginCommand != null && isPluginCommand) { | ||||||
|  |                 Log.info(getCommandIdentity(eventuallyBadCommandToReplace) + " found in the dispatcher. Restoring the vanilla command."); | ||||||
|  |                 vanillaPaperDispatcher.getRoot().getChildren().removeIf(c -> c.getName().equals(name)); | ||||||
|  |                 vanillaPaperDispatcher.getRoot().addChild(getAliasNode(targetCommand, name)); | ||||||
|  |             } | ||||||
|  |             /*else if (isPluginCommand == null) { | ||||||
|  |                 Log.info(getCommandIdentity(eventuallyBadCommandToReplace) + " found in the dispatcher. Unsure if we restore the vanilla command."); | ||||||
|  |             }*/ | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     private final Plugin plugin; |     private final Plugin plugin; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * The command node of this command. |      * The command node of this command. | ||||||
|      */ |      */ | ||||||
|     protected final LiteralCommandNode<BukkitBrigadierCommandSource> commandNode; |     protected LiteralCommandNode<CommandSourceStack> commandNode; | ||||||
|  |     /** | ||||||
|  |      * The command requested aliases. | ||||||
|  |      */ | ||||||
|  |     protected final String[] aliases; | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The command description. | ||||||
|  |      */ | ||||||
|  |     protected final String description; | ||||||
|  |  | ||||||
|     private final RegistrationPolicy registrationPolicy; |     private final RegistrationPolicy registrationPolicy; | ||||||
|  |  | ||||||
|     private Set<String> registeredAliases; |     private Set<String> registeredAliases; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Instanciate this command instance. |      * Instantiate this command instance. | ||||||
|      * |      * | ||||||
|      * @param pl the plugin instance. |      * @param pl the plugin instance. | ||||||
|      * @param regPolicy the registration policy for this command. |      * @param regPolicy the registration policy for this command. | ||||||
| @@ -143,17 +164,17 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga | |||||||
|     public PaperBrigadierCommand(Plugin pl, RegistrationPolicy regPolicy) { |     public PaperBrigadierCommand(Plugin pl, RegistrationPolicy regPolicy) { | ||||||
|         plugin = pl; |         plugin = pl; | ||||||
|         registrationPolicy = regPolicy; |         registrationPolicy = regPolicy; | ||||||
|         commandNode = buildCommand().build(); |         String[] aliasesTmp = getAliases(); | ||||||
|         postBuildCommand(commandNode); |         aliases = aliasesTmp == null ? new String[0] : aliasesTmp; | ||||||
|  |         description = getDescription(); | ||||||
|         register(); |         register(); | ||||||
|         Bukkit.getPluginManager().registerEvents(this, plugin); |         //try { | ||||||
|         try { |         //    PandalibPaperPermissions.addPermissionMapping("minecraft.command." + commandNode.getLiteral().toLowerCase(), getTargetPermission().toLowerCase()); | ||||||
|             PandalibPaperPermissions.addPermissionMapping("minecraft.command." + commandNode.getLiteral().toLowerCase(), getTargetPermission().toLowerCase()); |         //} catch (NoClassDefFoundError ignored) { } | ||||||
|         } catch (NoClassDefFoundError ignored) { } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Instanciate this command isntance with a registration policy of {@link RegistrationPolicy#ONLY_BASE_COMMAND}. |      * Instantiate this command instance with a registration policy of {@link RegistrationPolicy#ONLY_BASE_COMMAND}. | ||||||
|      * @param pl the plugin instance. |      * @param pl the plugin instance. | ||||||
|      */ |      */ | ||||||
|     public PaperBrigadierCommand(Plugin pl) { |     public PaperBrigadierCommand(Plugin pl) { | ||||||
| @@ -164,163 +185,179 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga | |||||||
|  |  | ||||||
|  |  | ||||||
|     private void register() { |     private void register() { | ||||||
|  |         plugin.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event -> { | ||||||
|  |             updateVanillaPaperDispatcher(event.registrar().getDispatcher()); | ||||||
|  |  | ||||||
|         String[] aliases = getAliases(); |             commandNode = buildCommand().build(); | ||||||
|         if (aliases == null) |             postBuildCommand(commandNode); | ||||||
|             aliases = new String[0]; |  | ||||||
|  |  | ||||||
|         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<>(); |             registeredAliases = new HashSet<>(event.registrar().register(commandNode, description, List.of(aliases))); | ||||||
|         registerNode(commandNode, false); |             doPostRegistrationFixes(); | ||||||
|         registerAlias(pluginName + ":" + commandNode.getLiteral(), true); |  | ||||||
|  |  | ||||||
|         for (String alias : aliases) { |             if (registrationPolicy == RegistrationPolicy.ALL) { | ||||||
|             registerAlias(alias, false); |                 // enforce registration of aliases | ||||||
|             registerAlias(pluginName + ":" + alias, true); |                 for (String alias : aliases) { | ||||||
|  |                     if (!registeredAliases.contains(alias)) { | ||||||
|  |                         Log.info("Command /" + commandNode.getName() + ": forcing registration of alias " + alias); | ||||||
|  |                         registeredAliases.addAll(event.registrar().register(getAliasNode(commandNode, alias), description)); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |             Bukkit.getServer().getScheduler().runTask(plugin, () -> { | ||||||
|  |                 if (vanillaPaperDispatcher == null) | ||||||
|  |                     return; | ||||||
|  |  | ||||||
|  |                 Set<String> forceRegistrationAgain = new HashSet<>(); | ||||||
|  |                 forceRegistrationAgain.add(commandNode.getName()); | ||||||
|  |                 if (registrationPolicy == RegistrationPolicy.ALL) | ||||||
|  |                     forceRegistrationAgain.addAll(List.of(aliases)); | ||||||
|  |  | ||||||
|  |                 for (String aliasToForce : forceRegistrationAgain) { | ||||||
|  |                     CommandNode<CommandSourceStack> actualNode = vanillaPaperDispatcher.getRoot().getChild(aliasToForce); | ||||||
|  |                     if (actualNode != null) { | ||||||
|  |                         //Log.info("Forcing registration of alias /" + aliasToForce + " for command /" + commandNode.getName() + ": replacing " + getCommandIdentity(actualNode) + "?"); | ||||||
|  |                         if (PluginCommandNode.REFLECT.get().isInstance(actualNode)) { | ||||||
|  |                             PluginCommandNode pcn = wrap(actualNode, PluginCommandNode.class); | ||||||
|  |                             if (pcn.getPlugin().equals(plugin)) | ||||||
|  |                                 return; | ||||||
|  |                         } | ||||||
|  |                         else if (BukkitCommandNode.REFLECT.get().isInstance(actualNode)) { | ||||||
|  |                             BukkitCommandNode bcn = wrap(actualNode, BukkitCommandNode.class); | ||||||
|  |                             if (bcn.getBukkitCommand() instanceof PluginCommand pc && pc.getPlugin().equals(plugin)) | ||||||
|  |                                 return; | ||||||
|  |                         } | ||||||
|  |                         vanillaPaperDispatcher.getRoot().getChildren().removeIf(c -> c.getName().equals(aliasToForce)); | ||||||
|  |                     } | ||||||
|  |                 /*else { | ||||||
|  |                     Log.info("Forcing registration of alias /" + aliasToForce + " for command /" + commandNode.getName() + ": no command found for alias. Adding alias."); | ||||||
|  |                 }*/ | ||||||
|  |                     LiteralCommandNode<CommandSourceStack> newPCN = unwrap(new PluginCommandNode(aliasToForce, plugin.getPluginMeta(), commandNode, description)); | ||||||
|  |                     vanillaPaperDispatcher.getRoot().addChild(newPCN); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     private void doPostRegistrationFixes() { | ||||||
|  |         postRegistrationFixNode(new HashSet<>(), commandNode); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private void postRegistrationFixNode(Set<CommandNode<CommandSourceStack>> fixedNodes, CommandNode<CommandSourceStack> originalNode) { | ||||||
|  |         if (originalNode instanceof RootCommandNode) | ||||||
|  |             return; | ||||||
|  |         if (fixedNodes.contains(originalNode)) | ||||||
|  |             return; | ||||||
|  |         fixedNodes.add(originalNode); | ||||||
|  |         if (originalNode.getRedirect() != null) { | ||||||
|  |             try { | ||||||
|  |                 @SuppressWarnings("rawtypes") | ||||||
|  |                 ReflectClass<CommandNode> cmdNodeClass = Reflect.ofClass(CommandNode.class); | ||||||
|  |                 @SuppressWarnings("unchecked") | ||||||
|  |                 CommandNode<CommandSourceStack> unwrappedNode = (CommandNode<CommandSourceStack>) cmdNodeClass.field("unwrappedCached").getValue(originalNode); | ||||||
|  |                 if (unwrappedNode != null) { | ||||||
|  |                     cmdNodeClass.field("modifier").setValue(unwrappedNode, cmdNodeClass.field("modifier").getValue(originalNode)); | ||||||
|  |                     cmdNodeClass.field("forks").setValue(unwrappedNode, cmdNodeClass.field("forks").getValue(originalNode)); | ||||||
|  |                 } | ||||||
|  |             } catch (IllegalAccessException | NoSuchFieldException e) { | ||||||
|  |                 throw new RuntimeException(e); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             postRegistrationFixNode(fixedNodes, originalNode.getRedirect()); | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             try { | ||||||
|  |                 for (CommandNode<CommandSourceStack> child : originalNode.getChildren()) | ||||||
|  |                     postRegistrationFixNode(fixedNodes, child); | ||||||
|  |             } catch (UnsupportedOperationException ignored) { | ||||||
|  |                 // in case getChildren is not possible (vanilla commands are wrapped by Paper API) | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     private void registerAlias(String alias, boolean prefixed) { |     private static LiteralCommandNode<CommandSourceStack> getAliasNode(CommandNode<CommandSourceStack> commandNode, String alias) { | ||||||
|         LiteralCommandNode<BukkitBrigadierCommandSource> node = literal(alias) |          return LiteralArgumentBuilder.<CommandSourceStack>literal(alias) | ||||||
|                 .requires(commandNode.getRequirement()) |                 .requires(commandNode.getRequirement()) | ||||||
|                 .executes(commandNode.getCommand()) |                 .executes(commandNode.getCommand()) | ||||||
|                 .redirect(commandNode) |                 .redirect(commandNode) | ||||||
|                 .build(); |                 .build(); | ||||||
|         registerNode(node, prefixed); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     private static String getCommandIdentity(CommandNode<CommandSourceStack> command) { | ||||||
|     private void registerNode(LiteralCommandNode<BukkitBrigadierCommandSource> node, boolean prefixed) { |         if (PluginCommandNode.REFLECT.get().isInstance(command)) { | ||||||
|         RootCommandNode<BukkitBrigadierCommandSource> root = getRootNode(); |             PluginCommandNode wrappedPCN = wrap(command, PluginCommandNode.class); | ||||||
|         String name = node.getLiteral(); |             return "Node /" + command.getName() + " from plugin " + wrappedPCN.getPlugin().getName(); | ||||||
|         boolean isAlias = node.getRedirect() == commandNode; |         } | ||||||
|         boolean forceRegistration = switch (registrationPolicy) { |         else if (BukkitCommandNode.REFLECT.get().isInstance(command)) { | ||||||
|             case NONE -> false; |             BukkitCommandNode wrappedBCN = wrap(command, BukkitCommandNode.class); | ||||||
|             case ONLY_BASE_COMMAND -> prefixed || !isAlias; |             Command bukkitCmd = wrappedBCN.getBukkitCommand(); | ||||||
|             case ALL -> true; |             if (bukkitCmd instanceof PluginCommand cmd) { | ||||||
|         }; |                 return "Node /" + command.getName() + " wrapping Bukkit command /" + bukkitCmd.getName() + " from plugin " + cmd.getPlugin().getName(); | ||||||
|  |  | ||||||
|         // nmsDispatcher integration and conflit resolution |  | ||||||
|         boolean nmsRegister = false, nmsRegistered = false; |  | ||||||
|         CommandNode<BukkitBrigadierCommandSource> nmsConflited = root.getChild(name); |  | ||||||
|         if (nmsConflited != null) { |  | ||||||
|  |  | ||||||
|             if (isFromThisCommand(nmsConflited)) { |  | ||||||
|                 // this command is already registered in NMS. Don’t need to register again |  | ||||||
|                 nmsRegistered = true; |  | ||||||
|             } |             } | ||||||
|             else if (forceRegistration) { |             else if (VanillaCommandWrapper.REFLECT.get().isInstance(bukkitCmd)) { | ||||||
|                 nmsRegister = true; |                 VanillaCommandWrapper vcw = wrap(bukkitCmd, VanillaCommandWrapper.class); | ||||||
|                 Log.info("Overwriting Brigadier command /" + name); |                 CommandNode<CommandSourceStack> vanillaCmd = vcw.vanillaCommand(); | ||||||
|             } |                 if (vanillaCmd != command) | ||||||
|             else if (prefixed || !isAlias) { |                     return "Node /" + command.getName() + " wrapping non-plugin command /" + bukkitCmd.getName() + " wrapping: " + getCommandIdentity(vcw.vanillaCommand()); | ||||||
|                 Log.severe("/" + name + " already in NMS Brigadier instance." |                 else | ||||||
|                         + " Wont replace it because registration is not forced for prefixed or initial name of a command."); |                     return "Node /" + command.getName() + " wrapping non-plugin command /" + bukkitCmd.getName() + " wrapping back the node (risk of StackOverflow?)"; | ||||||
|             } |  | ||||||
|             else { // conflict, wont replace, not forced but only an alias anyway |  | ||||||
|                 Log.info("/" + name + " already in NMS Brigadier instance." |  | ||||||
|                         + " Wont replace it because registration is not forced for a non-prefixed alias."); |  | ||||||
|             } |             } | ||||||
|  |             else | ||||||
|  |                 return "Node /" + command.getName() + " wrapping " + bukkitCmd.getClass().getName() + " /" + bukkitCmd.getName(); | ||||||
|         } |         } | ||||||
|         else { |         else { | ||||||
|             nmsRegister = true; |             return "Node /" + command.getName() + " (unspecific)"; | ||||||
|         } |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|         if (nmsRegister) { |  | ||||||
|             @SuppressWarnings("unchecked") |     private static Boolean isPluginCommand(CommandNode<CommandSourceStack> command) { | ||||||
|             var rCommandNode = ReflectWrapper.wrapTyped(root, fr.pandacube.lib.paper.reflect.wrapper.brigadier.CommandNode.class); |         if (PluginCommandNode.REFLECT.get().isInstance(command)) { | ||||||
|             rCommandNode.removeCommand(name); |             return true; | ||||||
|             root.addChild(node); |  | ||||||
|             nmsRegistered = true; |  | ||||||
|         } |         } | ||||||
|  |         else if (BukkitCommandNode.REFLECT.get().isInstance(command)) { | ||||||
|         if (!nmsRegistered) { |             BukkitCommandNode wrappedBCN = wrap(command, BukkitCommandNode.class); | ||||||
|             return; |             Command bukkitCmd = wrappedBCN.getBukkitCommand(); | ||||||
|         } |             if (bukkitCmd instanceof PluginCommand) { | ||||||
|  |                 return true; | ||||||
|         registeredAliases.add(name); |  | ||||||
|  |  | ||||||
|         // bukkit dispatcher conflict resolution |  | ||||||
|         boolean bukkitRegister = false; |  | ||||||
|         CommandMap bukkitCmdMap = Bukkit.getCommandMap(); |  | ||||||
|         Command bukkitConflicted = bukkitCmdMap.getCommand(name); |  | ||||||
|         if (bukkitConflicted != null) { |  | ||||||
|             if (!isFromThisCommand(bukkitConflicted)) { |  | ||||||
|                 if (forceRegistration) { |  | ||||||
|                     bukkitRegister = true; |  | ||||||
|                     Log.info("Overwriting Bukkit command /" + name |  | ||||||
|                             + " (" + getCommandIdentity(bukkitConflicted) + ")"); |  | ||||||
|                 } |  | ||||||
|                 else if (prefixed || !isAlias) { |  | ||||||
|                     Log.severe("/" + name + " already in Bukkit dispatcher (" + getCommandIdentity(bukkitConflicted) + ")." + |  | ||||||
|                             " Wont replace it because registration is not forced for prefixed or initial name of a command."); |  | ||||||
|                 } |  | ||||||
|                 else { |  | ||||||
|                     Log.info("/" + name + " already in Bukkit dispatcher (" + getCommandIdentity(bukkitConflicted) + ")." + |  | ||||||
|                             " Wont replace it because registration is not forced for a non-prefixed alias."); |  | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
|  |             else if (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 { |         else { | ||||||
|             bukkitRegister = true; |             return false; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (bukkitRegister) { |  | ||||||
|             bukkitCmdMap.getKnownCommands().remove(name.toLowerCase()); |  | ||||||
|             if (bukkitConflicted != null) |  | ||||||
|                 bukkitConflicted.unregister(bukkitCmdMap); |  | ||||||
|  |  | ||||||
|             Command newCommand = new VanillaCommandWrapper(vanillaCommandDispatcher, node).__getRuntimeInstance(); |  | ||||||
|             bukkitCmdMap.getKnownCommands().put(name.toLowerCase(), newCommand); |  | ||||||
|             newCommand.register(bukkitCmdMap); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private boolean isFromThisCommand(CommandNode<BukkitBrigadierCommandSource> node) { |  | ||||||
|         return node == commandNode || node.getRedirect() == commandNode; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private boolean isFromThisCommand(Command bukkitCmd) { |  | ||||||
|         if (VanillaCommandWrapper.REFLECT.get().isInstance(bukkitCmd)) { |  | ||||||
|             return isFromThisCommand(ReflectWrapper.wrapTyped((BukkitCommand) bukkitCmd, VanillaCommandWrapper.class).vanillaCommand()); |  | ||||||
|         } |  | ||||||
|         return false; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private static String getCommandIdentity(Command bukkitCmd) { |  | ||||||
|         if (bukkitCmd instanceof PluginCommand cmd) { |  | ||||||
|             return "Bukkit command: /" + cmd.getName() + " from plugin " + cmd.getPlugin().getName(); |  | ||||||
|         } |  | ||||||
|         else if (VanillaCommandWrapper.REFLECT.get().isInstance(bukkitCmd)) { |  | ||||||
|             return "Vanilla command: /" + bukkitCmd.getName(); |  | ||||||
|         } |  | ||||||
|         else |  | ||||||
|             return bukkitCmd.getClass().getName() + ": /" + bukkitCmd.getName(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Player command sender event handler. |      * Gets the aliases that are actually registered in the server. | ||||||
|      * @param event the event. |      * @return the actually registered aliases. | ||||||
|      */ |      */ | ||||||
|     @EventHandler |     protected Set<String> getRegisteredAliases() { | ||||||
|     public void onPlayerCommandSend(PlayerCommandSendEvent event) { |         return Set.copyOf(registeredAliases); | ||||||
|         event.getCommands().removeAll(registeredAliases.stream().map(s -> "minecraft:" + s).toList()); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Server load event handler. |  | ||||||
|      * @param event the event. |  | ||||||
|      */ |  | ||||||
|     @EventHandler |  | ||||||
|     public void onServerLoad(ServerLoadEvent event) { |  | ||||||
|         register(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -339,6 +376,15 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga | |||||||
|      */ |      */ | ||||||
|     protected abstract String getTargetPermission(); |     protected abstract String getTargetPermission(); | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns the permission that should be tested instead of "minecraft.command.cmdName". The conversion from the | ||||||
|  |      * minecraft prefixed permission node to the returned node is done by the {@code pandalib-paper-permissions} if it | ||||||
|  |      * is present in the classpath during runtime. | ||||||
|  |      * @return the permission that should be tested instead of "minecraft.command.cmdName". | ||||||
|  |      */ | ||||||
|  |     protected String getDescription() { | ||||||
|  |         return "A command from " + plugin.getName(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -352,13 +398,17 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga | |||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     public boolean isConsole(BukkitBrigadierCommandSource wrapper) { |  | ||||||
|  |     @Override | ||||||
|  |     public boolean isConsole(CommandSourceStack wrapper) { | ||||||
|         return isConsole(getCommandSender(wrapper)); |         return isConsole(getCommandSender(wrapper)); | ||||||
|     } |     } | ||||||
|     public boolean isPlayer(BukkitBrigadierCommandSource wrapper) { |     @Override | ||||||
|  |     public boolean isPlayer(CommandSourceStack wrapper) { | ||||||
|         return isPlayer(getCommandSender(wrapper)); |         return isPlayer(getCommandSender(wrapper)); | ||||||
|     } |     } | ||||||
|     public Predicate<BukkitBrigadierCommandSource> hasPermission(String permission) { |     @Override | ||||||
|  |     public Predicate<CommandSourceStack> hasPermission(String permission) { | ||||||
|         return wrapper -> getCommandSender(wrapper).hasPermission(permission); |         return wrapper -> getCommandSender(wrapper).hasPermission(permission); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -392,7 +442,7 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga | |||||||
|      * @param context the command context from which to get the Bukkit command sender. |      * @param context the command context from which to get the Bukkit command sender. | ||||||
|      * @return 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()); |         return getCommandSender(context.getSource()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -401,8 +451,8 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga | |||||||
|      * @param wrapper the wrapper from which to get the Bukkit command sender. |      * @param wrapper the wrapper from which to get the Bukkit command sender. | ||||||
|      * @return the Bukkit command sender. |      * @return the Bukkit command sender. | ||||||
|      */ |      */ | ||||||
|     public static CommandSender getCommandSender(BukkitBrigadierCommandSource wrapper) { |     public static CommandSender getCommandSender(CommandSourceStack wrapper) { | ||||||
|         return wrapper.getBukkitSender(); |         return wrapper.getSender(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -410,13 +460,13 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga | |||||||
|      * @param sender the command sender. |      * @param sender the command sender. | ||||||
|      * @return a new instance of a command sender wrapper for the provided command sender. |      * @return a new instance of a command sender wrapper for the provided command sender. | ||||||
|      */ |      */ | ||||||
|     public static BukkitBrigadierCommandSource getBrigadierCommandSource(CommandSender sender) { |     public static CommandSourceStack getBrigadierCommandSource(CommandSender sender) { | ||||||
|         return VanillaCommandWrapper.getListener(sender); |         throw new UnsupportedOperationException("The 1.20.6 Paper API update uses a different wrapper for Brigadier command sender."); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * A suggestions supplier that suggests the names of the currently connected players (that the command sender can see). |      * A suggestion supplier that suggests the names of the currently connected players (that the command sender can see). | ||||||
|      */ |      */ | ||||||
|     public static final SuggestionsSupplier<CommandSender> TAB_PLAYER_CURRENT_SERVER =  (sender, ti, token, a) -> { |     public static final SuggestionsSupplier<CommandSender> TAB_PLAYER_CURRENT_SERVER =  (sender, ti, token, a) -> { | ||||||
|         @SuppressWarnings("unchecked") |         @SuppressWarnings("unchecked") | ||||||
| @@ -430,7 +480,7 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga | |||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * A suggestions supplier that suggests the names of the worlds currently loaded on this server. |      * A suggestion supplier that suggests the names of the worlds currently loaded on this server. | ||||||
|      */ |      */ | ||||||
|     public static final SuggestionsSupplier<CommandSender> TAB_WORLDS = SuggestionsSupplier.fromStreamSupplier(() -> Bukkit.getWorlds().stream().map(World::getName)); |     public static final SuggestionsSupplier<CommandSender> TAB_WORLDS = SuggestionsSupplier.fromStreamSupplier(() -> Bukkit.getWorlds().stream().map(World::getName)); | ||||||
|  |  | ||||||
| @@ -440,7 +490,7 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga | |||||||
|      * @param suggestions the suggestions to wrap. |      * @param suggestions the suggestions to wrap. | ||||||
|      * @return a {@link SuggestionProvider} generating the suggestions from the provided {@link SuggestionsSupplier}. |      * @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); |         return wrapSuggestions(suggestions, PaperBrigadierCommand::getCommandSender); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -453,12 +503,15 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga | |||||||
|      * @param cmd the command executor to wrap. |      * @param cmd the command executor to wrap. | ||||||
|      * @return a wrapper command executor. |      * @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 -> { |         return context -> { | ||||||
|             try { |             try { | ||||||
|                 return cmd.run(context); |                 return cmd.run(context); | ||||||
|             } catch(CommandSyntaxException e) { |             } catch(CommandSyntaxException e) { | ||||||
|                 throw e; |                 throw e; | ||||||
|  |             } catch (BadCommandUsage e) { | ||||||
|  |                 getCommandSender(context).sendMessage(Chat.failureText("Error while using the command: " + e.getMessage())); | ||||||
|  |                 return 0; | ||||||
|             } catch (Throwable t) { |             } catch (Throwable t) { | ||||||
|                 Log.severe(t); |                 Log.severe(t); | ||||||
|                 getCommandSender(context).sendMessage(Chat.failureText("Error while executing the command: " + t)); |                 getCommandSender(context).sendMessage(Chat.failureText("Error while executing the command: " + t)); | ||||||
| @@ -475,120 +528,9 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga | |||||||
|  |  | ||||||
|  |  | ||||||
|     /* |     /* | ||||||
|      * Minecraft argument type |      * Minecraft's argument type | ||||||
|      */ |      */ | ||||||
|  |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Creates a new instance of the Brigadier argument type {@code minecraft:entity}. |  | ||||||
|      * @param singleTarget if this argument takes only a single target. |  | ||||||
|      * @param playersOnly if this argument takes players only. |  | ||||||
|      * @return the {@code minecraft:entity} argument type with the specified parameters. |  | ||||||
|      */ |  | ||||||
|     public static ArgumentType<Object> argumentMinecraftEntity(boolean singleTarget, boolean playersOnly) { |  | ||||||
|         if (playersOnly) { |  | ||||||
|             return singleTarget ? EntityArgument.player() : EntityArgument.players(); |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             return singleTarget ? EntityArgument.entity() : EntityArgument.entities(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Gets the value of the provided argument of type {@code minecraft:entity} (list of entities), from the provided context. |  | ||||||
|      * @param context the command execution context. |  | ||||||
|      * @param argument the argument name. |  | ||||||
|      * @return the value of the argument, or null if not found. |  | ||||||
|      */ |  | ||||||
|     public List<Entity> tryGetMinecraftEntityArgument(CommandContext<BukkitBrigadierCommandSource> context, String argument) { |  | ||||||
|         EntitySelector es = ReflectWrapper.wrap(tryGetArgument(context, argument, EntitySelector.MAPPING.runtimeClass()), EntitySelector.class); |  | ||||||
|         if (es == null) |  | ||||||
|             return null; |  | ||||||
|         List<fr.pandacube.lib.paper.reflect.wrapper.minecraft.world.Entity> nmsEntityList = es.findEntities(context.getSource()); |  | ||||||
|         List<Entity> entityList = new ArrayList<>(nmsEntityList.size()); |  | ||||||
|         for (fr.pandacube.lib.paper.reflect.wrapper.minecraft.world.Entity nmsEntity : nmsEntityList) { |  | ||||||
|             entityList.add(nmsEntity.getBukkitEntity()); |  | ||||||
|         } |  | ||||||
|         return entityList; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Gets the value of the provided argument of type {@code minecraft:entity} (list of players), from the provided context. |  | ||||||
|      * @param context the command execution context. |  | ||||||
|      * @param argument the argument name. |  | ||||||
|      * @return the value of the argument, or null if not found. |  | ||||||
|      */ |  | ||||||
|     public List<Player> tryGetMinecraftEntityArgumentPlayers(CommandContext<BukkitBrigadierCommandSource> context, String argument) { |  | ||||||
|         EntitySelector es = ReflectWrapper.wrap(tryGetArgument(context, argument, EntitySelector.MAPPING.runtimeClass()), EntitySelector.class); |  | ||||||
|         if (es == null) |  | ||||||
|             return null; |  | ||||||
|         List<ServerPlayer> nmsPlayerList = es.findPlayers(context.getSource()); |  | ||||||
|         List<Player> playerList = new ArrayList<>(nmsPlayerList.size()); |  | ||||||
|         for (ServerPlayer nmsPlayer : nmsPlayerList) { |  | ||||||
|             playerList.add(nmsPlayer.getBukkitEntity()); |  | ||||||
|         } |  | ||||||
|         return playerList; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Gets the value of the provided argument of type {@code minecraft:entity} (one entity), from the provided context. |  | ||||||
|      * @param context the command execution context. |  | ||||||
|      * @param argument the argument name. |  | ||||||
|      * @return the value of the argument, or null if not found. |  | ||||||
|      */ |  | ||||||
|     public Entity tryGetMinecraftEntityArgumentOneEntity(CommandContext<BukkitBrigadierCommandSource> context, String argument) { |  | ||||||
|         EntitySelector es = ReflectWrapper.wrap(tryGetArgument(context, argument, EntitySelector.MAPPING.runtimeClass()), EntitySelector.class); |  | ||||||
|         if (es == null) |  | ||||||
|             return null; |  | ||||||
|         fr.pandacube.lib.paper.reflect.wrapper.minecraft.world.Entity nmsEntity = es.findSingleEntity(context.getSource()); |  | ||||||
|         return nmsEntity == null ? null : nmsEntity.getBukkitEntity(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Gets the value of the provided argument of type {@code minecraft:entity} (one player), from the provided context. |  | ||||||
|      * @param context the command execution context. |  | ||||||
|      * @param argument the argument name. |  | ||||||
|      * @return the value of the argument, or null if not found. |  | ||||||
|      */ |  | ||||||
|     public Player tryGetMinecraftEntityArgumentOnePlayer(CommandContext<BukkitBrigadierCommandSource> context, String argument) { |  | ||||||
|         EntitySelector es = ReflectWrapper.wrap(tryGetArgument(context, argument, EntitySelector.MAPPING.runtimeClass()), EntitySelector.class); |  | ||||||
|         if (es == null) |  | ||||||
|             return null; |  | ||||||
|         ServerPlayer nmsPlayer = es.findSinglePlayer(context.getSource()); |  | ||||||
|         return nmsPlayer == null ? null : nmsPlayer.getBukkitEntity(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Creates a new instance of the Brigadier argument type {@code minecraft:block_pos}. |  | ||||||
|      * @return the {@code minecraft:block_pos} argument type. |  | ||||||
|      */ |  | ||||||
|     public static ArgumentType<Object> argumentMinecraftBlockPosition() { |  | ||||||
|         return BlockPosArgument.blockPos(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Gets the value of the provided argument of type {@code minecraft:block_pos}, from the provided context. |  | ||||||
|      * @param context the command execution context. |  | ||||||
|      * @param argument the argument name. |  | ||||||
|      * @param deflt a defualt value if the argument is not found. |  | ||||||
|      * @return the value of the argument. |  | ||||||
|      */ |  | ||||||
|     public BlockVector tryGetMinecraftBlockPositionArgument(CommandContext<BukkitBrigadierCommandSource> context, |  | ||||||
|                                                             String argument, BlockVector deflt) { |  | ||||||
|         return tryGetArgument(context, argument, Coordinates.MAPPING.runtimeClass(), nmsCoord -> { |  | ||||||
|             BlockPos bp = ReflectWrapper.wrap(nmsCoord, Coordinates.class).getBlockPos(context.getSource()); |  | ||||||
|             return new BlockVector(bp.getX(), bp.getY(), bp.getZ()); |  | ||||||
|         }, deflt); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Creates a new instance of the Brigadier argument type {@code minecraft:vec3}. |      * Creates a new instance of the Brigadier argument type {@code minecraft:vec3}. | ||||||
|      * @return the {@code minecraft:vec3} argument type. |      * @return the {@code minecraft:vec3} argument type. | ||||||
| @@ -601,14 +543,14 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga | |||||||
|      * Gets the value of the provided argument of type {@code minecraft:vec3}, from the provided context. |      * Gets the value of the provided argument of type {@code minecraft:vec3}, from the provided context. | ||||||
|      * @param context the command execution context. |      * @param context the command execution context. | ||||||
|      * @param argument the argument name. |      * @param argument the argument name. | ||||||
|      * @param deflt a defualt value if the argument is not found. |      * @param deflt a default value if the argument is not found. | ||||||
|      * @return the value of the argument. |      * @return the value of the argument. | ||||||
|      */ |      */ | ||||||
|     public Vector tryGetMinecraftVec3Argument(CommandContext<BukkitBrigadierCommandSource> context, String argument, |     public Vector tryGetMinecraftVec3Argument(CommandContext<CommandSourceStack> context, String argument, | ||||||
|                                               Vector deflt) { |                                               Vector deflt) { | ||||||
|         return tryGetArgument(context, argument, Coordinates.MAPPING.runtimeClass(), |         return tryGetArgument(context, argument, Coordinates.REFLECT.get(), | ||||||
|                 nmsCoord -> CraftVector.toBukkit( |                 nmsCoordinate -> CraftVector.toBukkit( | ||||||
|                         ReflectWrapper.wrap(nmsCoord, Coordinates.class).getPosition(context.getSource()) |                         wrap(nmsCoordinate, Coordinates.class).getPosition(context.getSource()) | ||||||
|                 ), |                 ), | ||||||
|                 deflt); |                 deflt); | ||||||
|     } |     } | ||||||
| @@ -616,44 +558,11 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga | |||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Creates a new instance of the Brigadier argument type {@code minecraft:component}. |  | ||||||
|      * @return the {@code minecraft:component} argument type. |  | ||||||
|      */ |  | ||||||
|     public static ArgumentType<Object> argumentMinecraftChatComponent() { |  | ||||||
|         return ComponentArgument.textComponent(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Gets the value of the provided argument of type {@code minecraft:component}, from the provided context. |  | ||||||
|      * @param context the command execution context. |  | ||||||
|      * @param argument the argument name. |  | ||||||
|      * @param deflt a defualt value if the argument is not found. |  | ||||||
|      * @return the value of the argument. |  | ||||||
|      */ |  | ||||||
|     public Component tryGetMinecraftChatComponentArgument(CommandContext<BukkitBrigadierCommandSource> context, |  | ||||||
|                                                           String argument, Component deflt) { |  | ||||||
|         return tryGetArgument(context, argument, |  | ||||||
|                 fr.pandacube.lib.paper.reflect.wrapper.minecraft.network.chat.Component.MAPPING.runtimeClass(), |  | ||||||
|                 nmsComp -> PaperAdventure.asAdventure( |  | ||||||
|                         ReflectWrapper.wrap(nmsComp, |  | ||||||
|                                 fr.pandacube.lib.paper.reflect.wrapper.minecraft.network.chat.Component.class) |  | ||||||
|                 ), |  | ||||||
|                 deflt); |  | ||||||
|  |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * All possible choices on how to force the registration of a command, based on certain conditions. |      * All possible choices on how to force the registration of a command, based on certain conditions. | ||||||
|      */ |      */ | ||||||
|     public enum RegistrationPolicy { |     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 |          * 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. |          * exists in the vanilla Brigadier dispatcher. | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user