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 | 
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,3 +1,5 @@ | ||||
| /.idea | ||||
| /*/target | ||||
| dependency-reduced-pom.xml | ||||
|  | ||||
| *.iml | ||||
| @@ -3,7 +3,7 @@ | ||||
| ### Development library for Minecraft server applications and plugins | ||||
|  | ||||
| This repository contains a collection of maven modules that are used for the development of our Minecraft server. Those | ||||
| modules are made open source so they can be used by other developpers. Each of them provides different functionalities | ||||
| modules are made open source, so they can be used by other developers. Each of them provides different functionalities | ||||
| that are detailed in their respective Readme file (if any). | ||||
|  | ||||
| - `pandalib-util` General purpose utility and helper classes; | ||||
| @@ -18,10 +18,10 @@ that are detailed in their respective Readme file (if any). | ||||
| - `pandalib-players` A library to handle classes representing online or offline players; | ||||
| - `pandalib-players-permissible` An extension of `pandalib-players` with support for the permission system `pandalib-permissions`; | ||||
| - `pandalib-netapi` A poorly designed, but working TCP network library; | ||||
| - `pandalib-net` A better-designed, packet-based TCP network library (_still in development_); | ||||
| - `pandalib-config` Utility and helper classes to handle configuration related files and folders; | ||||
| - `pandalib-commands` An abstract command manager working on top of [Brigadier](https://github.com/Mojang/brigadier); | ||||
| - `pandalib-cli` Utility and helper classes for a standalone CLI Java application. | ||||
| - `pandalib-core` A catch-all module for some helper classes that didn’t have their own module yet; | ||||
| - `pandalib-cli` Utility and helper classes for a standalone CLI Java application; | ||||
| - `pandalib-core` A catch-all module for some helper classes that didn't have their own module yet; | ||||
|  | ||||
| ### Use in your projects | ||||
|  | ||||
|   | ||||
							
								
								
									
										1
									
								
								pandalib-bungee-chat/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										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 { | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Registers event listener to redirect permission checks to {@code pandalib-permissions}. | ||||
|      * @param bungeePlugin a BungeeCord plugin. | ||||
| @@ -35,6 +34,8 @@ public class PandalibBungeePermissions implements Listener { | ||||
|     } | ||||
|  | ||||
|  | ||||
|     private PandalibBungeePermissions() {} | ||||
|  | ||||
|     /** | ||||
|      * Event handler called when a plugin asks if a player has a permission. | ||||
|      * @param event the permission check event. | ||||
|   | ||||
| @@ -33,7 +33,7 @@ | ||||
|         </dependency> | ||||
|         <dependency> | ||||
|             <groupId>fr.pandacube.lib</groupId> | ||||
|             <artifactId>pandalib-chat</artifactId> | ||||
|             <artifactId>pandalib-bungee-chat</artifactId> | ||||
|             <version>${project.version}</version> | ||||
|         </dependency> | ||||
|         <dependency> | ||||
|   | ||||
| @@ -1,29 +1,49 @@ | ||||
| package fr.pandacube.lib.bungee; | ||||
|  | ||||
| import fr.pandacube.lib.bungee.util.DailyLogRotateFileHandler; | ||||
| import fr.pandacube.lib.bungee.util.BungeeDailyLogRotateFileHandler; | ||||
| import fr.pandacube.lib.bungee.util.PluginMessagePassthrough; | ||||
| import net.md_5.bungee.api.plugin.Plugin; | ||||
|  | ||||
| /** | ||||
|  * General class used to initialize some tools of pandalib-bungee, following the bungee plugin's lifecycle. | ||||
|  */ | ||||
| public class PandaLibBungee { | ||||
|  | ||||
|     private static Plugin plugin; | ||||
|  | ||||
|     /** | ||||
|      * Method to be called in {@link Plugin#onLoad()} method. | ||||
|      * @param plugin the plugin instance. | ||||
|      */ | ||||
|     public static void onLoad(Plugin plugin) { | ||||
|         PandaLibBungee.plugin = plugin; | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Method to be called in {@link Plugin#onEnable()} method. | ||||
|      */ | ||||
|     public static void onEnable() { | ||||
|         PluginMessagePassthrough.init(plugin); | ||||
|         DailyLogRotateFileHandler.init(true); | ||||
|         BungeeDailyLogRotateFileHandler.init(true); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Method to be called in {@link Plugin#onDisable()} method. | ||||
|      */ | ||||
|     public static void disable() { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the plugin instance. | ||||
|      * @return the plugin instance. | ||||
|      */ | ||||
|     public static Plugin getPlugin() { | ||||
|         return plugin; | ||||
|     } | ||||
|  | ||||
|     private PandaLibBungee() {} | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -6,11 +6,40 @@ import java.io.File; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
|  | ||||
| /** | ||||
|  * Class that holds the configuration varables for {@link BungeeBackupManager}. | ||||
|  */ | ||||
| @SuppressWarnings("CanBeFinal") | ||||
| public class BungeeBackupConfig { | ||||
|     /** | ||||
|      * Tells if the working directory of the current bungee instance should be backed up. | ||||
|      */ | ||||
|     public boolean workdirBackupEnabled = true; | ||||
|     /** | ||||
|      * Tells if the old logs of the current bungee instance should be backed up. | ||||
|      */ | ||||
|     public boolean logsBackupEnabled = true; | ||||
|     /** | ||||
|      * The cron scheduling of when the workdir backup occurs. | ||||
|      */ | ||||
|     public String scheduling = "0 2 * * *"; // cron format, here is every day at 2am | ||||
|     /** | ||||
|      * The destination directory for the backups. | ||||
|      */ | ||||
|     public File backupDirectory = null; | ||||
|     /** | ||||
|      * The configuration handling the cleaning of the backup directory. | ||||
|      */ | ||||
|     public BackupCleaner workdirBackupCleaner = BackupCleaner.KEEPING_1_EVERY_N_MONTH(3).merge(BackupCleaner.KEEPING_N_LAST(5)); | ||||
|     /** | ||||
|      * A list of ignored files or directory in the workdir to exclude from the backup. | ||||
|      */ | ||||
|     public List<String> workdirIgnoreList = new ArrayList<>(); | ||||
|  | ||||
|     /** | ||||
|      * Creates a new {@link BungeeBackupConfig}. | ||||
|      */ | ||||
|     public BungeeBackupConfig() { | ||||
|  | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -6,10 +6,17 @@ import fr.pandacube.lib.core.backup.RotatedLogsBackupProcess; | ||||
|  | ||||
| import java.io.File; | ||||
|  | ||||
| /** | ||||
|  * Handles the backup processes for a Bungeecord instance. | ||||
|  */ | ||||
| public class BungeeBackupManager extends BackupManager { | ||||
|  | ||||
| 	BungeeBackupConfig config; | ||||
|  | ||||
| 	/** | ||||
| 	 * Instanciate a new {@link BungeeBackupManager}. | ||||
| 	 * @param config the configuration. | ||||
| 	 */ | ||||
| 	public BungeeBackupManager(BungeeBackupConfig config) { | ||||
| 		super(config.backupDirectory); | ||||
| 		setConfig(config); | ||||
| @@ -24,12 +31,19 @@ public class BungeeBackupManager extends BackupManager { | ||||
| 		super.addProcess(process); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Sets a new configuration for this backup manager. | ||||
| 	 * @param config the new configuration. | ||||
| 	 */ | ||||
| 	public void setConfig(BungeeBackupConfig config) { | ||||
| 		this.config = config; | ||||
| 		backupQueue.forEach(this::updateProcessConfig); | ||||
| 	} | ||||
|  | ||||
|  | ||||
| 	/** | ||||
| 	 * Deploys the new configuration to the provided backup process. | ||||
| 	 * @param process the process on which to apply the new config. | ||||
| 	 */ | ||||
| 	public void updateProcessConfig(BackupProcess process) { | ||||
| 		if (process instanceof BungeeWorkdirProcess) { | ||||
| 			process.setEnabled(config.workdirBackupEnabled); | ||||
|   | ||||
| @@ -5,8 +5,15 @@ import fr.pandacube.lib.core.backup.BackupProcess; | ||||
| import java.io.File; | ||||
| import java.util.function.BiPredicate; | ||||
|  | ||||
| /** | ||||
|  * The backup process responsible for the working directory of the current Bungeecord instance. | ||||
|  */ | ||||
| public class BungeeWorkdirProcess extends BackupProcess { | ||||
|  | ||||
| 	/** | ||||
| 	 * Instantiates this backup process. | ||||
| 	 * @param bm the backup manager. | ||||
| 	 */ | ||||
| 	protected BungeeWorkdirProcess(BungeeBackupManager bm) { | ||||
| 		super(bm, "workdir"); | ||||
| 	} | ||||
| @@ -18,15 +25,12 @@ public class BungeeWorkdirProcess extends BackupProcess { | ||||
| 	 | ||||
| 	 | ||||
| 	public BiPredicate<File, String> getFilenameFilter() { | ||||
| 		return new BiPredicate<>() { | ||||
| 			@Override | ||||
| 			public boolean test(File file, String path) { | ||||
| 		return (file, path) -> { | ||||
| 			if (new File(getSourceDir(), "logs").equals(file)) | ||||
| 				return false; | ||||
| 			if (file.isFile() && file.getName().endsWith(".lck")) | ||||
| 				return false; | ||||
| 			return BungeeWorkdirProcess.super.getFilenameFilter().test(file, path); | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| 	 | ||||
|   | ||||
| @@ -7,7 +7,7 @@ import com.mojang.brigadier.suggestion.Suggestions; | ||||
| import com.mojang.brigadier.tree.LiteralCommandNode; | ||||
| import fr.pandacube.lib.commands.BrigadierCommand; | ||||
| import fr.pandacube.lib.commands.SuggestionsSupplier; | ||||
| import fr.pandacube.lib.util.Log; | ||||
| import fr.pandacube.lib.util.log.Log; | ||||
| import net.md_5.bungee.api.CommandSender; | ||||
| import net.md_5.bungee.api.ProxyServer; | ||||
| import net.md_5.bungee.api.connection.ProxiedPlayer; | ||||
| @@ -28,10 +28,10 @@ public abstract class BungeeBrigadierCommand extends BrigadierCommand<CommandSen | ||||
| 	/** | ||||
| 	 * The command dispatcher. | ||||
| 	 */ | ||||
| 	protected BungeeBrigadierDispatcher dispatcher = BungeeBrigadierDispatcher.getInstance(); | ||||
| 	protected final BungeeBrigadierDispatcher dispatcher = BungeeBrigadierDispatcher.getInstance(); | ||||
|  | ||||
| 	/** | ||||
| 	 * Instanciate this command instance. | ||||
| 	 * Instantiate this command instance. | ||||
| 	 */ | ||||
| 	public BungeeBrigadierCommand() { | ||||
| 		LiteralCommandNode<CommandSender> commandNode; | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| package fr.pandacube.lib.bungee.commands; | ||||
|  | ||||
| import fr.pandacube.lib.chat.Chat; | ||||
| import fr.pandacube.lib.bungee.chat.ChatBungee; | ||||
| import fr.pandacube.lib.commands.BrigadierDispatcher; | ||||
| import net.kyori.adventure.text.ComponentLike; | ||||
| import net.md_5.bungee.api.CommandSender; | ||||
| @@ -39,7 +39,7 @@ public class BungeeBrigadierDispatcher extends BrigadierDispatcher<CommandSender | ||||
| 	 */ | ||||
| 	public BungeeBrigadierDispatcher(Plugin pl) { | ||||
| 		if (instance != null) | ||||
| 			throw new IllegalStateException("Cannot instanciante more than one BungeeBrigadierDispatcher"); | ||||
| 			throw new IllegalStateException("Cannot instantiate more than one BungeeBrigadierDispatcher"); | ||||
| 		instance = this; | ||||
| 		plugin = pl; | ||||
| 		ProxyServer.getInstance().getPluginManager().registerListener(plugin, this); | ||||
| @@ -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. | ||||
| 	 */ | ||||
| 	@EventHandler | ||||
| @@ -71,6 +71,6 @@ public class BungeeBrigadierDispatcher extends BrigadierDispatcher<CommandSender | ||||
|  | ||||
| 	@Override | ||||
| 	protected void sendSenderMessage(CommandSender sender, ComponentLike message) { | ||||
| 		sender.sendMessage(Chat.toBungee(message.asComponent())); | ||||
| 		sender.sendMessage(ChatBungee.toBungee(message.asComponent())); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,13 +1,12 @@ | ||||
| package fr.pandacube.lib.bungee.players; | ||||
|  | ||||
| import java.util.Locale; | ||||
| import java.util.UUID; | ||||
|  | ||||
| import fr.pandacube.lib.bungee.chat.ChatBungee; | ||||
| import fr.pandacube.lib.core.mc_version.ProtocolVersion; | ||||
| import fr.pandacube.lib.players.standalone.AbstractOnlinePlayer; | ||||
| import fr.pandacube.lib.reflect.Reflect; | ||||
| import io.netty.buffer.ByteBuf; | ||||
| import io.netty.buffer.ByteBufAllocator; | ||||
| import net.kyori.adventure.identity.Identified; | ||||
| import net.kyori.adventure.text.Component; | ||||
| import net.kyori.adventure.text.ComponentLike; | ||||
| import net.md_5.bungee.api.ProxyServer; | ||||
| import net.md_5.bungee.api.SkinConfiguration; | ||||
| import net.md_5.bungee.api.connection.ProxiedPlayer; | ||||
| @@ -18,10 +17,7 @@ import net.md_5.bungee.protocol.DefinedPacket; | ||||
| import net.md_5.bungee.protocol.packet.ClientSettings; | ||||
| import net.md_5.bungee.protocol.packet.PluginMessage; | ||||
|  | ||||
| import fr.pandacube.lib.chat.Chat; | ||||
| import fr.pandacube.lib.players.standalone.AbstractOnlinePlayer; | ||||
| import fr.pandacube.lib.reflect.Reflect; | ||||
| import fr.pandacube.lib.util.MinecraftVersion; | ||||
| import java.util.Locale; | ||||
|  | ||||
| /** | ||||
|  * Represents any online player on a Bungeecord proxy. | ||||
| @@ -53,11 +49,11 @@ public interface BungeeOnlinePlayer extends BungeeOffPlayer, AbstractOnlinePlaye | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Gets the minecraft version of this player’s client. | ||||
|      * @return the minecraft version of this player’s client. | ||||
|      * Gets the protocol version of this player’s client. | ||||
|      * @return the protocol version of this player’s client. | ||||
|      */ | ||||
|     default MinecraftVersion getMinecraftVersion() { | ||||
|         return MinecraftVersion.getVersion(getBungeeProxiedPlayer().getPendingConnection().getVersion()); | ||||
|     default ProtocolVersion getProtocolVersion() { | ||||
|         return ProtocolVersion.ofProtocol(getBungeeProxiedPlayer().getPendingConnection().getVersion()); | ||||
|     } | ||||
|  | ||||
|  | ||||
| @@ -88,33 +84,13 @@ public interface BungeeOnlinePlayer extends BungeeOffPlayer, AbstractOnlinePlaye | ||||
|  | ||||
|     @Override | ||||
|     default void sendMessage(Component message) { | ||||
|         getBungeeProxiedPlayer().sendMessage(Chat.toBungee(message)); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     default void sendMessage(ComponentLike message, UUID sender) { | ||||
|         getBungeeProxiedPlayer().sendMessage(sender, Chat.toBungee(message.asComponent())); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     default void sendMessage(Component message, Identified sender) { | ||||
|         getBungeeProxiedPlayer().sendMessage(sender == null ? null : sender.identity().uuid(), Chat.toBungee(message)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Display the provided message in the 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())); | ||||
|         getBungeeProxiedPlayer().sendMessage(ChatBungee.toBungee(message)); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     default void sendTitle(Component title, Component subtitle, int fadeIn, int stay, int fadeOut) { | ||||
|         ProxyServer.getInstance().createTitle() | ||||
|                 .title(Chat.toBungee(title)).subTitle(Chat.toBungee(subtitle)) | ||||
|                 .title(ChatBungee.toBungee(title)).subTitle(ChatBungee.toBungee(subtitle)) | ||||
|                 .fadeIn(fadeIn).stay(stay).fadeOut(fadeOut) | ||||
|                 .send(getBungeeProxiedPlayer()); | ||||
|     } | ||||
|   | ||||
| @@ -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> | ||||
|             <groupId>net.kyori</groupId> | ||||
|             <artifactId>adventure-api</artifactId> | ||||
|             <version>4.11.0</version> | ||||
|             <version>4.15.0</version> | ||||
|         </dependency> | ||||
|         <dependency> | ||||
|             <groupId>net.kyori</groupId> | ||||
|             <artifactId>adventure-platform-bungeecord</artifactId> | ||||
|             <version>4.1.1</version> | ||||
|             <artifactId>adventure-text-serializer-gson</artifactId> | ||||
|             <version>4.13.0</version> | ||||
|         </dependency> | ||||
|         <dependency> | ||||
|             <groupId>net.kyori</groupId> | ||||
|             <artifactId>adventure-text-serializer-legacy</artifactId> | ||||
|             <version>4.13.0</version> | ||||
|         </dependency> | ||||
|         <dependency> | ||||
|             <groupId>net.kyori</groupId> | ||||
|             <artifactId>adventure-text-serializer-plain</artifactId> | ||||
|             <version>4.11.0</version> | ||||
|             <version>4.15.0</version> | ||||
|         </dependency> | ||||
|         <dependency> | ||||
|             <groupId>net.kyori</groupId> | ||||
|             <artifactId>adventure-text-minimessage</artifactId> | ||||
|             <version>4.15.0</version> | ||||
|         </dependency> | ||||
|  | ||||
|  | ||||
|  | ||||
|         <dependency> | ||||
|             <groupId>net.md-5</groupId> | ||||
|             <artifactId>bungeecord-chat</artifactId> | ||||
|             <version>${bungeecord.version}</version> | ||||
|             <scope>compile</scope> | ||||
|             <groupId>com.google.code.gson</groupId> | ||||
|             <artifactId>gson</artifactId> | ||||
|             <version>2.10.1</version> | ||||
|         </dependency> | ||||
|     </dependencies> | ||||
|  | ||||
|   | ||||
| @@ -1,15 +1,12 @@ | ||||
| package fr.pandacube.lib.chat; | ||||
|  | ||||
| import java.awt.Color; | ||||
| import java.util.Objects; | ||||
| import java.util.function.Consumer; | ||||
| import java.util.function.UnaryOperator; | ||||
|  | ||||
| import net.kyori.adventure.key.Key; | ||||
| import net.kyori.adventure.text.Component; | ||||
| import net.kyori.adventure.text.ComponentBuilder; | ||||
| import net.kyori.adventure.text.ComponentLike; | ||||
| import net.kyori.adventure.text.TextComponent; | ||||
| import net.kyori.adventure.text.TranslationArgument; | ||||
| import net.kyori.adventure.text.TranslationArgumentLike; | ||||
| import net.kyori.adventure.text.event.ClickEvent; | ||||
| import net.kyori.adventure.text.event.HoverEvent; | ||||
| import net.kyori.adventure.text.event.HoverEventSource; | ||||
| @@ -18,11 +15,17 @@ import net.kyori.adventure.text.format.Style; | ||||
| import net.kyori.adventure.text.format.TextColor; | ||||
| import net.kyori.adventure.text.format.TextDecoration; | ||||
| import net.kyori.adventure.text.format.TextDecoration.State; | ||||
| import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer; | ||||
| import net.kyori.adventure.text.minimessage.MiniMessage; | ||||
| import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; | ||||
| import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; | ||||
| import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; | ||||
| import net.md_5.bungee.api.ChatColor; | ||||
| import net.md_5.bungee.api.chat.BaseComponent; | ||||
| import org.jetbrains.annotations.NotNull; | ||||
|  | ||||
| import java.awt.*; | ||||
| import java.util.Locale; | ||||
| import java.util.Objects; | ||||
| import java.util.function.Consumer; | ||||
| import java.util.function.UnaryOperator; | ||||
|  | ||||
| /** | ||||
|  * A builder for chat components. | ||||
| @@ -30,10 +33,10 @@ import net.md_5.bungee.api.chat.BaseComponent; | ||||
|  * Use one of the provided static methods to create a new instance. | ||||
|  * <p> | ||||
|  * This class implements {@link ComponentLike} and {@link HoverEventSource} so they can be used directly in | ||||
|  * Adventure API and its implentation without using the final methods of this builder. | ||||
|  * Adventure API and its implementation without using the final methods of this builder. | ||||
|  * <p> | ||||
|  * The unique possible concrete subclass of this class, {@link FormatableChat}, takes care of the formating of the | ||||
|  * builded component. The rationale for this design is explained in the documentation of {@link FormatableChat}. | ||||
|  * The unique possible concrete subclass of this class, {@link FormatableChat}, takes care of the formatting of the | ||||
|  * built component. The rationale for this design is explained in the documentation of {@link FormatableChat}. | ||||
|  */ | ||||
| public abstract sealed class Chat extends ChatStatic implements HoverEventSource<Component>, ComponentLike { | ||||
|  | ||||
| @@ -60,61 +63,70 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource | ||||
|  | ||||
|     /** | ||||
|      * Builds the component into Adventure Component instance. | ||||
|      * @return the {@link Component} builded from this {@link Chat} component. | ||||
|      * @return the {@link Component} built from this {@link Chat} component. | ||||
|      */ | ||||
|     public Component getAdv() { | ||||
|     public Component get() { | ||||
|         return builder.build(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Builds the component into BungeeCord {@link BaseComponent} instance. | ||||
|      * @return the {@link BaseComponent} builded from this {@link Chat} component. | ||||
|      */ | ||||
|     public BaseComponent get() { | ||||
|         return toBungee(getAdv()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Builds the component into BungeeCord {@link BaseComponent} array. | ||||
|      * @return the {@link BaseComponent} array builded from this {@link Chat} component. | ||||
|      */ | ||||
|     public BaseComponent[] getAsArray() { | ||||
|         return toBungeeArray(getAdv()); | ||||
|     } | ||||
|  | ||||
|     private static final LegacyComponentSerializer LEGACY_SERIALIZER_BUNGEE_FIENDLY = LegacyComponentSerializer.builder() | ||||
|     private static final LegacyComponentSerializer LEGACY_SERIALIZER_BUNGEE_FRIENDLY = LegacyComponentSerializer.builder() | ||||
|             .hexColors() | ||||
|             .useUnusualXRepeatedCharacterHexFormat() | ||||
|             .build(); | ||||
|  | ||||
|     /** | ||||
|      * Converts the builded component into legacy text. | ||||
|      * Converts the built component into legacy text. | ||||
|      * @return the legacy text. RGB colors are in BungeeCord format. | ||||
|      */ | ||||
|     public String getLegacyText() { | ||||
|         return LEGACY_SERIALIZER_BUNGEE_FIENDLY.serialize(getAdv()); | ||||
|         return LEGACY_SERIALIZER_BUNGEE_FRIENDLY.serialize(get()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Converts the builded component into plain text. | ||||
|      * Converts the built component into plain text. | ||||
|      * @return the plain text of this component. | ||||
|      */ | ||||
|     public String getPlainText() { | ||||
|         return PlainTextComponentSerializer.plainText().serializeOr(getAdv(), ""); | ||||
|         return PlainTextComponentSerializer.plainText().serializeOr(get(), ""); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public HoverEvent<Component> asHoverEvent(UnaryOperator<Component> op) { | ||||
|         return HoverEvent.showText(op.apply(getAdv())); | ||||
|     public @NotNull HoverEvent<Component> asHoverEvent(@NotNull UnaryOperator<Component> op) { | ||||
|         return HoverEvent.showText(op.apply(get())); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Builds the component into Adventure Component instance. | ||||
|      * @return the {@link Component} builded from this {@link Chat} component. | ||||
|      * @return the {@link Component} built from this {@link Chat} component. | ||||
|      */ | ||||
|     @Override | ||||
|     public Component asComponent() { | ||||
|         return getAdv(); | ||||
|     public @NotNull Component asComponent() { | ||||
|         return get(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Builds the component into Adventure Component instance, also down sampling the RGB colors to named colors. | ||||
|      * @return the {@link Component} built from this {@link Chat} component, with down-sampled colors. | ||||
|      */ | ||||
|     public Component getAsDownSampledColorsComponent() { | ||||
|         String json = GsonComponentSerializer.colorDownsamplingGson().serialize(get()); | ||||
|         return GsonComponentSerializer.gson().deserialize(json); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns a new {@link Chat} consisting of this {@link Chat} instance, with the RGB colors down-sampled to named colors. | ||||
|      * @return a new {@link Chat} instance, with down-sampled colors. | ||||
|      */ | ||||
|     public Chat getAsDownSampledColors() { | ||||
|         return chatComponent(getAsDownSampledColorsComponent()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns a MiniMessage representation of this {@link Chat} component. | ||||
|      * @return the MiniMessage representation if this {@link Chat} component. | ||||
|      */ | ||||
|     public String getMiniMessage() { | ||||
|         return MiniMessage.miniMessage().serialize(get()); | ||||
|     } | ||||
|  | ||||
|  | ||||
| @@ -154,15 +166,6 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Appends a BungeeCord {@link BaseComponent} to this component. | ||||
|      * @param comp the component to append. | ||||
|      * @return this. | ||||
|      */ | ||||
|     public Chat then(BaseComponent comp) { | ||||
|         return then(toAdventure(comp)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Appends a component to this component. | ||||
|      * @param comp the component to append. | ||||
| @@ -177,15 +180,6 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource | ||||
|         return then(comp.asComponent()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Appends a BungeeCord {@link BaseComponent} array to this component. | ||||
|      * @param comp the components to append. | ||||
|      * @return this. | ||||
|      */ | ||||
|     public Chat then(BaseComponent[] comp) { | ||||
|         return then(toAdventure(comp)); | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -260,7 +254,7 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource | ||||
|      * @param comp the component. | ||||
|      * @return this. | ||||
|      */ | ||||
|     public Chat thenPlayerName(Component comp) { return then(playerNameComponent(comp)); } | ||||
|     public Chat thenPlayerName(ComponentLike comp) { return then(playerNameComponent(comp)); } | ||||
|  | ||||
|     /** | ||||
|      * Appends a component consisting of a new line. | ||||
| @@ -269,12 +263,26 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource | ||||
|     public Chat thenNewLine() { return then(Component.newline()); } | ||||
|  | ||||
|     /** | ||||
|      * Appends a component with the provided legacy text as its content. | ||||
|      * @param legacyText the legacy text. | ||||
|      * Appends a component with the provided legacy text as its content, using the section {@code "§"} character. | ||||
|      * @param legacyText the legacy text that uses the {@code "§"} character. | ||||
|      * @return this. | ||||
|      */ | ||||
|     public Chat thenLegacyText(Object legacyText) { return then(legacyText(legacyText)); } | ||||
|  | ||||
|     /** | ||||
|      * Appends a component with the provided legacy text as its content, using the ampersand {@code "&"} character. | ||||
|      * @param legacyText the legacy text that uses the {@code "&"} character. | ||||
|      * @return this. | ||||
|      */ | ||||
|     public Chat thenLegacyAmpersandText(Object legacyText) { return then(legacyAmpersandText(legacyText)); } | ||||
|  | ||||
|     /** | ||||
|      * Appends a component with the provided MiniMessage text as its content. | ||||
|      * @param miniMessageText the MiniMessage text. | ||||
|      * @return this. | ||||
|      */ | ||||
|     public Chat thenMiniMessage(String miniMessageText) { return then(miniMessageText(miniMessageText)); } | ||||
|  | ||||
|     /** | ||||
|      * Appends a component with the provided translation key and parameters. | ||||
|      * @param key the translation key. | ||||
| @@ -284,8 +292,8 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource | ||||
|     public Chat thenTranslation(String key, Object... with) { return then(translation(key, with)); } | ||||
|  | ||||
|     /** | ||||
|      * Appends a component with the provided keybind. | ||||
|      * @param key the keybind to display. | ||||
|      * Appends a component with the provided keybinding. | ||||
|      * @param key the keybinding to display. | ||||
|      * @return this. | ||||
|      */ | ||||
|     public Chat thenKeyBind(String key) { return then(keybind(key)); } | ||||
| @@ -443,50 +451,28 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Appends a component filling a line of chat (or console) with the configured decoration character and | ||||
|      * Appends a component filling a chat line with the configured decoration character and | ||||
|      * color and a left-aligned text. | ||||
|      * @param leftText the text aligned to the left. | ||||
|      * @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character | ||||
|      * @return a new {@link FormatableChat} filling a chat line with the configured decoration character | ||||
|      *         and color and a left-aligned text. | ||||
|      */ | ||||
|     public Chat thenLeftText(ComponentLike leftText) { return then(leftText(leftText, console)); } | ||||
|  | ||||
|     /** | ||||
|      * Appends a component filling a line of chat (or console) with the configured decoration character and | ||||
|      * color and a left-aligned text. | ||||
|      * @param leftText the text aligned to the left. | ||||
|      * @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character | ||||
|      *         and color and a left-aligned text. | ||||
|      * @deprecated uses Bungeecord chat API. | ||||
|      */ | ||||
|     @Deprecated | ||||
|     public Chat thenLeftText(BaseComponent leftText) { return thenLeftText(chatComponent(leftText)); } | ||||
|  | ||||
|     /** | ||||
|      * Appends a component filling a line of chat (or console) with the configured decoration character and | ||||
|      * Appends a component filling a chat line with the configured decoration character and | ||||
|      * color and a right-aligned text. | ||||
|      * @param rightText the text aligned to the right. | ||||
|      * @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character | ||||
|      * @return a new {@link FormatableChat} filling a chat line with the configured decoration character | ||||
|      *         and color and a right-aligned text. | ||||
|      */ | ||||
|     public Chat thenRightText(ComponentLike rightText) { return then(rightText(rightText, console)); } | ||||
|  | ||||
|     /** | ||||
|      * Appends a component filling a line of chat (or console) with the configured decoration character and | ||||
|      * color and a right-aligned text. | ||||
|      * @param rightText the text aligned to the right. | ||||
|      * @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character | ||||
|      *         and color and a right-aligned text. | ||||
|      * @deprecated uses Bungeecord chat API. | ||||
|      */ | ||||
|     @Deprecated | ||||
|     public Chat thenRightText(BaseComponent rightText) { return thenRightText(chatComponent(rightText)); } | ||||
|  | ||||
|     /** | ||||
|      * Appends a component filling a line of chat (or console) with the configured decoration character and | ||||
|      * Appends a component filling a chat line with the configured decoration character and | ||||
|      * color and a centered text. | ||||
|      * @param centerText the text aligned to the center. | ||||
|      * @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character | ||||
|      * @return a new {@link FormatableChat} filling a chat line with the configured decoration character | ||||
|      *         and color and a centered text. | ||||
|      */ | ||||
|     public Chat thenCenterText(ComponentLike centerText) { | ||||
| @@ -494,21 +480,8 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Appends a component filling a line of chat (or console) with the configured decoration character and | ||||
|      * color and a centered text. | ||||
|      * @param centerText the text aligned to the center. | ||||
|      * @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character | ||||
|      *         and color and a centered text. | ||||
|      * @deprecated uses Bungeecord chat API. | ||||
|      */ | ||||
|     @Deprecated | ||||
|     public Chat thenCenterText(BaseComponent centerText) { | ||||
|         return thenCenterText(chatComponent(centerText)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Appends a component filling a line of chat (or console) with the configured decoration character and color. | ||||
|      * @return a new {@link FormatableChat} filling a line of chat (or console) with a decoration character and color. | ||||
|      * Appends a component filling a chat line with the configured decoration character and color. | ||||
|      * @return a new {@link FormatableChat} filling a chat line with a decoration character and color. | ||||
|      */ | ||||
|     public Chat thenFilledLine() { return then(filledLine(console)); } | ||||
|  | ||||
| @@ -534,11 +507,11 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource | ||||
|      *         .append("!").color(ChatColor.RED) | ||||
|      *         .create(); | ||||
|      * }</pre> | ||||
|      * Here, when you call a formating method (like {@code bold(boolean)} or {@code color(ChatColor)}) after the | ||||
|      * {@code append(String)} method, the formating apply to the last sub-component appended. | ||||
|      * Here, when you call a formatting method (like {@code bold(boolean)} or {@code color(ChatColor)}) after the | ||||
|      * {@code append(String)} method, the formatting apply to the last subcomponent appended. | ||||
|      * <p> | ||||
|      * In our design, we want the formating to apply to the currently builded component, not the last appended one. | ||||
|      * The purpose is to make the component structure clearer and have better control of the formating over the | ||||
|      * In our design, we want the formatting to apply to the currently built component, not the last appended one. | ||||
|      * The purpose is to make the component structure clearer and have better control of the formatting over the | ||||
|      * component hierarchy. | ||||
|      * Here is the equivalent of the above code, with the {@link Chat} API: | ||||
|      * <pre>{@code | ||||
| @@ -547,9 +520,9 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource | ||||
|      *         .thenText("!"); // short for .then(Chat.text("!")) | ||||
|      *         // the red color for "!" is not needed because the parent component is already red. | ||||
|      * }</pre> | ||||
|      * When calling {@link #then(Component) #then(...)} on a {@link FormatableChat}, the method returns itself, casted | ||||
|      * to {@link Chat}, to prevent future formating (that the programmer would think it formats the previously appended | ||||
|      * sub-component). If the formatting of the currently builded component is needed, since {@link Chat} is a sealed | ||||
|      * When calling {@link #then(Component) #then(...)} on a {@link FormatableChat}, the method returns itself, cast | ||||
|      * to {@link Chat}, to prevent future formatting (that the programmer would think it formats the previously appended | ||||
|      * subcomponent). If the formatting of the currently built component is needed, since {@link Chat} is a sealed | ||||
|      * class which only subclass is {@link FormatableChat}, you can cast the builder, and use the format methods again. | ||||
|      * <pre>{@code | ||||
|      * Chat component = Chat.text("Hello ").red() | ||||
| @@ -585,12 +558,6 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource | ||||
|          * @return this. | ||||
|          */ | ||||
|         public FormatableChat color(TextColor c) { builder.color(c); return this; } | ||||
|         /** | ||||
|          * Sets the color of this component. | ||||
|          * @param c the color. | ||||
|          * @return this. | ||||
|          */ | ||||
|         public FormatableChat color(ChatColor c) { return color(c == null ? null : TextColor.color(c.getColor().getRGB())); } | ||||
|         /** | ||||
|          * Sets the color of this component. | ||||
|          * @param c the color. | ||||
| @@ -602,7 +569,16 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource | ||||
|          * @param c the color. | ||||
|          * @return this. | ||||
|          */ | ||||
|         public FormatableChat color(String c) { return color(c == null ? null : ChatColor.of(c)); } | ||||
|         public FormatableChat color(String c) { | ||||
|             if (c == null) | ||||
|                 return color((TextColor) null); | ||||
|             TextColor tc = c.startsWith("#") | ||||
|                     ? TextColor.fromCSSHexString(c) | ||||
|                     : NamedTextColor.NAMES.value(c.toLowerCase(Locale.ROOT)); | ||||
|             if (tc == null) | ||||
|                 throw new IllegalArgumentException("Invalid color string '" + c + "'."); | ||||
|             return color(tc); | ||||
|         } | ||||
|  | ||||
|  | ||||
|         /** | ||||
| @@ -880,18 +856,6 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource | ||||
|          * @return this. | ||||
|          */ | ||||
|         public FormatableChat hover(ComponentLike v) { return hover(v.asComponent()); } | ||||
|         /** | ||||
|          * Configure this component to show the provided component when hovered. | ||||
|          * @param v the component to show. | ||||
|          * @return this. | ||||
|          */ | ||||
|         public FormatableChat hover(BaseComponent v) { return hover(toAdventure(v)); } | ||||
|         /** | ||||
|          * Configure this component to show the provided component when hovered. | ||||
|          * @param v the component to show. | ||||
|          * @return this. | ||||
|          */ | ||||
|         public FormatableChat hover(BaseComponent[] v) { return hover(toAdventure(v)); } | ||||
|         /** | ||||
|          * Configure this component to show the provided legacy text when hovered. | ||||
|          * @param legacyText the legacy text to show. | ||||
| @@ -919,7 +883,7 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource | ||||
|  | ||||
|     @Override | ||||
|     public int hashCode() { | ||||
|         return getAdv().hashCode(); | ||||
|         return get().hashCode(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -930,63 +894,44 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource | ||||
|  | ||||
|  | ||||
|  | ||||
|     /* package */ static ComponentLike filterObjToComponentLike(Object v) { | ||||
|         return switch (v) { | ||||
|             case ComponentLike componentLike -> componentLike; | ||||
|             case null, default -> Component.text(Objects.toString(v)); | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     /* package */ static ComponentLike[] filterObjToComponentLike(Object[] values) { | ||||
|         if (values == null) | ||||
|             return null; | ||||
|         ComponentLike[] ret = new ComponentLike[values.length]; | ||||
|         for (int i = 0; i < values.length; i++) { | ||||
|             Object v = values[i]; | ||||
|             if (v instanceof BaseComponent[]) | ||||
|                 ret[i] = toAdventure((BaseComponent[]) v); | ||||
|             else if (v instanceof BaseComponent) | ||||
|                 ret[i] = toAdventure((BaseComponent) v); | ||||
|             else if (v instanceof ComponentLike) | ||||
|                 ret[i] = (ComponentLike) v; | ||||
|             else | ||||
|                 ret[i] = Component.text(Objects.toString(v)); | ||||
|             ret[i] = filterObjToComponentLike(values[i]); | ||||
|         } | ||||
|         return ret; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Converts the Bungee {@link BaseComponent} array into Adventure {@link Component}. | ||||
|      * @param components the Bungee {@link BaseComponent} array. | ||||
|      * @return a {@link Component}. | ||||
|      */ | ||||
|     public static Component toAdventure(BaseComponent[] components) { | ||||
|         return BungeeComponentSerializer.get().deserialize(components); | ||||
|     /* package */ static TranslationArgumentLike[] filterObjToTranslationArgumentLike(Object[] values) { | ||||
|         if (values == null) | ||||
|             return null; | ||||
|         TranslationArgumentLike[] ret = new TranslationArgumentLike[values.length]; | ||||
|         for (int i = 0; i < values.length; i++) { | ||||
|             Object v = values[i]; | ||||
|             if (v instanceof Number n) | ||||
|                 ret[i] = TranslationArgument.numeric(n); | ||||
|             else if (v instanceof Boolean b) | ||||
|                 ret[i] = TranslationArgument.bool(b); | ||||
|             else | ||||
|                 ret[i] = TranslationArgument.component(filterObjToComponentLike(values[i])); | ||||
|         } | ||||
|     /** | ||||
|      * Converts the Bungee {@link BaseComponent} into Adventure {@link Component}. | ||||
|      * @param component the Bungee {@link BaseComponent}. | ||||
|      * @return a {@link Component}. | ||||
|      */ | ||||
|     public static Component toAdventure(BaseComponent component) { | ||||
|         return toAdventure(new BaseComponent[] { component }); | ||||
|         return ret; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Converts the Adventure {@link Component} into Bungee {@link BaseComponent} array. | ||||
|      * @param component the Adventure {@link Component}. | ||||
|      * @return a {@link BaseComponent} array. | ||||
|      */ | ||||
|     public static BaseComponent[] toBungeeArray(Component component) { | ||||
|         return BungeeComponentSerializer.get().serialize(component); | ||||
|     } | ||||
|     /** | ||||
|      * Converts the Adventure {@link Component} into Bungee {@link BaseComponent}. | ||||
|      * @param component the Adventure {@link Component}. | ||||
|      * @return a {@link BaseComponent}. | ||||
|      */ | ||||
|     public static BaseComponent toBungee(Component component) { | ||||
|         BaseComponent[] arr = toBungeeArray(component); | ||||
|         return arr.length == 1 ? arr[0] : new net.md_5.bungee.api.chat.TextComponent(arr); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Force the italic formating to be set to false if it is not explicitely set in the component. | ||||
|      * Force the italic formatting to be set to false if it is not explicitly set in the component. | ||||
|      * This is useful for item lores that defaults to italic in the game UI. | ||||
|      * @param c the {@link Chat} in which to set the italic property if needed. | ||||
|      * @return the provided {@link Chat} instance. | ||||
|   | ||||
| @@ -4,15 +4,30 @@ import java.util.ArrayList; | ||||
| import java.util.List; | ||||
|  | ||||
| import net.kyori.adventure.text.format.TextColor; | ||||
| import org.jetbrains.annotations.NotNull; | ||||
|  | ||||
| /** | ||||
|  * A custom gradient with a least 2 colors in it. | ||||
|  * A custom gradient with at least 2 colors in it. | ||||
|  */ | ||||
| public class ChatColorGradient { | ||||
|     private record GradientColor(float location, TextColor color) { } | ||||
|  | ||||
|     private record GradientColor( | ||||
|             float location, | ||||
|             TextColor color | ||||
|     ) implements Comparable<GradientColor> { | ||||
|         @Override | ||||
|         public int compareTo(@NotNull ChatColorGradient.GradientColor o) { | ||||
|             return Float.compare(location(), o.location()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private final List<GradientColor> colors = new ArrayList<>(); | ||||
|  | ||||
|     /** | ||||
|      * Create the custom gradient. | ||||
|      */ | ||||
|     public ChatColorGradient() {} | ||||
|  | ||||
|     /** | ||||
|      * Put a specific color at a specific location in the gradient. | ||||
|      * @param gradientLocation the location in the gradient. | ||||
| @@ -21,6 +36,7 @@ public class ChatColorGradient { | ||||
|      */ | ||||
|     public synchronized ChatColorGradient add(float gradientLocation, TextColor gradientColor) { | ||||
|         colors.add(new GradientColor(gradientLocation, gradientColor)); | ||||
|         colors.sort(null); | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
| @@ -31,25 +47,26 @@ public class ChatColorGradient { | ||||
|      */ | ||||
|     public synchronized TextColor pickColorAt(float gradientLocation) { | ||||
|         if (colors.isEmpty()) | ||||
|             throw new IllegalStateException("Must define at least one color in this ChatValueGradient instance."); | ||||
|             throw new IllegalStateException("Must define at least one color in this ChatColorGradient instance."); | ||||
|         if (colors.size() == 1) | ||||
|             return colors.get(0).color(); | ||||
|             return colors.getFirst().color(); | ||||
|  | ||||
|         colors.sort((p1, p2) -> Float.compare(p1.location(), p2.location())); | ||||
|  | ||||
|         if (gradientLocation <= colors.get(0).location()) | ||||
|             return colors.get(0).color(); | ||||
|         if (gradientLocation >= colors.get(colors.size() - 1).location()) | ||||
|             return colors.get(colors.size() - 1).color(); | ||||
|  | ||||
|         int p1 = 1; | ||||
|         for (; p1 < colors.size(); p1++) { | ||||
|             if (colors.get(p1).location() >= gradientLocation) | ||||
|         int i = 0; | ||||
|         for (; i < colors.size(); i++) { | ||||
|             if (gradientLocation <= colors.get(i).location()) | ||||
|                 break; | ||||
|         } | ||||
|         int p0 = p1 - 1; | ||||
|         float v0 = colors.get(p0).location(), v1 = colors.get(p1).location(); | ||||
|         TextColor cc0 = colors.get(p0).color(), cc1 = colors.get(p1).color(); | ||||
|         return ChatColorUtil.interpolateColor(v0, v1, gradientLocation, cc0, cc1); | ||||
|  | ||||
|         if (i == 0) | ||||
|             return colors.get(i).color(); | ||||
|         if (i == colors.size()) | ||||
|             return colors.getLast().color(); | ||||
|  | ||||
|         int p = i - 1; | ||||
|         float pLoc = colors.get(p).location(); | ||||
|         float iLoc = colors.get(i).location(); | ||||
|         TextColor pCol = colors.get(p).color(); | ||||
|         TextColor iCol = colors.get(i).color(); | ||||
|         return ChatColorUtil.interpolateColor(pLoc, iLoc, gradientLocation, pCol, iCol); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,14 +1,14 @@ | ||||
| package fr.pandacube.lib.chat; | ||||
|  | ||||
| import net.kyori.adventure.text.format.TextColor; | ||||
| import net.kyori.adventure.text.format.TextDecoration; | ||||
| import net.kyori.adventure.text.format.TextFormat; | ||||
| import net.kyori.adventure.util.RGBLike; | ||||
|  | ||||
| import java.util.regex.Pattern; | ||||
|  | ||||
| import net.kyori.adventure.text.format.NamedTextColor; | ||||
| import net.kyori.adventure.text.format.TextColor; | ||||
| import net.kyori.adventure.util.RGBLike; | ||||
| import net.md_5.bungee.api.ChatColor; | ||||
|  | ||||
| /** | ||||
|  * Provides methods to manipulate legacy colors and {@link ChatColor} class. | ||||
|  * Provides methods to manipulate legacy colors. | ||||
|  */ | ||||
| public class ChatColorUtil { | ||||
|  | ||||
| @@ -30,7 +30,7 @@ public class ChatColorUtil { | ||||
|      * Returns the legacy format needed to reproduce the format at the end of the provided legacy text. | ||||
|      * Supports standard chat colors and formats, BungeeCord Chat rgb format and EssentialsX rgb format. | ||||
|      * The RGB value from EssentialsX format is converted to BungeeCord Chat when included in the returned value. | ||||
|      * @param legacyText the legacy formated text. | ||||
|      * @param legacyText the legacy formatted text. | ||||
|      * @return the active format at the end of the provided text. | ||||
|      */ | ||||
|     public static String getLastColors(String legacyText) { | ||||
| @@ -38,12 +38,12 @@ public class ChatColorUtil { | ||||
|         int length = legacyText.length(); | ||||
|  | ||||
|         for (int index = length - 2; index >= 0; index--) { | ||||
|             if (legacyText.charAt(index) == ChatColor.COLOR_CHAR) { | ||||
|             if (legacyText.charAt(index) == LegacyChatFormat.COLOR_CHAR) { | ||||
|  | ||||
|                 // detection of rgb color §x§0§1§2§3§4§5 | ||||
|                 String rgb; | ||||
|                 if (index > 11 | ||||
|                         && legacyText.charAt(index - 12) == ChatColor.COLOR_CHAR | ||||
|                         && legacyText.charAt(index - 12) == LegacyChatFormat.COLOR_CHAR | ||||
|                         && (legacyText.charAt(index - 11) == 'x' | ||||
|                         || legacyText.charAt(index - 11) == 'X') | ||||
|                         && HEX_COLOR_PATTERN.matcher(rgb = legacyText.substring(index - 12, index + 2)).matches()) { | ||||
| @@ -64,7 +64,7 @@ public class ChatColorUtil { | ||||
|  | ||||
|                 // try detect non-rgb format | ||||
|                 char colorChar = legacyText.charAt(index + 1); | ||||
|                 ChatColor legacyColor = getChatColorByChar(colorChar); | ||||
|                 LegacyChatFormat legacyColor = LegacyChatFormat.of(colorChar); | ||||
|  | ||||
|                 if (legacyColor != null) { | ||||
|                     result.insert(0, legacyColor); | ||||
| @@ -83,15 +83,6 @@ public class ChatColorUtil { | ||||
|         return result.toString(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the {@link ChatColor} associated with the provided char, case insensitive. | ||||
|      * @param code the case insensitive char code. | ||||
|      * @return the corresponding {@link ChatColor}. | ||||
|      */ | ||||
|     public static ChatColor getChatColorByChar(char code) { | ||||
|         return ChatColor.getByChar(Character.toLowerCase(code)); | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -99,7 +90,7 @@ public class ChatColorUtil { | ||||
|      * Translate the color code of the provided string, that uses the alt color char, to the {@code §} color code | ||||
|      * format. | ||||
|      * <p> | ||||
|      * This method is the improved version of {@link ChatColor#translateAlternateColorCodes(char, String)}, | ||||
|      * This method is the improved version of Bukkit’s {@code ChatColor.translateAlternateColorCodes(char, String)}, | ||||
|      * because it takes into account essentials RGB color code, and {@code altColorChar} escaping (by doubling it). | ||||
|      * Essentials RGB color code are converted to Bungee chat RGB format, so the returned string can be converted | ||||
|      * to component (see {@link Chat#legacyText(Object)}). | ||||
| @@ -112,7 +103,7 @@ public class ChatColorUtil { | ||||
|      */ | ||||
|     public static String translateAlternateColorCodes(char altColorChar, String textToTranslate) | ||||
|     { | ||||
|         char colorChar = ChatColor.COLOR_CHAR; | ||||
|         char colorChar = LegacyChatFormat.COLOR_CHAR; | ||||
|         StringBuilder acc = new StringBuilder(); | ||||
|         char[] b = textToTranslate.toCharArray(); | ||||
|         for ( int i = 0; i < b.length; i++ ) | ||||
| @@ -180,7 +171,7 @@ public class ChatColorUtil { | ||||
|      * @return the text fully italic. | ||||
|      */ | ||||
|     public static String forceItalic(String legacyText) { | ||||
|         return forceFormat(legacyText, ChatColor.ITALIC); | ||||
|         return forceFormat(legacyText, TextDecoration.ITALIC); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -190,7 +181,7 @@ public class ChatColorUtil { | ||||
|      * @return the text fully bold. | ||||
|      */ | ||||
|     public static String forceBold(String legacyText) { | ||||
|         return forceFormat(legacyText, ChatColor.BOLD); | ||||
|         return forceFormat(legacyText, TextDecoration.BOLD); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -200,7 +191,7 @@ public class ChatColorUtil { | ||||
|      * @return the text fully underlined. | ||||
|      */ | ||||
|     public static String forceUnderline(String legacyText) { | ||||
|         return forceFormat(legacyText, ChatColor.UNDERLINE); | ||||
|         return forceFormat(legacyText, TextDecoration.UNDERLINED); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -210,7 +201,7 @@ public class ChatColorUtil { | ||||
|      * @return the text fully stroked through. | ||||
|      */ | ||||
|     public static String forceStrikethrough(String legacyText) { | ||||
|         return forceFormat(legacyText, ChatColor.STRIKETHROUGH); | ||||
|         return forceFormat(legacyText, TextDecoration.STRIKETHROUGH); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -220,15 +211,16 @@ public class ChatColorUtil { | ||||
|      * @return the text fully obfuscated. | ||||
|      */ | ||||
|     public static String forceObfuscated(String legacyText) { | ||||
|         return forceFormat(legacyText, ChatColor.MAGIC); | ||||
|         return forceFormat(legacyText, TextDecoration.OBFUSCATED); | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|     private static String forceFormat(String legacyText, ChatColor format) { | ||||
|     private static String forceFormat(String legacyText, TextFormat format) { | ||||
|         String formatStr = LegacyChatFormat.of(format).toString(); | ||||
|         return format + legacyText | ||||
|                 .replace(format.toString(), "") // remove previous tag to make the result cleaner | ||||
|                 .replaceAll("§([a-frA-FR\\d])", "§$1" + format); | ||||
|                 .replace(formatStr, "") // remove previous tag to make the result cleaner | ||||
|                 .replaceAll("§([a-frA-FR\\d])", "§$1" + formatStr); | ||||
|     } | ||||
|  | ||||
|  | ||||
| @@ -243,40 +235,12 @@ public class ChatColorUtil { | ||||
|      * @return the resulting text. | ||||
|      */ | ||||
|     public static String resetToColor(String legacyText, String color) { | ||||
|         return legacyText.replace(ChatColor.RESET.toString(), color); | ||||
|         return legacyText.replace(LegacyChatFormat.RESET.toString(), color); | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Converts the provided {@link ChatColor} to its Adventure counterpart. | ||||
|      * @param bungee a BungeeCord {@link ChatColor} instance. | ||||
|      * @return the {@link TextColor} equivalent to the provided {@link ChatColor}. | ||||
|      */ | ||||
|     public static TextColor toAdventure(ChatColor bungee) { | ||||
|         if (bungee == null) | ||||
|             return null; | ||||
|         if (bungee.getColor() == null) | ||||
|             throw new IllegalArgumentException("The provided Bungee ChatColor must be an actual color (not format nor reset)."); | ||||
|         return TextColor.color(bungee.getColor().getRGB()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Converts the provided {@link TextColor} to its BungeeCord counterpart. | ||||
|      * @param col a Adventure {@link TextColor} instance. | ||||
|      * @return the {@link ChatColor} equivalent to the provided {@link TextColor}. | ||||
|      */ | ||||
|     public static ChatColor toBungee(TextColor col) { | ||||
|         if (col == null) | ||||
|             return null; | ||||
|         if (col instanceof NamedTextColor) { | ||||
|             return ChatColor.of(((NamedTextColor) col).toString()); | ||||
|         } | ||||
|         return ChatColor.of(col.asHexString()); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Create a color, interpolating between 2 colors. | ||||
|      * @param v0 the value corresponding to color {@code cc0}. | ||||
| @@ -293,4 +257,7 @@ public class ChatColorUtil { | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|     private ChatColorUtil() {} | ||||
|  | ||||
| } | ||||
| @@ -8,6 +8,7 @@ import net.kyori.adventure.text.format.TextColor; | ||||
| /** | ||||
|  * Class holding static configuration values for chat component rendering. | ||||
|  */ | ||||
| @SuppressWarnings("CanBeFinal") | ||||
| public class ChatConfig { | ||||
|  | ||||
|     /** | ||||
| @@ -29,7 +30,7 @@ public class ChatConfig { | ||||
|     /** | ||||
|      * The color used for successful messages. | ||||
|      */ | ||||
|     public static TextColor successColor = PandaTheme.CHAT_GREEN_SATMAX; | ||||
|     public static TextColor successColor = PandaTheme.CHAT_GREEN_MAX_SAT; | ||||
|  | ||||
|     /** | ||||
|      * The color used for error/failure messages. | ||||
| @@ -49,7 +50,7 @@ public class ChatConfig { | ||||
|     /** | ||||
|      * The color used to display data in a message. | ||||
|      */ | ||||
|     public static TextColor dataColor = PandaTheme.CHAT_GRAY_MID; | ||||
|     public static TextColor dataColor = NamedTextColor.GRAY; | ||||
|  | ||||
|     /** | ||||
|      * The color used for displayed URLs and clickable URLs. | ||||
| @@ -67,14 +68,14 @@ public class ChatConfig { | ||||
|     public static TextColor highlightedCommandColor = NamedTextColor.WHITE; | ||||
|  | ||||
|     /** | ||||
|      * The color used for broadcasted messages. | ||||
|      * The color used for broadcast messages. | ||||
|      * It is often used in combination with {@link #prefix}. | ||||
|      */ | ||||
|     public static TextColor broadcastColor = NamedTextColor.YELLOW; | ||||
|      | ||||
|     /** | ||||
|      * The prefix used for prefixed messages. | ||||
|      * It can be a sylized name of the server, like {@code "[Pandacube] "}. | ||||
|      * It can be a stylized name of the server, like {@code "[Pandacube] "}. | ||||
|      * It is often used in combination with {@link #broadcastColor}. | ||||
|      */ | ||||
|     public static Supplier<Chat> prefix = PandaTheme::CHAT_MESSAGE_PREFIX; | ||||
| @@ -86,7 +87,7 @@ public class ChatConfig { | ||||
|      */ | ||||
|     public static int getPrefixWidth(boolean console) { | ||||
|         Chat c; | ||||
|         return prefix == null ? 0 : (c = prefix.get()) == null ? 0 : ChatUtil.componentWidth(c.getAdv(), console); | ||||
|         return prefix == null ? 0 : (c = prefix.get()) == null ? 0 : ChatUtil.componentWidth(c.get(), console); | ||||
|     } | ||||
|  | ||||
|  | ||||
| @@ -104,7 +105,7 @@ public class ChatConfig { | ||||
|         public static final TextColor CHAT_GREEN_4 = TextColor.fromHexString("#abe3b0"); // h=126 s=50 l=78 | ||||
|  | ||||
|         /** 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. */ | ||||
|         public static final TextColor CHAT_GREEN_1_SAT = TextColor.fromHexString("#20d532"); // h=126 s=50 l=48 | ||||
|         /** Green 2 saturated color. */ | ||||
| @@ -156,5 +157,9 @@ public class ChatConfig { | ||||
|                     .thenText("] "); | ||||
|         } | ||||
|  | ||||
|         private PandaTheme() {} | ||||
|  | ||||
|     } | ||||
|  | ||||
|     private ChatConfig() {} | ||||
| } | ||||
|   | ||||
| @@ -59,6 +59,7 @@ public class ChatFilledLine implements ComponentLike { | ||||
|     private boolean decorationBold = false; | ||||
|     private int nbSide = ChatConfig.nbCharMargin; | ||||
|     private boolean spacesAroundText = false; | ||||
|     private boolean spacesDecorationRightSide = false; | ||||
|     private boolean console = false; | ||||
|     private Integer maxWidth = null; | ||||
|  | ||||
| @@ -116,6 +117,16 @@ public class ChatFilledLine implements ComponentLike { | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * If the {@link #decoChar(char)} is set to space, also add spaces at the right of the text | ||||
|      * to reach the desired width. | ||||
|      * @return this. | ||||
|      */ | ||||
|     public ChatFilledLine spacesDecorationRightSide() { | ||||
|         spacesDecorationRightSide = true; | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Configure if the line will be rendered on console or not. | ||||
|      * @param console true for console, false for game UI. | ||||
| @@ -140,7 +151,7 @@ public class ChatFilledLine implements ComponentLike { | ||||
|  | ||||
|     /** | ||||
|      * Renders this line to a {@link FormatableChat}. | ||||
|      * @return a new {@link FormatableChat} builded by this {@link ChatFilledLine}. | ||||
|      * @return a new {@link FormatableChat} built by this {@link ChatFilledLine}. | ||||
|      */ | ||||
|     public FormatableChat toChat() { | ||||
|         int maxWidth = (this.maxWidth != null) | ||||
| @@ -184,7 +195,7 @@ public class ChatFilledLine implements ComponentLike { | ||||
|         Chat d = Chat.chat() | ||||
|                 .then(Chat.text(ChatUtil.repeatedChar(decorationChar, nbCharLeft)).color(decorationColor).bold(decorationBold)) | ||||
|                 .then(text); | ||||
|         if (decorationChar != ' ') | ||||
|         if (decorationChar != ' ' || spacesDecorationRightSide) | ||||
|             d.then(Chat.text(ChatUtil.repeatedChar(decorationChar, nbCharRight)).color(decorationColor).bold(decorationBold)); | ||||
|         return (FormatableChat) d; | ||||
|     } | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| package fr.pandacube.lib.chat; | ||||
|  | ||||
| import java.util.Objects; | ||||
|  | ||||
| import fr.pandacube.lib.chat.Chat.FormatableChat; | ||||
| import net.kyori.adventure.text.BlockNBTComponent; | ||||
| import net.kyori.adventure.text.Component; | ||||
| import net.kyori.adventure.text.ComponentBuilder; | ||||
| @@ -16,10 +15,10 @@ import net.kyori.adventure.text.TranslatableComponent; | ||||
| import net.kyori.adventure.text.event.HoverEventSource; | ||||
| import net.kyori.adventure.text.format.NamedTextColor; | ||||
| import net.kyori.adventure.text.format.TextColor; | ||||
| import net.kyori.adventure.text.minimessage.MiniMessage; | ||||
| import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; | ||||
| import net.md_5.bungee.api.chat.BaseComponent; | ||||
|  | ||||
| import fr.pandacube.lib.chat.Chat.FormatableChat; | ||||
| import java.util.Objects; | ||||
|  | ||||
| /** | ||||
|  * Abstract class holding the publicly accessible methods to create an instance of {@link Chat} component. | ||||
| @@ -32,15 +31,6 @@ public abstract class ChatStatic { | ||||
|         return new FormatableChat(componentToBuilder(c)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Creates a {@link FormatableChat} from the provided Bungee {@link BaseComponent}. | ||||
|      * @param c the {@link BaseComponent}. | ||||
|      * @return a new {@link FormatableChat}. | ||||
|      */ | ||||
|     public static FormatableChat chatComponent(BaseComponent c) { | ||||
|         return new FormatableChat(componentToBuilder(Chat.toAdventure(c))); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Creates a {@link FormatableChat} from the provided {@link ComponentLike}. | ||||
|      * If the provided component is an instance of {@link Chat}, its content will be duplicated, and the provided one | ||||
| @@ -60,15 +50,6 @@ public abstract class ChatStatic { | ||||
|         return new FormatableChat(Component.text()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Creates a {@link FormatableChat} from the provided Bungee {@link BaseComponent BaseComponent[]}. | ||||
|      * @param c the array of {@link BaseComponent}. | ||||
|      * @return a new {@link FormatableChat}. | ||||
|      */ | ||||
|     public static FormatableChat chatComponent(BaseComponent[] c) { | ||||
|         return chatComponent(Chat.toAdventure(c)); | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -91,18 +72,58 @@ public abstract class ChatStatic { | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Creates a {@link FormatableChat} with the provided legacy text as its content. | ||||
|      * @param legacyText the legacy text to use as the content. | ||||
|      * Creates a {@link FormatableChat} with the provided legacy text as its content, using the section {@code "§"} | ||||
|      * character. | ||||
|      * @param legacyText the legacy text to use as the content, that uses the {@code "§"} character. | ||||
|      * @return a new {@link FormatableChat} with the provided text as its content. | ||||
|      * @throws IllegalArgumentException If the {@code plainText} parameter is instance of {@link Chat} or | ||||
|      * @throws IllegalArgumentException If the {@code legacyText} parameter is instance of {@link Chat} or | ||||
|      *         {@link Component}. The caller should use {@link #chatComponent(ComponentLike)} | ||||
|      *         instead. | ||||
|      */ | ||||
|     public static FormatableChat legacyText(Object legacyText) { | ||||
|         return legacyText(legacyText, LegacyComponentSerializer.SECTION_CHAR); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Creates a {@link FormatableChat} with the provided legacy text as its content, using the ampersand {@code "&"} | ||||
|      * character. | ||||
|      * @param legacyText the legacy text to use as the content, that uses the {@code "&"} character. | ||||
|      * @return a new {@link FormatableChat} with the provided text as its content. | ||||
|      * @throws IllegalArgumentException If the {@code legacyText} parameter is instance of {@link Chat} or | ||||
|      *         {@link Component}. The caller should use {@link #chatComponent(ComponentLike)} | ||||
|      *         instead. | ||||
|      */ | ||||
|     public static FormatableChat legacyAmpersandText(Object legacyText) { | ||||
|         return legacyText(legacyText, LegacyComponentSerializer.AMPERSAND_CHAR); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Creates a {@link FormatableChat} with the provided legacy text as its content, using the specified | ||||
|      * legacyCharacter. | ||||
|      * @param legacyText the legacy text to use as the content. | ||||
|      * @param legacyCharacter the character used in the provided text to prefix color and format code. | ||||
|      * @return a new {@link FormatableChat} with the provided text as its content. | ||||
|      * @throws IllegalArgumentException If the {@code legacyText} parameter is instance of {@link Chat} or | ||||
|      *         {@link Component}. The caller should use {@link #chatComponent(ComponentLike)} | ||||
|      *         instead. | ||||
|      */ | ||||
|     private static FormatableChat legacyText(Object legacyText, char legacyCharacter) { | ||||
|         if (legacyText instanceof ComponentLike) { | ||||
|             throw new IllegalArgumentException("Expected any object except instance of " + ComponentLike.class + ". Received " + legacyText + ". Please use ChatStatic.chatComponent(ComponentLike) instead."); | ||||
|         } | ||||
|         return chatComponent(LegacyComponentSerializer.legacySection().deserialize(Objects.toString(legacyText))); | ||||
|         return chatComponent(LegacyComponentSerializer.legacy(legacyCharacter).deserialize(Objects.toString(legacyText))); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Creates a {@link FormatableChat} with the provided MiniMessage text as its content. | ||||
|      * @param miniMessageText the MiniMessage text to use as the content. | ||||
|      * @return a new {@link FormatableChat} with the provided text as its content. | ||||
|      */ | ||||
|     public static FormatableChat miniMessageText(String miniMessageText) { | ||||
|         return chatComponent(MiniMessage.miniMessage().deserialize(miniMessageText)); | ||||
|     } | ||||
|  | ||||
|  | ||||
| @@ -207,7 +228,7 @@ public abstract class ChatStatic { | ||||
|      * @param c the {@link Component}. | ||||
|      * @return a new {@link FormatableChat}. | ||||
|      */ | ||||
|     public static FormatableChat playerNameComponent(Component c) { | ||||
|     public static FormatableChat playerNameComponent(ComponentLike c) { | ||||
|         FormatableChat fc = chatComponent(c); | ||||
|         fc.builder.colorIfAbsent(NamedTextColor.WHITE); | ||||
|         return fc; | ||||
| @@ -223,13 +244,13 @@ public abstract class ChatStatic { | ||||
|      * @return a new {@link FormatableChat} with the provided translation key and parameters. | ||||
|      */ | ||||
|     public static FormatableChat translation(String key, Object... with) { | ||||
|         return new FormatableChat(Component.translatable().key(key).args(Chat.filterObjToComponentLike(with))); | ||||
|         return new FormatableChat(Component.translatable().key(key).arguments(Chat.filterObjToTranslationArgumentLike(with))); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Creates a {@link FormatableChat} with the provided keybind. | ||||
|      * @param key the keybind to display. | ||||
|      * @return a new {@link FormatableChat} with the provided keybind. | ||||
|      * Creates a {@link FormatableChat} with the provided keybinding. | ||||
|      * @param key the keybinding to display. | ||||
|      * @return a new {@link FormatableChat} with the provided keybinding. | ||||
|      */ | ||||
|     public static FormatableChat keybind(String key) { | ||||
|         return new FormatableChat(Component.keybind().keybind(key)); | ||||
| @@ -451,12 +472,12 @@ public abstract class ChatStatic { | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Creates a {@link FormatableChat} filling a line of chat (or console) with decoration and a left-aligned text. | ||||
|      * Creates a {@link FormatableChat} filling a chat line with decoration and a left-aligned text. | ||||
|      * @param text the text aligned to the left. | ||||
|      * @param decorationChar the character used for decoration around the text. | ||||
|      * @param decorationColor the color used for the decoration characters. | ||||
|      * @param console if the line is rendered on console (true) or IG (false). | ||||
|      * @return a new {@link FormatableChat} filling a line of chat (or console) with decoration and a left-aligned text. | ||||
|      * @return a new {@link FormatableChat} filling a chat line with decoration and a left-aligned text. | ||||
|      * @see ChatFilledLine#leftText(ComponentLike) | ||||
|      */ | ||||
|     public static FormatableChat leftText(ComponentLike text, char decorationChar, TextColor decorationColor, boolean console) { | ||||
| @@ -464,11 +485,11 @@ public abstract class ChatStatic { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Creates a {@link FormatableChat} filling a line of chat (or console) with the configured decoration character and | ||||
|      * Creates a {@link FormatableChat} filling a chat line with the configured decoration character and | ||||
|      * color and a left-aligned text. | ||||
|      * @param text the text aligned to the left. | ||||
|      * @param console if the line is rendered on console (true) or IG (false). | ||||
|      * @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character | ||||
|      * @return a new {@link FormatableChat} filling a chat line with the configured decoration character | ||||
|      *         and color and a left-aligned text. | ||||
|      * @see ChatFilledLine#leftText(ComponentLike) | ||||
|      * @see ChatConfig#decorationChar | ||||
| @@ -479,12 +500,12 @@ public abstract class ChatStatic { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Creates a {@link FormatableChat} filling a line of chat (or console) with decoration and a right-aligned text. | ||||
|      * Creates a {@link FormatableChat} filling a chat line with decoration and a right-aligned text. | ||||
|      * @param text the text aligned to the right. | ||||
|      * @param decorationChar the character used for decoration around the text. | ||||
|      * @param decorationColor the color used for the decoration characters. | ||||
|      * @param console if the line is rendered on console (true) or IG (false). | ||||
|      * @return a new {@link FormatableChat} filling a line of chat (or console) with decoration and a right-aligned | ||||
|      * @return a new {@link FormatableChat} filling a chat line with decoration and a right-aligned | ||||
|      *         text. | ||||
|      * @see ChatFilledLine#rightText(ComponentLike) | ||||
|      */ | ||||
| @@ -493,11 +514,11 @@ public abstract class ChatStatic { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Creates a {@link FormatableChat} filling a line of chat (or console) with the configured decoration character and | ||||
|      * Creates a {@link FormatableChat} filling a chat line with the configured decoration character and | ||||
|      * color and a right-aligned text. | ||||
|      * @param text the text aligned to the right. | ||||
|      * @param console if the line is rendered on console (true) or IG (false). | ||||
|      * @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character | ||||
|      * @return a new {@link FormatableChat} filling a chat line with the configured decoration character | ||||
|      *         and color and a right-aligned text. | ||||
|      * @see ChatFilledLine#rightText(ComponentLike) | ||||
|      * @see ChatConfig#decorationChar | ||||
| @@ -508,12 +529,12 @@ public abstract class ChatStatic { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Creates a {@link FormatableChat} filling a line of chat (or console) with decoration and a centered text. | ||||
|      * Creates a {@link FormatableChat} filling a chat line with decoration and a centered text. | ||||
|      * @param text the text aligned to the center. | ||||
|      * @param decorationChar the character used for decoration around the text. | ||||
|      * @param decorationColor the color used for the decoration characters. | ||||
|      * @param console if the line is rendered on console (true) or IG (false). | ||||
|      * @return a new {@link FormatableChat} filling a line of chat (or console) with decoration and a centered text. | ||||
|      * @return a new {@link FormatableChat} filling a chat line with decoration and a centered text. | ||||
|      * @see ChatFilledLine#centerText(ComponentLike) | ||||
|      */ | ||||
|     public static FormatableChat centerText(ComponentLike text, char decorationChar, TextColor decorationColor, boolean console) { | ||||
| @@ -521,11 +542,11 @@ public abstract class ChatStatic { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Creates a {@link FormatableChat} filling a line of chat (or console) with the configured decoration character and | ||||
|      * Creates a {@link FormatableChat} filling a chat line with the configured decoration character and | ||||
|      * color and a centered text. | ||||
|      * @param text the text aligned to the center. | ||||
|      * @param console if the line is rendered on console (true) or IG (false). | ||||
|      * @return a new {@link FormatableChat} filling a line of chat (or console) with the configured decoration character | ||||
|      * @return a new {@link FormatableChat} filling a chat line with the configured decoration character | ||||
|      *         and color and a centered text. | ||||
|      * @see ChatFilledLine#centerText(ComponentLike) | ||||
|      * @see ChatConfig#decorationChar | ||||
| @@ -536,11 +557,11 @@ public abstract class ChatStatic { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Creates a {@link FormatableChat} filling a line of chat (or console) with a decoration character and color. | ||||
|      * Creates a {@link FormatableChat} filling a chat line with a decoration character and color. | ||||
|      * @param decorationChar the character used for decoration. | ||||
|      * @param decorationColor the color used for the decoration characters. | ||||
|      * @param console if the line is rendered on console (true) or IG (false). | ||||
|      * @return a new {@link FormatableChat} filling a line of chat (or console) with a decoration character and color. | ||||
|      * @return a new {@link FormatableChat} filling a chat line with a decoration character and color. | ||||
|      * @see ChatFilledLine#filled() | ||||
|      */ | ||||
|     public static FormatableChat filledLine(char decorationChar, TextColor decorationColor, boolean console) { | ||||
| @@ -548,10 +569,10 @@ public abstract class ChatStatic { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Creates a {@link FormatableChat} filling a line of chat (or console) with the configured decoration character and | ||||
|      * Creates a {@link FormatableChat} filling a chat line with the configured decoration character and | ||||
|      * color. | ||||
|      * @param console if the line is rendered on console (true) or IG (false). | ||||
|      * @return a new {@link FormatableChat} filling a line of chat (or console) with a decoration character and color. | ||||
|      * @return a new {@link FormatableChat} filling a chat line with a decoration character and color. | ||||
|      * @see ChatFilledLine#filled() | ||||
|      * @see ChatConfig#decorationChar | ||||
|      * @see ChatConfig#decorationColor | ||||
| @@ -591,52 +612,39 @@ public abstract class ChatStatic { | ||||
|  | ||||
|  | ||||
|     private static ComponentBuilder<?, ?> componentToBuilder(Component c) { | ||||
|         ComponentBuilder<?, ?> builder; | ||||
|         if (c instanceof TextComponent) { | ||||
|             builder = Component.text() | ||||
|                     .content(((TextComponent) c).content()); | ||||
|         } | ||||
|         else if (c instanceof TranslatableComponent) { | ||||
|             builder = Component.translatable() | ||||
|                     .key(((TranslatableComponent) c).key()) | ||||
|                     .args(((TranslatableComponent) c).args()); | ||||
|         } | ||||
|         else if (c instanceof SelectorComponent) { | ||||
|             builder = Component.selector() | ||||
|                     .pattern(((SelectorComponent) c).pattern()); | ||||
|         } | ||||
|         else if (c instanceof ScoreComponent) { | ||||
|             builder = Component.score() | ||||
|                     .name(((ScoreComponent) c).name()) | ||||
|                     .objective(((ScoreComponent) c).objective()); | ||||
|         } | ||||
|         else if (c instanceof KeybindComponent) { | ||||
|             builder = Component.keybind() | ||||
|                     .keybind(((KeybindComponent) c).keybind()); | ||||
|         } | ||||
|         else if (c instanceof BlockNBTComponent) { | ||||
|             builder = Component.blockNBT() | ||||
|                     .interpret(((BlockNBTComponent) c).interpret()) | ||||
|                     .nbtPath(((BlockNBTComponent) c).nbtPath()) | ||||
|                     .pos(((BlockNBTComponent) c).pos()); | ||||
|         } | ||||
|         else if (c instanceof EntityNBTComponent) { | ||||
|             builder = Component.entityNBT() | ||||
|                     .interpret(((EntityNBTComponent) c).interpret()) | ||||
|                     .nbtPath(((EntityNBTComponent) c).nbtPath()) | ||||
|                     .selector(((EntityNBTComponent) c).selector()); | ||||
|         } | ||||
|         else if (c instanceof StorageNBTComponent) { | ||||
|             builder = Component.storageNBT() | ||||
|                     .interpret(((StorageNBTComponent) c).interpret()) | ||||
|                     .nbtPath(((StorageNBTComponent) c).nbtPath()) | ||||
|                     .storage(((StorageNBTComponent) c).storage()); | ||||
|         } | ||||
|         else { | ||||
|             throw new IllegalArgumentException("Unknows component type " + c.getClass()); | ||||
|         } | ||||
|         ComponentBuilder<?, ?> builder = switch (c) { | ||||
|             case TextComponent textComponent -> Component.text() | ||||
|                     .content(textComponent.content()); | ||||
|             case TranslatableComponent translatableComponent -> Component.translatable() | ||||
|                     .key(translatableComponent.key()).arguments(translatableComponent.arguments()); | ||||
|             case SelectorComponent selectorComponent -> Component.selector() | ||||
|                     .pattern(selectorComponent.pattern()); | ||||
|             case ScoreComponent scoreComponent -> Component.score() | ||||
|                     .name(scoreComponent.name()) | ||||
|                     .objective(scoreComponent.objective()); | ||||
|             case KeybindComponent keybindComponent -> Component.keybind() | ||||
|                     .keybind(keybindComponent.keybind()); | ||||
|             case BlockNBTComponent blockNBTComponent -> Component.blockNBT() | ||||
|                     .interpret(blockNBTComponent.interpret()) | ||||
|                     .nbtPath(blockNBTComponent.nbtPath()) | ||||
|                     .pos(blockNBTComponent.pos()); | ||||
|             case EntityNBTComponent entityNBTComponent -> Component.entityNBT() | ||||
|                     .interpret(entityNBTComponent.interpret()) | ||||
|                     .nbtPath(entityNBTComponent.nbtPath()) | ||||
|                     .selector(entityNBTComponent.selector()); | ||||
|             case StorageNBTComponent storageNBTComponent -> Component.storageNBT() | ||||
|                     .interpret(storageNBTComponent.interpret()) | ||||
|                     .nbtPath(storageNBTComponent.nbtPath()) | ||||
|                     .storage(storageNBTComponent.storage()); | ||||
|             case null, default -> throw new IllegalArgumentException("Unknown component type " + (c == null ? "null" : c.getClass())); | ||||
|         }; | ||||
|         return builder.style(c.style()).append(c.children()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Creates a new {@link ChatStatic} instance. | ||||
|      */ | ||||
|     protected ChatStatic() {} | ||||
|  | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,13 @@ | ||||
| package fr.pandacube.lib.chat; | ||||
|  | ||||
| import net.kyori.adventure.text.Component; | ||||
| import net.kyori.adventure.text.ComponentLike; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
|  | ||||
| /** | ||||
|  * A tree structure of {@link Chat} component intended to be rendered in chat using {@link #render(boolean)}. | ||||
|  * A tree structure of chat {@link Component} intended to be rendered in chat using {@link #render(boolean)}. | ||||
|  */ | ||||
| public class ChatTreeNode { | ||||
|  | ||||
| @@ -19,7 +22,7 @@ public class ChatTreeNode { | ||||
|     /** | ||||
|      * The component for the current node. | ||||
|      */ | ||||
|     public final Chat component; | ||||
|     public final ComponentLike component; | ||||
|  | ||||
|     /** | ||||
|      * Children nodes. | ||||
| @@ -27,10 +30,10 @@ public class ChatTreeNode { | ||||
|     public final List<ChatTreeNode> children = new ArrayList<>(); | ||||
|  | ||||
|     /** | ||||
|      * Construct an new {@link ChatTreeNode}. | ||||
|      * Construct a new {@link ChatTreeNode}. | ||||
|      * @param cmp the component for the current node. | ||||
|      */ | ||||
|     public ChatTreeNode(Chat cmp) { | ||||
|     public ChatTreeNode(ComponentLike cmp) { | ||||
|         component = cmp; | ||||
|     } | ||||
|  | ||||
| @@ -48,9 +51,9 @@ public class ChatTreeNode { | ||||
|      * Generate a tree view based on this tree structure. | ||||
|      * <p> | ||||
|      * Each element in the returned list represent 1 line of this tree view. | ||||
|      * Thus, the caller may send each line separately or at once depending of the quantity of data. | ||||
|      * Thus, the caller may send each line separately or at once, depending on the quantity of data. | ||||
|      * @param console true to render for console, false otherwise. | ||||
|      * @return an array of component, each element being a single line. | ||||
|      * @return a list of component, each element being a single line. | ||||
|      */ | ||||
|     public List<Chat> render(boolean console) { | ||||
|         List<Chat> ret = new ArrayList<>(); | ||||
|   | ||||
| @@ -1,5 +1,17 @@ | ||||
| package fr.pandacube.lib.chat; | ||||
|  | ||||
| import fr.pandacube.lib.chat.Chat.FormatableChat; | ||||
| import net.kyori.adventure.text.Component; | ||||
| import net.kyori.adventure.text.ComponentLike; | ||||
| import net.kyori.adventure.text.TextComponent; | ||||
| import net.kyori.adventure.text.TranslatableComponent; | ||||
| import net.kyori.adventure.text.TranslationArgument; | ||||
| import net.kyori.adventure.text.format.NamedTextColor; | ||||
| import net.kyori.adventure.text.format.TextColor; | ||||
| import net.kyori.adventure.text.format.TextDecoration; | ||||
| import net.kyori.adventure.text.format.TextDecoration.State; | ||||
| import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.Collections; | ||||
| @@ -10,19 +22,10 @@ import java.util.Set; | ||||
| import java.util.TreeSet; | ||||
| import java.util.stream.Collectors; | ||||
|  | ||||
| import net.kyori.adventure.text.Component; | ||||
| import net.kyori.adventure.text.TextComponent; | ||||
| import net.kyori.adventure.text.TranslatableComponent; | ||||
| import net.kyori.adventure.text.format.NamedTextColor; | ||||
| import net.kyori.adventure.text.format.TextColor; | ||||
| import net.kyori.adventure.text.format.TextDecoration; | ||||
| import net.kyori.adventure.text.format.TextDecoration.State; | ||||
| import net.md_5.bungee.api.ChatColor; | ||||
|  | ||||
| import fr.pandacube.lib.chat.Chat.FormatableChat; | ||||
| import static fr.pandacube.lib.chat.ChatStatic.chat; | ||||
|  | ||||
| /** | ||||
|  * Provides various methods and properties to manipulate text displayed in chat an other parts of the game. | ||||
|  * Provides various methods and properties to manipulate text displayed in chat and other parts of the game. | ||||
|  */ | ||||
| public class ChatUtil { | ||||
|  | ||||
| @@ -48,7 +51,7 @@ public class ChatUtil { | ||||
|  | ||||
|     /** | ||||
|      * Mapping indicating the text pixel with for specific characters in the default Minecraft font. | ||||
|      * If a character 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; | ||||
|     static { | ||||
| @@ -112,7 +115,7 @@ public class ChatUtil { | ||||
|      * @param nbPages the number of pages. | ||||
|      * @param nbPagesToDisplay the number of pages to display around the first page, the last page and the | ||||
|      *                         {@code currentPage}. | ||||
|      * @return a {@link Chat} containging the created page navigator. | ||||
|      * @return a {@link Chat} containing the created page navigator. | ||||
|      */ | ||||
|     public static Chat createPagination(String prefix, String cmdFormat, int currentPage, int nbPages, int nbPagesToDisplay) { | ||||
|         Set<Integer> pagesToDisplay = new TreeSet<>(); | ||||
| @@ -127,7 +130,7 @@ public class ChatUtil { | ||||
|                 pagesToDisplay.add(i); | ||||
|         } | ||||
|  | ||||
|         Chat d = ChatStatic.chat().thenLegacyText(prefix); | ||||
|         Chat d = chat().thenLegacyText(prefix); | ||||
|         boolean first = true; | ||||
|         int previous = 0; | ||||
|  | ||||
| @@ -149,11 +152,11 @@ public class ChatUtil { | ||||
|             else | ||||
|                 first = false; | ||||
|  | ||||
|             FormatableChat pDisp = Chat.clickableCommand(Chat.text(page), String.format(cmdFormat, page), Chat.text("Aller à la page " + page)); | ||||
|             FormatableChat pDisplay = Chat.clickableCommand(Chat.text(page), String.format(cmdFormat, page), Chat.text("Aller à la page " + page)); | ||||
|             if (page == currentPage) { | ||||
|                 pDisp.highlightedCommandColor(); | ||||
|                 pDisplay.highlightedCommandColor(); | ||||
|             } | ||||
|             d.then(pDisp); | ||||
|             d.then(pDisplay); | ||||
|  | ||||
|             previous = page; | ||||
|         } | ||||
| @@ -167,6 +170,58 @@ public class ChatUtil { | ||||
|  | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Do like {@link String#join(CharSequence, Iterable)}, but for components, and the last separator is different from | ||||
|      * the others. It is useful when enumerating things in a sentence, for instance : | ||||
|      * <code>"a thing<u>, </u>a thing<u> and </u>a thing"</code> | ||||
|      * (the coma being the usual separator, and {@code " and "} being the final separator). | ||||
|      * @param regularSeparator the separator used everywhere except between the two last components to join. | ||||
|      * @param finalSeparator the separator used between the two last components to join. | ||||
|      * @param elements the components to join. | ||||
|      * @return a new {@link Chat} instance with all the provided {@code component} joined using the separators. | ||||
|      */ | ||||
|     public static FormatableChat joinGrammatically(ComponentLike regularSeparator, ComponentLike finalSeparator, List<? extends ComponentLike> elements) { | ||||
|         int size = elements == null ? 0 : elements.size(); | ||||
|         int last = size - 1; | ||||
|         return switch (size) { | ||||
|             case 0, 1, 2 -> join(finalSeparator, elements); | ||||
|             default -> (FormatableChat) join(regularSeparator, elements.subList(0, last)) | ||||
|                     .then(finalSeparator) | ||||
|                     .then(elements.get(last)); | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Do like {@link String#join(CharSequence, Iterable)}, but for components. | ||||
|      * @param separator the separator used everywhere except between the two last components to join. | ||||
|      * @param elements the components to join. | ||||
|      * @return a new {@link Chat} instance with all the provided {@code component} joined using the separators. | ||||
|      */ | ||||
|     public static FormatableChat join(ComponentLike separator, Iterable<? extends ComponentLike> elements) { | ||||
|         FormatableChat c = chat(); | ||||
|         if (elements == null) | ||||
|             return c; | ||||
|         boolean first = true; | ||||
|         for (ComponentLike el : elements) { | ||||
|             if (!first) { | ||||
|                 c.then(separator); | ||||
|             } | ||||
|             c.then(el); | ||||
|             first = false; | ||||
|         } | ||||
|         return c; | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -210,8 +265,8 @@ public class ChatUtil { | ||||
|             count += strWidth(((TextComponent)component).content(), console, actuallyBold); | ||||
|         } | ||||
|         else if (component instanceof TranslatableComponent) { | ||||
|             for (Component c : ((TranslatableComponent)component).args()) | ||||
|                 count += componentWidth(c, console, actuallyBold); | ||||
|             for (TranslationArgument c : ((TranslatableComponent)component).arguments()) | ||||
|                 count += componentWidth(c.asComponent(), console, actuallyBold); | ||||
|         } | ||||
|  | ||||
|         for (Component c : component.children()) | ||||
| @@ -258,7 +313,7 @@ public class ChatUtil { | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Wraps the provided text in multiple lines, taking into account the legacy formating. | ||||
|      * Wraps the provided text in multiple lines, taking into account the legacy formatting. | ||||
|      * <p> | ||||
|      * This method only takes into account IG text width. Use a regular text-wrapper for console instead. | ||||
|      * @param legacyText the text to wrap. | ||||
| @@ -272,7 +327,7 @@ public class ChatUtil { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Wraps the provided text in multiple lines, taking into account the legacy formating. | ||||
|      * Wraps the provided text in multiple lines, taking into account the legacy formatting. | ||||
|      * <p> | ||||
|      * This method only takes into account IG text width. Use a regular text-wrapper for console instead. | ||||
|      * @param legacyText the text to wrap. | ||||
| @@ -295,7 +350,7 @@ public class ChatUtil { | ||||
|  | ||||
|         do { | ||||
|             char c = legacyText.charAt(index); | ||||
|             if (c == ChatColor.COLOR_CHAR && index < legacyText.length() - 1) { | ||||
|             if (c == LegacyComponentSerializer.SECTION_CHAR && index < legacyText.length() - 1) { | ||||
|                 currentWord.append(c); | ||||
|                 c = legacyText.charAt(++index); | ||||
|                 currentWord.append(c); | ||||
| @@ -369,7 +424,7 @@ public class ChatUtil { | ||||
|  | ||||
|     /** | ||||
|      * Try to render a matrix of {@link Chat} components into a table in the chat or console. | ||||
|      * @param data the component, in the form of {@link List} of {@link List} of {@link Chat}. The englobing list holds | ||||
|      * @param data the component, in the form of {@link List} of {@link List} of {@link Chat}. The parent list holds | ||||
|      *             the table lines (line 0 being the top line). Each sublist holds the cells content (element 0 is the | ||||
|      *             leftText one). The row lengths can be different. | ||||
|      * @param space a spacer to put between columns. | ||||
| @@ -377,12 +432,12 @@ public class ChatUtil { | ||||
|      *                alignment, much harder). | ||||
|      * @return a List containing each rendered line of the table. | ||||
|      */ | ||||
|     public static List<Component> renderTable(List<List<Chat>> data, String space, boolean console) { | ||||
|     public static List<Component> renderTable(List<List<ComponentLike>> data, String space, boolean console) { | ||||
|         List<List<Component>> compRows = new ArrayList<>(data.size()); | ||||
|         for (List<Chat> row : data) { | ||||
|         for (List<ComponentLike> row : data) { | ||||
|             List<Component> compRow = new ArrayList<>(row.size()); | ||||
|             for (Chat c : row) { | ||||
|                 compRow.add(c.getAdv()); | ||||
|             for (ComponentLike c : row) { | ||||
|                 compRow.add(c.asComponent()); | ||||
|             } | ||||
|             compRows.add(compRow); | ||||
|         } | ||||
| @@ -392,7 +447,7 @@ public class ChatUtil { | ||||
|  | ||||
|     /** | ||||
|      * Try to render a matrix of {@link Component} components into a table in the chat or console. | ||||
|      * @param data the component, in the form of {@link List} of {@link List} of {@link Component}. The englobing list holds | ||||
|      * @param data the component, in the form of {@link List} of {@link List} of {@link Component}. The parent list holds | ||||
|      *             the table lines (line 0 being the top line). Each sublist holds the cells content (element 0 is the | ||||
|      *             leftText one). The row lengths can be different. | ||||
|      * @param space a spacer to put between columns. | ||||
| @@ -416,7 +471,7 @@ public class ChatUtil { | ||||
|         // create the lines with appropriate spacing | ||||
|         List<Component> spacedRows = new ArrayList<>(data.size()); | ||||
|         for (List<Component> row : data) { | ||||
|             Chat spacedRow = Chat.chat(); | ||||
|             Chat spacedRow = chat(); | ||||
|             for (int i = 0; i < row.size() - 1; i++) { | ||||
|                 int w = componentWidth(row.get(i), console); | ||||
|                 int padding = nbPixelPerColumn.get(i) - w; | ||||
| @@ -425,8 +480,8 @@ public class ChatUtil { | ||||
|                 spacedRow.thenText(space); | ||||
|             } | ||||
|             if (!row.isEmpty()) | ||||
|                 spacedRow.then(row.get(row.size() - 1)); | ||||
|             spacedRows.add(spacedRow.getAdv()); | ||||
|                 spacedRow.then(row.getLast()); | ||||
|             spacedRows.add(spacedRow.get()); | ||||
|         } | ||||
|  | ||||
|         return spacedRows; | ||||
| @@ -448,14 +503,14 @@ public class ChatUtil { | ||||
|      */ | ||||
|     public static Component customWidthSpace(int width, boolean console) { | ||||
|         if (console) | ||||
|             return Chat.text(" ".repeat(width)).getAdv(); | ||||
|             return Chat.text(" ".repeat(width)).get(); | ||||
|         return switch (width) { | ||||
|             case 0, 1 -> Component.empty(); | ||||
|             case 2 -> Chat.text(".").black().getAdv(); | ||||
|             case 3 -> Chat.text("`").black().getAdv(); | ||||
|             case 6 -> Chat.text(". ").black().getAdv(); | ||||
|             case 7 -> Chat.text("` ").black().getAdv(); | ||||
|             case 11 -> Chat.text("`  ").black().getAdv(); | ||||
|             case 2 -> Chat.text(".").black().get(); | ||||
|             case 3 -> Chat.text("`").black().get(); | ||||
|             case 6 -> Chat.text(". ").black().get(); | ||||
|             case 7 -> Chat.text("` ").black().get(); | ||||
|             case 11 -> Chat.text("`  ").black().get(); | ||||
|             default -> { | ||||
|                 int nbSpace = width / 4; | ||||
|                 int nbBold = width % 4; | ||||
| @@ -464,13 +519,13 @@ public class ChatUtil { | ||||
|                     if (nbBold > 0) { | ||||
|                         yield Chat.text(" ".repeat(nbNotBold)).bold(false) | ||||
|                                 .then(Chat.text(" ".repeat(nbBold)).bold(true)) | ||||
|                                 .getAdv(); | ||||
|                                 .get(); | ||||
|                     } | ||||
|                     else | ||||
|                         yield Chat.text(" ".repeat(nbNotBold)).bold(false).getAdv(); | ||||
|                         yield Chat.text(" ".repeat(nbNotBold)).bold(false).get(); | ||||
|                 } | ||||
|                 else if (nbBold > 0) { | ||||
|                     yield Chat.text(" ".repeat(nbBold)).bold(true).getAdv(); | ||||
|                     yield Chat.text(" ".repeat(nbBold)).bold(true).get(); | ||||
|                 } | ||||
|                 throw new IllegalStateException("Should not be here (width=" + width + "; nbSpace=" + nbSpace + "; nbBold=" + nbBold + "; nbNotBold=" + nbNotBold + ")"); | ||||
|             } | ||||
| @@ -505,9 +560,9 @@ public class ChatUtil { | ||||
|     private static final char PROGRESS_BAR_FULL_CHAR = '|'; | ||||
|  | ||||
|     /** | ||||
|      * Generate a (eventually multi-part) progress bar using text. | ||||
|      * Generate a (eventually multipart) progress bar using text. | ||||
|      * @param values the values to render in the progress bar. | ||||
|      * @param colors the colors attributed to each values. | ||||
|      * @param colors the colors attributed to each value. | ||||
|      * @param total the total value of the progress bar. | ||||
|      * @param width the width in which the progress bar should fit (in pixel for IG, in character count for console) | ||||
|      * @param console true if the progress bar is intended to be displayed on the console, false if it’s in game chat. | ||||
| @@ -602,5 +657,6 @@ public class ChatUtil { | ||||
|         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,11 +15,6 @@ | ||||
|     <packaging>jar</packaging> | ||||
|      | ||||
| 	<repositories> | ||||
| 		<repository> | ||||
| 			<id>minecraft-libraries</id> | ||||
| 			<name>Minecraft Libraries</name> | ||||
| 			<url>https://libraries.minecraft.net</url> | ||||
| 		</repository> | ||||
|         <repository> | ||||
|             <id>bungeecord-repo</id> | ||||
|             <url>https://oss.sonatype.org/content/repositories/snapshots</url> | ||||
| @@ -42,15 +37,14 @@ | ||||
|             <artifactId>pandalib-commands</artifactId> | ||||
|             <version>${project.version}</version> | ||||
|         </dependency> | ||||
|  | ||||
|         <dependency> | ||||
|             <groupId>net.md-5</groupId> | ||||
|             <artifactId>bungeecord-log</artifactId> | ||||
|             <version>${bungeecord.version}</version> | ||||
|             <groupId>fr.pandacube.lib</groupId> | ||||
|             <artifactId>pandalib-config</artifactId> | ||||
|             <version>${project.version}</version> | ||||
|         </dependency> | ||||
|         <dependency> | ||||
|             <groupId>net.md-5</groupId> | ||||
|             <artifactId>bungeecord-config</artifactId> | ||||
|             <artifactId>bungeecord-log</artifactId> | ||||
|             <version>${bungeecord.version}</version> | ||||
|         </dependency> | ||||
|  | ||||
|   | ||||
| @@ -8,10 +8,10 @@ import fr.pandacube.lib.cli.log.CLILogger; | ||||
| import jline.console.ConsoleReader; | ||||
| import org.fusesource.jansi.AnsiConsole; | ||||
|  | ||||
| import fr.pandacube.lib.util.Log; | ||||
| import fr.pandacube.lib.util.log.Log; | ||||
|  | ||||
| /** | ||||
|  * Class to hangle general standard IO operation for a CLI application. It uses 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. | ||||
|  */ | ||||
| public class CLI extends Thread { | ||||
| @@ -33,7 +33,7 @@ public class CLI extends Thread { | ||||
| 		reader.setPrompt(">"); | ||||
| 		reader.addCompleter(CLIBrigadierDispatcher.instance); | ||||
|  | ||||
| 		// configuration du formatteur pour le logger | ||||
| 		// configure logger's formatter | ||||
| 		System.setProperty("net.md_5.bungee.log-date-format", "yyyy-MM-dd HH:mm:ss"); | ||||
| 		logger = CLILogger.getLogger(this); | ||||
| 	} | ||||
|   | ||||
| @@ -2,7 +2,8 @@ package fr.pandacube.lib.cli; | ||||
|  | ||||
| import fr.pandacube.lib.cli.commands.CommandAdmin; | ||||
| import fr.pandacube.lib.cli.commands.CommandStop; | ||||
| import fr.pandacube.lib.util.Log; | ||||
| import fr.pandacube.lib.cli.log.CLILogger; | ||||
| import fr.pandacube.lib.util.log.Log; | ||||
|  | ||||
| import java.util.concurrent.atomic.AtomicBoolean; | ||||
| import java.util.logging.Logger; | ||||
| @@ -14,17 +15,23 @@ public abstract class CLIApplication { | ||||
|  | ||||
|     private static CLIApplication instance; | ||||
|  | ||||
|     /** | ||||
|      * Returns the current application instance. | ||||
|      * @return the current application instance. | ||||
|      */ | ||||
|     public static CLIApplication getInstance() { | ||||
|         return instance; | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * The instance of {@link CLI} for this application. | ||||
|      */ | ||||
|     public final CLI cli; | ||||
|  | ||||
|     /** | ||||
|      * Creates a new application instance. | ||||
|      */ | ||||
|     protected CLIApplication() { | ||||
|         instance = this; | ||||
|         CLI tmpCLI = null; | ||||
| @@ -46,7 +53,7 @@ public abstract class CLIApplication { | ||||
|             new CommandAdmin(); | ||||
|             new CommandStop(); | ||||
|  | ||||
|             Runtime.getRuntime().addShutdownHook(new Thread(this::end)); | ||||
|             Runtime.getRuntime().addShutdownHook(shutdownThread); | ||||
|  | ||||
|             cli.start(); // actually starts the CLI thread | ||||
|  | ||||
| @@ -56,16 +63,23 @@ public abstract class CLIApplication { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the application's {@link Logger}. | ||||
|      * @return the application's {@link Logger}. | ||||
|      */ | ||||
|     public Logger getLogger() { | ||||
|         return cli.getLogger(); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     private final Object stopLock = new Object(); | ||||
|     private final Thread shutdownThread = new Thread(this::stop); | ||||
|  | ||||
|     private final AtomicBoolean stopping = new AtomicBoolean(false); | ||||
|  | ||||
|     /** | ||||
|      * Stops this application. | ||||
|      */ | ||||
|     public final void stop() { | ||||
|         synchronized (stopLock) { | ||||
|         synchronized (stopping) { | ||||
|             if (stopping.get()) | ||||
|                 return; | ||||
| @@ -78,25 +92,45 @@ public abstract class CLIApplication { | ||||
|             Log.severe("Error stopping application " + getName() + " version " + getClass().getPackage().getImplementationVersion(), t); | ||||
|         } finally { | ||||
|             Log.info("Bye bye."); | ||||
|  | ||||
|             CLILogger.actuallyResetLogManager(); | ||||
|             if (!Thread.currentThread().equals(shutdownThread)) | ||||
|                 System.exit(0); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Tells if this application is currently stopping, that is the {@link #stop()} method has been called. | ||||
|      * @return true if the application is stopping, false otherwise. | ||||
|      */ | ||||
|     public boolean isStopping() { | ||||
|         return stopping.get(); | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Gets the name of this application. | ||||
|      * @return the name of this application. | ||||
|      */ | ||||
|     public abstract String getName(); | ||||
|  | ||||
|     /** | ||||
|      * Method to override to initialize stuff in this application. | ||||
|      * This method is called on instanciation of this Application. | ||||
|      * @throws Exception If an exception is thrown, the application will not start. | ||||
|      */ | ||||
|     protected abstract void start() throws Exception; | ||||
|  | ||||
|     /** | ||||
|      * Method to override to reload specific stuff in this application. | ||||
|      * This method is called by using the command {@code admin reload}. | ||||
|      */ | ||||
|     public abstract void reload(); | ||||
|  | ||||
|     /** | ||||
|      * Method to override to execute stuff when this application stops. | ||||
|      * This method is called once before this application terminates, possibly from a shutdown hook Thread. | ||||
|      */ | ||||
|     protected abstract void end(); | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -15,7 +15,7 @@ import java.util.function.Predicate; | ||||
| public abstract class CLIBrigadierCommand extends BrigadierCommand<CLICommandSender> { | ||||
|  | ||||
| 	/** | ||||
| 	 * Instanciate this command instance. | ||||
| 	 * Instantiate this command instance. | ||||
| 	 */ | ||||
| 	public CLIBrigadierCommand() { | ||||
| 		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(); | ||||
|  | ||||
|  | ||||
| 	private CLIBrigadierDispatcher() {} | ||||
|  | ||||
|  | ||||
| 	/** | ||||
| 	 * 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. | ||||
| 	 */ | ||||
| 	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.identity.Identity; | ||||
| import net.kyori.adventure.text.Component; | ||||
| import org.jetbrains.annotations.NotNull; | ||||
|  | ||||
| /** | ||||
|  * A command sender. | ||||
| @@ -41,5 +42,5 @@ public interface CLICommandSender extends Audience { | ||||
|     void sendMessage(String message); | ||||
|  | ||||
|     @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; | ||||
|  | ||||
| import fr.pandacube.lib.chat.Chat; | ||||
| import fr.pandacube.lib.util.Log; | ||||
| import fr.pandacube.lib.util.log.Log; | ||||
| import net.kyori.adventure.audience.MessageType; | ||||
| import net.kyori.adventure.identity.Identity; | ||||
| import net.kyori.adventure.text.Component; | ||||
| import org.jetbrains.annotations.NotNull; | ||||
|  | ||||
| /** | ||||
|  * The console command sender. | ||||
|  */ | ||||
| public class CLIConsoleCommandSender implements CLICommandSender { | ||||
|  | ||||
|     /** | ||||
|      * Creates a new console command sender. | ||||
|      */ | ||||
|     protected CLIConsoleCommandSender() {} | ||||
|  | ||||
|     public String getName() { | ||||
|         return "Console"; | ||||
|     } | ||||
| @@ -31,7 +38,7 @@ public class CLIConsoleCommandSender implements CLICommandSender { | ||||
|     } | ||||
|  | ||||
|     @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()); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -17,8 +17,8 @@ import fr.pandacube.lib.chat.Chat; | ||||
| import fr.pandacube.lib.chat.Chat.FormatableChat; | ||||
| import fr.pandacube.lib.chat.ChatTreeNode; | ||||
| import fr.pandacube.lib.cli.CLIApplication; | ||||
| import fr.pandacube.lib.util.Log; | ||||
| import net.md_5.bungee.api.chat.BaseComponent; | ||||
| import fr.pandacube.lib.util.log.Log; | ||||
| import net.kyori.adventure.text.Component; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| @@ -31,8 +31,16 @@ import static fr.pandacube.lib.chat.ChatStatic.failureText; | ||||
| import static fr.pandacube.lib.chat.ChatStatic.successText; | ||||
| import static fr.pandacube.lib.chat.ChatStatic.text; | ||||
|  | ||||
| /** | ||||
|  * The {@code admin} command for a {@link CLIApplication}. | ||||
|  */ | ||||
| public class CommandAdmin extends CLIBrigadierCommand { | ||||
|  | ||||
| 	/** | ||||
| 	 * Initializes the admin command. | ||||
| 	 */ | ||||
| 	public CommandAdmin() {} | ||||
|  | ||||
| 	@Override | ||||
| 	protected LiteralArgumentBuilder<CLICommandSender> buildCommand() { | ||||
| 		return literal("admin") | ||||
| @@ -170,13 +178,13 @@ public class CommandAdmin extends CLIBrigadierCommand { | ||||
| 		} | ||||
|  | ||||
|  | ||||
| 		ChatTreeNode dispTree = new ChatTreeNode(d); | ||||
| 		ChatTreeNode displayTree = new ChatTreeNode(d); | ||||
| 		 | ||||
| 		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) | ||||
| 			throw new IllegalArgumentException("node must not be null"); | ||||
| 		FormatableChat d; | ||||
| @@ -257,8 +265,8 @@ public class CommandAdmin extends CLIBrigadierCommand { | ||||
| 	 | ||||
| 	 | ||||
| 	private static class DisplayCommandNode { | ||||
| 		List<CommandNode<CLICommandSender>> nodes = new ArrayList<>(); | ||||
| 		List<DisplayCommandNode> children = new ArrayList<>(); | ||||
| 		final List<CommandNode<CLICommandSender>> nodes = new ArrayList<>(); | ||||
| 		final List<DisplayCommandNode> children = new ArrayList<>(); | ||||
| 		 | ||||
| 		void addInline(CommandNode<CLICommandSender> node) { | ||||
| 			nodes.add(node); | ||||
|   | ||||
| @@ -5,10 +5,15 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder; | ||||
| import fr.pandacube.lib.cli.CLIApplication; | ||||
|  | ||||
| /** | ||||
|  * /stop (/end) command. | ||||
|  * the {@code stop} (or {@code end}) command for a {@link CLIApplication}. | ||||
|  */ | ||||
| public class CommandStop extends CLIBrigadierCommand { | ||||
|  | ||||
| 	/** | ||||
| 	 * Initializes the admin command. | ||||
| 	 */ | ||||
| 	public CommandStop() {} | ||||
|  | ||||
| 	@Override | ||||
| 	protected LiteralArgumentBuilder<CLICommandSender> buildCommand() { | ||||
| 		return literal("stop") | ||||
|   | ||||
| @@ -1,8 +1,10 @@ | ||||
| package fr.pandacube.lib.cli.log; | ||||
|  | ||||
| import fr.pandacube.lib.cli.CLI; | ||||
| import fr.pandacube.lib.util.Log; | ||||
| import fr.pandacube.lib.cli.CLIApplication; | ||||
| import fr.pandacube.lib.util.ThrowableUtil; | ||||
| import fr.pandacube.lib.util.log.DailyLogRotateFileHandler; | ||||
| import fr.pandacube.lib.util.log.Log; | ||||
| import net.md_5.bungee.log.ColouredWriter; | ||||
| import net.md_5.bungee.log.ConciseFormatter; | ||||
|  | ||||
| @@ -12,6 +14,7 @@ import java.io.PrintStream; | ||||
| import java.util.Scanner; | ||||
| import java.util.logging.Handler; | ||||
| import java.util.logging.Level; | ||||
| import java.util.logging.LogManager; | ||||
| import java.util.logging.Logger; | ||||
|  | ||||
| /** | ||||
| @@ -19,12 +22,35 @@ import java.util.logging.Logger; | ||||
|  */ | ||||
| public class CLILogger { | ||||
|  | ||||
| 	static { | ||||
| 		System.setProperty("java.util.logging.manager", ShutdownHookDelayerLogManager.class.getName()); | ||||
| 	} | ||||
|  | ||||
| 	private static Logger logger = null; | ||||
|  | ||||
|  | ||||
| 	private static class ShutdownHookDelayerLogManager extends LogManager { | ||||
| 		static ShutdownHookDelayerLogManager instance; | ||||
| 		public ShutdownHookDelayerLogManager() { instance = this; } | ||||
| 		@Override public void reset() { /* don't reset yet. */ } | ||||
| 		private void actuallyReset() { super.reset(); } | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Tells the LogManager to actually reset. | ||||
| 	 * <p> | ||||
| 	 * This method is called by the shutdown hook of {@link CLIApplication}, because the {@link CLILogger} uses a custom | ||||
| 	 * {@link LogManager} that bypass the reset process during the shutdown of the process. | ||||
| 	 */ | ||||
| 	public static void actuallyResetLogManager() { | ||||
| 		ShutdownHookDelayerLogManager.instance.actuallyReset(); | ||||
| 	} | ||||
|  | ||||
|  | ||||
| 	/** | ||||
| 	 * Initialize and return the logger for this application. | ||||
| 	 * @param cli the CLI instance to use | ||||
| 	 * @return the logger of this application. | ||||
| 	 * @return the logger for this application. | ||||
| 	 */ | ||||
| 	public static synchronized Logger getLogger(CLI cli) { | ||||
| 		if (logger == null) { | ||||
| @@ -45,6 +71,8 @@ public class CLILogger { | ||||
| 	        System.setOut(newRedirector(logger, Level.INFO)); | ||||
|  | ||||
| 			Log.setLogger(logger); | ||||
|  | ||||
| 			Thread.setDefaultUncaughtExceptionHandler((t, e) -> Log.severe("Uncaught Exception in thread " + t.getName(), e)); | ||||
| 		} | ||||
| 		return logger; | ||||
| 	} | ||||
| @@ -70,4 +98,6 @@ public class CLILogger { | ||||
| 		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.suggestion.SuggestionProvider; | ||||
| import com.mojang.brigadier.tree.LiteralCommandNode; | ||||
| import fr.pandacube.lib.util.Log; | ||||
| import fr.pandacube.lib.util.log.Log; | ||||
|  | ||||
| import java.util.Arrays; | ||||
| import java.util.List; | ||||
| import java.util.function.Function; | ||||
| import java.util.function.Predicate; | ||||
|  | ||||
| @@ -24,12 +25,17 @@ import java.util.function.Predicate; | ||||
|  */ | ||||
| public abstract class BrigadierCommand<S> { | ||||
|  | ||||
|     /** | ||||
|      * Creates a Brigadier command. | ||||
|      */ | ||||
|     public BrigadierCommand() {} | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Returns a builder for this command. | ||||
|      * Concrete class should include any element in the builder that is needed to build the command (sub-commands and | ||||
|      * arguments, requirements, redirection, ...). | ||||
|      * If any of the sub-commands and arguments needs to know the {@link LiteralCommandNode} builded from the returned | ||||
|      * If any of the sub-commands and arguments needs to know the {@link LiteralCommandNode} built from the returned | ||||
|      * {@link LiteralArgumentBuilder}, this can be done by overriding {@link #postBuildCommand(LiteralCommandNode)}. | ||||
|      * @return a builder for this command. | ||||
|      */ | ||||
| @@ -37,16 +43,16 @@ public abstract class BrigadierCommand<S> { | ||||
|  | ||||
|     /** | ||||
|      * Method to override if the reference to the command node has to be known when building the subcommands. | ||||
|      * @param commandNode the command node builded from {@link #buildCommand()}. | ||||
|      * @param commandNode the command node built from {@link #buildCommand()}. | ||||
|      */ | ||||
|     protected void postBuildCommand(LiteralCommandNode<S> commandNode) { | ||||
|         // default implementation does nothing. | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Method to override if this command have any aliases. | ||||
|      * Method to override if this command has any aliases. | ||||
|      * @return an array of string corresponding to the aliases. This must not include the orignal command name (that | ||||
|      * is the name of the literal command node builded from {@link #buildCommand()}). | ||||
|      * is the name of the literal command node built from {@link #buildCommand()}). | ||||
|      */ | ||||
|     protected String[] getAliases() { | ||||
|         return new String[0]; | ||||
| @@ -235,10 +241,13 @@ public abstract class BrigadierCommand<S> { | ||||
|                 args = Arrays.copyOf(args, args.length + 1); | ||||
|                 args[args.length - 1] = message.substring(tokenStartPos); | ||||
|  | ||||
|                 for (String s : suggestions.getSuggestions(sender, args.length - 1, args[args.length - 1], args)) { | ||||
|                 List<String> wrappedResult = suggestions.getSuggestions(sender, args.length - 1, args[args.length - 1], args); | ||||
|                 if (wrappedResult != null) { | ||||
|                     for (String s : wrappedResult) { | ||||
|                         if (s != null) | ||||
|                             builder.suggest(s); | ||||
|                     } | ||||
|                 } | ||||
|             } catch (Throwable e) { | ||||
|                 Log.severe("Error while tab-completing '" + message + "' for " + sender, e); | ||||
|             } | ||||
|   | ||||
| @@ -6,14 +6,14 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException; | ||||
| import com.mojang.brigadier.suggestion.Suggestions; | ||||
| import com.mojang.brigadier.tree.LiteralCommandNode; | ||||
| import fr.pandacube.lib.chat.Chat; | ||||
| import fr.pandacube.lib.util.Log; | ||||
| import fr.pandacube.lib.util.log.Log; | ||||
| import net.kyori.adventure.text.ComponentLike; | ||||
|  | ||||
| import java.util.concurrent.CompletableFuture; | ||||
|  | ||||
| /** | ||||
|  * Abstract class that holds a Brigadier {@link CommandDispatcher} instance. | ||||
|  * Subclasses contains logic to integrate this commands dispatcher into their environment (like Bungee or CLI app). | ||||
|  * Subclasses contain logic to integrate this commands dispatcher into their environment (like Bungee or CLI app). | ||||
|  * @param <S> the command source (or command sender) type. | ||||
|  */ | ||||
| public abstract class BrigadierDispatcher<S> { | ||||
| @@ -21,6 +21,11 @@ public abstract class BrigadierDispatcher<S> { | ||||
|  | ||||
|     private final CommandDispatcher<S> dispatcher = new CommandDispatcher<>(); | ||||
|  | ||||
|     /** | ||||
|      * Creates a new Dispatcher instance. | ||||
|      */ | ||||
|     public BrigadierDispatcher() {} | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Registers the provided command node into this dispatcher. | ||||
| @@ -43,7 +48,7 @@ public abstract class BrigadierDispatcher<S> { | ||||
|     /** | ||||
|      * Executes the provided command as the provided sender. | ||||
|      * @param sender the command sender. | ||||
|      * @param commandWithoutSlash the command, without the eventual slash at the begining. | ||||
|      * @param commandWithoutSlash the command, without the eventual slash at the beginning. | ||||
|      * @return the value returned by the executed command. | ||||
|      */ | ||||
|     public int execute(S sender, String commandWithoutSlash) { | ||||
|   | ||||
| @@ -18,7 +18,7 @@ import java.util.List; | ||||
| import java.util.concurrent.CompletableFuture; | ||||
|  | ||||
| /** | ||||
|  * Utility methods to replace some functionalities of Brigadier, especialy suggestion sorting that we don’t like. | ||||
|  * Utility methods to replace some functionalities of Brigadier, especially suggestion sorting that we don’t like. | ||||
|  */ | ||||
| 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; | ||||
|  | ||||
| /** | ||||
|  * Functionnal interface providing suggestions for an argument of a command. | ||||
|  * Functional interface providing suggestions for an argument of a command. | ||||
|  * @param <S> the type of the command sender. | ||||
|  */ | ||||
| @FunctionalInterface | ||||
| @@ -66,7 +66,7 @@ public interface SuggestionsSupplier<S> { | ||||
| 	 * Filter the provided {@link Stream} of string according to the provided token, using the filter returned by {@link #filter(String)}, | ||||
| 	 * then returns the strings collected into a {@link List}. | ||||
| 	 * <p> | ||||
| 	 * This methods consume the provided stream, so will not be usable anymore. | ||||
| 	 * This method consume the provided stream, so will not be usable anymore. | ||||
| 	 * @param stream the stream to filter and collet. | ||||
| 	 * @param token the token to consider for filtering. | ||||
| 	 * @return the stream, filtered and collected into a {@link List}. | ||||
|   | ||||
| @@ -10,20 +10,24 @@ | ||||
|     </parent> | ||||
|     <modelVersion>4.0.0</modelVersion> | ||||
| 
 | ||||
|     <artifactId>pandalib-net</artifactId> | ||||
|     <artifactId>pandalib-config</artifactId> | ||||
|     <packaging>jar</packaging> | ||||
| 
 | ||||
|     <repositories> | ||||
|         <repository> | ||||
|             <id>bungeecord-repo</id> | ||||
|             <url>https://oss.sonatype.org/content/repositories/snapshots</url> | ||||
|         </repository> | ||||
|     </repositories> | ||||
| 
 | ||||
|     <dependencies> | ||||
| 
 | ||||
|         <dependency> | ||||
|             <groupId>fr.pandacube.lib</groupId> | ||||
|             <artifactId>pandalib-util</artifactId> | ||||
|             <version>${project.version}</version> | ||||
|         </dependency> | ||||
|         <dependency> | ||||
|             <groupId>com.google.guava</groupId> | ||||
|             <artifactId>guava</artifactId> | ||||
|             <version>31.0.1-jre</version> | ||||
|             <groupId>net.md-5</groupId> | ||||
|             <artifactId>bungeecord-config</artifactId> | ||||
|             <version>${bungeecord.version}</version> | ||||
|         </dependency> | ||||
| 
 | ||||
|     </dependencies> | ||||
| 
 | ||||
| </project> | ||||
| @@ -1,23 +1,19 @@ | ||||
| package fr.pandacube.lib.core.config; | ||||
| package fr.pandacube.lib.config; | ||||
| 
 | ||||
| import java.io.BufferedReader; | ||||
| import java.io.File; | ||||
| import java.io.FileReader; | ||||
| import java.io.IOException; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.List; | ||||
| 
 | ||||
| import fr.pandacube.lib.chat.ChatColorUtil; | ||||
| import fr.pandacube.lib.util.Log; | ||||
| 
 | ||||
| /** | ||||
|  * Class that loads a specific config file or directory. | ||||
|  */ | ||||
| public abstract class AbstractConfig { | ||||
| 	 | ||||
| 	/** | ||||
| 	 * The {@link File} corresponging to this config file or directory. | ||||
| 	 * The {@link File} corresponding to this config file or directory. | ||||
| 	 */ | ||||
| 	protected final File configFile; | ||||
| 	 | ||||
| @@ -57,7 +53,7 @@ public abstract class AbstractConfig { | ||||
| 		while ((line = reader.readLine()) != null) { | ||||
| 			String trimmedLine = line.trim(); | ||||
| 			 | ||||
| 			if (ignoreEmpty && trimmedLine.equals("")) | ||||
| 			if (ignoreEmpty && trimmedLine.isEmpty()) | ||||
| 				continue; | ||||
| 			 | ||||
| 			if (ignoreHashtagComment && trimmedLine.startsWith("#")) | ||||
| @@ -94,7 +90,8 @@ public abstract class AbstractConfig { | ||||
| 	 * @return the list of files in the config directory, or null if this config is not a directory. | ||||
| 	 */ | ||||
| 	protected List<File> getFileList() { | ||||
| 		return configFile.isDirectory() ? Arrays.asList(configFile.listFiles()) : null; | ||||
| 		File[] arr = configFile.listFiles(); | ||||
| 		return arr != null ? List.of(arr) : null; | ||||
| 	} | ||||
| 	 | ||||
| 	 | ||||
| @@ -105,7 +102,7 @@ public abstract class AbstractConfig { | ||||
| 	 * Splits the provided string into a list of permission nodes. | ||||
| 	 * The permission nodes must be separated by {@code ";"}. | ||||
| 	 * @param perms one or more permissions nodes, separated by {@code ";"}. | ||||
| 	 * @return {@code null} if the parameter is null or is equal to {@code "*"}, or the string splitted using {@code ";"}. | ||||
| 	 * @return {@code null} if the parameter is null or is equal to {@code "*"}, or the string split using {@code ";"}. | ||||
| 	 */ | ||||
| 	public static List<String> splitPermissionsString(String perms) { | ||||
| 		if (perms == null || perms.equals("*")) | ||||
| @@ -114,25 +111,6 @@ public abstract class AbstractConfig { | ||||
| 	} | ||||
| 
 | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Utility method to that translate the {@code '&'} formated string to the legacy format. | ||||
| 	 * @param string the string to convert. | ||||
| 	 * @return a legacy formated string (using {@code '§'}). | ||||
| 	 */ | ||||
| 	public static String getTranslatedColorCode(String string) { | ||||
| 		return ChatColorUtil.translateAlternateColorCodes('&', string); | ||||
| 	} | ||||
| 
 | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Logs the message as a warning into console, prefixed with the context of this config. | ||||
| 	 * @param message the message to log. | ||||
| 	 */ | ||||
| 	protected void warning(String message) { | ||||
| 		Log.warning("Error in configuration '"+configFile.getName()+"': " + message); | ||||
| 	} | ||||
| 
 | ||||
| 
 | ||||
| 	/** | ||||
| 	 * The type of config. | ||||
| 	 */ | ||||
| @@ -1,17 +1,17 @@ | ||||
| package fr.pandacube.lib.core.config; | ||||
| package fr.pandacube.lib.config; | ||||
| 
 | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| 
 | ||||
| /** | ||||
|  * An abstract manager for a set of configuration files and folders. | ||||
|  * Its uses is to manage the loading/reloading of the configuration of a plugin. | ||||
|  * It's uses to manage the loading/reloading of the configuration of a plugin. | ||||
|  */ | ||||
| public abstract class AbstractConfigManager { | ||||
| 
 | ||||
| 	/** | ||||
| 	 * The global configuration directory. | ||||
| 	 * May be the one provided by the environmenet API (like Plugin.getPluginFolder() in Bukkit). | ||||
| 	 * It may be the one provided by the environment API (like Plugin.getPluginFolder() in Bukkit). | ||||
| 	 */ | ||||
| 	protected final File configDir; | ||||
| 
 | ||||
| @@ -37,6 +37,12 @@ | ||||
|             <version>${project.version}</version> | ||||
|         </dependency> | ||||
|  | ||||
|         <dependency> | ||||
|             <groupId>com.google.guava</groupId> | ||||
|             <artifactId>guava</artifactId> | ||||
|             <version>${guava.version}</version> | ||||
|         </dependency> | ||||
|  | ||||
|         <!-- Cron expression interpreter --> | ||||
|         <dependency> | ||||
|             <groupId>ch.eitchnet</groupId> | ||||
| @@ -50,7 +56,7 @@ | ||||
|             <plugin> | ||||
|                 <groupId>org.apache.maven.plugins</groupId> | ||||
|                 <artifactId>maven-shade-plugin</artifactId> | ||||
|                 <version>3.3.0</version> | ||||
|                 <version>3.5.2</version> | ||||
|                 <executions> | ||||
|                     <execution> | ||||
|                         <phase>package</phase> | ||||
| @@ -85,6 +91,28 @@ | ||||
|                     </execution> | ||||
|                 </executions> | ||||
|             </plugin> | ||||
|  | ||||
|             <plugin> | ||||
|                 <groupId>com.googlecode.maven-download-plugin</groupId> | ||||
|                 <artifactId>download-maven-plugin</artifactId> | ||||
|                 <version>1.7.0</version> | ||||
|                 <executions> | ||||
|                     <execution> | ||||
|                         <id>mcversion-download</id> | ||||
|                         <phase>compile</phase> | ||||
|                         <goals> | ||||
|                             <goal>wget</goal> | ||||
|                         </goals> | ||||
|                     </execution> | ||||
|                 </executions> | ||||
|                 <configuration> | ||||
|                     <url>https://api.pandacube.fr/rest/mcversion</url> | ||||
|                     <outputDirectory>${project.basedir}/src/main/resources/fr/pandacube/lib/core/mc_version</outputDirectory> | ||||
|                     <outputFileName>mcversion.json</outputFileName> | ||||
|                     <skipCache>true</skipCache> | ||||
|                     <overwrite>true</overwrite> | ||||
|                 </configuration> | ||||
|             </plugin> | ||||
|         </plugins> | ||||
|     </build> | ||||
| </project> | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| package fr.pandacube.lib.core.backup; | ||||
|  | ||||
| import fr.pandacube.lib.chat.Chat; | ||||
| import fr.pandacube.lib.util.Log; | ||||
| import net.md_5.bungee.api.ChatColor; | ||||
| import fr.pandacube.lib.chat.LegacyChatFormat; | ||||
| import fr.pandacube.lib.util.log.Log; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.time.LocalDateTime; | ||||
| @@ -18,7 +18,7 @@ import static fr.pandacube.lib.chat.ChatStatic.text; | ||||
|  | ||||
| /** | ||||
|  * 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>> { | ||||
|  | ||||
| @@ -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 | ||||
|      * 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. | ||||
|      */ | ||||
|     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 | ||||
| @@ -99,15 +104,17 @@ public abstract class BackupCleaner implements UnaryOperator<TreeSet<LocalDateTi | ||||
|      */ | ||||
|     public void cleanupArchives(File archiveDir, String compressDisplayName) { | ||||
|         String[] files = archiveDir.list(); | ||||
|         if (files == null) | ||||
|             return; | ||||
|  | ||||
|         Log.info("[Backup] Cleaning up backup directory " + ChatColor.GRAY + compressDisplayName + ChatColor.RESET + "..."); | ||||
|         Log.info("[Backup] Cleaning up backup directory " + LegacyChatFormat.GRAY + compressDisplayName + LegacyChatFormat.RESET + "..."); | ||||
|  | ||||
|         TreeMap<LocalDateTime, File> datedFiles = new TreeMap<>(); | ||||
|  | ||||
|         for (String filename : files) { | ||||
|             File file = new File(archiveDir, filename); | ||||
|             if (!filename.matches("\\d{8}-\\d{6}\\.zip")) { | ||||
|                 Log.warning("[Backup] " + ChatColor.GRAY + compressDisplayName + ChatColor.RESET + " Invalid file in backup directory: " + filename); | ||||
|                 Log.warning("[Backup] " + LegacyChatFormat.GRAY + compressDisplayName + LegacyChatFormat.RESET + " Invalid file in backup directory: " + filename); | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
| @@ -116,7 +123,7 @@ public abstract class BackupCleaner implements UnaryOperator<TreeSet<LocalDateTi | ||||
|             try { | ||||
|                 ldt = LocalDateTime.parse(dateTimeStr, BackupProcess.dateFileNameFormatter); | ||||
|             } catch (DateTimeParseException e) { | ||||
|                 Log.warning("[Backup] " + ChatColor.GRAY + compressDisplayName + ChatColor.RESET + " Unable to parse file name to a date-time: " + filename, e); | ||||
|                 Log.warning("[Backup] " + LegacyChatFormat.GRAY + compressDisplayName + LegacyChatFormat.RESET + " Unable to parse file name to a date-time: " + filename, e); | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
| @@ -149,7 +156,7 @@ public abstract class BackupCleaner implements UnaryOperator<TreeSet<LocalDateTi | ||||
|         if (testOnly || oneDeleted) | ||||
|             Log.warning(c.getLegacyText()); | ||||
|  | ||||
|         Log.info("[Backup] Backup directory " + ChatColor.GRAY + compressDisplayName + ChatColor.RESET + " cleaned."); | ||||
|         Log.info("[Backup] Backup directory " + LegacyChatFormat.GRAY + compressDisplayName + LegacyChatFormat.RESET + " cleaned."); | ||||
|     } | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| package fr.pandacube.lib.core.backup; | ||||
|  | ||||
| import fr.pandacube.lib.util.Log; | ||||
| import fr.pandacube.lib.util.log.Log; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.time.ZonedDateTime; | ||||
| @@ -33,11 +33,14 @@ public class BackupManager extends TimerTask { | ||||
|     private final Timer schedulerTimer = new Timer(); | ||||
|  | ||||
|     /** | ||||
|      * Instanciate a new backup manager. | ||||
|      * Instantiate a new backup manager. | ||||
|      * @param backupDirectory the root backup directory. | ||||
|      */ | ||||
|     public BackupManager(File backupDirectory) { | ||||
|         this.backupDirectory = backupDirectory; | ||||
|         if (!backupDirectory.exists()) { | ||||
|             backupDirectory.mkdirs(); | ||||
|         } | ||||
|         persist = new Persist(this); | ||||
|  | ||||
|  | ||||
| @@ -63,7 +66,13 @@ public class BackupManager extends TimerTask { | ||||
|         return backupDirectory; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Tells if a backup is currently running. | ||||
|      * @return true if a backup is running, false otherwise. | ||||
|      */ | ||||
|     public synchronized boolean isBackupRunning() { | ||||
|         return runningBackup.get() != null; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     public synchronized void run() { | ||||
| @@ -87,11 +96,12 @@ public class BackupManager extends TimerTask { | ||||
|      * Disables this backup manager, canceling scheduled backups. | ||||
|      * It will wait for a currently running backup to finish before returning. | ||||
|      */ | ||||
|     @SuppressWarnings("BusyWait") | ||||
|     public synchronized void onDisable() { | ||||
|  | ||||
|         schedulerTimer.cancel(); | ||||
|  | ||||
|         if (runningBackup.get() != null) { | ||||
|         if (isBackupRunning()) { | ||||
|             Log.warning("[Backup] Waiting after the end of a backup..."); | ||||
|             BackupProcess tmp; | ||||
|             while ((tmp = runningBackup.get()) != null) { | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| package fr.pandacube.lib.core.backup; | ||||
|  | ||||
| import fc.cron.CronExpression; | ||||
| import fr.pandacube.lib.chat.LegacyChatFormat; | ||||
| import fr.pandacube.lib.core.cron.CronScheduler; | ||||
| import fr.pandacube.lib.util.FileUtils; | ||||
| import fr.pandacube.lib.util.Log; | ||||
| import net.md_5.bungee.api.ChatColor; | ||||
| import fr.pandacube.lib.util.log.Log; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.text.DateFormat; | ||||
| @@ -40,7 +40,7 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab | ||||
|     private List<String> ignoreList = new ArrayList<>(); | ||||
|  | ||||
|     /** | ||||
|      * Instanciates a new backup process. | ||||
|      * Instantiates a new backup process. | ||||
|      * @param bm the associated backup manager. | ||||
|      * @param n the process identifier. | ||||
|      */ | ||||
| @@ -123,7 +123,7 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab | ||||
|  | ||||
|     /** | ||||
|      * 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); | ||||
|  | ||||
| @@ -209,7 +209,7 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab | ||||
|             File sourceDir = getSourceDir(); | ||||
|  | ||||
|             if (!sourceDir.exists()) { | ||||
|                 Log.warning("[Backup] Unable to compress " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + ": source directory " + sourceDir + " doesn’t exist"); | ||||
|                 Log.warning("[Backup] Unable to compress " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + ": source directory " + sourceDir + " doesn't exist"); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
| @@ -219,7 +219,7 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab | ||||
|             onBackupStart(); | ||||
|  | ||||
|             new Thread(() -> { | ||||
|                 Log.info("[Backup] Starting for " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " ..."); | ||||
|                 Log.info("[Backup] Starting for " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + " ..."); | ||||
|  | ||||
|                 compressor = new ZipCompressor(sourceDir, target, 9, filter); | ||||
|  | ||||
| @@ -229,7 +229,7 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab | ||||
|  | ||||
|                     success = true; | ||||
|  | ||||
|                     Log.info("[Backup] Finished for " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET); | ||||
|                     Log.info("[Backup] Finished for " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET); | ||||
|  | ||||
|                     try { | ||||
|                         BackupCleaner cleaner = getBackupCleaner(); | ||||
| @@ -267,7 +267,7 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab | ||||
|      * Logs the scheduling status of this backup process. | ||||
|      */ | ||||
|     public void displayNextSchedule() { | ||||
|         Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " next backup on " | ||||
|         Log.info("[Backup] " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + " next backup on " | ||||
|                 + DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(new Date(getNext()))); | ||||
|     } | ||||
|  | ||||
| @@ -297,7 +297,7 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab | ||||
|     public void logProgress() { | ||||
|         if (compressor == null) | ||||
|             return; | ||||
|         Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + ": " + compressor.getState().getLegacyText()); | ||||
|         Log.info("[Backup] " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + ": " + compressor.getState().getLegacyText()); | ||||
|     } | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -3,7 +3,7 @@ package fr.pandacube.lib.core.backup; | ||||
| import com.google.gson.JsonParseException; | ||||
| import com.google.gson.reflect.TypeToken; | ||||
| import fr.pandacube.lib.core.json.Json; | ||||
| import fr.pandacube.lib.util.Log; | ||||
| import fr.pandacube.lib.util.log.Log; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.FileReader; | ||||
| @@ -26,7 +26,7 @@ public class Persist { | ||||
| 	// 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. | ||||
| 	 */ | ||||
| 	public Persist(BackupManager bm) { | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| package fr.pandacube.lib.core.backup; | ||||
|  | ||||
| import com.google.common.io.Files; | ||||
| import fr.pandacube.lib.util.Log; | ||||
| import net.md_5.bungee.api.ChatColor; | ||||
| import fr.pandacube.lib.chat.LegacyChatFormat; | ||||
| import fr.pandacube.lib.util.log.Log; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| @@ -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 | ||||
|      *                    the backup manager (false). | ||||
|      * @param sourceLogDir the directory where the rotated log files are stored, usually {@code ./logs/}. | ||||
|      * @param logFileRegexPattern the pattern to match the rotated log files (usually dated log files, 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) { | ||||
|         super(bm, "logs"); | ||||
| @@ -52,7 +53,7 @@ public class RotatedLogsBackupProcess extends BackupProcess { | ||||
|         if (!getSourceDir().isDirectory()) | ||||
|             return; | ||||
|  | ||||
|         Log.info("[Backup] Starting for " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " ..."); | ||||
|         Log.info("[Backup] Starting for " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + " ..."); | ||||
|  | ||||
|         try { | ||||
|             // wait a little after the log message above, in case the log file rotation has to be performed. | ||||
| @@ -81,9 +82,9 @@ public class RotatedLogsBackupProcess extends BackupProcess { | ||||
|  | ||||
|             success = true; | ||||
|  | ||||
|             Log.info("[Backup] Finished for " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET); | ||||
|             Log.info("[Backup] Finished for " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET); | ||||
|         } catch (Exception e) { | ||||
|             Log.severe("[Backup] Failed for : " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET, e); | ||||
|             Log.severe("[Backup] Failed for : " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET, e); | ||||
|         } finally { | ||||
|             onBackupEnd(success); | ||||
|  | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import java.io.File; | ||||
| import java.io.FileOutputStream; | ||||
| import java.io.IOException; | ||||
| import java.nio.file.Files; | ||||
| import java.nio.file.NoSuchFileException; | ||||
| import java.nio.file.attribute.BasicFileAttributes; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| @@ -117,7 +118,11 @@ public class ZipCompressor { | ||||
| 			} | ||||
| 			 | ||||
| 			for (Entry entry : entriesToCompress) { | ||||
| 				try { | ||||
| 					entry.zip(); | ||||
| 				} catch (NoSuchFileException ignored) { | ||||
| 					// file has been deleted since | ||||
| 				} | ||||
| 			} | ||||
| 			 | ||||
| 			synchronized (stateLock) { | ||||
| @@ -158,8 +163,8 @@ public class ZipCompressor { | ||||
| 	} | ||||
| 	 | ||||
| 	private class Entry { | ||||
| 		File file; | ||||
| 		String entry; | ||||
| 		final File file; | ||||
| 		final String entry; | ||||
| 		Entry(File f, String e) { | ||||
| 			file = f; | ||||
| 			entry = e; | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import com.google.gson.JsonParseException; | ||||
| import com.google.gson.reflect.TypeToken; | ||||
| import fc.cron.CronExpression; | ||||
| import fr.pandacube.lib.core.json.Json; | ||||
| import fr.pandacube.lib.util.Log; | ||||
| import fr.pandacube.lib.util.log.Log; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.FileReader; | ||||
| @@ -54,10 +54,10 @@ public class CronScheduler { | ||||
|                 long now = System.currentTimeMillis(); | ||||
|  | ||||
|                 if (!tasks.isEmpty()) { | ||||
|                     CronTask next = tasks.get(0); | ||||
|                     CronTask next = tasks.getFirst(); | ||||
|                     if (next.nextRun <= now) { | ||||
|                         next.runAsync(); | ||||
|                         setLastRun(next.taskId, next.nextRun); | ||||
|                         setLastRun(next.taskId, now); | ||||
|                         onTaskUpdate(false); | ||||
|                         continue; | ||||
|                     } | ||||
| @@ -102,7 +102,7 @@ public class CronScheduler { | ||||
|  | ||||
|     /** | ||||
|      * Cancel a scheduled task. | ||||
|      * Will not stop a current execution of the task. If the task does not exists, it will not do anything. | ||||
|      * Will not stop a current execution of the task. If the task does not exist, it will not do anything. | ||||
|      * @param taskId the id of the task to cancel. | ||||
|      */ | ||||
|     public static void unSchedule(String taskId) { | ||||
| @@ -224,5 +224,6 @@ public class CronScheduler { | ||||
|                 .toEpochMilli(); | ||||
|     } | ||||
|  | ||||
|     private CronScheduler() {} | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -2,44 +2,94 @@ package fr.pandacube.lib.core.json; | ||||
|  | ||||
| import com.google.gson.Gson; | ||||
| import com.google.gson.GsonBuilder; | ||||
| import com.google.gson.JsonParseException; | ||||
| import com.google.gson.ToNumberStrategy; | ||||
| import com.google.gson.TypeAdapter; | ||||
| import com.google.gson.TypeAdapterFactory; | ||||
| 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.List; | ||||
| 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)}. | ||||
|  */ | ||||
| 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)}. | ||||
| 	 */ | ||||
| 	public static final Gson gson = build(Function.identity()); | ||||
|  | ||||
| 	/** | ||||
| 	 * {@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)}. | ||||
| 	 */ | ||||
| 	public static final Gson gsonPrettyPrinting = build(GsonBuilder::setPrettyPrinting); | ||||
|  | ||||
| 	/** | ||||
| 	 * {@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)}. | ||||
| 	 */ | ||||
| 	public static final Gson gsonSerializeNulls = build(GsonBuilder::serializeNulls); | ||||
|  | ||||
| 	/** | ||||
| 	 * {@link Gson} instance with {@link GsonBuilder#setLenient()}, {@link GsonBuilder#serializeNulls()}, | ||||
| 	 * {@link GsonBuilder#setPrettyPrinting()} and support for Java records and additionnal {@link TypeAdapterFactory} | ||||
| 	 * {@link GsonBuilder#setPrettyPrinting()} and support for Java records and additional {@link TypeAdapterFactory} | ||||
| 	 * provided with {@link #registerTypeAdapterFactory(TypeAdapterFactory)}. | ||||
| 	 */ | ||||
| 	public static final Gson gsonSerializeNullsPrettyPrinting = build(b -> b.serializeNulls().setPrettyPrinting()); | ||||
| @@ -53,6 +103,8 @@ public class Json { | ||||
| 	private static Gson build(Function<GsonBuilder, GsonBuilder> builderModifier) { | ||||
| 		GsonBuilder base = new GsonBuilder() | ||||
| 				.registerTypeAdapterFactory(new CustomAdapterFactory()) | ||||
| 				.disableHtmlEscaping() | ||||
| 				.setObjectToNumberStrategy(YAML_EQUIVALENT_NUMBER_STRATEGY) | ||||
| 				.setLenient(); | ||||
| 		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 { | ||||
| 		if (!hasGsonNativeRecordSupport()) | ||||
| 			registerTypeAdapterFactory(RecordTypeAdapter.FACTORY); | ||||
| 		registerTypeAdapterFactory(StackTraceElementAdapter.FACTORY); | ||||
| 		registerTypeAdapterFactory(ThrowableAdapter.FACTORY); | ||||
| 		registerTypeAdapterFactory(MinecraftVersionListAdapter.FACTORY); | ||||
| 	} | ||||
|  | ||||
|  | ||||
| 	/*public static void main(String[] args) { | ||||
| 		TypeToken<Map<String, Object>> MAP_STR_OBJ_TYPE = new TypeToken<>() { }; | ||||
| 		Map<String, Object> map = gson.fromJson("{" + | ||||
| 				"\"int\":34," + | ||||
| 				"\"long\":3272567356876864," + | ||||
| 				"\"bigint\":-737868677777837833757846576245765," + | ||||
| 				"\"float\":34.0" + | ||||
| 				"}", MAP_STR_OBJ_TYPE.getType()); | ||||
| 		for (String key : map.keySet()) { | ||||
| 			Object v = map.get(key); | ||||
| 			System.out.println(key + ": " + v + " (type " + v.getClass() + ")"); | ||||
| 		} | ||||
| 	}*/ | ||||
|  | ||||
| 	private Json() {} | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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.TypeAdapterFactory; | ||||
| import com.google.gson.internal.bind.TreeTypeAdapter; | ||||
| import fr.pandacube.lib.util.Log; | ||||
| import fr.pandacube.lib.util.log.Log; | ||||
| import fr.pandacube.lib.util.ThrowableUtil; | ||||
|  | ||||
| import java.lang.reflect.Constructor; | ||||
| @@ -30,6 +30,8 @@ public class ThrowableAdapter implements JsonSerializer<Throwable>, JsonDeserial | ||||
|  | ||||
|     /* package */ static final TypeAdapterFactory FACTORY = TreeTypeAdapter.newTypeHierarchyFactory(Throwable.class, new ThrowableAdapter()); | ||||
|  | ||||
|     private ThrowableAdapter() {} | ||||
|  | ||||
|  | ||||
|     @Override | ||||
|     public Throwable deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { | ||||
| @@ -43,7 +45,7 @@ public class ThrowableAdapter implements JsonSerializer<Throwable>, JsonDeserial | ||||
|         // handle types | ||||
|         Throwable t = null; | ||||
|         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) { | ||||
|             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() | ||||
|                 ? obj.get("suppressed").getAsJsonArray() : null; | ||||
|         if (suppressed != null) { | ||||
|             for (JsonElement jsonel : suppressed) { | ||||
|                 t.addSuppressed(context.deserialize(jsonel, Throwable.class)); | ||||
|             for (JsonElement jsonEl : suppressed) { | ||||
|                 t.addSuppressed(context.deserialize(jsonEl, Throwable.class)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -63,8 +65,8 @@ public class ThrowableAdapter implements JsonSerializer<Throwable>, JsonDeserial | ||||
|                 ? obj.get("stacktrace").getAsJsonArray() : null; | ||||
|         if (stacktrace != null) { | ||||
|             List<StackTraceElement> els = new ArrayList<>(); | ||||
|             for (JsonElement jsonel : stacktrace) { | ||||
|                 els.add(context.deserialize(jsonel, StackTraceElement.class)); | ||||
|             for (JsonElement jsonEl : stacktrace) { | ||||
|                 els.add(context.deserialize(jsonEl, StackTraceElement.class)); | ||||
|             } | ||||
|             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; | ||||
|         for (JsonElement clNameEl : types) { | ||||
|             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. | ||||
|          * @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}. | ||||
|   | ||||
| @@ -15,8 +15,8 @@ public class TypeConverter { | ||||
| 	/** | ||||
| 	 * Converts the provided object to an {@link Integer}. | ||||
| 	 * @param o the object to convert. | ||||
| 	 * @return a the object converted to an {@link Integer}. | ||||
| 	 * @throws ConvertionException is a conversion error occurs. | ||||
| 	 * @return the object converted to an {@link Integer}. | ||||
| 	 * @throws ConversionException is a conversion error occurs. | ||||
| 	 */ | ||||
| 	public static Integer toInteger(Object o) { | ||||
| 		if (o == null) { | ||||
| @@ -27,7 +27,7 @@ public class TypeConverter { | ||||
| 			try { | ||||
| 				return ((JsonElement)o).getAsInt(); | ||||
| 			} catch(UnsupportedOperationException e) { | ||||
| 				throw new ConvertionException(e); | ||||
| 				throw new ConversionException(e); | ||||
| 			} | ||||
| 		} | ||||
| 		 | ||||
| @@ -38,34 +38,34 @@ public class TypeConverter { | ||||
| 			try { | ||||
| 			return Integer.parseInt((String)o); | ||||
| 			} catch (NumberFormatException e) { | ||||
| 				throw new ConvertionException(e); | ||||
| 				throw new ConversionException(e); | ||||
| 			} | ||||
| 		} | ||||
| 		if (o instanceof Boolean) { | ||||
| 			return ((Boolean)o) ? 1 : 0; | ||||
| 		} | ||||
| 		 | ||||
| 		throw new ConvertionException("No integer convertion available for an instance of "+o.getClass()); | ||||
| 		throw new ConversionException("No integer conversion available for an instance of "+o.getClass()); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Converts the provided object to a primitive int. | ||||
| 	 * @param o the object to convert. | ||||
| 	 * @return a the object converted to a primitive int. | ||||
| 	 * @throws ConvertionException is a conversion error occurs. | ||||
| 	 * @return the object converted to a primitive int. | ||||
| 	 * @throws ConversionException is a conversion error occurs. | ||||
| 	 */ | ||||
| 	public static int toPrimInt(Object o) { | ||||
| 		Integer val = toInteger(o); | ||||
| 		if (val == null) | ||||
| 			throw new ConvertionException("null values can't be converted to primitive int"); | ||||
| 			throw new ConversionException("null values can't be converted to primitive int"); | ||||
| 		return val; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Converts the provided object to a {@link Double}. | ||||
| 	 * @param o the object to convert. | ||||
| 	 * @return a the object converted to a {@link Double}. | ||||
| 	 * @throws ConvertionException is a conversion error occurs. | ||||
| 	 * @return the object converted to a {@link Double}. | ||||
| 	 * @throws ConversionException is a conversion error occurs. | ||||
| 	 */ | ||||
| 	public static Double toDouble(Object o) { | ||||
| 		if (o == null) { | ||||
| @@ -76,7 +76,7 @@ public class TypeConverter { | ||||
| 			try { | ||||
| 			return ((JsonElement)o).getAsDouble(); | ||||
| 			} catch(UnsupportedOperationException e) { | ||||
| 				throw new ConvertionException(e); | ||||
| 				throw new ConversionException(e); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| @@ -87,35 +87,35 @@ public class TypeConverter { | ||||
| 			try { | ||||
| 			return Double.parseDouble((String)o); | ||||
| 			} catch (NumberFormatException e) { | ||||
| 				throw new ConvertionException(e); | ||||
| 				throw new ConversionException(e); | ||||
| 			} | ||||
| 		} | ||||
| 		if (o instanceof Boolean) { | ||||
| 			return ((Boolean)o) ? 1d : 0d; | ||||
| 		} | ||||
| 		 | ||||
| 		throw new ConvertionException("No double convertion available for an instance of "+o.getClass()); | ||||
| 		throw new ConversionException("No double conversion available for an instance of "+o.getClass()); | ||||
| 		 | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Converts the provided object to a primitive double. | ||||
| 	 * @param o the object to convert. | ||||
| 	 * @return a the object converted to a primitive double. | ||||
| 	 * @throws ConvertionException is a conversion error occurs. | ||||
| 	 * @return the object converted to a primitive double. | ||||
| 	 * @throws ConversionException is a conversion error occurs. | ||||
| 	 */ | ||||
| 	public static double toPrimDouble(Object o) { | ||||
| 		Double val = toDouble(o); | ||||
| 		if (val == null) | ||||
| 			throw new ConvertionException("null values can't converted to primitive int"); | ||||
| 			throw new ConversionException("null values can't converted to primitive int"); | ||||
| 		return val; | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Converts the provided object to a {@link String}. | ||||
| 	 * @param o the object to convert. | ||||
| 	 * @return a the object converted to a {@link String}. | ||||
| 	 * @throws ConvertionException is a conversion error occurs. | ||||
| 	 * @return the object converted to a {@link String}. | ||||
| 	 * @throws ConversionException is a conversion error occurs. | ||||
| 	 */ | ||||
| 	public static String toString(Object o) { | ||||
| 		if (o == null) { | ||||
| @@ -126,7 +126,7 @@ public class TypeConverter { | ||||
| 			try { | ||||
| 				return ((JsonElement)o).getAsString(); | ||||
| 			} catch(UnsupportedOperationException e) { | ||||
| 				throw new ConvertionException(e); | ||||
| 				throw new ConversionException(e); | ||||
| 			} | ||||
| 		} | ||||
| 		 | ||||
| @@ -134,7 +134,7 @@ public class TypeConverter { | ||||
| 			return o.toString(); | ||||
| 		} | ||||
|  | ||||
| 		throw new ConvertionException("No string convertion available for an instance of "+o.getClass()); | ||||
| 		throw new ConversionException("No string conversion available for an instance of "+o.getClass()); | ||||
| 		 | ||||
| 	} | ||||
| 	 | ||||
| @@ -144,8 +144,8 @@ public class TypeConverter { | ||||
| 	 * @param mapIntKeys if the String key representing an int should be duplicated as integer type, | ||||
| 	 * which map to the same value as the original String key. For example, if a key is "12" and map | ||||
| 	 * to the object <i>o</i>, an integer key 12 will be added and map to the same object <i>o</i>. | ||||
| 	 * @return a the object converted to a {@link Map}. | ||||
| 	 * @throws ConvertionException is a conversion error occurs. | ||||
| 	 * @return the object converted to a {@link Map}. | ||||
| 	 * @throws ConversionException is a conversion error occurs. | ||||
| 	 */ | ||||
| 	@SuppressWarnings("unchecked") | ||||
| 	public static Map<Object, Object> toMap(Object o, boolean mapIntKeys) { | ||||
| @@ -186,15 +186,15 @@ public class TypeConverter { | ||||
| 			return map; | ||||
| 		} | ||||
|  | ||||
| 		throw new ConvertionException("No Map convertion available for an instance of "+o.getClass()); | ||||
| 		throw new ConversionException("No Map conversion available for an instance of "+o.getClass()); | ||||
| 	} | ||||
|  | ||||
|  | ||||
| 	/** | ||||
| 	 * Converts the provided object to a {@link List}. | ||||
| 	 * @param o the object to convert. | ||||
| 	 * @return a the object converted to a {@link List}. | ||||
| 	 * @throws ConvertionException is a conversion error occurs. | ||||
| 	 * @return the object converted to a {@link List}. | ||||
| 	 * @throws ConversionException is a conversion error occurs. | ||||
| 	 */ | ||||
| 	@SuppressWarnings("unchecked") | ||||
| 	public static List<Object> toList(Object o) { | ||||
| @@ -217,7 +217,7 @@ public class TypeConverter { | ||||
| 		} | ||||
| 		 | ||||
|  | ||||
| 		throw new ConvertionException("No Map convertion available for an instance of "+o.getClass()); | ||||
| 		throw new ConversionException("No Map conversion available for an instance of "+o.getClass()); | ||||
| 		 | ||||
| 		 | ||||
| 		 | ||||
| @@ -225,17 +225,20 @@ public class TypeConverter { | ||||
|  | ||||
|  | ||||
| 	/** | ||||
| 	 * Thrown when a convertion error occurs. | ||||
| 	 * Thrown when a conversion error occurs. | ||||
| 	 */ | ||||
| 	public static class ConvertionException extends RuntimeException { | ||||
| 	public static class ConversionException extends RuntimeException { | ||||
| 		 | ||||
| 		private ConvertionException(String m) { | ||||
| 		private ConversionException(String m) { | ||||
| 			super(m); | ||||
| 		} | ||||
| 		private ConvertionException(Throwable t) { | ||||
| 		private ConversionException(Throwable t) { | ||||
| 			super(t); | ||||
| 		} | ||||
| 		 | ||||
| 	} | ||||
|  | ||||
|  | ||||
| 	private TypeConverter() {} | ||||
| 	 | ||||
| } | ||||
|   | ||||
| @@ -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.CacheBuilder; | ||||
| import fr.pandacube.lib.util.Log; | ||||
| import fr.pandacube.lib.util.log.Log; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.HashMap; | ||||
| @@ -15,7 +15,7 @@ import java.util.concurrent.ExecutionException; | ||||
| import java.util.stream.Collectors; | ||||
|  | ||||
| /** | ||||
|  * Utility class to manage searching among a set of {@link SearchResult} instances, using case insensitive keywords. | ||||
|  * Utility class to manage searching among a set of {@link SearchResult} instances, using case-insensitive keywords. | ||||
|  * The search engine is responsible for storing a database of entries ({@link SearchResult}) that can be searched using | ||||
|  * keywords. This class provides methods to returns a list of results for provided keywords, a list of keyword | ||||
|  * suggestions based on pre-typed keywords. | ||||
|   | ||||
| @@ -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> | ||||
|             <groupId>org.apache.commons</groupId> | ||||
|             <artifactId>commons-dbcp2</artifactId> | ||||
|             <version>2.9.0</version> | ||||
|             <version>2.12.0</version> | ||||
|         </dependency> | ||||
|     </dependencies> | ||||
|  | ||||
| @@ -36,7 +36,7 @@ | ||||
|             <plugin> | ||||
|                 <groupId>org.apache.maven.plugins</groupId> | ||||
|                 <artifactId>maven-shade-plugin</artifactId> | ||||
|                 <version>3.3.0</version> | ||||
|                 <version>3.5.2</version> | ||||
|                 <executions> | ||||
|                     <execution> | ||||
|                         <phase>package</phase> | ||||
| @@ -56,29 +56,32 @@ | ||||
|                                     <artifact>org.apache.commons:commons-dbcp2</artifact> | ||||
|                                     <excludes> | ||||
|                                         <exclude>META-INF/MANIFEST.MF</exclude> | ||||
|                                         <exclude>META-INF/versions/9/**</exclude> | ||||
|                                     </excludes> | ||||
|                                 </filter> | ||||
|                                 <filter> | ||||
|                                     <artifact>org.apache.commons:commons-pool2</artifact> | ||||
|                                     <excludes> | ||||
|                                         <exclude>META-INF/MANIFEST.MF</exclude> | ||||
|                                         <exclude>META-INF/versions/9/**</exclude> | ||||
|                                     </excludes> | ||||
|                                 </filter> | ||||
|                                 <filter> | ||||
|                                     <artifact>commons-logging:commons-logging</artifact> | ||||
|                                     <excludes> | ||||
|                                         <exclude>META-INF/MANIFEST.MF</exclude> | ||||
|                                         <exclude>META-INF/versions/9/**</exclude> | ||||
|                                     </excludes> | ||||
|                                 </filter> | ||||
|                             </filters> | ||||
|                             <relocations> | ||||
|                                 <relocation> | ||||
|                                     <pattern>org.apache.commons</pattern> | ||||
|                                     <shadedPattern>fr.pandacube.lib.db.shaded.commons</shadedPattern> | ||||
|                                     <pattern>org.apache.commons.dbcp2</pattern> | ||||
|                                     <shadedPattern>fr.pandacube.lib.db.shaded.commons.dbcp2</shadedPattern> | ||||
|                                 </relocation> | ||||
|                                 <relocation> | ||||
|                                     <pattern>org.apache.commons</pattern> | ||||
|                                     <shadedPattern>fr.pandacube.lib.db.shaded.commons</shadedPattern> | ||||
|                                     <pattern>org.apache.commons.pool2</pattern> | ||||
|                                     <shadedPattern>fr.pandacube.lib.db.shaded.commons.pool2</shadedPattern> | ||||
|                                 </relocation> | ||||
|                             </relocations> | ||||
|                             <transformers> | ||||
|   | ||||
| @@ -15,7 +15,7 @@ import java.util.Objects; | ||||
| import java.util.function.Consumer; | ||||
|  | ||||
| import fr.pandacube.lib.reflect.Reflect; | ||||
| import fr.pandacube.lib.util.Log; | ||||
| import fr.pandacube.lib.util.log.Log; | ||||
|  | ||||
| /** | ||||
|  * Static class to handle most of the database operations. | ||||
| @@ -52,10 +52,10 @@ public final class DB { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Initialialize the table represented by the provided class. | ||||
|      * Initialize the table represented by the provided class. | ||||
|      * @param elemClass the class representing a table. | ||||
|      * @param <E> the type representing the table. | ||||
|      * @throws DBInitTableException if the table failed to initialized. | ||||
|      * @throws DBInitTableException if the table failed to initialize. | ||||
|      */ | ||||
|     public static synchronized <E extends SQLElement<E>> void initTable(Class<E> elemClass) throws DBInitTableException { | ||||
|         if (connection == null) { | ||||
| @@ -111,7 +111,7 @@ public final class DB { | ||||
|      * @param elemClass the class representing a table. | ||||
|      * @return a table name. | ||||
|      * @param <E> the type representing the table. | ||||
|      * @throws DBInitTableException if the provided table had to be initialized and it failed to do so. | ||||
|      * @throws DBInitTableException if the provided table had to be initialized and failed to do so. | ||||
|      */ | ||||
|     public static <E extends SQLElement<E>> String getTableName(Class<E> elemClass) throws DBInitTableException { | ||||
|         initTable(elemClass); | ||||
| @@ -130,7 +130,7 @@ public final class DB { | ||||
|      * @param elemClass the class representing a table. | ||||
|      * @return the {@code id} field of the provided table. | ||||
|      * @param <E> the type representing the table. | ||||
|      * @throws DBInitTableException if the provided table had to be initialized and it failed to do so. | ||||
|      * @throws DBInitTableException if the provided table had to be initialized and failed to do so. | ||||
|      */ | ||||
|     @SuppressWarnings("unchecked") | ||||
|     public static <E extends SQLElement<E>> SQLField<E, Integer> getSQLIdField(Class<E> elemClass) throws DBInitTableException { | ||||
| @@ -226,8 +226,8 @@ public final class DB { | ||||
|      * @throws DBException if an error occurs when interacting with the database. | ||||
|      */ | ||||
|     public static <E extends SQLElement<E>> E getFirst(Class<E> elemClass, SQLWhere<E> where, SQLOrderBy<E> orderBy, Integer offset) throws DBException { | ||||
|         SQLElementList<E> elts = getAll(elemClass, where, orderBy, 1, offset); | ||||
|         return (elts.size() == 0) ? null : elts.get(0); | ||||
|         SQLElementList<E> elements = getAll(elemClass, where, orderBy, 1, offset); | ||||
|         return (elements.size() == 0) ? null : elements.get(0); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -294,15 +294,15 @@ public final class DB { | ||||
|      * @throws DBException if an error occurs when interacting with the database. | ||||
|      */ | ||||
|     public static <E extends SQLElement<E>> SQLElementList<E> getAll(Class<E> elemClass, SQLWhere<E> where, SQLOrderBy<E> orderBy, Integer limit, Integer offset) throws DBException { | ||||
|         SQLElementList<E> elmts = new SQLElementList<>(); | ||||
|         forEach(elemClass, where, orderBy, limit, offset, elmts::add); | ||||
|         return elmts; | ||||
|         SQLElementList<E> elements = new SQLElementList<>(); | ||||
|         forEach(elemClass, where, orderBy, limit, offset, elements::add); | ||||
|         return elements; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Iterate through all the entries from the provided table. | ||||
|      * @param elemClass the class representing a table. | ||||
|      * @param action the action to perform on each entries. | ||||
|      * @param action the action to perform on each entry. | ||||
|      * @param <E> the type representing the table. | ||||
|      * @throws DBException if an error occurs when interacting with the database. | ||||
|      */ | ||||
| @@ -314,7 +314,7 @@ public final class DB { | ||||
|      * Iterate through the entries from the provided table, using the provided {@code WHERE} clause. | ||||
|      * @param elemClass the class representing a table. | ||||
|      * @param where the {@code WHERE} clause of the query. | ||||
|      * @param action the action to perform on each entries. | ||||
|      * @param action the action to perform on each entry. | ||||
|      * @param <E> the type representing the table. | ||||
|      * @throws DBException if an error occurs when interacting with the database. | ||||
|      */ | ||||
| @@ -328,7 +328,7 @@ public final class DB { | ||||
|      * @param elemClass the class representing a table. | ||||
|      * @param where the {@code WHERE} clause of the query. | ||||
|      * @param orderBy the {@code ORDER BY} clause of the query. | ||||
|      * @param action the action to perform on each entries. | ||||
|      * @param action the action to perform on each entry. | ||||
|      * @param <E> the type representing the table. | ||||
|      * @throws DBException if an error occurs when interacting with the database. | ||||
|      */ | ||||
| @@ -343,7 +343,7 @@ public final class DB { | ||||
|      * @param where the {@code WHERE} clause of the query. | ||||
|      * @param orderBy the {@code ORDER BY} clause of the query. | ||||
|      * @param limit the {@code LIMIT} clause of the query. | ||||
|      * @param action the action to perform on each entries. | ||||
|      * @param action the action to perform on each entry. | ||||
|      * @param <E> the type representing the table. | ||||
|      * @throws DBException if an error occurs when interacting with the database. | ||||
|      */ | ||||
| @@ -359,7 +359,7 @@ public final class DB { | ||||
|      * @param orderBy the {@code ORDER BY} clause of the query. | ||||
|      * @param limit the {@code LIMIT} clause of the query. | ||||
|      * @param offset the {@code OFFSET} clause of the query. | ||||
|      * @param action the action to perform on each entries. | ||||
|      * @param action the action to perform on each entry. | ||||
|      * @param <E> the type representing the table. | ||||
|      * @throws DBException if an error occurs when interacting with the database. | ||||
|      */ | ||||
| @@ -577,7 +577,7 @@ public final class DB { | ||||
|     @SuppressWarnings("unchecked") | ||||
|     private static <E extends SQLElement<E>> E getElementInstance(ResultSet set, Class<E> elemClass) throws DBException { | ||||
|         try { | ||||
|             E instance = Reflect.ofClass(elemClass).constructor(int.class).instanciate(set.getInt("id")); | ||||
|             E instance = Reflect.ofClass(elemClass).constructor(int.class).instantiate(set.getInt("id")); | ||||
|  | ||||
|             int fieldCount = set.getMetaData().getColumnCount(); | ||||
|  | ||||
| @@ -623,7 +623,7 @@ public final class DB { | ||||
|  | ||||
|             return instance; | ||||
|         } catch (ReflectiveOperationException | IllegalArgumentException | SecurityException | SQLException e) { | ||||
|             throw new DBException("Can't instanciate " + elemClass.getName(), e); | ||||
|             throw new DBException("Can't instantiate " + elemClass.getName(), e); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -23,7 +23,8 @@ public class DBConnection { | ||||
|     public DBConnection(String host, int port, String dbname, String login, String password) { | ||||
|         this("jdbc:mysql://" + host + ":" + port + "/" + dbname | ||||
|                         + "?useUnicode=true" | ||||
|                         + "&useSSL=false" | ||||
|                         + "&sslMode=DISABLED" | ||||
|                         + "&allowPublicKeyRetrieval=true" | ||||
|                         + "&characterEncoding=utf8" | ||||
|                         + "&characterSetResults=utf8" | ||||
|                         + "&character_set_server=utf8mb4" | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| package fr.pandacube.lib.db; | ||||
|  | ||||
| /** | ||||
|  * Exception thrown when something bad happends when using the {@link DB} API. | ||||
|  * Exception thrown when something bad happens when using the {@link DB} API. | ||||
|  */ | ||||
| public class DBException extends Exception { | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| package fr.pandacube.lib.db; | ||||
|  | ||||
| /** | ||||
|  * Exception thrown when something bad happends when initializing a new table using {@link DB#initTable(Class)}. | ||||
|  * Exception thrown when something bad happens when initializing a new table using {@link DB#initTable(Class)}. | ||||
|  */ | ||||
| public class DBInitTableException extends DBException { | ||||
|  | ||||
|   | ||||
| @@ -26,7 +26,7 @@ import java.util.UUID; | ||||
| import java.util.stream.Collectors; | ||||
|  | ||||
| import fr.pandacube.lib.util.EnumUtil; | ||||
| import fr.pandacube.lib.util.Log; | ||||
| import fr.pandacube.lib.util.log.Log; | ||||
|  | ||||
| /** | ||||
|  * Represents an entry in a SQL table. Each subclass is for a specific table. | ||||
| @@ -119,7 +119,7 @@ public abstract class SQLElement<E extends SQLElement<E>> { | ||||
|  | ||||
|     /** | ||||
|      * Gets the name of the table in the database, without the prefix defined by {@link DB#init(DBConnection, String)}. | ||||
|      * @return The unprefixed name of the table in the database. | ||||
|      * @return The non-prefixed name of the table in the database. | ||||
|      */ | ||||
|     protected abstract String tableName(); | ||||
|  | ||||
| @@ -133,7 +133,7 @@ public abstract class SQLElement<E extends SQLElement<E>> { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Fills the values of this entry that are known to be nullable or have a default value. | ||||
|      * Fills the entries values that are known to be nullable or have a default value. | ||||
|      */ | ||||
|     @SuppressWarnings("unchecked") | ||||
|     private void initDefaultValues() { | ||||
| @@ -193,7 +193,7 @@ public abstract class SQLElement<E extends SQLElement<E>> { | ||||
|     /** | ||||
|      * Sets a value in this entry. | ||||
|      * <p> | ||||
|      * This is not good practice to set the {@code id} field of any entry, because 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 | ||||
|      * database. | ||||
|      * @param field the field to set. | ||||
| @@ -241,17 +241,24 @@ public abstract class SQLElement<E extends SQLElement<E>> { | ||||
|      * Gets the value of the provided field in this entry. | ||||
|      * @param field the field to get the value from. | ||||
|      * @return the value of the provided field in this entry. | ||||
|      * @throws IllegalArgumentException if the provided field is null or not from the table represented by this class. | ||||
|      * @throws IllegalStateException if the field is not nullable and there is no value set | ||||
|      * @param <T> the Java type of the field. | ||||
|      */ | ||||
|     public <T> T get(SQLField<E, T> field) { | ||||
|         if (field == null) throw new IllegalArgumentException("field can't be null"); | ||||
|         if (field == null) | ||||
|             throw new IllegalArgumentException("field can't be null"); | ||||
|         if (!fields.containsKey(field.getName()) || !fields.get(field.getName()).equals(field)) | ||||
|             throw new IllegalArgumentException("The provided field " + field + " is not from this table " + getClass().getName()); | ||||
|         if (values.containsKey(field)) { | ||||
|             @SuppressWarnings("unchecked") | ||||
|             T val = (T) values.get(field); | ||||
|             return val; | ||||
|         } | ||||
|         throw new IllegalArgumentException("The field '" + field.getName() + "' in this instance of " + getClass().getName() | ||||
|                 + " does not exist or is not set"); | ||||
|         if (field.nullable) | ||||
|             return null; | ||||
|         throw new IllegalStateException("The non-nullable field '" + field.getName() + "' in this instance of " + getClass().getName() | ||||
|                 + " is not set"); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -314,7 +321,7 @@ public abstract class SQLElement<E extends SQLElement<E>> { | ||||
|  | ||||
|     /** | ||||
|      * Saves this entry into the database, either by updating the already existing entry in it, or by creating a new | ||||
|      * entry if it doesn’t exist yet. | ||||
|      * entry if it doesn't exist yet. | ||||
|      * @return this. | ||||
|      * @throws DBException if an error occurs when interacting with the database. | ||||
|      */ | ||||
| @@ -354,7 +361,7 @@ public abstract class SQLElement<E extends SQLElement<E>> { | ||||
|                     first = false; | ||||
|                     concatValues.append(" ? "); | ||||
|                     concatFields.append("`").append(entry.getKey().getName()).append("`"); | ||||
|                     addValueToSQLObjectList(psValues, entry.getKey(), entry.getValue()); | ||||
|                     psValues.add(entry.getKey().fromJavaTypeToJDBCType(entry.getValue())); | ||||
|                 } | ||||
|  | ||||
|                 try (Connection c = db.getConnection(); | ||||
| @@ -382,20 +389,6 @@ public abstract class SQLElement<E extends SQLElement<E>> { | ||||
|         return (E) this; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     @SuppressWarnings({ "rawtypes", "unchecked" }) | ||||
|     /* package */ static <E extends SQLElement<E>> void addValueToSQLObjectList(List<Object> list, SQLField<E, ?> field, Object jValue) throws DBException { | ||||
|         if (jValue != null && field.type instanceof SQLCustomType) { | ||||
|             try { | ||||
|                 jValue = ((SQLCustomType)field.type).javaToDbConv.apply(jValue); | ||||
|             } catch (Exception e) { | ||||
|                 throw new DBException("Error while converting value of field '"+field.getName()+"' with SQLCustomType from "+field.type.getJavaType() | ||||
|                         +"(java source) to "+((SQLCustomType<?, ?>)field.type).intermediateJavaType+"(jdbc destination). The original value is '"+jValue+"'", e); | ||||
|             } | ||||
|         } | ||||
|         list.add(jValue); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Tells if this entry is currently stored in DB or not. | ||||
|      * @return true if this entry is currently stored in DB, or false otherwise. | ||||
| @@ -474,14 +467,14 @@ public abstract class SQLElement<E extends SQLElement<E>> { | ||||
|      * Creates a new SQL field. | ||||
|      * @param type the type of the field. | ||||
|      * @param nullable true if nullable, false if {@code NOT NULL}. | ||||
|      * @param autoIncr if {@code AUTO_INCREMENT}. | ||||
|      * @param autoIncrement if {@code AUTO_INCREMENT}. | ||||
|      * @param deflt a default value for this field. A null value indicate that this has no default value. | ||||
|      * @return the new SQL field. | ||||
|      * @param <E> the table type. | ||||
|      * @param <T> the Java type of this field. | ||||
|      */ | ||||
|     protected static <E extends SQLElement<E>, T> SQLField<E, T> field(SQLType<T> type, boolean nullable, boolean autoIncr, T deflt) { | ||||
|         return new SQLField<>(type, nullable, autoIncr, deflt); | ||||
|     protected static <E extends SQLElement<E>, T> SQLField<E, T> field(SQLType<T> type, boolean nullable, boolean autoIncrement, T deflt) { | ||||
|         return new SQLField<>(type, nullable, autoIncrement, deflt); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -500,13 +493,13 @@ public abstract class SQLElement<E extends SQLElement<E>> { | ||||
|      * Creates a new SQL field. | ||||
|      * @param type the type of the field. | ||||
|      * @param nullable true if nullable, false if {@code NOT NULL}. | ||||
|      * @param autoIncr if {@code AUTO_INCREMENT}. | ||||
|      * @param autoIncrement if {@code AUTO_INCREMENT}. | ||||
|      * @return the new SQL field. | ||||
|      * @param <E> the table type. | ||||
|      * @param <T> the Java type of this field. | ||||
|      */ | ||||
|     protected static <E extends SQLElement<E>, T> SQLField<E, T> field(SQLType<T> type, boolean nullable, boolean autoIncr) { | ||||
|         return new SQLField<>(type, nullable, autoIncr); | ||||
|     protected static <E extends SQLElement<E>, T> SQLField<E, T> field(SQLType<T> type, boolean nullable, boolean autoIncrement) { | ||||
|         return new SQLField<>(type, nullable, autoIncrement); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -16,6 +16,11 @@ import java.util.stream.Collectors; | ||||
|  */ | ||||
| public class SQLElementList<E extends SQLElement<E>> extends ArrayList<E> { | ||||
|  | ||||
|     /** | ||||
|      * Creates an empty list of sql elements. | ||||
|      */ | ||||
|     public SQLElementList() {} | ||||
|  | ||||
|     /** | ||||
|      * Stores all the values modified by {@link #setCommon(SQLField, Object)}. | ||||
|      */ | ||||
| @@ -46,7 +51,7 @@ public class SQLElementList<E extends SQLElement<E>> extends ArrayList<E> { | ||||
|             E emptyElement = elemClass.getConstructor().newInstance(); | ||||
|             emptyElement.set(field, value, false); | ||||
|         } catch (Exception e) { | ||||
|             throw new IllegalArgumentException("Illegal field or value or can't instanciante an empty instance of " | ||||
|             throw new IllegalArgumentException("Illegal field or value or can't instantiate an empty instance of " | ||||
|                     + elemClass.getName() + ". (the instance is only created to test validity of field and value)", e); | ||||
|         } | ||||
|  | ||||
| @@ -83,7 +88,7 @@ public class SQLElementList<E extends SQLElement<E>> extends ArrayList<E> { | ||||
|  | ||||
|     @SuppressWarnings("unchecked") | ||||
|     private void applyNewValuesToElements(List<E> storedEl) { | ||||
|         // applique les valeurs dans chaques objets de la liste | ||||
|         // applique les valeurs dans chaque objet de la liste | ||||
|         for (E el : storedEl) { | ||||
|             for (@SuppressWarnings("rawtypes") SQLField entry : modifiedValues.keySet()) { | ||||
|                 if (!el.isModified(entry)) { | ||||
| @@ -100,7 +105,7 @@ public class SQLElementList<E extends SQLElement<E>> extends ArrayList<E> { | ||||
|     /** | ||||
|      * Removes all the entries of this list from the database. | ||||
|      * This method has the same effect as calling the {@link SQLElement#delete()} method individually on each element, | ||||
|      * but with only one SQL query to delete all of the entries. | ||||
|      * but with only one SQL query to delete all the entries. | ||||
|      * <p> | ||||
|      * If you intend to remove the entries from the database just after fetching them, call directly the | ||||
|      * {@link DB#delete(Class, SQLWhere)} method instead. | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| package fr.pandacube.lib.db; | ||||
|  | ||||
| import fr.pandacube.lib.util.Log; | ||||
| import fr.pandacube.lib.util.log.Log; | ||||
|  | ||||
| /** | ||||
|  * A foreign key field in a SQL table. | ||||
| @@ -28,7 +28,7 @@ public class SQLFKField<F extends SQLElement<F>, T, P extends SQLElement<P>> ext | ||||
|             SQLField<F, Integer> f = DB.getSQLIdField(fkEl); | ||||
|             return new SQLFKField<>(f.type, nul, deflt, fkEl, f); | ||||
|         } catch (DBInitTableException e) { | ||||
|             Log.severe("Can't create Foreign key Field targetting id field of '"+fkEl+"'", e); | ||||
|             Log.severe("Can't create Foreign key Field targeting id field of '"+fkEl+"'", e); | ||||
|             return null; | ||||
|         } | ||||
|     } | ||||
| @@ -51,7 +51,7 @@ public class SQLFKField<F extends SQLElement<F>, T, P extends SQLElement<P>> ext | ||||
|         } | ||||
|  | ||||
|         if (fkF.getSQLElementType() == null) | ||||
|             throw new RuntimeException("Can't initialize foreign key. The primary key in the table " + fkEl.getName() + " is not properly initialized and can't be targetted by a forein key"); | ||||
|             throw new RuntimeException("Can't initialize foreign key. The primary key in the table " + fkEl.getName() + " is not properly initialized and can't be targeted by a foreign key"); | ||||
|         sqlPrimaryKeyField = fkF; | ||||
|         sqlForeignKeyElemClass = fkEl; | ||||
|     } | ||||
|   | ||||
| @@ -24,10 +24,10 @@ public class SQLField<E extends SQLElement<E>, T> { | ||||
|     /* package */ final boolean autoIncrement; | ||||
|     /* package */ final T defaultValue; | ||||
|  | ||||
|     /* package */ SQLField(SQLType<T> type, boolean nullable, boolean autoIncr, T deflt) { | ||||
|     /* package */ SQLField(SQLType<T> type, boolean nullable, boolean autoIncrement, T deflt) { | ||||
|         this.type = type; | ||||
|         this.nullable = nullable; | ||||
|         autoIncrement = autoIncr; | ||||
|         this.autoIncrement = autoIncrement; | ||||
|         defaultValue = deflt; | ||||
|     } | ||||
|  | ||||
| @@ -35,8 +35,8 @@ public class SQLField<E extends SQLElement<E>, T> { | ||||
|         this(type, nullable, false, null); | ||||
|     } | ||||
|  | ||||
|     /* package */ SQLField(SQLType<T> type, boolean nullable, boolean autoIncr) { | ||||
|         this(type, nullable, autoIncr, null); | ||||
|     /* package */ SQLField(SQLType<T> type, boolean nullable, boolean autoIncrement) { | ||||
|         this(type, nullable, autoIncrement, null); | ||||
|     } | ||||
|  | ||||
|     /* package */ SQLField(SQLType<T> type, boolean nullable, T deflt) { | ||||
| @@ -133,7 +133,7 @@ public class SQLField<E extends SQLElement<E>, T> { | ||||
|     /** | ||||
|      * Create a SQL {@code WHERE} expression comparing this field with the provided value using the {@code =} operator. | ||||
|      * @param r the value to compare with. | ||||
|      * @return a SQL {@code WHERE} expression. | ||||
|      * @return a new SQL {@code WHERE} expression. | ||||
|      */ | ||||
|     public fr.pandacube.lib.db.SQLWhere<E> eq(T r) { | ||||
|         return comp(SQLComparator.EQ, r); | ||||
| @@ -142,7 +142,7 @@ public class SQLField<E extends SQLElement<E>, T> { | ||||
|     /** | ||||
|      * Create a SQL {@code WHERE} expression comparing this field with the provided value using the {@code >=} operator. | ||||
|      * @param r the value to compare with. | ||||
|      * @return a SQL {@code WHERE} expression. | ||||
|      * @return a new SQL {@code WHERE} expression. | ||||
|      */ | ||||
|     public fr.pandacube.lib.db.SQLWhere<E> geq(T r) { | ||||
|         return comp(SQLComparator.GEQ, r); | ||||
| @@ -151,7 +151,7 @@ public class SQLField<E extends SQLElement<E>, T> { | ||||
|     /** | ||||
|      * Create a SQL {@code WHERE} expression comparing this field with the provided value using the {@code >} operator. | ||||
|      * @param r the value to compare with. | ||||
|      * @return a SQL {@code WHERE} expression. | ||||
|      * @return a new SQL {@code WHERE} expression. | ||||
|      */ | ||||
|     public fr.pandacube.lib.db.SQLWhere<E> gt(T r) { | ||||
|         return comp(SQLComparator.GT, r); | ||||
| @@ -160,7 +160,7 @@ public class SQLField<E extends SQLElement<E>, T> { | ||||
|     /** | ||||
|      * Create a SQL {@code WHERE} expression comparing this field with the provided value using the {@code <=} operator. | ||||
|      * @param r the value to compare with. | ||||
|      * @return a SQL {@code WHERE} expression. | ||||
|      * @return a new SQL {@code WHERE} expression. | ||||
|      */ | ||||
|     public fr.pandacube.lib.db.SQLWhere<E> leq(T r) { | ||||
|         return comp(SQLComparator.LEQ, r); | ||||
| @@ -169,7 +169,7 @@ public class SQLField<E extends SQLElement<E>, T> { | ||||
|     /** | ||||
|      * Create a SQL {@code WHERE} expression comparing this field with the provided value using the {@code <} operator. | ||||
|      * @param r the value to compare with. | ||||
|      * @return a SQL {@code WHERE} expression. | ||||
|      * @return a new SQL {@code WHERE} expression. | ||||
|      */ | ||||
|     public fr.pandacube.lib.db.SQLWhere<E> lt(T r) { | ||||
|         return comp(SQLComparator.LT, r); | ||||
| @@ -178,7 +178,7 @@ public class SQLField<E extends SQLElement<E>, T> { | ||||
|     /** | ||||
|      * Create a SQL {@code WHERE} expression comparing this field with the provided value using the {@code !=} operator. | ||||
|      * @param r the value to compare with. | ||||
|      * @return a SQL {@code WHERE} expression. | ||||
|      * @return a new SQL {@code WHERE} expression. | ||||
|      */ | ||||
|     public fr.pandacube.lib.db.SQLWhere<E> neq(T r) { | ||||
|         return comp(SQLComparator.NEQ, r); | ||||
| @@ -194,7 +194,7 @@ public class SQLField<E extends SQLElement<E>, T> { | ||||
|      * Create a SQL {@code WHERE} expression comparing this field with the provided value using the {@code LIKE} | ||||
|      * keyword. | ||||
|      * @param like the value to compare with. | ||||
|      * @return a SQL {@code WHERE} expression. | ||||
|      * @return a new SQL {@code WHERE} expression. | ||||
|      */ | ||||
|     public fr.pandacube.lib.db.SQLWhere<E> like(String like) { | ||||
|         return new SQLWhereLike<>(this, like); | ||||
| @@ -206,7 +206,7 @@ public class SQLField<E extends SQLElement<E>, T> { | ||||
|      * Create a SQL {@code WHERE} expression testing the presence of this field in the provided collection of value | ||||
|      * using the {@code IN} keyword. | ||||
|      * @param v the value to compare with. | ||||
|      * @return a SQL {@code WHERE} expression. | ||||
|      * @return a new SQL {@code WHERE} expression. | ||||
|      */ | ||||
|     public fr.pandacube.lib.db.SQLWhere<E> in(Collection<T> v) { | ||||
|         return new SQLWhereIn<>(this, v); | ||||
| @@ -216,7 +216,7 @@ public class SQLField<E extends SQLElement<E>, T> { | ||||
|  | ||||
|     /** | ||||
|      * Create a SQL {@code WHERE} expression testing the nullity of this field using the {@code IS NULL} keyword. | ||||
|      * @return a SQL {@code WHERE} expression. | ||||
|      * @return a new SQL {@code WHERE} expression. | ||||
|      */ | ||||
|     public fr.pandacube.lib.db.SQLWhere<E> isNull() { | ||||
|         return new SQLWhereNull<>(this, true); | ||||
| @@ -225,10 +225,35 @@ public class SQLField<E extends SQLElement<E>, T> { | ||||
|     /** | ||||
|      * Create a SQL {@code WHERE} expression testing the non-nullity of this field using the {@code IS NOT NULL} | ||||
|      * keyword. | ||||
|      * @return a SQL {@code WHERE} expression. | ||||
|      * @return a new SQL {@code WHERE} expression. | ||||
|      */ | ||||
|     public fr.pandacube.lib.db.SQLWhere<E> isNotNull() { | ||||
|         return new SQLWhereNull<>(this, false); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     @SuppressWarnings({"rawtypes", "unchecked"}) | ||||
|     /* package */ Object fromJavaTypeToJDBCType(Object value) throws DBException { | ||||
|         Object ret = value; | ||||
|         if (value != null && type instanceof SQLCustomType customType) { | ||||
|             try { | ||||
|                 ret = customType.javaToDbConv.apply(value); | ||||
|             } catch (Exception e) { | ||||
|                 throw new DBException("Error while converting value of field '" + name + "' with SQLCustomType from " + type.getJavaType() | ||||
|                         + "(java source) to " + customType.intermediateJavaType + "(jdbc destination). The original value is '" + value + "'", e); | ||||
|             } | ||||
|         } | ||||
|         return ret; | ||||
|     } | ||||
|  | ||||
|     /* package */ Collection<Object> fromListJavaTypeToJDBCType(Collection<?> values) throws DBException { | ||||
|         if (values == null) | ||||
|             return null; | ||||
|         List<Object> ret = new ArrayList<>(values.size()); | ||||
|         for (Object value : values) { | ||||
|             ret.add(fromJavaTypeToJDBCType(value)); | ||||
|         } | ||||
|         return ret; | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -6,7 +6,7 @@ import java.util.HashMap; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
|  | ||||
| import fr.pandacube.lib.util.Log; | ||||
| import fr.pandacube.lib.util.log.Log; | ||||
|  | ||||
| /** | ||||
|  * Builder for a SQL {@code UPDATE} query. | ||||
| @@ -74,7 +74,7 @@ public class SQLUpdateBuilder<E extends SQLElement<E>> { | ||||
|             if (!first) | ||||
|                 sql.append(", "); | ||||
|             sql.append("`").append(entry.getKey().getName()).append("` = ? "); | ||||
|             SQLElement.addValueToSQLObjectList(params, entry.getKey(), entry.getValue()); | ||||
|             params.add(entry.getKey().fromJavaTypeToJDBCType(entry.getValue())); | ||||
|             first = false; | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -3,15 +3,21 @@ package fr.pandacube.lib.db; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collection; | ||||
| import java.util.List; | ||||
| import java.util.Objects; | ||||
|  | ||||
| import fr.pandacube.lib.util.Log; | ||||
| import fr.pandacube.lib.util.log.Log; | ||||
|  | ||||
| /** | ||||
|  * A SQL {@code WHERE} expression. | ||||
|  * SQL {@code WHERE} expression. | ||||
|  * @param <E> the table type. | ||||
|  */ | ||||
| public abstract class SQLWhere<E extends SQLElement<E>> { | ||||
|  | ||||
|     /** | ||||
|      * Creates a SQL WHERE expression. | ||||
|      */ | ||||
|     protected SQLWhere() {} | ||||
|  | ||||
|     /* package */ abstract ParameterizedSQLString toSQL() throws DBException; | ||||
|  | ||||
|     @Override | ||||
| @@ -29,7 +35,7 @@ public abstract class SQLWhere<E extends SQLElement<E>> { | ||||
|      * Create a SQL {@code WHERE} expression that is true when this expression {@code AND} the provided expression is | ||||
|      * true. | ||||
|      * @param other the other expression. | ||||
|      * @return a SQL {@code WHERE} expression. | ||||
|      * @return a new SQL {@code WHERE} expression. | ||||
|      */ | ||||
|     public SQLWhere<E> and(SQLWhere<E> other) { | ||||
|         return SQLWhere.<E>and().and(this).and(other); | ||||
| @@ -39,7 +45,7 @@ public abstract class SQLWhere<E extends SQLElement<E>> { | ||||
|      * Create a SQL {@code WHERE} expression that is true when this expression {@code OR} the provided expression is | ||||
|      * true. | ||||
|      * @param other the other expression. | ||||
|      * @return a SQL {@code WHERE} expression. | ||||
|      * @return a new SQL {@code WHERE} expression. | ||||
|      */ | ||||
|     public SQLWhere<E> or(SQLWhere<E> other) { | ||||
|         return SQLWhere.<E>or().or(this).or(other); | ||||
| @@ -48,7 +54,7 @@ public abstract class SQLWhere<E extends SQLElement<E>> { | ||||
|  | ||||
|     /** | ||||
|      * Create a SQL {@code WHERE} expression builder joining multiple expressions with the {@code AND} operator. | ||||
|      * @return a SQL {@code WHERE} expression. | ||||
|      * @return a new SQL {@code WHERE} expression. | ||||
|      * @param <E> the table type. | ||||
|      */ | ||||
|     public static <E extends SQLElement<E>> SQLWhereAndBuilder<E> and() { | ||||
| @@ -57,13 +63,63 @@ public abstract class SQLWhere<E extends SQLElement<E>> { | ||||
|  | ||||
|     /** | ||||
|      * Create a SQL {@code WHERE} expression builder joining multiple expressions with the {@code OR} operator. | ||||
|      * @return a SQL {@code WHERE} expression. | ||||
|      * @return a new SQL {@code WHERE} expression. | ||||
|      * @param <E> the table type. | ||||
|      */ | ||||
|     public static <E extends SQLElement<E>> SQLWhereOrBuilder<E> or() { | ||||
|         return new SQLWhereOrBuilder<>(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Create a custom SQL {@code WHERE} expression. | ||||
|      * @param whereExpr the raw SQL {@code WHERE} expression. | ||||
|      * @return a new SQL {@code WHERE} expression. | ||||
|      * @param <E> the table type. | ||||
|      */ | ||||
|     public static <E extends SQLElement<E>> SQLWhere<E> expression(String whereExpr) { | ||||
|         return expression(whereExpr, List.of()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Create a custom SQL {@code WHERE} expression. | ||||
|      * @param whereExpr the raw SQL {@code WHERE} expression. | ||||
|      * @param params the parameters of the provided expression. | ||||
|      * @return a new SQL {@code WHERE} expression. | ||||
|      * @param <E> the table type. | ||||
|      */ | ||||
|     public static <E extends SQLElement<E>> SQLWhere<E> expression(String whereExpr, List<Object> params) { | ||||
|         return new SQLWhereCustomExpression<>(whereExpr, params); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Create a SQL {@code WHERE ... IN ...} expression with a custom left operand. | ||||
|      * @param leftExpr the raw SQL left operand. | ||||
|      * @param valuesIn the values on the right of the {@code IN} operator. | ||||
|      * @return a new SQL {@code WHERE} expression. | ||||
|      * @param <E> the table type. | ||||
|      */ | ||||
|     public static <E extends SQLElement<E>> SQLWhere<E> expressionIn(String leftExpr, Collection<?> valuesIn) { | ||||
|         return expressionIn(leftExpr, List.of(), valuesIn); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Create a SQL {@code WHERE ... IN ...} expression with a custom left operand. | ||||
|      * @param leftExpr the raw SQL left operand. | ||||
|      * @param leftParams the parameters of the left operand. | ||||
|      * @param valuesIn the values on the right of the {@code IN} operator. | ||||
|      * @return a new SQL {@code WHERE} expression. | ||||
|      * @param <E> the table type. | ||||
|      */ | ||||
|     public static <E extends SQLElement<E>> SQLWhere<E> expressionIn(String leftExpr, List<Object> leftParams, Collection<?> valuesIn) { | ||||
|         return new SQLWhereInCustom<>(leftExpr, leftParams, valuesIn); | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * A SQL {@code WHERE} expression builder joining multiple expressions with the {@code AND} or {@code OR} operator. | ||||
| @@ -207,9 +263,8 @@ public abstract class SQLWhere<E extends SQLElement<E>> { | ||||
|  | ||||
|         @Override | ||||
|         /* package */ ParameterizedSQLString toSQL() throws DBException { | ||||
|             List<Object> params = new ArrayList<>(); | ||||
|             SQLElement.addValueToSQLObjectList(params, left, right); | ||||
|             return new ParameterizedSQLString("`" + left.getName() + "` " + comp.sql + " ? ", params); | ||||
|             return new ParameterizedSQLString("`" + left.getName() + "` " + comp.sql + " ? ", | ||||
|                     List.of(left.fromJavaTypeToJDBCType(right))); | ||||
|         } | ||||
|  | ||||
|         /* package */ enum SQLComparator { | ||||
| @@ -241,33 +296,39 @@ public abstract class SQLWhere<E extends SQLElement<E>> { | ||||
|  | ||||
|  | ||||
|  | ||||
|     /* package */ static class SQLWhereIn<E extends SQLElement<E>> extends SQLWhere<E> { | ||||
|     /* package */ static class SQLWhereInCustom<E extends SQLElement<E>> extends SQLWhere<E> { | ||||
|  | ||||
|         private final SQLField<E, ?> field; | ||||
|         private final Collection<?> values; | ||||
|         private final String leftExpression; | ||||
|         private final List<Object> leftExpressionParameters; | ||||
|         protected Collection<?> collectionIn; | ||||
|  | ||||
|         /* package */ <T> SQLWhereIn(SQLField<E, T> f, Collection<T> v) { | ||||
|             if (f == null || v == null) | ||||
|                 throw new IllegalArgumentException("All arguments for SQLWhereIn constructor can't be null"); | ||||
|             field = f; | ||||
|             values = v; | ||||
|         /* package */ <T> SQLWhereInCustom(String leftExpr, List<Object> leftExprParams, Collection<T> collectionIn) { | ||||
|             if (leftExpr == null) | ||||
|                 throw new IllegalArgumentException("leftExpr can't be null"); | ||||
|             if (leftExprParams == null) | ||||
|                 leftExprParams = List.of(); | ||||
|             if (collectionIn == null) | ||||
|                 collectionIn = List.of(); | ||||
|             leftExpression = leftExpr; | ||||
|             leftExpressionParameters = leftExprParams; | ||||
|             this.collectionIn = collectionIn; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         /* package */ ParameterizedSQLString toSQL() throws DBException { | ||||
|             List<Object> params = new ArrayList<>(); | ||||
|  | ||||
|             if (values.isEmpty()) | ||||
|             if (collectionIn.isEmpty()) | ||||
|                 return new ParameterizedSQLString(" 1=0 ", params); | ||||
|  | ||||
|             for (Object v : values) | ||||
|                 SQLElement.addValueToSQLObjectList(params, field, v); | ||||
|             params.addAll(leftExpressionParameters); | ||||
|             params.addAll(collectionIn); | ||||
|  | ||||
|             char[] questions = new char[values.size() == 0 ? 0 : (values.size() * 2 - 1)]; | ||||
|             char[] questions = new char[collectionIn.size() * 2 - 1]; | ||||
|             for (int i = 0; i < questions.length; i++) | ||||
|                 questions[i] = i % 2 == 0 ? '?' : ','; | ||||
|  | ||||
|             return new ParameterizedSQLString("`" + field.getName() + "` IN (" + new String(questions) + ") ", params); | ||||
|             return new ParameterizedSQLString("(" + leftExpression + ") IN (" + new String(questions) + ") ", params); | ||||
|         } | ||||
|  | ||||
|     } | ||||
| @@ -277,6 +338,32 @@ public abstract class SQLWhere<E extends SQLElement<E>> { | ||||
|  | ||||
|  | ||||
|  | ||||
|     /* package */ static class SQLWhereIn<E extends SQLElement<E>> extends SQLWhereInCustom<E> { | ||||
|  | ||||
|         private final SQLField<E, ?> field; | ||||
|         private boolean collectionFiltered = false; | ||||
|  | ||||
|         /* package */ <T> SQLWhereIn(SQLField<E, T> f, Collection<T> v) { | ||||
|             super("`" + Objects.requireNonNull(f).getName() + "`", List.of(), v); | ||||
|             field = f; | ||||
|         } | ||||
|  | ||||
|  | ||||
|         @Override | ||||
|         ParameterizedSQLString toSQL() throws DBException { | ||||
|             if (!collectionFiltered) { | ||||
|                 collectionIn = field.fromListJavaTypeToJDBCType(collectionIn); | ||||
|                 collectionFiltered = true; | ||||
|             } | ||||
|             return super.toSQL(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|     /* package */ static class SQLWhereLike<E extends SQLElement<E>> extends SQLWhere<E> { | ||||
| @@ -334,6 +421,33 @@ public abstract class SQLWhere<E extends SQLElement<E>> { | ||||
|  | ||||
|  | ||||
|  | ||||
|     /* package */ static class SQLWhereCustomExpression<E extends SQLElement<E>> extends SQLWhere<E> { | ||||
|  | ||||
|         private final String sqlExpression; | ||||
|         private final List<Object> parameters; | ||||
|  | ||||
|         /* package */ SQLWhereCustomExpression(String sqlExpression, List<Object> parameters) { | ||||
|             if (sqlExpression == null) | ||||
|                 throw new IllegalArgumentException("sqlExpression can't be null"); | ||||
|             if (parameters == null) | ||||
|                 parameters = List.of(); | ||||
|             this.sqlExpression = sqlExpression; | ||||
|             this.parameters = parameters; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|             /* package */ ParameterizedSQLString toSQL() { | ||||
|             return new ParameterizedSQLString(sqlExpression, parameters); | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -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> | ||||
|     </dependencies> | ||||
|  | ||||
|     <properties> | ||||
|         <maven.javadoc.skip>true</maven.javadoc.skip> | ||||
|     </properties> | ||||
|  | ||||
| </project> | ||||
| @@ -18,7 +18,7 @@ public class ResponseAnalyser { | ||||
| 		if (socket == null || socket.isClosed() || socket.isInputShutdown()) | ||||
| 			throw new IllegalArgumentException("le socket doit être non null et doit être ouvert sur le flux d'entrée"); | ||||
|  | ||||
| 		// on lis la réponse | ||||
| 		// on lit la réponse | ||||
| 		BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); | ||||
|  | ||||
| 		String line; | ||||
|   | ||||
| @@ -5,15 +5,15 @@ import java.io.PrintStream; | ||||
| import java.net.InetAddress; | ||||
| import java.net.Socket; | ||||
|  | ||||
| import fr.pandacube.lib.util.Log; | ||||
| import fr.pandacube.lib.util.log.Log; | ||||
|  | ||||
| public abstract class AbstractRequestExecutor { | ||||
|  | ||||
| 	public final String command; | ||||
|  | ||||
| 	public AbstractRequestExecutor(String cmd, NetworkAPIListener napiListener) { | ||||
| 	public AbstractRequestExecutor(String cmd, NetworkAPIListener nAPIListener) { | ||||
| 		command = cmd.toLowerCase(); | ||||
| 		napiListener.registerRequestExecutor(command, this); | ||||
| 		nAPIListener.registerRequestExecutor(command, this); | ||||
| 	} | ||||
|  | ||||
| 	public void execute(String data, Socket socket) throws IOException { | ||||
| @@ -34,9 +34,8 @@ public abstract class AbstractRequestExecutor { | ||||
|  | ||||
| 	/** | ||||
| 	 * | ||||
| 	 * @param data La représentation sous forme de String des données envoyés | ||||
| 	 *        dans la requête | ||||
| 	 * @return La réponse à retourner au client | ||||
| 	 * @param data The String representation of the request data. | ||||
| 	 * @return The response to send back to the client. | ||||
| 	 */ | ||||
| 	protected abstract Response run(InetAddress source, String data); | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| package fr.pandacube.lib.netapi.server; | ||||
|  | ||||
| import fr.pandacube.lib.util.Log; | ||||
| import fr.pandacube.lib.util.log.Log; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.net.InetAddress; | ||||
| @@ -18,7 +18,7 @@ public class NetworkAPIListener implements Runnable { | ||||
| 	private final String name; | ||||
|  | ||||
| 	/** | ||||
| 	 * Instencie le côté serveur du NetworkAPI. | ||||
| 	 * Instancie le côté serveur du NetworkAPI. | ||||
| 	 * | ||||
| 	 * @param n nom du networkAPI (permet l'identification dans les logs) | ||||
| 	 * @param p le port d'écoute | ||||
| @@ -29,7 +29,7 @@ public class NetworkAPIListener implements Runnable { | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Instencie le côté serveur du NetworkAPI. | ||||
| 	 * Instancie le côté serveur du NetworkAPI. | ||||
| 	 * | ||||
| 	 * @param n nom du networkAPI (permet l'identification dans les logs) | ||||
| 	 * @param p le port d'écoute | ||||
| @@ -56,7 +56,6 @@ public class NetworkAPIListener implements Runnable { | ||||
| 		Log.info("NetworkAPI '" + name + "' à l'écoute sur le socket " + serverSocket.getLocalSocketAddress()); | ||||
|  | ||||
| 		try { | ||||
| 			// réception des connexion client | ||||
| 			while (!serverSocket.isClosed()) { | ||||
| 				Thread t = new Thread(new PacketExecutor(serverSocket.accept(), this)); | ||||
| 				t.setDaemon(true); | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import java.io.PrintStream; | ||||
| import java.net.Socket; | ||||
|  | ||||
| import fr.pandacube.lib.netapi.server.RequestAnalyser.BadRequestException; | ||||
| import fr.pandacube.lib.util.Log; | ||||
| import fr.pandacube.lib.util.log.Log; | ||||
|  | ||||
| /** | ||||
|  * Prends en charge un socket client et le transmet au gestionnaire de paquet | ||||
| @@ -20,9 +20,9 @@ public class PacketExecutor implements Runnable { | ||||
| 	private final Socket socket; | ||||
| 	private final NetworkAPIListener networkAPIListener; | ||||
|  | ||||
| 	public PacketExecutor(Socket s, NetworkAPIListener napiListener) { | ||||
| 	public PacketExecutor(Socket s, NetworkAPIListener nAPIListener) { | ||||
| 		socket = s; | ||||
| 		networkAPIListener = napiListener; | ||||
| 		networkAPIListener = nAPIListener; | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
|   | ||||
| @@ -10,23 +10,23 @@ public class RequestAnalyser { | ||||
| 	public final String command; | ||||
| 	public final String data; | ||||
|  | ||||
| 	public RequestAnalyser(Socket socket, NetworkAPIListener napiListener) throws IOException, BadRequestException { | ||||
| 		if (socket == null || socket.isClosed() || socket.isInputShutdown() || napiListener == null) | ||||
| 	public RequestAnalyser(Socket socket, NetworkAPIListener nAPIListener) throws IOException, BadRequestException { | ||||
| 		if (socket == null || socket.isClosed() || socket.isInputShutdown() || nAPIListener == null) | ||||
| 			throw new IllegalArgumentException( | ||||
| 					"le socket doit être non null et doit être ouvert sur le flux d'entrée et napiListener ne doit pas être null"); | ||||
| 					"le socket doit être non null et doit être ouvert sur le flux d'entrée et nAPIListener ne doit pas être null"); | ||||
|  | ||||
| 		// on lis la réponse | ||||
| 		// on lit la réponse | ||||
| 		BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); | ||||
|  | ||||
| 		String line; | ||||
|  | ||||
| 		// lecture de la première ligne | ||||
| 		line = in.readLine(); | ||||
| 		if (line == null || !line.equals(napiListener.pass)) throw new BadRequestException("wrong_password"); | ||||
| 		if (line == null || !line.equals(nAPIListener.pass)) throw new BadRequestException("wrong_password"); | ||||
|  | ||||
| 		// lecture de la deuxième ligne | ||||
| 		line = in.readLine(); | ||||
| 		if (line == null || napiListener.getRequestExecutor(line) == null) | ||||
| 		if (line == null || nAPIListener.getRequestExecutor(line) == null) | ||||
| 			throw new BadRequestException("command_not_exists"); | ||||
| 		command = line; | ||||
|  | ||||
|   | ||||
| @@ -16,7 +16,7 @@ | ||||
|     <repositories> | ||||
|         <repository> | ||||
|             <id>papermc</id> | ||||
|             <url>https://papermc.io/repo/repository/maven-public/</url> | ||||
|             <url>https://repo.papermc.io/repository/maven-public/</url> | ||||
|         </repository> | ||||
|  | ||||
|         <!-- WorldEdit --> | ||||
| @@ -25,7 +25,7 @@ | ||||
|             <url>https://maven.enginehub.org/repo/</url> | ||||
|         </repository> | ||||
|  | ||||
|         <!--  Vault and maybe other dependecies --> | ||||
|         <!--  Vault and maybe other dependencies --> | ||||
|         <repository> | ||||
|             <id>jitpack.io</id> | ||||
|             <url>https://jitpack.io</url> | ||||
| @@ -33,12 +33,12 @@ | ||||
|     </repositories> | ||||
|  | ||||
|     <dependencies> | ||||
|         <dependency> | ||||
|         <!-- <dependency> | ||||
|             <groupId>fr.pandacube.lib</groupId> | ||||
|             <artifactId>pandalib-players-permissible</artifactId> | ||||
|             <version>${project.version}</version> | ||||
|             <scope>provided</scope> | ||||
|         </dependency> | ||||
|         </dependency> --> | ||||
|         <dependency> | ||||
|             <groupId>fr.pandacube.lib</groupId> | ||||
|             <artifactId>pandalib-permissions</artifactId> | ||||
| @@ -77,7 +77,7 @@ | ||||
|         <dependency> | ||||
|             <groupId>com.sk89q.worldedit</groupId> | ||||
|             <artifactId>worldedit-bukkit</artifactId> | ||||
|             <version>7.2.9</version> | ||||
|             <version>7.2.19</version> | ||||
|             <scope>provided</scope> | ||||
|             <exclusions> | ||||
|                 <exclusion> | ||||
|   | ||||
| @@ -23,11 +23,11 @@ import org.bukkit.permissions.ServerOperator; | ||||
| import org.bukkit.plugin.java.JavaPlugin; | ||||
|  | ||||
| import fr.pandacube.lib.permissions.Permissions; | ||||
| import fr.pandacube.lib.util.Log; | ||||
| import fr.pandacube.lib.util.log.Log; | ||||
|  | ||||
| /** | ||||
|  * Class that integrates the {@code pandalib-permissions} system into a Bukkit/Spigot/Paper instance. | ||||
|  * The integration is made when calling {@link #init(JavaPlugin, String)}. | ||||
|  * The integration is made when calling {@link #onLoad(JavaPlugin, String)} and {@link #onEnable()}. | ||||
|  * The permission system must be initialized first, using {@link Permissions#init(Function)}. | ||||
|  * 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. | ||||
| @@ -38,18 +38,26 @@ public class PandalibPaperPermissions implements Listener { | ||||
| 	/* package */ static String serverName; | ||||
| 	/* package */ static final Map<String, String> permissionMap = new HashMap<>(); | ||||
|  | ||||
|  | ||||
| 	/** | ||||
| 	 * Integrates the {@code pandalib-permissions} system into the Bukkit server. | ||||
| 	 * Integrates the {@code pandalib-permissions} system into the Bukkit server, during the loading phase of the plugin. | ||||
| 	 * @param plugin a Bukkit plugin. | ||||
| 	 * @param serverName the name of the current server, used to fetch server specific permissions. Cannot be null. | ||||
| 	 *                   If this server in not in a multi-server configuration, use a dummy server name, like | ||||
| 	 *                   If this server in not in a multiserver configuration, use a dummy server name, like | ||||
| 	 *                   {@code ""} (empty string). | ||||
| 	 */ | ||||
| 	public static void init(JavaPlugin plugin, String serverName) { | ||||
| 	public static void onLoad(JavaPlugin plugin, String serverName) { | ||||
| 		PandalibPaperPermissions.plugin = plugin; | ||||
| 		PandalibPaperPermissions.serverName = serverName; | ||||
| 		PermissionsInjectorVault.onLoad(); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Integrates the {@code pandalib-permissions} system into the Bukkit server, during the enabling phase of the plugin. | ||||
| 	 */ | ||||
| 	public static void onEnable() { | ||||
| 		PermissionsInjectorBukkit.inject(Bukkit.getConsoleSender()); | ||||
| 		PermissionsInjectorVault.inject(); | ||||
| 		PermissionsInjectorVault.onEnable(); | ||||
| 		PermissionsInjectorWEPIF.inject(); | ||||
|  | ||||
| 		Bukkit.getPluginManager().registerEvents(new PandalibPaperPermissions(), plugin); | ||||
| @@ -74,6 +82,12 @@ public class PandalibPaperPermissions implements Listener { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|  | ||||
| 	/** | ||||
| 	 * Creates a {@link PandalibPaperPermissions} instance. | ||||
| 	 */ | ||||
| 	private PandalibPaperPermissions() {} | ||||
|  | ||||
| 	/** | ||||
| 	 * Player login event handler. | ||||
| 	 * @param event the event. | ||||
|   | ||||
| @@ -1,5 +1,23 @@ | ||||
| package fr.pandacube.lib.paper.permissions; | ||||
|  | ||||
| import com.google.common.cache.Cache; | ||||
| import com.google.common.cache.CacheBuilder; | ||||
| import com.google.common.cache.CacheLoader; | ||||
| import com.google.common.cache.LoadingCache; | ||||
| import fr.pandacube.lib.permissions.Permissions; | ||||
| import fr.pandacube.lib.reflect.Reflect; | ||||
| import fr.pandacube.lib.util.log.Log; | ||||
| import org.bukkit.command.CommandSender; | ||||
| import org.bukkit.command.ConsoleCommandSender; | ||||
| import org.bukkit.entity.Player; | ||||
| import org.bukkit.permissions.Permissible; | ||||
| import org.bukkit.permissions.PermissibleBase; | ||||
| import org.bukkit.permissions.Permission; | ||||
| import org.bukkit.permissions.PermissionAttachment; | ||||
| import org.bukkit.permissions.PermissionAttachmentInfo; | ||||
| import org.bukkit.plugin.Plugin; | ||||
| import org.jetbrains.annotations.NotNull; | ||||
|  | ||||
| import java.lang.reflect.Field; | ||||
| import java.util.HashMap; | ||||
| import java.util.LinkedHashSet; | ||||
| @@ -11,24 +29,6 @@ import java.util.concurrent.TimeUnit; | ||||
| import java.util.function.Function; | ||||
| import java.util.stream.Collectors; | ||||
|  | ||||
| import com.google.common.cache.Cache; | ||||
| import com.google.common.cache.CacheBuilder; | ||||
| import com.google.common.cache.CacheLoader; | ||||
| import com.google.common.cache.LoadingCache; | ||||
| import org.bukkit.command.CommandSender; | ||||
| import org.bukkit.command.ConsoleCommandSender; | ||||
| import org.bukkit.entity.Player; | ||||
| import org.bukkit.permissions.Permissible; | ||||
| import org.bukkit.permissions.PermissibleBase; | ||||
| import org.bukkit.permissions.Permission; | ||||
| import org.bukkit.permissions.PermissionAttachment; | ||||
| import org.bukkit.permissions.PermissionAttachmentInfo; | ||||
| import org.bukkit.plugin.Plugin; | ||||
|  | ||||
| import fr.pandacube.lib.permissions.Permissions; | ||||
| import fr.pandacube.lib.reflect.Reflect; | ||||
| import fr.pandacube.lib.util.Log; | ||||
|  | ||||
| /* package */ class PermissionsInjectorBukkit | ||||
| { | ||||
| 	 | ||||
| @@ -58,26 +58,24 @@ import fr.pandacube.lib.util.Log; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static void setPermissible(CommandSender sender, Permissible newpermissible) | ||||
|     private static void setPermissible(CommandSender sender, Permissible newPermissible) | ||||
|     { | ||||
|         try { | ||||
|             Field perm = getPermField(sender); | ||||
|             if (perm == null) | ||||
|                 return; | ||||
|             perm.setAccessible(true); | ||||
|             perm.set(sender, newpermissible); | ||||
|             perm.set(sender, newPermissible); | ||||
|         } | ||||
|         catch (Exception e) { | ||||
|             Log.severe(e); | ||||
|             throw new RuntimeException(e); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /* package */ static Permissible getPermissible(CommandSender sender) | ||||
|     { | ||||
|         Field perm = getPermField(sender); | ||||
|         if (perm == null) | ||||
|             return null; | ||||
|         try { | ||||
|             Field perm = getPermField(sender); | ||||
|             perm.setAccessible(true); | ||||
|             Permissible p = (Permissible) perm.get(sender); | ||||
|             if (p == null) { | ||||
| @@ -86,26 +84,19 @@ import fr.pandacube.lib.util.Log; | ||||
|             return p; | ||||
|         } | ||||
|         catch (Exception e) { | ||||
|             Log.severe(e); | ||||
|             throw new RuntimeException(e); | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     private static Field getPermField(CommandSender sender) | ||||
|     { | ||||
|     private static Field getPermField(CommandSender sender) throws NoSuchFieldException { | ||||
|     	if (sender == null) { | ||||
|     		throw new IllegalArgumentException("sender cannot be null"); | ||||
|     	} | ||||
|         try { | ||||
|         if (sender instanceof Player || sender instanceof ConsoleCommandSender) | ||||
|             return Reflect.ofClassOfInstance(sender).field("perm").get(); | ||||
|         else | ||||
|             throw new IllegalArgumentException("Unsupported type for sender: " + sender.getClass()); | ||||
|         } | ||||
|         catch (Exception e) { | ||||
|             Log.severe(e); | ||||
|         } | ||||
|         return null; | ||||
|  | ||||
|     } | ||||
|      | ||||
|     /* package */ static class PandaPermissible extends PermissibleBase | ||||
| @@ -118,7 +109,7 @@ import fr.pandacube.lib.util.Log; | ||||
|  | ||||
|         @SuppressWarnings("UnusedAssignment") | ||||
|         private boolean init = false; | ||||
|         /* assigment to false is necessary because of super class constructor calling the method recalculatePermission() | ||||
|         /* assignment to false is necessary because of super class constructor calling the method recalculatePermission() | ||||
|          * and we don’t want that. | ||||
|          */ | ||||
|  | ||||
| @@ -143,7 +134,7 @@ import fr.pandacube.lib.util.Log; | ||||
|         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.")) | ||||
| @@ -180,7 +171,7 @@ import fr.pandacube.lib.util.Log; | ||||
|         	if (res != null) | ||||
|         		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 | ||||
| @@ -214,22 +205,17 @@ import fr.pandacube.lib.util.Log; | ||||
|     			.build(); | ||||
|  | ||||
|         @Override | ||||
|         public Set<PermissionAttachmentInfo> getEffectivePermissions() | ||||
|         public @NotNull Set<PermissionAttachmentInfo> getEffectivePermissions() | ||||
|         { | ||||
|         	// PlotSquared uses this method to optimize permission range (plots.limit.10 for example) | ||||
|         	// MobArena uses this method when a player leave the arena | ||||
|         	// LibsDisguises uses this method (and only this one) to parse all the permissions | ||||
|  | ||||
|         	//Log.warning("There is a plugin calling CommandSender#getEffectivePermissions(). See the stacktrace to understand the reason for that.", new Throwable()); | ||||
|  | ||||
|         	String world = null; | ||||
|     		if (sender instanceof Player player) { | ||||
|                 world = player.getWorld().getName(); | ||||
|     		} | ||||
|         	 | ||||
|                 String world = player.getWorld().getName(); | ||||
|                 try { | ||||
|                     return effectivePermissionsListCache.get(world, () -> { | ||||
| 					// first get the superperms effective permissions (taht take isOp into accound) | ||||
|                         // 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())); | ||||
|  | ||||
| @@ -242,8 +228,11 @@ import fr.pandacube.lib.util.Log; | ||||
|                     }); | ||||
|                 } catch (ExecutionException e) { | ||||
|                     Log.severe(e); | ||||
| 				return oldPermissible.getEffectivePermissions(); | ||||
|                 } | ||||
|     		} | ||||
|         	 | ||||
|  | ||||
|             return oldPermissible.getEffectivePermissions(); | ||||
|         	 | ||||
|         } | ||||
|  | ||||
| @@ -260,7 +249,7 @@ import fr.pandacube.lib.util.Log; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public boolean isPermissionSet(String permission) | ||||
|         public boolean isPermissionSet(@NotNull String permission) | ||||
|         { | ||||
|         	Boolean res = hasPermissionOnServerInWorld(permission); | ||||
|         	if (res != null) | ||||
| @@ -278,31 +267,31 @@ import fr.pandacube.lib.util.Log; | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public PermissionAttachment addAttachment(Plugin plugin) | ||||
|         public @NotNull PermissionAttachment addAttachment(@NotNull Plugin plugin) | ||||
|         { | ||||
|             return oldPermissible.addAttachment(plugin); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public PermissionAttachment addAttachment(Plugin plugin, int ticks) | ||||
|         public PermissionAttachment addAttachment(@NotNull Plugin plugin, int ticks) | ||||
|         { | ||||
|             return oldPermissible.addAttachment(plugin, ticks); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value) | ||||
|         public @NotNull PermissionAttachment addAttachment(@NotNull Plugin plugin, @NotNull String name, boolean value) | ||||
|         { | ||||
|             return oldPermissible.addAttachment(plugin, name, value); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value, int ticks) | ||||
|         public PermissionAttachment addAttachment(@NotNull Plugin plugin, @NotNull String name, boolean value, int ticks) | ||||
|         { | ||||
|             return oldPermissible.addAttachment(plugin, name, value, ticks); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void removeAttachment(PermissionAttachment attachment) | ||||
|         public void removeAttachment(@NotNull PermissionAttachment attachment) | ||||
|         { | ||||
|             oldPermissible.removeAttachment(attachment); | ||||
|         } | ||||
|   | ||||
| @@ -2,7 +2,9 @@ package fr.pandacube.lib.paper.permissions; | ||||
|  | ||||
| import fr.pandacube.lib.permissions.PermGroup; | ||||
| import fr.pandacube.lib.permissions.Permissions; | ||||
| import fr.pandacube.lib.util.Log; | ||||
| import fr.pandacube.lib.util.log.Log; | ||||
| import net.milkbowl.vault.chat.Chat; | ||||
| import net.milkbowl.vault.permission.Permission; | ||||
| import org.bukkit.Bukkit; | ||||
| import org.bukkit.OfflinePlayer; | ||||
| import org.bukkit.plugin.ServicePriority; | ||||
| @@ -11,27 +13,54 @@ import java.util.List; | ||||
|  | ||||
| /* package */ class PermissionsInjectorVault { | ||||
|  | ||||
| 	private static final ServicePriority servicePriority = ServicePriority.Highest; | ||||
| 	 | ||||
| 	public static PandaVaultPermission permInstance; | ||||
|  | ||||
| 	public static void inject() { | ||||
| 	/** | ||||
| 	 * Vault injection needs to happen as soon as possible so other plugins detects it when they load. | ||||
| 	 */ | ||||
| 	public static void onLoad() { | ||||
| 		try { | ||||
| 			permInstance = new PandaVaultPermission(); | ||||
| 			PandaVaultChat chat = new PandaVaultChat(permInstance); | ||||
| 			Bukkit.getServicesManager().register(net.milkbowl.vault.permission.Permission.class, permInstance, | ||||
| 					PandalibPaperPermissions.plugin, ServicePriority.High); | ||||
| 			Bukkit.getServicesManager().register(net.milkbowl.vault.chat.Chat.class, chat, | ||||
| 					PandalibPaperPermissions.plugin, ServicePriority.High); | ||||
| 			Bukkit.getServicesManager().register(Permission.class, permInstance, | ||||
| 					PandalibPaperPermissions.plugin, servicePriority); | ||||
| 			Bukkit.getServicesManager().register(Chat.class, chat, | ||||
| 					PandalibPaperPermissions.plugin, servicePriority); | ||||
| 			Log.info("Providing permissions and chat prefix/suffix through Vault API."); | ||||
| 		} catch (NoClassDefFoundError e) { | ||||
| 			Log.warning("Vault plugin not detected. Not using it to provide permissions and prefix/suffix." + e.getMessage()); | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	public static void onEnable() { | ||||
| 		Bukkit.getScheduler().runTaskLater(PandalibPaperPermissions.plugin, | ||||
| 				PermissionsInjectorVault::checkServicesRegistration, 1); | ||||
| 	} | ||||
|  | ||||
|  | ||||
| 	private static void checkServicesRegistration() { | ||||
| 		Permission permService = Bukkit.getServicesManager().load(Permission.class); | ||||
| 		if (!(permService instanceof PandaVaultPermission)) { | ||||
| 			Log.severe("Check for Vault Permission service failed. " | ||||
| 					+ (permService == null ? "Service manager returned null." | ||||
| 					: ("Returned service is " + permService.getName() + " (" + permService.getClass().getName() + ")."))); | ||||
|  | ||||
| 		} | ||||
| 		Chat chatService = Bukkit.getServicesManager().load(Chat.class); | ||||
| 		if (!(chatService instanceof PandaVaultChat)) { | ||||
| 			Log.severe("Check for Vault Chat service failed. " | ||||
| 					+ (chatService == null ? "Service manager returned null." | ||||
| 					: ("Returned service is " + chatService.getName() + " (" + chatService.getClass().getName() + ")."))); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| 	/* package */ static class PandaVaultPermission extends net.milkbowl.vault.permission.Permission { | ||||
|  | ||||
| 	/* package */ static class PandaVaultPermission extends Permission { | ||||
| 		 | ||||
| 		private PandaVaultPermission() { } | ||||
|  | ||||
| @@ -45,6 +74,11 @@ import java.util.List; | ||||
| 			return PandalibPaperPermissions.plugin != null && PandalibPaperPermissions.plugin.isEnabled(); | ||||
| 		} | ||||
|  | ||||
| 		private void checkEnabled() { | ||||
| 			if (!isEnabled()) | ||||
| 				throw new IllegalStateException("Cannot provide permission service because plugin is disabled."); | ||||
| 		} | ||||
|  | ||||
| 		@Override | ||||
| 		public boolean hasSuperPermsCompat() { | ||||
| 			return true; | ||||
| @@ -58,6 +92,7 @@ import java.util.List; | ||||
| 		 | ||||
| 		@Override | ||||
| 		public boolean playerHas(String world, OfflinePlayer player, String permission) { | ||||
| 			checkEnabled(); | ||||
| 			Boolean res = Permissions.getPlayer(player.getUniqueId()).hasPermission(permission, PandalibPaperPermissions.serverName, world); | ||||
| 			if (res != null) | ||||
| 				return res; | ||||
| @@ -77,8 +112,10 @@ import java.util.List; | ||||
|  | ||||
| 		@Override | ||||
| 		public boolean playerAdd(String world, OfflinePlayer player, String permission) { | ||||
| 			checkEnabled(); | ||||
| 			String server = PandalibPaperPermissions.serverName; | ||||
| 			Permissions.getPlayer(player.getUniqueId()).addSelfPermission(permission, server, world); | ||||
| 			Permissions.clearPlayerCache(player.getUniqueId()); | ||||
| 			Log.info("A plugin added permission " + permission + " (server=" + server + ",world=" + world + ") to player " + player.getName() + " through Vault."); | ||||
| 			return true; | ||||
| 		} | ||||
| @@ -91,14 +128,17 @@ import java.util.List; | ||||
|  | ||||
| 		@Override | ||||
| 		public boolean playerRemove(String world, OfflinePlayer player, String permission) { | ||||
| 			checkEnabled(); | ||||
| 			String server = PandalibPaperPermissions.serverName; | ||||
| 			Permissions.getPlayer(player.getUniqueId()).removeSelfPermission(permission, server, world); | ||||
| 			Permissions.clearPlayerCache(player.getUniqueId()); | ||||
| 			Log.info("A plugin removed permission " + permission + " (server=" + server + ",world=" + world + ") to player " + player.getName() + " through Vault."); | ||||
| 			return true; | ||||
| 		} | ||||
|  | ||||
| 		@Override | ||||
| 		public boolean groupHas(String world, String group, String permission) { | ||||
| 			checkEnabled(); | ||||
| 			Boolean res = Permissions.getGroup(group).hasPermission(permission, PandalibPaperPermissions.serverName, world); | ||||
| 			if (res != null) | ||||
| 				return res; | ||||
| @@ -112,14 +152,14 @@ import java.util.List; | ||||
|  | ||||
| 		@Override | ||||
| 		public boolean groupAdd(String world, String group, String permission) { | ||||
| 			Log.warning(new Throwable("A plugin tried to add to group " + group + " (world=" + world + ") the permission " + permission | ||||
| 			Log.severe(new Throwable("A plugin tried to add to group " + group + " (world=" + world + ") the permission " + permission | ||||
| 					+ " through Vault but Pandalib does not support it.")); | ||||
| 			return false; | ||||
| 		} | ||||
|  | ||||
| 		@Override | ||||
| 		public boolean groupRemove(String world, String group, String permission) { | ||||
| 			Log.warning(new Throwable("A plugin tried to remove from group " + group + " (world=" + world + ") the permission " + permission | ||||
| 			Log.severe(new Throwable("A plugin tried to remove from group " + group + " (world=" + world + ") the permission " + permission | ||||
| 					+ " through Vault but Pandalib does not support it.")); | ||||
| 			return false; | ||||
| 		} | ||||
| @@ -132,13 +172,14 @@ import java.util.List; | ||||
| 		 | ||||
| 		@Override | ||||
| 		public boolean playerInGroup(String world, OfflinePlayer player, String group) { | ||||
| 			checkEnabled(); | ||||
| 			return Permissions.getPlayer(player.getUniqueId()).isInGroup(group); | ||||
| 		} | ||||
|  | ||||
| 		@Deprecated | ||||
| 		@Override | ||||
| 		public boolean playerAddGroup(String world, String player, String group) { | ||||
| 			Log.warning(new Throwable("A plugin tried to add player " + player + " (world=" + world + ") to permission group " + group | ||||
| 			Log.severe(new Throwable("A plugin tried to add player " + player + " (world=" + world + ") to permission group " + group | ||||
| 					+ " through Vault but Pandalib does not support it.")); | ||||
| 			return false; | ||||
| 		} | ||||
| @@ -146,7 +187,7 @@ import java.util.List; | ||||
| 		@Deprecated | ||||
| 		@Override | ||||
| 		public boolean playerRemoveGroup(String world, String player, String group) { | ||||
| 			Log.warning(new Throwable("A plugin tried to remove player " + player + " (world=" + world + ") from permission group " + group | ||||
| 			Log.severe(new Throwable("A plugin tried to remove player " + player + " (world=" + world + ") from permission group " + group | ||||
| 					+ " through Vault but Pandalib does not support it.")); | ||||
| 			return false; | ||||
| 		} | ||||
| @@ -159,6 +200,7 @@ import java.util.List; | ||||
| 		 | ||||
| 		@Override | ||||
| 		public String[] getPlayerGroups(String world, OfflinePlayer player) { | ||||
| 			checkEnabled(); | ||||
| 			List<String> groups = Permissions.getPlayer(player.getUniqueId()).getGroupsString(); | ||||
| 			return groups.toArray(new String[0]); | ||||
| 		} | ||||
| @@ -171,12 +213,14 @@ import java.util.List; | ||||
| 		 | ||||
| 		@Override | ||||
| 		public String getPrimaryGroup(String world, OfflinePlayer player) { | ||||
| 			checkEnabled(); | ||||
| 			return Permissions.getPlayer(player.getUniqueId()).getGroupsString().stream() | ||||
| 					.findFirst().orElse(null); | ||||
| 		} | ||||
|  | ||||
| 		@Override | ||||
| 		public String[] getGroups() { | ||||
| 			checkEnabled(); | ||||
| 			return Permissions.getGroups().stream() | ||||
| 					.map(PermGroup::getName).toArray(String[]::new); | ||||
| 		} | ||||
| @@ -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); | ||||
| 		} | ||||
|  | ||||
| @@ -205,6 +249,11 @@ import java.util.List; | ||||
| 			return PandalibPaperPermissions.plugin != null && PandalibPaperPermissions.plugin.isEnabled(); | ||||
| 		} | ||||
|  | ||||
| 		private void checkEnabled() { | ||||
| 			if (!isEnabled()) | ||||
| 				throw new IllegalStateException("Cannot provide permission service because plugin is disabled."); | ||||
| 		} | ||||
|  | ||||
| 		@Deprecated | ||||
| 		@Override | ||||
| 		public String getPlayerPrefix(String world, String player) { | ||||
| @@ -213,6 +262,7 @@ import java.util.List; | ||||
| 		 | ||||
| 		@Override | ||||
| 		public String getPlayerPrefix(String world, OfflinePlayer player) { | ||||
| 			checkEnabled(); | ||||
| 			return Permissions.getPlayer(player.getUniqueId()).getPrefix(); | ||||
| 		} | ||||
|  | ||||
| @@ -224,16 +274,19 @@ import java.util.List; | ||||
| 		 | ||||
| 		@Override | ||||
| 		public String getPlayerSuffix(String world, OfflinePlayer player) { | ||||
| 			checkEnabled(); | ||||
| 			return Permissions.getPlayer(player.getUniqueId()).getSuffix(); | ||||
| 		} | ||||
|  | ||||
| 		@Override | ||||
| 		public String getGroupPrefix(String world, String group) { | ||||
| 			checkEnabled(); | ||||
| 			return Permissions.getGroup(group).getPrefix(); | ||||
| 		} | ||||
|  | ||||
| 		@Override | ||||
| 		public String getGroupSuffix(String world, String group) { | ||||
| 			checkEnabled(); | ||||
| 			return Permissions.getGroup(group).getSuffix(); | ||||
| 		} | ||||
|  | ||||
|   | ||||
| @@ -11,7 +11,7 @@ import org.bukkit.plugin.ServicePriority; | ||||
|  | ||||
| import fr.pandacube.lib.permissions.PermPlayer; | ||||
| import fr.pandacube.lib.permissions.Permissions; | ||||
| import fr.pandacube.lib.util.Log; | ||||
| import fr.pandacube.lib.util.log.Log; | ||||
|  | ||||
| /* package */ class PermissionsInjectorWEPIF { | ||||
| 	 | ||||
|   | ||||
| @@ -16,7 +16,7 @@ | ||||
|     <repositories> | ||||
|         <repository> | ||||
|             <id>papermc</id> | ||||
|             <url>https://papermc.io/repo/repository/maven-public/</url> | ||||
|             <url>https://repo.papermc.io/repository/maven-public/</url> | ||||
|         </repository> | ||||
|         <repository> | ||||
|             <id>fabricmc</id> | ||||
| @@ -71,6 +71,12 @@ | ||||
|             <version>${project.version}</version> | ||||
|         </dependency> | ||||
|  | ||||
|         <dependency> | ||||
|             <groupId>fr.pandacube.lib</groupId> | ||||
|             <artifactId>pandalib-bungee-chat</artifactId> | ||||
|             <version>${project.version}</version> | ||||
|         </dependency> | ||||
|  | ||||
|         <dependency> | ||||
|             <groupId>fr.pandacube.lib</groupId> | ||||
|             <artifactId>pandalib-paper-permissions</artifactId> | ||||
| @@ -84,19 +90,6 @@ | ||||
|             <artifactId>paper-api</artifactId> | ||||
|             <version>${paper.version}-SNAPSHOT</version> | ||||
|         </dependency> | ||||
|         <dependency> | ||||
|             <groupId>io.papermc.paper</groupId> | ||||
|             <artifactId>paper-mojangapi</artifactId> | ||||
|             <version>${paper.version}-SNAPSHOT</version> | ||||
|         </dependency> | ||||
|  | ||||
|         <!-- Needed to read obfuscation mapping file. Already included in Paper --> | ||||
|         <dependency> | ||||
|             <groupId>net.fabricmc</groupId> | ||||
|             <artifactId>mapping-io</artifactId> | ||||
|             <version>0.3.0</version> | ||||
|             <scope>provided</scope> | ||||
|         </dependency> | ||||
|     </dependencies> | ||||
|  | ||||
|     <build> | ||||
|   | ||||
| @@ -1,29 +1,51 @@ | ||||
| package fr.pandacube.lib.paper; | ||||
|  | ||||
| import fr.pandacube.lib.paper.event.ServerStopEvent; | ||||
| import fr.pandacube.lib.paper.json.PaperJson; | ||||
| import fr.pandacube.lib.paper.modules.PerformanceAnalysisManager; | ||||
| import org.bukkit.plugin.Plugin; | ||||
|  | ||||
| /** | ||||
|  * Main class for pandalib-paper. | ||||
|  */ | ||||
| public class PandaLibPaper { | ||||
| 	 | ||||
| 	private static Plugin plugin; | ||||
|  | ||||
| 	/** | ||||
| 	 * Method to call in plugin's {@link Plugin#onLoad()} method. | ||||
| 	 * @param plugin the plugin instance. | ||||
| 	 */ | ||||
| 	public static void onLoad(Plugin plugin) { | ||||
| 		PandaLibPaper.plugin = plugin; | ||||
| 		PaperJson.init(); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Method to call in plugin's {@link Plugin#onEnable()} method. | ||||
| 	 */ | ||||
| 	public static void onEnable() { | ||||
| 		PerformanceAnalysisManager.getInstance(); // initialize | ||||
| 		ServerStopEvent.init(); | ||||
| 	} | ||||
|  | ||||
|  | ||||
|  | ||||
| 	/** | ||||
| 	 * Method to call in plugin's {@link Plugin#onDisable()} method. | ||||
| 	 */ | ||||
| 	public static void disable() { | ||||
| 		PerformanceAnalysisManager.getInstance().cancelInternalBossBar(); | ||||
| 		PerformanceAnalysisManager.getInstance().deinit(); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Gets the plugin instance. | ||||
| 	 * @return the plugin instance provided with {@link #onLoad(Plugin)}. | ||||
| 	 */ | ||||
| 	public static Plugin getPlugin() { | ||||
| 		return plugin; | ||||
| 	} | ||||
|  | ||||
| 	private PandaLibPaper() {} | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -6,13 +6,65 @@ import java.io.File; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
|  | ||||
| /** | ||||
|  * A basic class holding configuration for {@link PaperBackupManager}. | ||||
|  */ | ||||
| @SuppressWarnings("CanBeFinal") | ||||
| public class PaperBackupConfig { | ||||
|  | ||||
|     /** | ||||
|      * Creates a new Paper backup config. | ||||
|      */ | ||||
|     public PaperBackupConfig() {} | ||||
|  | ||||
|     /** | ||||
|      * Set to true to enable worlds backup. | ||||
|      * Defaults to true. | ||||
|      */ | ||||
|     public boolean worldBackupEnabled = true; | ||||
|  | ||||
|     /** | ||||
|      * Set to true to enable the backup of the working directory. | ||||
|      * The workdir backup will already ignore the logs directory and any world folder (folder with a level.dat file in it). | ||||
|      * Defaults to true. | ||||
|      */ | ||||
|     public boolean workdirBackupEnabled = true; | ||||
|  | ||||
|     /** | ||||
|      * Set to true to enable the backup of logs. | ||||
|      * Defaults to true. | ||||
|      */ | ||||
|     public boolean logsBackupEnabled = true; | ||||
|  | ||||
|     /** | ||||
|      * The cron-formatted scheduling of the worlds and workdir backups. | ||||
|      * The default value is {@code "0 2 * * *"}, that is every day at 2am. | ||||
|      */ | ||||
|     public String scheduling = "0 2 * * *"; // cron format, here is every day at 2am | ||||
|  | ||||
|     /** | ||||
|      * The backup target directory. | ||||
|      * Must be set (defaults to null). | ||||
|      */ | ||||
|     public File backupDirectory = null; | ||||
|  | ||||
|     /** | ||||
|      * The backup cleaner for the worlds backup. | ||||
|      * Defaults to keep 1 backup every 3 month + the last 5 backups. | ||||
|      */ | ||||
|     public BackupCleaner worldBackupCleaner = BackupCleaner.KEEPING_1_EVERY_N_MONTH(3).merge(BackupCleaner.KEEPING_N_LAST(5)); | ||||
|  | ||||
|     /** | ||||
|      * The backup cleaner for the workdir backup. | ||||
|      * Defaults to keep 1 backup every 3 month + the last 5 backups. | ||||
|      */ | ||||
|     public BackupCleaner workdirBackupCleaner = BackupCleaner.KEEPING_1_EVERY_N_MONTH(3).merge(BackupCleaner.KEEPING_N_LAST(5)); | ||||
|  | ||||
|     /** | ||||
|      * The list of files or directory to ignore. | ||||
|      * Defaults to none. | ||||
|      * The workdir backup will already ignore the logs directory and any world folder (folder with a level.dat file in it). | ||||
|      */ | ||||
|     public List<String> workdirIgnoreList = new ArrayList<>(); | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -21,13 +21,21 @@ import java.util.HashMap; | ||||
| import java.util.HashSet; | ||||
| import java.util.Map; | ||||
| import java.util.Set; | ||||
| import java.util.concurrent.CancellationException; | ||||
|  | ||||
| /** | ||||
|  * The backup manager for Paper servers. | ||||
|  */ | ||||
| public class PaperBackupManager extends BackupManager implements Listener { | ||||
|  | ||||
| 	private final Map<String, PaperWorldProcess> compressWorlds = new HashMap<>(); | ||||
|  | ||||
| 	PaperBackupConfig config; | ||||
|  | ||||
| 	/** | ||||
| 	 * Instantiate a new backup manager. | ||||
| 	 * @param config the configuration of the backups. | ||||
| 	 */ | ||||
| 	public PaperBackupManager(PaperBackupConfig config) { | ||||
| 		super(config.backupDirectory); | ||||
| 		setConfig(config); | ||||
| @@ -48,13 +56,17 @@ public class PaperBackupManager extends BackupManager implements Listener { | ||||
| 		super.addProcess(process); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Updates the backups config | ||||
| 	 * @param config the new config. | ||||
| 	 */ | ||||
| 	public void setConfig(PaperBackupConfig config) { | ||||
| 		this.config = config; | ||||
| 		backupQueue.forEach(this::updateProcessConfig); | ||||
| 	} | ||||
|  | ||||
|  | ||||
| 	public void updateProcessConfig(BackupProcess process) { | ||||
| 	private void updateProcessConfig(BackupProcess process) { | ||||
| 		if (process instanceof PaperWorkdirProcess) { | ||||
| 			process.setEnabled(config.workdirBackupEnabled); | ||||
| 			process.setBackupCleaner(config.workdirBackupCleaner); | ||||
| @@ -76,6 +88,8 @@ public class PaperBackupManager extends BackupManager implements Listener { | ||||
| 	public void run() { | ||||
| 		try { | ||||
| 			SchedulerUtil.runOnServerThreadAndWait(super::run); | ||||
| 		} catch (CancellationException ignored) { | ||||
|  | ||||
|         } catch (Exception e) { | ||||
| 			throw new RuntimeException(e); | ||||
| 		} | ||||
| @@ -116,12 +130,12 @@ public class PaperBackupManager extends BackupManager implements Listener { | ||||
| 	private final Set<String> dirtyForSave = new HashSet<>(); | ||||
|  | ||||
| 	@EventHandler(priority = EventPriority.MONITOR) | ||||
| 	public void onWorldLoad(WorldLoadEvent event) { | ||||
| 	void onWorldLoad(WorldLoadEvent event) { | ||||
| 		initWorldProcess(event.getWorld().getName()); | ||||
| 	} | ||||
|  | ||||
| 	@EventHandler(priority = EventPriority.MONITOR) | ||||
| 	public void onWorldSave(WorldSaveEvent event) { | ||||
| 	void onWorldSave(WorldSaveEvent event) { | ||||
| 		if (event.getWorld().getLoadedChunks().length > 0 | ||||
| 				|| dirtyForSave.contains(event.getWorld().getName())) { | ||||
| 			compressWorlds.get(event.getWorld().getName()).setDirtyAfterSave(); | ||||
| @@ -134,18 +148,18 @@ public class PaperBackupManager extends BackupManager implements Listener { | ||||
|  | ||||
|  | ||||
| 	@EventHandler(priority = EventPriority.MONITOR) | ||||
| 	public void onPlayerChangeWorldEvent(PlayerChangedWorldEvent event) { | ||||
| 	void onPlayerChangeWorldEvent(PlayerChangedWorldEvent event) { | ||||
| 		dirtyForSave.add(event.getFrom().getName()); | ||||
| 		dirtyForSave.add(event.getPlayer().getWorld().getName()); | ||||
| 	} | ||||
|  | ||||
| 	@EventHandler(priority = EventPriority.MONITOR) | ||||
| 	public void onPlayerJoin(PlayerJoinEvent event) { | ||||
| 	void onPlayerJoin(PlayerJoinEvent event) { | ||||
| 		dirtyForSave.add(event.getPlayer().getWorld().getName()); | ||||
| 	} | ||||
|  | ||||
| 	@EventHandler(priority = EventPriority.MONITOR) | ||||
| 	public void onPlayerQuit(PlayerQuitEvent event) { | ||||
| 	void onPlayerQuit(PlayerQuitEvent event) { | ||||
| 		dirtyForSave.add(event.getPlayer().getWorld().getName()); | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -10,11 +10,19 @@ import net.kyori.adventure.bossbar.BossBar.Color; | ||||
| import net.kyori.adventure.bossbar.BossBar.Overlay; | ||||
| import org.bukkit.Bukkit; | ||||
|  | ||||
| /** | ||||
|  * A backup process with specific logic around Paper server. | ||||
|  */ | ||||
| public abstract class PaperBackupProcess extends BackupProcess { | ||||
|  | ||||
|  | ||||
| 	private BossBar bossBar; | ||||
|  | ||||
| 	/** | ||||
| 	 * Instantiates a new backup process. | ||||
| 	 * @param bm the associated backup manager. | ||||
| 	 * @param id the process identifier. | ||||
| 	 */ | ||||
| 	protected PaperBackupProcess(PaperBackupManager bm, String id) { | ||||
| 		super(bm, id); | ||||
| 	} | ||||
|   | ||||
| @@ -1,23 +1,24 @@ | ||||
| package fr.pandacube.lib.paper.backup; | ||||
|  | ||||
| import fr.pandacube.lib.util.Log; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.text.DateFormat; | ||||
| import java.util.Date; | ||||
| import java.util.function.BiPredicate; | ||||
|  | ||||
| /** | ||||
|  * A backup process with specific logic around Paper server working directory. | ||||
|  */ | ||||
| public class PaperWorkdirProcess extends PaperBackupProcess { | ||||
|  | ||||
| 	/** | ||||
| 	 * Instantiates a new backup process for the paper server working directory. | ||||
| 	 * @param bm the associated backup manager. | ||||
| 	 */ | ||||
| 	protected PaperWorkdirProcess(PaperBackupManager bm) { | ||||
| 		super(bm, "workdir"); | ||||
| 	} | ||||
|  | ||||
|  | ||||
| 	public BiPredicate<File, String> getFilenameFilter() { | ||||
| 		return new BiPredicate<File, String>() { | ||||
| 			@Override | ||||
| 			public boolean test(File file, String path) { | ||||
| 		return (file, path) -> { | ||||
| 			if (file.isDirectory() && new File(file, "level.dat").exists()) | ||||
| 				return false; | ||||
| 			if (new File(getSourceDir(), "logs").equals(file)) | ||||
| @@ -25,7 +26,6 @@ public class PaperWorkdirProcess extends PaperBackupProcess { | ||||
| 			if (file.isFile() && file.getName().endsWith(".lck")) | ||||
| 				return false; | ||||
| 			return PaperWorkdirProcess.super.getFilenameFilter().test(file, path); | ||||
| 			} | ||||
| 		}; | ||||
| 	} | ||||
| 	 | ||||
|   | ||||
| @@ -1,25 +1,32 @@ | ||||
| package fr.pandacube.lib.paper.backup; | ||||
|  | ||||
| import fr.pandacube.lib.chat.LegacyChatFormat; | ||||
| import fr.pandacube.lib.paper.scheduler.SchedulerUtil; | ||||
| import fr.pandacube.lib.paper.util.WorldUtil; | ||||
| import fr.pandacube.lib.util.Log; | ||||
| import net.md_5.bungee.api.ChatColor; | ||||
| import fr.pandacube.lib.paper.world.WorldUtil; | ||||
| import fr.pandacube.lib.util.log.Log; | ||||
| import org.bukkit.Bukkit; | ||||
| import org.bukkit.World; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.text.DateFormat; | ||||
| import java.util.Date; | ||||
| import java.util.function.BiPredicate; | ||||
|  | ||||
| /** | ||||
|  * A backup process with specific logic around Paper server world. | ||||
|  */ | ||||
| public class PaperWorldProcess extends PaperBackupProcess { | ||||
| 	private final String worldName; | ||||
| 	 | ||||
| 	private boolean autoSave = true; | ||||
|  | ||||
| 	protected PaperWorldProcess(PaperBackupManager bm, final String n) { | ||||
| 		super(bm, "worlds/" + n); | ||||
| 		worldName = n; | ||||
| 	/** | ||||
| 	 * Instantiates a new backup process for a world. | ||||
| 	 * @param bm the associated backup manager. | ||||
| 	 * @param worldName the name of the world. | ||||
| 	 */ | ||||
| 	protected PaperWorldProcess(PaperBackupManager bm, final String worldName) { | ||||
| 		super(bm, "worlds/" + worldName); | ||||
| 		this.worldName = worldName; | ||||
| 	} | ||||
| 	 | ||||
| 	private World getWorld() { | ||||
| @@ -63,11 +70,11 @@ public class PaperWorldProcess extends PaperBackupProcess { | ||||
|  | ||||
| 	public void displayNextSchedule() { | ||||
| 		if (hasNextScheduled()) { | ||||
| 			Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " is dirty. Next backup on " | ||||
| 			Log.info("[Backup] " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + " is dirty. Next backup on " | ||||
| 					+ DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(new Date(getNext()))); | ||||
| 		} | ||||
| 		else { | ||||
| 			Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " is clean. Next backup not scheduled."); | ||||
| 			Log.info("[Backup] " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + " is clean. Next backup not scheduled."); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -81,7 +88,7 @@ public class PaperWorldProcess extends PaperBackupProcess { | ||||
| 	public void setDirtyAfterSave() { | ||||
| 		if (!isDirty()) { // don't set dirty if it is already | ||||
| 			setDirtySinceNow(); | ||||
| 			Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " was saved and is now dirty. Next backup on " | ||||
| 			Log.info("[Backup] " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + " was saved and is now dirty. Next backup on " | ||||
| 					+ DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG) | ||||
| 					.format(new Date(getNext())) | ||||
| 			); | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| package fr.pandacube.lib.paper.commands; | ||||
|  | ||||
| import com.destroystokyo.paper.brigadier.BukkitBrigadierCommandSource; | ||||
| import com.mojang.brigadier.CommandDispatcher; | ||||
| import com.mojang.brigadier.arguments.ArgumentType; | ||||
| import com.mojang.brigadier.builder.LiteralArgumentBuilder; | ||||
| import com.mojang.brigadier.context.CommandContext; | ||||
| import com.mojang.brigadier.exceptions.CommandSyntaxException; | ||||
| import com.mojang.brigadier.suggestion.SuggestionProvider; | ||||
| @@ -10,116 +10,128 @@ import com.mojang.brigadier.tree.CommandNode; | ||||
| import com.mojang.brigadier.tree.LiteralCommandNode; | ||||
| import com.mojang.brigadier.tree.RootCommandNode; | ||||
| import fr.pandacube.lib.chat.Chat; | ||||
| import fr.pandacube.lib.commands.BadCommandUsage; | ||||
| import fr.pandacube.lib.commands.BrigadierCommand; | ||||
| import fr.pandacube.lib.commands.SuggestionsSupplier; | ||||
| import fr.pandacube.lib.paper.permissions.PandalibPaperPermissions; | ||||
| import fr.pandacube.lib.paper.reflect.PandalibPaperReflect; | ||||
| import fr.pandacube.lib.paper.reflect.wrapper.craftbukkit.CraftServer; | ||||
| import fr.pandacube.lib.paper.PandaLibPaper; | ||||
| import fr.pandacube.lib.paper.reflect.wrapper.craftbukkit.CraftVector; | ||||
| import fr.pandacube.lib.paper.reflect.wrapper.craftbukkit.VanillaCommandWrapper; | ||||
| import fr.pandacube.lib.paper.reflect.wrapper.minecraft.commands.BlockPosArgument; | ||||
| import fr.pandacube.lib.paper.reflect.wrapper.minecraft.commands.Commands; | ||||
| import fr.pandacube.lib.paper.reflect.wrapper.minecraft.commands.ComponentArgument; | ||||
| import fr.pandacube.lib.paper.reflect.wrapper.minecraft.commands.Coordinates; | ||||
| import fr.pandacube.lib.paper.reflect.wrapper.minecraft.commands.EntityArgument; | ||||
| import fr.pandacube.lib.paper.reflect.wrapper.minecraft.commands.EntitySelector; | ||||
| import fr.pandacube.lib.paper.reflect.wrapper.minecraft.commands.Vec3Argument; | ||||
| import fr.pandacube.lib.paper.reflect.wrapper.minecraft.core.BlockPos; | ||||
| import fr.pandacube.lib.paper.reflect.wrapper.minecraft.server.ServerPlayer; | ||||
| import fr.pandacube.lib.paper.reflect.wrapper.paper.PaperAdventure; | ||||
| import fr.pandacube.lib.paper.reflect.wrapper.paper.commands.BukkitCommandNode; | ||||
| import fr.pandacube.lib.paper.reflect.wrapper.paper.commands.PluginCommandNode; | ||||
| import fr.pandacube.lib.players.standalone.AbstractOffPlayer; | ||||
| import fr.pandacube.lib.players.standalone.AbstractOnlinePlayer; | ||||
| import fr.pandacube.lib.players.standalone.AbstractPlayerManager; | ||||
| import fr.pandacube.lib.reflect.wrapper.ReflectWrapper; | ||||
| import fr.pandacube.lib.util.Log; | ||||
| import net.kyori.adventure.text.Component; | ||||
| import fr.pandacube.lib.reflect.Reflect; | ||||
| import fr.pandacube.lib.reflect.ReflectClass; | ||||
| import fr.pandacube.lib.util.log.Log; | ||||
| import io.papermc.paper.command.brigadier.CommandSourceStack; | ||||
| import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents; | ||||
| import org.bukkit.Bukkit; | ||||
| import org.bukkit.World; | ||||
| import org.bukkit.command.Command; | ||||
| import org.bukkit.command.CommandMap; | ||||
| import org.bukkit.command.CommandSender; | ||||
| import org.bukkit.command.ConsoleCommandSender; | ||||
| import org.bukkit.command.PluginCommand; | ||||
| import org.bukkit.command.defaults.BukkitCommand; | ||||
| import org.bukkit.entity.Entity; | ||||
| import org.bukkit.entity.Player; | ||||
| import org.bukkit.event.EventHandler; | ||||
| import org.bukkit.event.Listener; | ||||
| import org.bukkit.event.player.PlayerCommandSendEvent; | ||||
| import org.bukkit.event.server.ServerLoadEvent; | ||||
| import org.bukkit.plugin.Plugin; | ||||
| import org.bukkit.util.BlockVector; | ||||
| import org.bukkit.util.Vector; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.lang.reflect.InvocationTargetException; | ||||
| import java.util.HashSet; | ||||
| import java.util.List; | ||||
| import java.util.Set; | ||||
| import java.util.function.Predicate; | ||||
| import java.util.stream.Stream; | ||||
|  | ||||
| import static fr.pandacube.lib.reflect.wrapper.ReflectWrapper.unwrap; | ||||
| import static fr.pandacube.lib.reflect.wrapper.ReflectWrapper.wrap; | ||||
|  | ||||
| /** | ||||
|  * Abstract class to hold a command to be integrated into a Paper server vanilla command dispatcher. | ||||
|  */ | ||||
| public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBrigadierCommandSource> implements Listener { | ||||
| @SuppressWarnings("UnstableApiUsage") | ||||
| public abstract class PaperBrigadierCommand extends BrigadierCommand<CommandSourceStack> implements Listener { | ||||
|  | ||||
|     private static final Commands vanillaCommandDispatcher; | ||||
|     private static final CommandDispatcher<BukkitBrigadierCommandSource> nmsDispatcher; | ||||
|     private static CommandDispatcher<CommandSourceStack> vanillaPaperDispatcher = null; | ||||
|  | ||||
|     static { | ||||
|         PandalibPaperReflect.init(); | ||||
|         vanillaCommandDispatcher = ReflectWrapper.wrapTyped(Bukkit.getServer(), CraftServer.class) | ||||
|                 .getServer() | ||||
|                 .vanillaCommandDispatcher(); | ||||
|         nmsDispatcher = vanillaCommandDispatcher.dispatcher(); | ||||
|     /** | ||||
|      * Gets the Brigadier dispatcher provided by paper API during {@link LifecycleEvents#COMMANDS}. | ||||
|      * <p> | ||||
|      * This Dispatcher is not the vanilla one. Instead, Paper implementation wraps the vanilla one to handle proper registration | ||||
|      * of commands from plugins. | ||||
|      * @return the Brigadier dispatcher. | ||||
|      */ | ||||
|     public static CommandDispatcher<CommandSourceStack> getVanillaPaperDispatcher() { | ||||
|         return vanillaPaperDispatcher; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Removes a plugin command that overrides a vanilla command, so the vanilla command functionnalities are fully | ||||
|      * Gets the root node of the dispatcher from {@link #getVanillaPaperDispatcher()}. | ||||
|      * @return the root node, or null if {@link #getVanillaPaperDispatcher()} is also null. | ||||
|      */ | ||||
|     public static RootCommandNode<CommandSourceStack> getRootNode() { | ||||
|         return vanillaPaperDispatcher == null ? null : vanillaPaperDispatcher.getRoot(); | ||||
|     } | ||||
|  | ||||
|     private static void updateVanillaPaperDispatcher(CommandDispatcher<CommandSourceStack> newDispatcher) { | ||||
|         if (vanillaPaperDispatcher == null || newDispatcher != vanillaPaperDispatcher) { | ||||
|             vanillaPaperDispatcher = newDispatcher; | ||||
|  | ||||
|             // vanillaPaperDispatcher.getRoot() is not the real root but a wrapped root. Trying to map the fake root with the real one to trick the Paper/Brigadier (un)wrapper | ||||
|             RootCommandNode<CommandSourceStack> wrappedRoot = vanillaPaperDispatcher.getRoot(); | ||||
|             ReflectClass<?> apiMirrorRootNodeClass = Reflect.ofClassOfInstance(wrappedRoot); | ||||
|             try { | ||||
|                 RootCommandNode<?> unwrappedRoot = ((CommandDispatcher<?>) apiMirrorRootNodeClass.method("getDispatcher").invoke(wrappedRoot)).getRoot(); | ||||
|  | ||||
|                 Reflect.ofClass(CommandNode.class).field("unwrappedCached").setValue(wrappedRoot, unwrappedRoot); | ||||
|                 Reflect.ofClass(CommandNode.class).field("wrappedCached").setValue(unwrappedRoot, wrappedRoot); | ||||
|  | ||||
|             } catch (InvocationTargetException|IllegalAccessException|NoSuchMethodException|NoSuchFieldException e) { | ||||
|                 Log.severe("Unable to trick the Paper/Brigadier unwrapper to properly handle commands redirecting to root command node.", e); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Removes a plugin command that overrides a vanilla command, so the vanilla command functionalities are fully | ||||
|      * restored (so, not only the usage, but also the suggestions and the command structure sent to the client). | ||||
|      * @param name the name of the command to restore. | ||||
|      */ | ||||
|     public static void restoreVanillaCommand(String name) { | ||||
|         CommandMap bukkitCmdMap = Bukkit.getCommandMap(); | ||||
|         Command bukkitCommand = bukkitCmdMap.getCommand(name); | ||||
|         if (bukkitCommand != null) { | ||||
|             if (VanillaCommandWrapper.REFLECT.get().isInstance(bukkitCommand)) { | ||||
|                 //Log.info("Command /" + name + " is already a vanilla command."); | ||||
|  | ||||
|         PandaLibPaper.getPlugin().getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, | ||||
|                 event -> updateVanillaPaperDispatcher(event.registrar().getDispatcher())); | ||||
|  | ||||
|  | ||||
|         Bukkit.getServer().getScheduler().runTask(PandaLibPaper.getPlugin(), () -> { | ||||
|             if (vanillaPaperDispatcher == null) | ||||
|                 return; | ||||
|  | ||||
|             CommandNode<CommandSourceStack> targetCommand = vanillaPaperDispatcher.getRoot().getChild("minecraft:" + name); | ||||
|             if (targetCommand == null) { | ||||
|                 Log.warning("There is no vanilla command '" + name + "' to restore."); | ||||
|                 return; | ||||
|             } | ||||
|             Log.info("Removing Bukkit command /" + name + " (" + getCommandIdentity(bukkitCommand) + ")"); | ||||
|             bukkitCmdMap.getKnownCommands().remove(name.toLowerCase(java.util.Locale.ENGLISH)); | ||||
|             bukkitCommand.unregister(bukkitCmdMap); | ||||
|  | ||||
|             LiteralCommandNode<BukkitBrigadierCommandSource> node = (LiteralCommandNode<BukkitBrigadierCommandSource>) getRootNode().getChild(name); | ||||
|             Command newCommand = new VanillaCommandWrapper(vanillaCommandDispatcher, node).__getRuntimeInstance(); | ||||
|             bukkitCmdMap.getKnownCommands().put(name.toLowerCase(), newCommand); | ||||
|             newCommand.register(bukkitCmdMap); | ||||
|             CommandNode<CommandSourceStack> eventuallyBadCommandToReplace = vanillaPaperDispatcher.getRoot().getChild(name); | ||||
|             Boolean isPluginCommand = isPluginCommand(eventuallyBadCommandToReplace); | ||||
|             if (isPluginCommand != null && isPluginCommand) { | ||||
|                 Log.info(getCommandIdentity(eventuallyBadCommandToReplace) + " found in the dispatcher. Restoring the vanilla command."); | ||||
|                 vanillaPaperDispatcher.getRoot().getChildren().removeIf(c -> c.getName().equals(name)); | ||||
|                 vanillaPaperDispatcher.getRoot().addChild(getAliasNode(targetCommand, name)); | ||||
|             } | ||||
|             /*else if (isPluginCommand == null) { | ||||
|                 Log.info(getCommandIdentity(eventuallyBadCommandToReplace) + " found in the dispatcher. Unsure if we restore the vanilla command."); | ||||
|             }*/ | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Returns the vanilla instance of the Brigadier dispatcher. | ||||
|      * @return the vanilla instance of the Brigadier dispatcher. | ||||
|      */ | ||||
|     public static CommandDispatcher<BukkitBrigadierCommandSource> getNMSDispatcher() { | ||||
|         return nmsDispatcher; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the root command node of the Brigadier dispatcher. | ||||
|      * @return the root command node of the Brigadier dispatcher. | ||||
|      */ | ||||
|     protected static RootCommandNode<BukkitBrigadierCommandSource> getRootNode() { | ||||
|         return nmsDispatcher.getRoot(); | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -128,14 +140,23 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga | ||||
|     /** | ||||
|      * The command node of this command. | ||||
|      */ | ||||
|     protected final LiteralCommandNode<BukkitBrigadierCommandSource> commandNode; | ||||
|     protected LiteralCommandNode<CommandSourceStack> commandNode; | ||||
|     /** | ||||
|      * The command requested aliases. | ||||
|      */ | ||||
|     protected final String[] aliases; | ||||
|  | ||||
|     /** | ||||
|      * The command description. | ||||
|      */ | ||||
|     protected final String description; | ||||
|  | ||||
|     private final RegistrationPolicy registrationPolicy; | ||||
|  | ||||
|     private Set<String> registeredAliases; | ||||
|  | ||||
|     /** | ||||
|      * Instanciate this command instance. | ||||
|      * Instantiate this command instance. | ||||
|      * | ||||
|      * @param pl the plugin instance. | ||||
|      * @param regPolicy the registration policy for this command. | ||||
| @@ -143,17 +164,17 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga | ||||
|     public PaperBrigadierCommand(Plugin pl, RegistrationPolicy regPolicy) { | ||||
|         plugin = pl; | ||||
|         registrationPolicy = regPolicy; | ||||
|         commandNode = buildCommand().build(); | ||||
|         postBuildCommand(commandNode); | ||||
|         String[] aliasesTmp = getAliases(); | ||||
|         aliases = aliasesTmp == null ? new String[0] : aliasesTmp; | ||||
|         description = getDescription(); | ||||
|         register(); | ||||
|         Bukkit.getPluginManager().registerEvents(this, plugin); | ||||
|         try { | ||||
|             PandalibPaperPermissions.addPermissionMapping("minecraft.command." + commandNode.getLiteral().toLowerCase(), getTargetPermission().toLowerCase()); | ||||
|         } catch (NoClassDefFoundError ignored) { } | ||||
|         //try { | ||||
|         //    PandalibPaperPermissions.addPermissionMapping("minecraft.command." + commandNode.getLiteral().toLowerCase(), getTargetPermission().toLowerCase()); | ||||
|         //} catch (NoClassDefFoundError ignored) { } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Instanciate this command isntance with a registration policy of {@link RegistrationPolicy#ONLY_BASE_COMMAND}. | ||||
|      * Instantiate this command instance with a registration policy of {@link RegistrationPolicy#ONLY_BASE_COMMAND}. | ||||
|      * @param pl the plugin instance. | ||||
|      */ | ||||
|     public PaperBrigadierCommand(Plugin pl) { | ||||
| @@ -164,163 +185,179 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga | ||||
|  | ||||
|  | ||||
|     private void register() { | ||||
|         plugin.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event -> { | ||||
|             updateVanillaPaperDispatcher(event.registrar().getDispatcher()); | ||||
|  | ||||
|         String[] aliases = getAliases(); | ||||
|         if (aliases == null) | ||||
|             aliases = new String[0]; | ||||
|             commandNode = buildCommand().build(); | ||||
|             postBuildCommand(commandNode); | ||||
|  | ||||
|         String pluginName = plugin.getName().toLowerCase(); | ||||
|             if (vanillaPaperDispatcher.getRoot().getChild(commandNode.getName()) != null) { | ||||
|                 Log.info("Command /" + commandNode.getName() + " found in the vanilla dispatcher during initial command registration. Replacing it by force."); | ||||
|                 vanillaPaperDispatcher.getRoot().getChildren().removeIf(c -> c.getName().equals(commandNode.getName())); | ||||
|             } | ||||
|  | ||||
|         registeredAliases = new HashSet<>(); | ||||
|         registerNode(commandNode, false); | ||||
|         registerAlias(pluginName + ":" + commandNode.getLiteral(), true); | ||||
|             registeredAliases = new HashSet<>(event.registrar().register(commandNode, description, List.of(aliases))); | ||||
|             doPostRegistrationFixes(); | ||||
|  | ||||
|             if (registrationPolicy == RegistrationPolicy.ALL) { | ||||
|                 // enforce registration of aliases | ||||
|                 for (String alias : aliases) { | ||||
|             registerAlias(alias, false); | ||||
|             registerAlias(pluginName + ":" + alias, true); | ||||
|                     if (!registeredAliases.contains(alias)) { | ||||
|                         Log.info("Command /" + commandNode.getName() + ": forcing registration of alias " + alias); | ||||
|                         registeredAliases.addAll(event.registrar().register(getAliasNode(commandNode, alias), description)); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|  | ||||
|     private void registerAlias(String alias, boolean prefixed) { | ||||
|         LiteralCommandNode<BukkitBrigadierCommandSource> node = literal(alias) | ||||
|             Bukkit.getServer().getScheduler().runTask(plugin, () -> { | ||||
|                 if (vanillaPaperDispatcher == null) | ||||
|                     return; | ||||
|  | ||||
|                 Set<String> forceRegistrationAgain = new HashSet<>(); | ||||
|                 forceRegistrationAgain.add(commandNode.getName()); | ||||
|                 if (registrationPolicy == RegistrationPolicy.ALL) | ||||
|                     forceRegistrationAgain.addAll(List.of(aliases)); | ||||
|  | ||||
|                 for (String aliasToForce : forceRegistrationAgain) { | ||||
|                     CommandNode<CommandSourceStack> actualNode = vanillaPaperDispatcher.getRoot().getChild(aliasToForce); | ||||
|                     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 static LiteralCommandNode<CommandSourceStack> getAliasNode(CommandNode<CommandSourceStack> commandNode, String alias) { | ||||
|          return LiteralArgumentBuilder.<CommandSourceStack>literal(alias) | ||||
|                 .requires(commandNode.getRequirement()) | ||||
|                 .executes(commandNode.getCommand()) | ||||
|                 .redirect(commandNode) | ||||
|                 .build(); | ||||
|         registerNode(node, prefixed); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     private void registerNode(LiteralCommandNode<BukkitBrigadierCommandSource> node, boolean prefixed) { | ||||
|         RootCommandNode<BukkitBrigadierCommandSource> root = getRootNode(); | ||||
|         String name = node.getLiteral(); | ||||
|         boolean isAlias = node.getRedirect() == commandNode; | ||||
|         boolean forceRegistration = switch (registrationPolicy) { | ||||
|             case NONE -> false; | ||||
|             case ONLY_BASE_COMMAND -> prefixed || !isAlias; | ||||
|             case ALL -> true; | ||||
|         }; | ||||
|  | ||||
|         // nmsDispatcher integration and conflit resolution | ||||
|         boolean nmsRegister = false, nmsRegistered = false; | ||||
|         CommandNode<BukkitBrigadierCommandSource> nmsConflited = root.getChild(name); | ||||
|         if (nmsConflited != null) { | ||||
|  | ||||
|             if (isFromThisCommand(nmsConflited)) { | ||||
|                 // this command is already registered in NMS. Don’t need to register again | ||||
|                 nmsRegistered = true; | ||||
|     private static String getCommandIdentity(CommandNode<CommandSourceStack> command) { | ||||
|         if (PluginCommandNode.REFLECT.get().isInstance(command)) { | ||||
|             PluginCommandNode wrappedPCN = wrap(command, PluginCommandNode.class); | ||||
|             return "Node /" + command.getName() + " from plugin " + wrappedPCN.getPlugin().getName(); | ||||
|         } | ||||
|             else if (forceRegistration) { | ||||
|                 nmsRegister = true; | ||||
|                 Log.info("Overwriting Brigadier command /" + name); | ||||
|             } | ||||
|             else if (prefixed || !isAlias) { | ||||
|                 Log.severe("/" + name + " already in NMS Brigadier instance." | ||||
|                         + " Wont replace it because registration is not forced for prefixed or initial name of a command."); | ||||
|             } | ||||
|             else { // conflict, wont replace, not forced but only an alias anyway | ||||
|                 Log.info("/" + name + " already in NMS Brigadier instance." | ||||
|                         + " Wont replace it because registration is not forced for a non-prefixed alias."); | ||||
|             } | ||||
|         } | ||||
|         else { | ||||
|             nmsRegister = true; | ||||
|         } | ||||
|  | ||||
|         if (nmsRegister) { | ||||
|             @SuppressWarnings("unchecked") | ||||
|             var rCommandNode = ReflectWrapper.wrapTyped(root, fr.pandacube.lib.paper.reflect.wrapper.brigadier.CommandNode.class); | ||||
|             rCommandNode.removeCommand(name); | ||||
|             root.addChild(node); | ||||
|             nmsRegistered = true; | ||||
|         } | ||||
|  | ||||
|         if (!nmsRegistered) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         registeredAliases.add(name); | ||||
|  | ||||
|         // bukkit dispatcher conflict resolution | ||||
|         boolean bukkitRegister = false; | ||||
|         CommandMap bukkitCmdMap = Bukkit.getCommandMap(); | ||||
|         Command bukkitConflicted = bukkitCmdMap.getCommand(name); | ||||
|         if (bukkitConflicted != null) { | ||||
|             if (!isFromThisCommand(bukkitConflicted)) { | ||||
|                 if (forceRegistration) { | ||||
|                     bukkitRegister = true; | ||||
|                     Log.info("Overwriting Bukkit command /" + name | ||||
|                             + " (" + getCommandIdentity(bukkitConflicted) + ")"); | ||||
|                 } | ||||
|                 else if (prefixed || !isAlias) { | ||||
|                     Log.severe("/" + name + " already in Bukkit dispatcher (" + getCommandIdentity(bukkitConflicted) + ")." + | ||||
|                             " Wont replace it because registration is not forced for prefixed or initial name of a command."); | ||||
|                 } | ||||
|                 else { | ||||
|                     Log.info("/" + name + " already in Bukkit dispatcher (" + getCommandIdentity(bukkitConflicted) + ")." + | ||||
|                             " Wont replace it because registration is not forced for a non-prefixed alias."); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         else { | ||||
|             bukkitRegister = true; | ||||
|         } | ||||
|  | ||||
|         if (bukkitRegister) { | ||||
|             bukkitCmdMap.getKnownCommands().remove(name.toLowerCase()); | ||||
|             if (bukkitConflicted != null) | ||||
|                 bukkitConflicted.unregister(bukkitCmdMap); | ||||
|  | ||||
|             Command newCommand = new VanillaCommandWrapper(vanillaCommandDispatcher, node).__getRuntimeInstance(); | ||||
|             bukkitCmdMap.getKnownCommands().put(name.toLowerCase(), newCommand); | ||||
|             newCommand.register(bukkitCmdMap); | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     private boolean isFromThisCommand(CommandNode<BukkitBrigadierCommandSource> node) { | ||||
|         return node == commandNode || node.getRedirect() == commandNode; | ||||
|     } | ||||
|  | ||||
|     private boolean isFromThisCommand(Command bukkitCmd) { | ||||
|         if (VanillaCommandWrapper.REFLECT.get().isInstance(bukkitCmd)) { | ||||
|             return isFromThisCommand(ReflectWrapper.wrapTyped((BukkitCommand) bukkitCmd, VanillaCommandWrapper.class).vanillaCommand()); | ||||
|         } | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     private static String getCommandIdentity(Command bukkitCmd) { | ||||
|         else if (BukkitCommandNode.REFLECT.get().isInstance(command)) { | ||||
|             BukkitCommandNode wrappedBCN = wrap(command, BukkitCommandNode.class); | ||||
|             Command bukkitCmd = wrappedBCN.getBukkitCommand(); | ||||
|             if (bukkitCmd instanceof PluginCommand cmd) { | ||||
|             return "Bukkit command: /" + cmd.getName() + " from plugin " + cmd.getPlugin().getName(); | ||||
|                 return "Node /" + command.getName() + " wrapping Bukkit command /" + bukkitCmd.getName() + " from plugin " + cmd.getPlugin().getName(); | ||||
|             } | ||||
|             else if (VanillaCommandWrapper.REFLECT.get().isInstance(bukkitCmd)) { | ||||
|             return "Vanilla command: /" + bukkitCmd.getName(); | ||||
|                 VanillaCommandWrapper vcw = wrap(bukkitCmd, VanillaCommandWrapper.class); | ||||
|                 CommandNode<CommandSourceStack> vanillaCmd = vcw.vanillaCommand(); | ||||
|                 if (vanillaCmd != command) | ||||
|                     return "Node /" + command.getName() + " wrapping non-plugin command /" + bukkitCmd.getName() + " wrapping: " + getCommandIdentity(vcw.vanillaCommand()); | ||||
|                 else | ||||
|                     return "Node /" + command.getName() + " wrapping non-plugin command /" + bukkitCmd.getName() + " wrapping back the node (risk of StackOverflow?)"; | ||||
|             } | ||||
|             else | ||||
|             return bukkitCmd.getClass().getName() + ": /" + bukkitCmd.getName(); | ||||
|                 return "Node /" + command.getName() + " wrapping " + bukkitCmd.getClass().getName() + " /" + bukkitCmd.getName(); | ||||
|         } | ||||
|         else { | ||||
|             return "Node /" + command.getName() + " (unspecific)"; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     private static Boolean isPluginCommand(CommandNode<CommandSourceStack> command) { | ||||
|         if (PluginCommandNode.REFLECT.get().isInstance(command)) { | ||||
|             return true; | ||||
|         } | ||||
|         else if (BukkitCommandNode.REFLECT.get().isInstance(command)) { | ||||
|             BukkitCommandNode wrappedBCN = wrap(command, BukkitCommandNode.class); | ||||
|             Command bukkitCmd = wrappedBCN.getBukkitCommand(); | ||||
|             if (bukkitCmd instanceof PluginCommand) { | ||||
|                 return true; | ||||
|             } | ||||
|             else if (VanillaCommandWrapper.REFLECT.get().isInstance(bukkitCmd)) { | ||||
|                 VanillaCommandWrapper vcw = wrap(bukkitCmd, VanillaCommandWrapper.class); | ||||
|                 CommandNode<CommandSourceStack> vanillaCmd = vcw.vanillaCommand(); | ||||
|                 if (vanillaCmd != command) | ||||
|                     return isPluginCommand(vcw.vanillaCommand()); | ||||
|                 else | ||||
|                     return false; | ||||
|             } | ||||
|             else | ||||
|                 return null; | ||||
|         } | ||||
|         else { | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Player command sender event handler. | ||||
|      * @param event the event. | ||||
|      * Gets the aliases that are actually registered in the server. | ||||
|      * @return the actually registered aliases. | ||||
|      */ | ||||
|     @EventHandler | ||||
|     public void onPlayerCommandSend(PlayerCommandSendEvent event) { | ||||
|         event.getCommands().removeAll(registeredAliases.stream().map(s -> "minecraft:" + s).toList()); | ||||
|     protected Set<String> getRegisteredAliases() { | ||||
|         return Set.copyOf(registeredAliases); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Server load event handler. | ||||
|      * @param event the event. | ||||
|      */ | ||||
|     @EventHandler | ||||
|     public void onServerLoad(ServerLoadEvent event) { | ||||
|         register(); | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -339,6 +376,15 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga | ||||
|      */ | ||||
|     protected abstract String getTargetPermission(); | ||||
|  | ||||
|     /** | ||||
|      * Returns the permission that should be tested instead of "minecraft.command.cmdName". The conversion from the | ||||
|      * minecraft prefixed permission node to the returned node is done by the {@code pandalib-paper-permissions} if it | ||||
|      * is present in the classpath during runtime. | ||||
|      * @return the permission that should be tested instead of "minecraft.command.cmdName". | ||||
|      */ | ||||
|     protected String getDescription() { | ||||
|         return "A command from " + plugin.getName(); | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -352,13 +398,17 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga | ||||
|  | ||||
|  | ||||
|  | ||||
|     public boolean isConsole(BukkitBrigadierCommandSource wrapper) { | ||||
|  | ||||
|     @Override | ||||
|     public boolean isConsole(CommandSourceStack wrapper) { | ||||
|         return isConsole(getCommandSender(wrapper)); | ||||
|     } | ||||
|     public boolean isPlayer(BukkitBrigadierCommandSource wrapper) { | ||||
|     @Override | ||||
|     public boolean isPlayer(CommandSourceStack wrapper) { | ||||
|         return isPlayer(getCommandSender(wrapper)); | ||||
|     } | ||||
|     public Predicate<BukkitBrigadierCommandSource> hasPermission(String permission) { | ||||
|     @Override | ||||
|     public Predicate<CommandSourceStack> hasPermission(String permission) { | ||||
|         return wrapper -> getCommandSender(wrapper).hasPermission(permission); | ||||
|     } | ||||
|  | ||||
| @@ -392,7 +442,7 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga | ||||
|      * @param context the command context from which to get the Bukkit command sender. | ||||
|      * @return the Bukkit command sender. | ||||
|      */ | ||||
|     public static CommandSender getCommandSender(CommandContext<BukkitBrigadierCommandSource> context) { | ||||
|     public static CommandSender getCommandSender(CommandContext<CommandSourceStack> context) { | ||||
|         return getCommandSender(context.getSource()); | ||||
|     } | ||||
|  | ||||
| @@ -401,8 +451,8 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga | ||||
|      * @param wrapper the wrapper from which to get the Bukkit command sender. | ||||
|      * @return the Bukkit command sender. | ||||
|      */ | ||||
|     public static CommandSender getCommandSender(BukkitBrigadierCommandSource wrapper) { | ||||
|         return wrapper.getBukkitSender(); | ||||
|     public static CommandSender getCommandSender(CommandSourceStack wrapper) { | ||||
|         return wrapper.getSender(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -410,13 +460,13 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga | ||||
|      * @param sender the command sender. | ||||
|      * @return a new instance of a command sender wrapper for the provided command sender. | ||||
|      */ | ||||
|     public static BukkitBrigadierCommandSource getBrigadierCommandSource(CommandSender sender) { | ||||
|         return VanillaCommandWrapper.getListener(sender); | ||||
|     public static CommandSourceStack getBrigadierCommandSource(CommandSender sender) { | ||||
|         throw new UnsupportedOperationException("The 1.20.6 Paper API update uses a different wrapper for Brigadier command sender."); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * A suggestions supplier that suggests the names of the currently connected players (that the command sender can see). | ||||
|      * A suggestion supplier that suggests the names of the currently connected players (that the command sender can see). | ||||
|      */ | ||||
|     public static final SuggestionsSupplier<CommandSender> TAB_PLAYER_CURRENT_SERVER =  (sender, ti, token, a) -> { | ||||
|         @SuppressWarnings("unchecked") | ||||
| @@ -430,7 +480,7 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * A suggestions supplier that suggests the names of the worlds currently loaded on this server. | ||||
|      * A suggestion supplier that suggests the names of the worlds currently loaded on this server. | ||||
|      */ | ||||
|     public static final SuggestionsSupplier<CommandSender> TAB_WORLDS = SuggestionsSupplier.fromStreamSupplier(() -> Bukkit.getWorlds().stream().map(World::getName)); | ||||
|  | ||||
| @@ -440,7 +490,7 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga | ||||
|      * @param suggestions the suggestions to wrap. | ||||
|      * @return a {@link SuggestionProvider} generating the suggestions from the provided {@link SuggestionsSupplier}. | ||||
|      */ | ||||
|     protected SuggestionProvider<BukkitBrigadierCommandSource> wrapSuggestions(SuggestionsSupplier<CommandSender> suggestions) { | ||||
|     public SuggestionProvider<CommandSourceStack> wrapSuggestions(SuggestionsSupplier<CommandSender> suggestions) { | ||||
|         return wrapSuggestions(suggestions, PaperBrigadierCommand::getCommandSender); | ||||
|     } | ||||
|  | ||||
| @@ -453,12 +503,15 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga | ||||
|      * @param cmd the command executor to wrap. | ||||
|      * @return a wrapper command executor. | ||||
|      */ | ||||
|     protected static com.mojang.brigadier.Command<BukkitBrigadierCommandSource> wrapCommand(com.mojang.brigadier.Command<BukkitBrigadierCommandSource> cmd) { | ||||
|     protected static com.mojang.brigadier.Command<CommandSourceStack> wrapCommand(com.mojang.brigadier.Command<CommandSourceStack> cmd) { | ||||
|         return context -> { | ||||
|             try { | ||||
|                 return cmd.run(context); | ||||
|             } catch(CommandSyntaxException e) { | ||||
|                 throw e; | ||||
|             } catch (BadCommandUsage e) { | ||||
|                 getCommandSender(context).sendMessage(Chat.failureText("Error while using the command: " + e.getMessage())); | ||||
|                 return 0; | ||||
|             } catch (Throwable t) { | ||||
|                 Log.severe(t); | ||||
|                 getCommandSender(context).sendMessage(Chat.failureText("Error while executing the command: " + t)); | ||||
| @@ -475,120 +528,9 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga | ||||
|  | ||||
|  | ||||
|     /* | ||||
|      * Minecraft argument type | ||||
|      * Minecraft's argument type | ||||
|      */ | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Creates a new instance of the Brigadier argument type {@code minecraft:entity}. | ||||
|      * @param singleTarget if this argument takes only a single target. | ||||
|      * @param playersOnly if this argument takes players only. | ||||
|      * @return the {@code minecraft:entity} argument type with the specified parameters. | ||||
|      */ | ||||
|     public static ArgumentType<Object> argumentMinecraftEntity(boolean singleTarget, boolean playersOnly) { | ||||
|         if (playersOnly) { | ||||
|             return singleTarget ? EntityArgument.player() : EntityArgument.players(); | ||||
|         } | ||||
|         else { | ||||
|             return singleTarget ? EntityArgument.entity() : EntityArgument.entities(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Gets the value of the provided argument of type {@code minecraft:entity} (list of entities), from the provided context. | ||||
|      * @param context the command execution context. | ||||
|      * @param argument the argument name. | ||||
|      * @return the value of the argument, or null if not found. | ||||
|      */ | ||||
|     public List<Entity> tryGetMinecraftEntityArgument(CommandContext<BukkitBrigadierCommandSource> context, String argument) { | ||||
|         EntitySelector es = ReflectWrapper.wrap(tryGetArgument(context, argument, EntitySelector.MAPPING.runtimeClass()), EntitySelector.class); | ||||
|         if (es == null) | ||||
|             return null; | ||||
|         List<fr.pandacube.lib.paper.reflect.wrapper.minecraft.world.Entity> nmsEntityList = es.findEntities(context.getSource()); | ||||
|         List<Entity> entityList = new ArrayList<>(nmsEntityList.size()); | ||||
|         for (fr.pandacube.lib.paper.reflect.wrapper.minecraft.world.Entity nmsEntity : nmsEntityList) { | ||||
|             entityList.add(nmsEntity.getBukkitEntity()); | ||||
|         } | ||||
|         return entityList; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Gets the value of the provided argument of type {@code minecraft:entity} (list of players), from the provided context. | ||||
|      * @param context the command execution context. | ||||
|      * @param argument the argument name. | ||||
|      * @return the value of the argument, or null if not found. | ||||
|      */ | ||||
|     public List<Player> tryGetMinecraftEntityArgumentPlayers(CommandContext<BukkitBrigadierCommandSource> context, String argument) { | ||||
|         EntitySelector es = ReflectWrapper.wrap(tryGetArgument(context, argument, EntitySelector.MAPPING.runtimeClass()), EntitySelector.class); | ||||
|         if (es == null) | ||||
|             return null; | ||||
|         List<ServerPlayer> nmsPlayerList = es.findPlayers(context.getSource()); | ||||
|         List<Player> playerList = new ArrayList<>(nmsPlayerList.size()); | ||||
|         for (ServerPlayer nmsPlayer : nmsPlayerList) { | ||||
|             playerList.add(nmsPlayer.getBukkitEntity()); | ||||
|         } | ||||
|         return playerList; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Gets the value of the provided argument of type {@code minecraft:entity} (one entity), from the provided context. | ||||
|      * @param context the command execution context. | ||||
|      * @param argument the argument name. | ||||
|      * @return the value of the argument, or null if not found. | ||||
|      */ | ||||
|     public Entity tryGetMinecraftEntityArgumentOneEntity(CommandContext<BukkitBrigadierCommandSource> context, String argument) { | ||||
|         EntitySelector es = ReflectWrapper.wrap(tryGetArgument(context, argument, EntitySelector.MAPPING.runtimeClass()), EntitySelector.class); | ||||
|         if (es == null) | ||||
|             return null; | ||||
|         fr.pandacube.lib.paper.reflect.wrapper.minecraft.world.Entity nmsEntity = es.findSingleEntity(context.getSource()); | ||||
|         return nmsEntity == null ? null : nmsEntity.getBukkitEntity(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Gets the value of the provided argument of type {@code minecraft:entity} (one player), from the provided context. | ||||
|      * @param context the command execution context. | ||||
|      * @param argument the argument name. | ||||
|      * @return the value of the argument, or null if not found. | ||||
|      */ | ||||
|     public Player tryGetMinecraftEntityArgumentOnePlayer(CommandContext<BukkitBrigadierCommandSource> context, String argument) { | ||||
|         EntitySelector es = ReflectWrapper.wrap(tryGetArgument(context, argument, EntitySelector.MAPPING.runtimeClass()), EntitySelector.class); | ||||
|         if (es == null) | ||||
|             return null; | ||||
|         ServerPlayer nmsPlayer = es.findSinglePlayer(context.getSource()); | ||||
|         return nmsPlayer == null ? null : nmsPlayer.getBukkitEntity(); | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Creates a new instance of the Brigadier argument type {@code minecraft:block_pos}. | ||||
|      * @return the {@code minecraft:block_pos} argument type. | ||||
|      */ | ||||
|     public static ArgumentType<Object> argumentMinecraftBlockPosition() { | ||||
|         return BlockPosArgument.blockPos(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Gets the value of the provided argument of type {@code minecraft:block_pos}, from the provided context. | ||||
|      * @param context the command execution context. | ||||
|      * @param argument the argument name. | ||||
|      * @param deflt a defualt value if the argument is not found. | ||||
|      * @return the value of the argument. | ||||
|      */ | ||||
|     public BlockVector tryGetMinecraftBlockPositionArgument(CommandContext<BukkitBrigadierCommandSource> context, | ||||
|                                                             String argument, BlockVector deflt) { | ||||
|         return tryGetArgument(context, argument, Coordinates.MAPPING.runtimeClass(), nmsCoord -> { | ||||
|             BlockPos bp = ReflectWrapper.wrap(nmsCoord, Coordinates.class).getBlockPos(context.getSource()); | ||||
|             return new BlockVector(bp.getX(), bp.getY(), bp.getZ()); | ||||
|         }, deflt); | ||||
|     } | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Creates a new instance of the Brigadier argument type {@code minecraft:vec3}. | ||||
|      * @return the {@code minecraft:vec3} argument type. | ||||
| @@ -601,14 +543,14 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga | ||||
|      * Gets the value of the provided argument of type {@code minecraft:vec3}, from the provided context. | ||||
|      * @param context the command execution context. | ||||
|      * @param argument the argument name. | ||||
|      * @param deflt a defualt value if the argument is not found. | ||||
|      * @param deflt a default value if the argument is not found. | ||||
|      * @return the value of the argument. | ||||
|      */ | ||||
|     public Vector tryGetMinecraftVec3Argument(CommandContext<BukkitBrigadierCommandSource> context, String argument, | ||||
|     public Vector tryGetMinecraftVec3Argument(CommandContext<CommandSourceStack> context, String argument, | ||||
|                                               Vector deflt) { | ||||
|         return tryGetArgument(context, argument, Coordinates.MAPPING.runtimeClass(), | ||||
|                 nmsCoord -> CraftVector.toBukkit( | ||||
|                         ReflectWrapper.wrap(nmsCoord, Coordinates.class).getPosition(context.getSource()) | ||||
|         return tryGetArgument(context, argument, Coordinates.REFLECT.get(), | ||||
|                 nmsCoordinate -> CraftVector.toBukkit( | ||||
|                         wrap(nmsCoordinate, Coordinates.class).getPosition(context.getSource()) | ||||
|                 ), | ||||
|                 deflt); | ||||
|     } | ||||
| @@ -616,44 +558,11 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga | ||||
|  | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Creates a new instance of the Brigadier argument type {@code minecraft:component}. | ||||
|      * @return the {@code minecraft:component} argument type. | ||||
|      */ | ||||
|     public static ArgumentType<Object> argumentMinecraftChatComponent() { | ||||
|         return ComponentArgument.textComponent(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Gets the value of the provided argument of type {@code minecraft:component}, from the provided context. | ||||
|      * @param context the command execution context. | ||||
|      * @param argument the argument name. | ||||
|      * @param deflt a defualt value if the argument is not found. | ||||
|      * @return the value of the argument. | ||||
|      */ | ||||
|     public Component tryGetMinecraftChatComponentArgument(CommandContext<BukkitBrigadierCommandSource> context, | ||||
|                                                           String argument, Component deflt) { | ||||
|         return tryGetArgument(context, argument, | ||||
|                 fr.pandacube.lib.paper.reflect.wrapper.minecraft.network.chat.Component.MAPPING.runtimeClass(), | ||||
|                 nmsComp -> PaperAdventure.asAdventure( | ||||
|                         ReflectWrapper.wrap(nmsComp, | ||||
|                                 fr.pandacube.lib.paper.reflect.wrapper.minecraft.network.chat.Component.class) | ||||
|                 ), | ||||
|                 deflt); | ||||
|  | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * All possible choices on how to force the registration of a command, based on certain conditions. | ||||
|      */ | ||||
|     public enum RegistrationPolicy { | ||||
|         /** | ||||
|          * Do not force to register a command node or an alias if there is already a command with that name in the | ||||
|          * vanilla Brigadier dispatcher. | ||||
|          * Note that all plugin-name-prefixed aliases will be registered anyway. | ||||
|          */ | ||||
|         NONE, | ||||
|         /** | ||||
|          * Force only the base command (but not the aliases) to be registered, even if a command with that name already | ||||
|          * exists in the vanilla Brigadier dispatcher. | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user