Compare commits

..

135 Commits

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

It is useful for anonymous permission test (for instance, listing only the servers that are publicly visible for the ping server list)
2023-08-14 01:49:56 +02:00
2d950117d3 New BlockSet super-interface for AABBBlock and AABBBlockGroup + reorganized classes related to geometry to a new package 2023-08-14 00:43:01 +02:00
2f476ce8f2 Relative teleport API in PaperOnlinePlayer 2023-08-14 00:38:05 +02:00
75e292b1b8 Fixing big mistake in SchedulerUtil.runOnServerThread(). Provided Runnable was run twice if the method was called from Server Thread. 2023-08-03 23:32:29 +02:00
2969d51f72 Don't use random UUID for custom player heads 2023-07-29 13:39:13 +02:00
c0e0097b7b Some static values in PaperOnlinePlayer about default player movement speed 2023-07-28 22:56:25 +02:00
d047be35d9 Custom Bukkit event ServerStopEvent 2023-07-15 16:25:26 +02:00
5fb17be4c7 Add a method in BackupManager to check if a backup is currently running. 2023-07-14 19:07:02 +02:00
d7bb56e0b2 Fix CLIApplication shutdown hook 2023-07-14 16:26:30 +02:00
9e7d89cf70 Reimplement ChatColorGradient.pickColorAt() to make it less error-prone 2023-07-04 23:15:06 +02:00
79e4bb90f7 Inject permissions system into Vault on plugin load 2023-06-25 16:32:46 +02:00
736e0f0c23 PermissionsInjectorVault registers Vault services on highest priority and checks services access after server starts. 2023-06-25 14:52:02 +02:00
7481b12111 Fix FileUtils.copy not accepting regular file as source. Also made it handle directory merging. 2023-06-25 13:17:46 +02:00
7c4fd78680 Update some dependencies + Gson now natively supports record 2023-06-24 16:28:53 +02:00
8c25fb0dd1 Build against paper API 1.20 2023-06-24 15:53:26 +02:00
d5a2aa1c30 Completed implementation of PaperClientOptions 2023-06-23 23:43:16 +02:00
7d5060d09b Improved ItemStackBuilder + removed GUIInventory unused methods 2023-06-22 21:52:02 +02:00
a46e066669 also new ChatUtil.join() method 2023-06-20 21:32:52 +02:00
a4b33a1af7 new ChatUtil.joinGrammatically() method, using components 2023-06-20 18:38:07 +02:00
262 changed files with 7088 additions and 5369 deletions

4
.gitignore vendored
View File

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

View File

@@ -18,8 +18,9 @@ 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-config` Utility and helper classes to handle configuration related files and folders;
- `pandalib-commands` An abstract command manager working on top of [Brigadier](https://github.com/Mojang/brigadier);
- `pandalib-cli` Utility and helper classes for a standalone CLI Java application.
- `pandalib-cli` Utility and helper classes for a standalone CLI Java application;
- `pandalib-core` A catch-all module for some helper classes that didn't have their own module yet;
### Use in your projects

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,26 +4,46 @@ 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);
BungeeDailyLogRotateFileHandler.init(true);
}
/**
* Method to be called in {@link Plugin#onDisable()} method.
*/
public static void disable() {
}
/**
* Returns the plugin instance.
* @return the plugin instance.
*/
public static Plugin getPlugin() {
return plugin;
}
private PandaLibBungee() {}
}

View File

@@ -6,12 +6,40 @@ import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* Class that holds the configuration 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() {
}
}

View File

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

View File

@@ -5,8 +5,15 @@ import fr.pandacube.lib.core.backup.BackupProcess;
import java.io.File;
import java.util.function.BiPredicate;
/**
* The backup process responsible for the working directory of the current Bungeecord instance.
*/
public class BungeeWorkdirProcess extends BackupProcess {
/**
* Instantiates this backup process.
* @param bm the backup manager.
*/
protected BungeeWorkdirProcess(BungeeBackupManager bm) {
super(bm, "workdir");
}

View File

@@ -7,7 +7,7 @@ import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.tree.LiteralCommandNode;
import fr.pandacube.lib.commands.BrigadierCommand;
import fr.pandacube.lib.commands.SuggestionsSupplier;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.connection.ProxiedPlayer;

View File

@@ -1,6 +1,6 @@
package fr.pandacube.lib.bungee.commands;
import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.bungee.chat.ChatBungee;
import fr.pandacube.lib.commands.BrigadierDispatcher;
import net.kyori.adventure.text.ComponentLike;
import net.md_5.bungee.api.CommandSender;
@@ -71,6 +71,6 @@ public class BungeeBrigadierDispatcher extends BrigadierDispatcher<CommandSender
@Override
protected void sendSenderMessage(CommandSender sender, ComponentLike message) {
sender.sendMessage(Chat.toBungee(message.asComponent()));
sender.sendMessage(ChatBungee.toBungee(message.asComponent()));
}
}

View File

@@ -1,6 +1,6 @@
package fr.pandacube.lib.bungee.players;
import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.bungee.chat.ChatBungee;
import fr.pandacube.lib.core.mc_version.ProtocolVersion;
import fr.pandacube.lib.players.standalone.AbstractOnlinePlayer;
import fr.pandacube.lib.reflect.Reflect;
@@ -84,13 +84,13 @@ public interface BungeeOnlinePlayer extends BungeeOffPlayer, AbstractOnlinePlaye
@Override
default void sendMessage(Component message) {
getBungeeProxiedPlayer().sendMessage(Chat.toBungee(message));
getBungeeProxiedPlayer().sendMessage(ChatBungee.toBungee(message));
}
@Override
default void sendTitle(Component title, Component subtitle, int fadeIn, int stay, int fadeOut) {
ProxyServer.getInstance().createTitle()
.title(Chat.toBungee(title)).subTitle(Chat.toBungee(subtitle))
.title(ChatBungee.toBungee(title)).subTitle(ChatBungee.toBungee(subtitle))
.fadeIn(fadeIn).stay(stay).fadeOut(fadeOut)
.send(getBungeeProxiedPlayer());
}

View File

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

View File

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

View File

@@ -1,15 +1,12 @@
package fr.pandacube.lib.chat;
import java.awt.Color;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.UnaryOperator;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentBuilder;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.TranslationArgument;
import net.kyori.adventure.text.TranslationArgumentLike;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.event.HoverEventSource;
@@ -18,13 +15,18 @@ import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.format.TextDecoration.State;
import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
import org.jetbrains.annotations.NotNull;
import java.awt.*;
import java.util.Locale;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.UnaryOperator;
/**
* A builder for chat components.
* <p>
@@ -63,26 +65,10 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
* Builds the component into Adventure Component instance.
* @return the {@link Component} built from this {@link Chat} component.
*/
public Component getAdv() {
public Component get() {
return builder.build();
}
/**
* Builds the component into BungeeCord {@link BaseComponent} instance.
* @return the {@link BaseComponent} built from this {@link Chat} component.
*/
public BaseComponent get() {
return toBungee(getAdv());
}
/**
* Builds the component into BungeeCord {@link BaseComponent} array.
* @return the {@link BaseComponent} array built from this {@link Chat} component.
*/
public BaseComponent[] getAsArray() {
return toBungeeArray(getAdv());
}
private static final LegacyComponentSerializer LEGACY_SERIALIZER_BUNGEE_FRIENDLY = LegacyComponentSerializer.builder()
.hexColors()
.useUnusualXRepeatedCharacterHexFormat()
@@ -93,7 +79,7 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
* @return the legacy text. RGB colors are in BungeeCord format.
*/
public String getLegacyText() {
return LEGACY_SERIALIZER_BUNGEE_FRIENDLY.serialize(getAdv());
return LEGACY_SERIALIZER_BUNGEE_FRIENDLY.serialize(get());
}
/**
@@ -101,12 +87,12 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
* @return the plain text of this component.
*/
public String getPlainText() {
return PlainTextComponentSerializer.plainText().serializeOr(getAdv(), "");
return PlainTextComponentSerializer.plainText().serializeOr(get(), "");
}
@Override
public @NotNull HoverEvent<Component> asHoverEvent(@NotNull UnaryOperator<Component> op) {
return HoverEvent.showText(op.apply(getAdv()));
return HoverEvent.showText(op.apply(get()));
}
/**
@@ -115,7 +101,32 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
*/
@Override
public @NotNull Component asComponent() {
return getAdv();
return get();
}
/**
* Builds the component into Adventure Component instance, also down sampling the RGB colors to named colors.
* @return the {@link Component} built from this {@link Chat} component, with down-sampled colors.
*/
public Component getAsDownSampledColorsComponent() {
String json = GsonComponentSerializer.colorDownsamplingGson().serialize(get());
return GsonComponentSerializer.gson().deserialize(json);
}
/**
* Returns a new {@link Chat} consisting of this {@link Chat} instance, with the RGB colors down-sampled to named colors.
* @return a new {@link Chat} instance, with down-sampled colors.
*/
public Chat getAsDownSampledColors() {
return chatComponent(getAsDownSampledColorsComponent());
}
/**
* Returns a MiniMessage representation of this {@link Chat} component.
* @return the MiniMessage representation if this {@link Chat} component.
*/
public String getMiniMessage() {
return MiniMessage.miniMessage().serialize(get());
}
@@ -155,15 +166,6 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
return this;
}
/**
* Appends a BungeeCord {@link BaseComponent} to this component.
* @param comp the component to append.
* @return this.
*/
public Chat then(BaseComponent comp) {
return then(toAdventure(comp));
}
/**
* Appends a component to this component.
* @param comp the component to append.
@@ -178,15 +180,6 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
return then(comp.asComponent());
}
/**
* Appends a BungeeCord {@link BaseComponent} array to this component.
* @param comp the components to append.
* @return this.
*/
public Chat then(BaseComponent[] comp) {
return then(toAdventure(comp));
}
@@ -261,7 +254,7 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
* @param comp the component.
* @return this.
*/
public Chat thenPlayerName(Component comp) { return then(playerNameComponent(comp)); }
public Chat thenPlayerName(ComponentLike comp) { return then(playerNameComponent(comp)); }
/**
* Appends a component consisting of a new line.
@@ -270,12 +263,26 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
public Chat thenNewLine() { return then(Component.newline()); }
/**
* Appends a component with the provided legacy text as its content.
* @param legacyText the legacy text.
* Appends a component with the provided legacy text as its content, using the section {@code "§"} character.
* @param legacyText the legacy text that uses the {@code "§"} character.
* @return this.
*/
public Chat thenLegacyText(Object legacyText) { return then(legacyText(legacyText)); }
/**
* Appends a component with the provided legacy text as its content, using the ampersand {@code "&"} character.
* @param legacyText the legacy text that uses the {@code "&"} character.
* @return this.
*/
public Chat thenLegacyAmpersandText(Object legacyText) { return then(legacyAmpersandText(legacyText)); }
/**
* Appends a component with the provided MiniMessage text as its content.
* @param miniMessageText the MiniMessage text.
* @return this.
*/
public Chat thenMiniMessage(String miniMessageText) { return then(miniMessageText(miniMessageText)); }
/**
* Appends a component with the provided translation key and parameters.
* @param key the translation key.
@@ -452,17 +459,6 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
*/
public Chat thenLeftText(ComponentLike leftText) { return then(leftText(leftText, console)); }
/**
* Appends a component filling a chat line with the configured decoration character and
* color and a left-aligned text.
* @param leftText the text aligned to the left.
* @return a new {@link FormatableChat} filling a chat line with the configured decoration character
* and color and a left-aligned text.
* @deprecated uses Bungeecord chat API.
*/
@Deprecated
public Chat thenLeftText(BaseComponent leftText) { return thenLeftText(chatComponent(leftText)); }
/**
* Appends a component filling a chat line with the configured decoration character and
* color and a right-aligned text.
@@ -472,17 +468,6 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
*/
public Chat thenRightText(ComponentLike rightText) { return then(rightText(rightText, console)); }
/**
* Appends a component filling a chat line with the configured decoration character and
* color and a right-aligned text.
* @param rightText the text aligned to the right.
* @return a new {@link FormatableChat} filling a chat line with the configured decoration character
* and color and a right-aligned text.
* @deprecated uses Bungeecord chat API.
*/
@Deprecated
public Chat thenRightText(BaseComponent rightText) { return thenRightText(chatComponent(rightText)); }
/**
* Appends a component filling a chat line with the configured decoration character and
* color and a centered text.
@@ -494,19 +479,6 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
return then(centerText(centerText, console));
}
/**
* Appends a component filling a chat line with the configured decoration character and
* color and a centered text.
* @param centerText the text aligned to the center.
* @return a new {@link FormatableChat} filling a chat line with the configured decoration character
* and color and a centered text.
* @deprecated uses Bungeecord chat API.
*/
@Deprecated
public Chat thenCenterText(BaseComponent centerText) {
return thenCenterText(chatComponent(centerText));
}
/**
* Appends a component filling a chat line with the configured decoration character and color.
* @return a new {@link FormatableChat} filling a chat line with a decoration character and color.
@@ -586,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.
@@ -603,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);
}
/**
@@ -881,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.
@@ -920,7 +883,7 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
@Override
public int hashCode() {
return getAdv().hashCode();
return get().hashCode();
}
@Override
@@ -931,60 +894,41 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
/* package */ static ComponentLike filterObjToComponentLike(Object v) {
return switch (v) {
case ComponentLike componentLike -> componentLike;
case null, default -> Component.text(Objects.toString(v));
};
}
/* package */ static ComponentLike[] filterObjToComponentLike(Object[] values) {
if (values == null)
return null;
ComponentLike[] ret = new ComponentLike[values.length];
for (int i = 0; i < values.length; i++) {
Object v = values[i];
if (v instanceof BaseComponent[])
ret[i] = toAdventure((BaseComponent[]) v);
else if (v instanceof BaseComponent)
ret[i] = toAdventure((BaseComponent) v);
else if (v instanceof ComponentLike)
ret[i] = (ComponentLike) v;
else
ret[i] = Component.text(Objects.toString(v));
ret[i] = filterObjToComponentLike(values[i]);
}
return ret;
}
/**
* Converts the Bungee {@link BaseComponent} array into Adventure {@link Component}.
* @param components the Bungee {@link BaseComponent} array.
* @return a {@link Component}.
*/
public static Component toAdventure(BaseComponent[] components) {
return BungeeComponentSerializer.get().deserialize(components);
}
/**
* Converts the Bungee {@link BaseComponent} into Adventure {@link Component}.
* @param component the Bungee {@link BaseComponent}.
* @return a {@link Component}.
*/
public static Component toAdventure(BaseComponent component) {
return toAdventure(new BaseComponent[] { component });
/* package */ static TranslationArgumentLike[] filterObjToTranslationArgumentLike(Object[] values) {
if (values == null)
return null;
TranslationArgumentLike[] ret = new TranslationArgumentLike[values.length];
for (int i = 0; i < values.length; i++) {
Object v = values[i];
if (v instanceof Number n)
ret[i] = TranslationArgument.numeric(n);
else if (v instanceof Boolean b)
ret[i] = TranslationArgument.bool(b);
else
ret[i] = TranslationArgument.component(filterObjToComponentLike(values[i]));
}
return ret;
}
/**
* Converts the Adventure {@link Component} into Bungee {@link BaseComponent} array.
* @param component the Adventure {@link Component}.
* @return a {@link BaseComponent} array.
*/
public static BaseComponent[] toBungeeArray(Component component) {
return BungeeComponentSerializer.get().serialize(component);
}
/**
* Converts the Adventure {@link Component} into Bungee {@link BaseComponent}.
* @param component the Adventure {@link Component}.
* @return a {@link BaseComponent}.
*/
public static BaseComponent toBungee(Component component) {
BaseComponent[] arr = toBungeeArray(component);
return arr.length == 1 ? arr[0] : new net.md_5.bungee.api.chat.TextComponent(arr);
}
/**
* Force the italic formatting to be set to false if it is not explicitly set in the component.

View File

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

View File

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

View File

@@ -50,7 +50,7 @@ public class ChatConfig {
/**
* The color used to display data in a message.
*/
public static TextColor dataColor = PandaTheme.CHAT_GRAY_MID;
public static TextColor dataColor = NamedTextColor.GRAY;
/**
* The color used for displayed URLs and clickable URLs.
@@ -87,7 +87,7 @@ public class ChatConfig {
*/
public static int getPrefixWidth(boolean console) {
Chat c;
return prefix == null ? 0 : (c = prefix.get()) == null ? 0 : ChatUtil.componentWidth(c.getAdv(), console);
return prefix == null ? 0 : (c = prefix.get()) == null ? 0 : ChatUtil.componentWidth(c.get(), console);
}
@@ -157,5 +157,9 @@ public class ChatConfig {
.thenText("] ");
}
private PandaTheme() {}
}
private ChatConfig() {}
}

View File

@@ -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.
@@ -184,7 +195,7 @@ public class ChatFilledLine implements ComponentLike {
Chat d = Chat.chat()
.then(Chat.text(ChatUtil.repeatedChar(decorationChar, nbCharLeft)).color(decorationColor).bold(decorationBold))
.then(text);
if (decorationChar != ' ')
if (decorationChar != ' ' || spacesDecorationRightSide)
d.then(Chat.text(ChatUtil.repeatedChar(decorationChar, nbCharRight)).color(decorationColor).bold(decorationBold));
return (FormatableChat) d;
}

View File

@@ -1,7 +1,6 @@
package fr.pandacube.lib.chat;
import java.util.Objects;
import fr.pandacube.lib.chat.Chat.FormatableChat;
import net.kyori.adventure.text.BlockNBTComponent;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentBuilder;
@@ -16,10 +15,10 @@ import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.event.HoverEventSource;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.md_5.bungee.api.chat.BaseComponent;
import fr.pandacube.lib.chat.Chat.FormatableChat;
import java.util.Objects;
/**
* Abstract class holding the publicly accessible methods to create an instance of {@link Chat} component.
@@ -32,15 +31,6 @@ public abstract class ChatStatic {
return new FormatableChat(componentToBuilder(c));
}
/**
* Creates a {@link FormatableChat} from the provided Bungee {@link BaseComponent}.
* @param c the {@link BaseComponent}.
* @return a new {@link FormatableChat}.
*/
public static FormatableChat chatComponent(BaseComponent c) {
return new FormatableChat(componentToBuilder(Chat.toAdventure(c)));
}
/**
* Creates a {@link FormatableChat} from the provided {@link ComponentLike}.
* If the provided component is an instance of {@link Chat}, its content will be duplicated, and the provided one
@@ -60,15 +50,6 @@ public abstract class ChatStatic {
return new FormatableChat(Component.text());
}
/**
* Creates a {@link FormatableChat} from the provided Bungee {@link BaseComponent BaseComponent[]}.
* @param c the array of {@link BaseComponent}.
* @return a new {@link FormatableChat}.
*/
public static FormatableChat chatComponent(BaseComponent[] c) {
return chatComponent(Chat.toAdventure(c));
}
@@ -91,18 +72,58 @@ public abstract class ChatStatic {
/**
* Creates a {@link FormatableChat} with the provided legacy text as its content.
* @param legacyText the legacy text to use as the content.
* Creates a {@link FormatableChat} with the provided legacy text as its content, using the section {@code "§"}
* character.
* @param legacyText the legacy text to use as the content, that uses the {@code "§"} character.
* @return a new {@link FormatableChat} with the provided text as its content.
* @throws IllegalArgumentException If the {@code plainText} parameter is instance of {@link Chat} or
* @throws IllegalArgumentException If the {@code legacyText} parameter is instance of {@link Chat} or
* {@link Component}. The caller should use {@link #chatComponent(ComponentLike)}
* instead.
*/
public static FormatableChat legacyText(Object legacyText) {
return legacyText(legacyText, LegacyComponentSerializer.SECTION_CHAR);
}
/**
* Creates a {@link FormatableChat} with the provided legacy text as its content, using the ampersand {@code "&"}
* character.
* @param legacyText the legacy text to use as the content, that uses the {@code "&"} character.
* @return a new {@link FormatableChat} with the provided text as its content.
* @throws IllegalArgumentException If the {@code legacyText} parameter is instance of {@link Chat} or
* {@link Component}. The caller should use {@link #chatComponent(ComponentLike)}
* instead.
*/
public static FormatableChat legacyAmpersandText(Object legacyText) {
return legacyText(legacyText, LegacyComponentSerializer.AMPERSAND_CHAR);
}
/**
* Creates a {@link FormatableChat} with the provided legacy text as its content, using the specified
* legacyCharacter.
* @param legacyText the legacy text to use as the content.
* @param legacyCharacter the character used in the provided text to prefix color and format code.
* @return a new {@link FormatableChat} with the provided text as its content.
* @throws IllegalArgumentException If the {@code legacyText} parameter is instance of {@link Chat} or
* {@link Component}. The caller should use {@link #chatComponent(ComponentLike)}
* instead.
*/
private static FormatableChat legacyText(Object legacyText, char legacyCharacter) {
if (legacyText instanceof ComponentLike) {
throw new IllegalArgumentException("Expected any object except instance of " + ComponentLike.class + ". Received " + legacyText + ". Please use ChatStatic.chatComponent(ComponentLike) instead.");
}
return chatComponent(LegacyComponentSerializer.legacySection().deserialize(Objects.toString(legacyText)));
return chatComponent(LegacyComponentSerializer.legacy(legacyCharacter).deserialize(Objects.toString(legacyText)));
}
/**
* Creates a {@link FormatableChat} with the provided MiniMessage text as its content.
* @param miniMessageText the MiniMessage text to use as the content.
* @return a new {@link FormatableChat} with the provided text as its content.
*/
public static FormatableChat miniMessageText(String miniMessageText) {
return chatComponent(MiniMessage.miniMessage().deserialize(miniMessageText));
}
@@ -207,7 +228,7 @@ public abstract class ChatStatic {
* @param c the {@link Component}.
* @return a new {@link FormatableChat}.
*/
public static FormatableChat playerNameComponent(Component c) {
public static FormatableChat playerNameComponent(ComponentLike c) {
FormatableChat fc = chatComponent(c);
fc.builder.colorIfAbsent(NamedTextColor.WHITE);
return fc;
@@ -223,7 +244,7 @@ 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)));
}
/**
@@ -591,52 +612,39 @@ public abstract class ChatStatic {
private static ComponentBuilder<?, ?> componentToBuilder(Component c) {
ComponentBuilder<?, ?> builder;
if (c instanceof TextComponent) {
builder = Component.text()
.content(((TextComponent) c).content());
}
else if (c instanceof TranslatableComponent) {
builder = Component.translatable()
.key(((TranslatableComponent) c).key())
.args(((TranslatableComponent) c).args());
}
else if (c instanceof SelectorComponent) {
builder = Component.selector()
.pattern(((SelectorComponent) c).pattern());
}
else if (c instanceof ScoreComponent) {
builder = Component.score()
.name(((ScoreComponent) c).name())
.objective(((ScoreComponent) c).objective());
}
else if (c instanceof KeybindComponent) {
builder = Component.keybind()
.keybind(((KeybindComponent) c).keybind());
}
else if (c instanceof BlockNBTComponent) {
builder = Component.blockNBT()
.interpret(((BlockNBTComponent) c).interpret())
.nbtPath(((BlockNBTComponent) c).nbtPath())
.pos(((BlockNBTComponent) c).pos());
}
else if (c instanceof EntityNBTComponent) {
builder = Component.entityNBT()
.interpret(((EntityNBTComponent) c).interpret())
.nbtPath(((EntityNBTComponent) c).nbtPath())
.selector(((EntityNBTComponent) c).selector());
}
else if (c instanceof StorageNBTComponent) {
builder = Component.storageNBT()
.interpret(((StorageNBTComponent) c).interpret())
.nbtPath(((StorageNBTComponent) c).nbtPath())
.storage(((StorageNBTComponent) c).storage());
}
else {
throw new IllegalArgumentException("Unknown component type " + c.getClass());
}
ComponentBuilder<?, ?> builder = switch (c) {
case TextComponent textComponent -> Component.text()
.content(textComponent.content());
case TranslatableComponent translatableComponent -> Component.translatable()
.key(translatableComponent.key()).arguments(translatableComponent.arguments());
case SelectorComponent selectorComponent -> Component.selector()
.pattern(selectorComponent.pattern());
case ScoreComponent scoreComponent -> Component.score()
.name(scoreComponent.name())
.objective(scoreComponent.objective());
case KeybindComponent keybindComponent -> Component.keybind()
.keybind(keybindComponent.keybind());
case BlockNBTComponent blockNBTComponent -> Component.blockNBT()
.interpret(blockNBTComponent.interpret())
.nbtPath(blockNBTComponent.nbtPath())
.pos(blockNBTComponent.pos());
case EntityNBTComponent entityNBTComponent -> Component.entityNBT()
.interpret(entityNBTComponent.interpret())
.nbtPath(entityNBTComponent.nbtPath())
.selector(entityNBTComponent.selector());
case StorageNBTComponent storageNBTComponent -> Component.storageNBT()
.interpret(storageNBTComponent.interpret())
.nbtPath(storageNBTComponent.nbtPath())
.storage(storageNBTComponent.storage());
case null, default -> throw new IllegalArgumentException("Unknown component type " + (c == null ? "null" : c.getClass()));
};
return builder.style(c.style()).append(c.children());
}
/**
* Creates a new {@link ChatStatic} instance.
*/
protected ChatStatic() {}
}

View File

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

View File

@@ -1,5 +1,17 @@
package fr.pandacube.lib.chat;
import fr.pandacube.lib.chat.Chat.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,16 +22,7 @@ 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 and other parts of the game.
@@ -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;
@@ -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())
@@ -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);
@@ -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);
}
@@ -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 + ")");
}
@@ -602,5 +657,6 @@ public class ChatUtil {
return str;
}
private ChatUtil() {}
}

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ 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 handle general standard IO operation for a CLI application. It uses Jlines {@link ConsoleReader} for the

View File

@@ -2,7 +2,8 @@ package fr.pandacube.lib.cli;
import fr.pandacube.lib.cli.commands.CommandAdmin;
import fr.pandacube.lib.cli.commands.CommandStop;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.cli.log.CLILogger;
import fr.pandacube.lib.util.log.Log;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
@@ -14,17 +15,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,48 +63,74 @@ public abstract class CLIApplication {
}
}
/**
* Returns the application's {@link Logger}.
* @return the application's {@link Logger}.
*/
public Logger getLogger() {
return cli.getLogger();
}
private final Object stopLock = new Object();
private final Thread shutdownThread = new Thread(this::stop);
private final AtomicBoolean stopping = new AtomicBoolean(false);
@SuppressWarnings("finally")
/**
* Stops this application.
*/
public final void stop() {
synchronized (stopLock) {
synchronized (stopping) {
if (stopping.get())
return;
stopping.set(true);
}
Log.info("Stopping " + getName() + " version " + getClass().getPackage().getImplementationVersion());
try {
end();
} catch (Throwable t) {
Log.severe("Error stopping application " + getName() + " version " + getClass().getPackage().getImplementationVersion(), t);
} finally {
Log.info("Bye bye.");
System.exit(0);
}
synchronized (stopping) {
if (stopping.get())
return;
stopping.set(true);
}
Log.info("Stopping " + getName() + " version " + getClass().getPackage().getImplementationVersion());
try {
end();
} catch (Throwable t) {
Log.severe("Error stopping application " + getName() + " version " + getClass().getPackage().getImplementationVersion(), t);
} finally {
Log.info("Bye bye.");
CLILogger.actuallyResetLogManager();
if (!Thread.currentThread().equals(shutdownThread))
System.exit(0);
}
}
/**
* Tells if this application is currently stopping, that is the {@link #stop()} method has been called.
* @return true if the application is stopping, false otherwise.
*/
public boolean isStopping() {
return stopping.get();
}
/**
* Gets the name of this application.
* @return the name of this application.
*/
public abstract String getName();
/**
* Method to override to initialize stuff in this application.
* This method is called on instanciation of this Application.
* @throws Exception If an exception is thrown, the application will not start.
*/
protected abstract void start() throws Exception;
/**
* Method to override to reload specific stuff in this application.
* This method is called by using the command {@code admin reload}.
*/
public abstract void reload();
/**
* Method to override to execute stuff when this application stops.
* This method is called once before this application terminates, possibly from a shutdown hook Thread.
*/
protected abstract void end();

View File

@@ -25,6 +25,9 @@ public class CLIBrigadierDispatcher extends BrigadierDispatcher<CLICommandSender
public static final CLICommandSender CLI_CONSOLE_COMMAND_SENDER = new CLIConsoleCommandSender();
private CLIBrigadierDispatcher() {}
/**
* Executes the provided command as the console.
* @param commandWithoutSlash the command, without the eventual slash at the beginning.

View File

@@ -1,7 +1,7 @@
package fr.pandacube.lib.cli.commands;
import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
import net.kyori.adventure.audience.MessageType;
import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.text.Component;
@@ -11,6 +11,12 @@ import org.jetbrains.annotations.NotNull;
* The console command sender.
*/
public class CLIConsoleCommandSender implements CLICommandSender {
/**
* Creates a new console command sender.
*/
protected CLIConsoleCommandSender() {}
public String getName() {
return "Console";
}

View File

@@ -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")
@@ -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;

View File

@@ -5,10 +5,15 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import fr.pandacube.lib.cli.CLIApplication;
/**
* /stop (/end) command.
* the {@code stop} (or {@code end}) command for a {@link CLIApplication}.
*/
public class CommandStop extends CLIBrigadierCommand {
/**
* Initializes the admin command.
*/
public CommandStop() {}
@Override
protected LiteralArgumentBuilder<CLICommandSender> buildCommand() {
return literal("stop")

View File

@@ -1,9 +1,10 @@
package fr.pandacube.lib.cli.log;
import fr.pandacube.lib.cli.CLI;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.cli.CLIApplication;
import fr.pandacube.lib.util.ThrowableUtil;
import fr.pandacube.lib.util.logs.DailyLogRotateFileHandler;
import fr.pandacube.lib.util.log.DailyLogRotateFileHandler;
import fr.pandacube.lib.util.log.Log;
import net.md_5.bungee.log.ColouredWriter;
import net.md_5.bungee.log.ConciseFormatter;
@@ -13,6 +14,7 @@ import java.io.PrintStream;
import java.util.Scanner;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
/**
@@ -20,8 +22,31 @@ import java.util.logging.Logger;
*/
public class CLILogger {
static {
System.setProperty("java.util.logging.manager", ShutdownHookDelayerLogManager.class.getName());
}
private static Logger logger = null;
private static class ShutdownHookDelayerLogManager extends LogManager {
static ShutdownHookDelayerLogManager instance;
public ShutdownHookDelayerLogManager() { instance = this; }
@Override public void reset() { /* don't reset yet. */ }
private void actuallyReset() { super.reset(); }
}
/**
* Tells the LogManager to actually reset.
* <p>
* This method is called by the shutdown hook of {@link CLIApplication}, because the {@link CLILogger} uses a custom
* {@link LogManager} that bypass the reset process during the shutdown of the process.
*/
public static void actuallyResetLogManager() {
ShutdownHookDelayerLogManager.instance.actuallyReset();
}
/**
* Initialize and return the logger for this application.
* @param cli the CLI instance to use
@@ -46,6 +71,8 @@ public class CLILogger {
System.setOut(newRedirector(logger, Level.INFO));
Log.setLogger(logger);
Thread.setDefaultUncaughtExceptionHandler((t, e) -> Log.severe("Uncaught Exception in thread " + t.getName(), e));
}
return logger;
}
@@ -70,5 +97,7 @@ public class CLILogger {
t.start();
return ps;
}
private CLILogger() {}
}

View File

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

View File

@@ -10,9 +10,10 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.tree.LiteralCommandNode;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
@@ -24,6 +25,11 @@ import java.util.function.Predicate;
*/
public abstract class BrigadierCommand<S> {
/**
* Creates a Brigadier command.
*/
public BrigadierCommand() {}
/**
* Returns a builder for this command.
@@ -235,9 +241,12 @@ public abstract class BrigadierCommand<S> {
args = Arrays.copyOf(args, args.length + 1);
args[args.length - 1] = message.substring(tokenStartPos);
for (String s : suggestions.getSuggestions(sender, args.length - 1, args[args.length - 1], args)) {
if (s != null)
builder.suggest(s);
List<String> wrappedResult = suggestions.getSuggestions(sender, args.length - 1, args[args.length - 1], args);
if (wrappedResult != null) {
for (String s : wrappedResult) {
if (s != null)
builder.suggest(s);
}
}
} catch (Throwable e) {
Log.severe("Error while tab-completing '" + message + "' for " + sender, e);

View File

@@ -6,7 +6,7 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.tree.LiteralCommandNode;
import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
import net.kyori.adventure.text.ComponentLike;
import java.util.concurrent.CompletableFuture;
@@ -21,6 +21,11 @@ public abstract class BrigadierDispatcher<S> {
private final CommandDispatcher<S> dispatcher = new CommandDispatcher<>();
/**
* Creates a new Dispatcher instance.
*/
public BrigadierDispatcher() {}
/**
* Registers the provided command node into this dispatcher.

View File

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

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

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

View File

@@ -1,7 +1,4 @@
package fr.pandacube.lib.core.config;
import fr.pandacube.lib.chat.ChatColorUtil;
import fr.pandacube.lib.util.Log;
package fr.pandacube.lib.config;
import java.io.BufferedReader;
import java.io.File;
@@ -56,7 +53,7 @@ public abstract class AbstractConfig {
while ((line = reader.readLine()) != null) {
String trimmedLine = line.trim();
if (ignoreEmpty && trimmedLine.equals(""))
if (ignoreEmpty && trimmedLine.isEmpty())
continue;
if (ignoreHashtagComment && trimmedLine.startsWith("#"))
@@ -114,25 +111,6 @@ public abstract class AbstractConfig {
}
/**
* Utility method to that translate the {@code '&'} formatted string to the legacy format.
* @param string the string to convert.
* @return a legacy formatted string (using {@code '§'}).
*/
public static String getTranslatedColorCode(String string) {
return ChatColorUtil.translateAlternateColorCodes('&', string);
}
/**
* Logs the message as a warning into console, prefixed with the context of this config.
* @param message the message to log.
*/
protected void warning(String message) {
Log.warning("Error in configuration '"+configFile.getName()+"': " + message);
}
/**
* The type of config.
*/

View File

@@ -1,4 +1,4 @@
package fr.pandacube.lib.core.config;
package fr.pandacube.lib.config;
import java.io.File;
import java.io.IOException;

View File

@@ -37,6 +37,12 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<!-- Cron expression interpreter -->
<dependency>
<groupId>ch.eitchnet</groupId>
@@ -50,7 +56,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.3.0</version>
<version>3.5.2</version>
<executions>
<execution>
<phase>package</phase>
@@ -104,6 +110,7 @@
<outputDirectory>${project.basedir}/src/main/resources/fr/pandacube/lib/core/mc_version</outputDirectory>
<outputFileName>mcversion.json</outputFileName>
<skipCache>true</skipCache>
<overwrite>true</overwrite>
</configuration>
</plugin>
</plugins>

View File

@@ -1,8 +1,8 @@
package fr.pandacube.lib.core.backup;
import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.util.Log;
import net.md_5.bungee.api.ChatColor;
import fr.pandacube.lib.chat.LegacyChatFormat;
import fr.pandacube.lib.util.log.Log;
import java.io.File;
import java.time.LocalDateTime;
@@ -70,6 +70,11 @@ public abstract class BackupCleaner implements UnaryOperator<TreeSet<LocalDateTi
};
}
/**
* Create a backup cleaner.
*/
public BackupCleaner() {}
/**
* Creates a new {@link BackupCleaner} that keeps the archives kept by this {@link BackupCleaner} or by the provided
@@ -102,14 +107,14 @@ public abstract class BackupCleaner implements UnaryOperator<TreeSet<LocalDateTi
if (files == null)
return;
Log.info("[Backup] Cleaning up backup directory " + ChatColor.GRAY + compressDisplayName + ChatColor.RESET + "...");
Log.info("[Backup] Cleaning up backup directory " + LegacyChatFormat.GRAY + compressDisplayName + LegacyChatFormat.RESET + "...");
TreeMap<LocalDateTime, File> datedFiles = new TreeMap<>();
for (String filename : files) {
File file = new File(archiveDir, filename);
if (!filename.matches("\\d{8}-\\d{6}\\.zip")) {
Log.warning("[Backup] " + ChatColor.GRAY + compressDisplayName + ChatColor.RESET + " Invalid file in backup directory: " + filename);
Log.warning("[Backup] " + LegacyChatFormat.GRAY + compressDisplayName + LegacyChatFormat.RESET + " Invalid file in backup directory: " + filename);
continue;
}
@@ -118,7 +123,7 @@ public abstract class BackupCleaner implements UnaryOperator<TreeSet<LocalDateTi
try {
ldt = LocalDateTime.parse(dateTimeStr, BackupProcess.dateFileNameFormatter);
} catch (DateTimeParseException e) {
Log.warning("[Backup] " + ChatColor.GRAY + compressDisplayName + ChatColor.RESET + " Unable to parse file name to a date-time: " + filename, e);
Log.warning("[Backup] " + LegacyChatFormat.GRAY + compressDisplayName + LegacyChatFormat.RESET + " Unable to parse file name to a date-time: " + filename, e);
continue;
}
@@ -151,7 +156,7 @@ public abstract class BackupCleaner implements UnaryOperator<TreeSet<LocalDateTi
if (testOnly || oneDeleted)
Log.warning(c.getLegacyText());
Log.info("[Backup] Backup directory " + ChatColor.GRAY + compressDisplayName + ChatColor.RESET + " cleaned.");
Log.info("[Backup] Backup directory " + LegacyChatFormat.GRAY + compressDisplayName + LegacyChatFormat.RESET + " cleaned.");
}

View File

@@ -1,6 +1,6 @@
package fr.pandacube.lib.core.backup;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
import java.io.File;
import java.time.ZonedDateTime;
@@ -66,7 +66,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() {
@@ -90,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) {

View File

@@ -1,10 +1,10 @@
package fr.pandacube.lib.core.backup;
import fc.cron.CronExpression;
import fr.pandacube.lib.chat.LegacyChatFormat;
import fr.pandacube.lib.core.cron.CronScheduler;
import fr.pandacube.lib.util.FileUtils;
import fr.pandacube.lib.util.Log;
import net.md_5.bungee.api.ChatColor;
import fr.pandacube.lib.util.log.Log;
import java.io.File;
import java.text.DateFormat;
@@ -209,7 +209,7 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab
File sourceDir = getSourceDir();
if (!sourceDir.exists()) {
Log.warning("[Backup] Unable to compress " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + ": source directory " + sourceDir + " doesn't exist");
Log.warning("[Backup] Unable to compress " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + ": source directory " + sourceDir + " doesn't exist");
return;
}
@@ -219,7 +219,7 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab
onBackupStart();
new Thread(() -> {
Log.info("[Backup] Starting for " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " ...");
Log.info("[Backup] Starting for " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + " ...");
compressor = new ZipCompressor(sourceDir, target, 9, filter);
@@ -229,7 +229,7 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab
success = true;
Log.info("[Backup] Finished for " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET);
Log.info("[Backup] Finished for " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET);
try {
BackupCleaner cleaner = getBackupCleaner();
@@ -267,7 +267,7 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab
* Logs the scheduling status of this backup process.
*/
public void displayNextSchedule() {
Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " next backup on "
Log.info("[Backup] " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + " next backup on "
+ DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG).format(new Date(getNext())));
}
@@ -297,7 +297,7 @@ public abstract class BackupProcess implements Comparable<BackupProcess>, Runnab
public void logProgress() {
if (compressor == null)
return;
Log.info("[Backup] " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + ": " + compressor.getState().getLegacyText());
Log.info("[Backup] " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + ": " + compressor.getState().getLegacyText());
}

View File

@@ -3,7 +3,7 @@ package fr.pandacube.lib.core.backup;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import fr.pandacube.lib.core.json.Json;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
import java.io.File;
import java.io.FileReader;

View File

@@ -1,8 +1,8 @@
package fr.pandacube.lib.core.backup;
import com.google.common.io.Files;
import fr.pandacube.lib.util.Log;
import net.md_5.bungee.api.ChatColor;
import fr.pandacube.lib.chat.LegacyChatFormat;
import fr.pandacube.lib.util.log.Log;
import java.io.File;
import java.io.IOException;
@@ -53,7 +53,7 @@ public class RotatedLogsBackupProcess extends BackupProcess {
if (!getSourceDir().isDirectory())
return;
Log.info("[Backup] Starting for " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET + " ...");
Log.info("[Backup] Starting for " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET + " ...");
try {
// wait a little after the log message above, in case the log file rotation has to be performed.
@@ -82,9 +82,9 @@ public class RotatedLogsBackupProcess extends BackupProcess {
success = true;
Log.info("[Backup] Finished for " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET);
Log.info("[Backup] Finished for " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET);
} catch (Exception e) {
Log.severe("[Backup] Failed for : " + ChatColor.GRAY + getDisplayName() + ChatColor.RESET, e);
Log.severe("[Backup] Failed for : " + LegacyChatFormat.GRAY + getDisplayName() + LegacyChatFormat.RESET, e);
} finally {
onBackupEnd(success);

View File

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

View File

@@ -4,7 +4,7 @@ import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import fc.cron.CronExpression;
import fr.pandacube.lib.core.json.Json;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
import java.io.File;
import java.io.FileReader;
@@ -54,7 +54,7 @@ public class CronScheduler {
long now = System.currentTimeMillis();
if (!tasks.isEmpty()) {
CronTask next = tasks.get(0);
CronTask next = tasks.getFirst();
if (next.nextRun <= now) {
next.runAsync();
setLastRun(next.taskId, now);
@@ -224,5 +224,6 @@ public class CronScheduler {
.toEpochMilli();
}
private CronScheduler() {}
}

View File

@@ -2,11 +2,15 @@ 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;
@@ -17,6 +21,52 @@ import java.util.function.Function;
*/
public class Json {
/**
* Makes Gson deserialize numbers to Number subclasses the same way SnakeYAML does
*/
private static final ToNumberStrategy YAML_EQUIVALENT_NUMBER_STRATEGY = in -> {
String value = in.nextString();
// YAML uses Regex to resolve values as INT or FLOAT (see org.yaml.snakeyaml.resolver.Resolver), trying FLOAT first.
// We see in the regex that FLOAT MUST have a "." in the string, but INT must not, so we try that.
boolean isFloat = value.contains(".");
if (isFloat) {
// if 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)}.
@@ -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() {}
}

View File

@@ -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);
}
}
}
}

View File

@@ -10,7 +10,7 @@ import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.bind.TreeTypeAdapter;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
import fr.pandacube.lib.util.ThrowableUtil;
import java.lang.reflect.Constructor;
@@ -30,6 +30,8 @@ public class ThrowableAdapter implements JsonSerializer<Throwable>, JsonDeserial
/* package */ static final TypeAdapterFactory FACTORY = TreeTypeAdapter.newTypeHierarchyFactory(Throwable.class, new ThrowableAdapter());
private ThrowableAdapter() {}
@Override
public Throwable deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {

View File

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

View File

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

View File

@@ -135,4 +135,7 @@ public class MinecraftVersionUtil {
return set;
}
private MinecraftVersionUtil() {}
}

View File

@@ -1,7 +1,7 @@
package fr.pandacube.lib.core.mc_version;
import fr.pandacube.lib.core.json.Json;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
import fr.pandacube.lib.util.StringUtil;
import org.jetbrains.annotations.NotNull;
@@ -156,8 +156,9 @@ public class ProtocolVersion implements Comparable<ProtocolVersion> {
* @return all the {@link ProtocolVersion} currently known by this class.
*/
public static List<ProtocolVersion> allKnownProtocolVersions() {
return versionList.get().versionsOfProtocol().keySet().stream()
.map(ProtocolVersion::ofProtocol)
return versionList.get().versionsOfProtocol().entrySet().stream()
.filter(e -> e.getValue() != null && !e.getValue().isEmpty())
.map(e -> new ProtocolVersion(e.getKey(), List.copyOf(e.getValue())))
.toList();
}
@@ -215,6 +216,22 @@ public class ProtocolVersion implements Comparable<ProtocolVersion> {
return displayOptimizedListOfVersions(List.of(this), finalWordSeparator);
}
/**
* Gets the first (earliest) Minecraft version that supports this protocol version.
* @return the first (earliest) Minecraft version that supports this protocol version.
*/
public String getFirstVersion() {
return versions.get(0);
}
/**
* Gets the last (latest) Minecraft version that supports this protocol version.
* @return the last (latest) Minecraft version that supports this protocol version.
*/
public String getLastVersion() {
return versions.get(versions.size() - 1);
}
@Override
public boolean equals(Object o) {
return o instanceof ProtocolVersion pv && protocolVersionNumber == pv.protocolVersionNumber;

View File

@@ -2,7 +2,7 @@ package fr.pandacube.lib.core.search;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
import java.util.ArrayList;
import java.util.HashMap;

View File

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

View File

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

View File

@@ -15,7 +15,7 @@ import java.util.Objects;
import java.util.function.Consumer;
import fr.pandacube.lib.reflect.Reflect;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
/**
* Static class to handle most of the database operations.

View File

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

View File

@@ -26,7 +26,7 @@ import java.util.UUID;
import java.util.stream.Collectors;
import fr.pandacube.lib.util.EnumUtil;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
/**
* Represents an entry in a SQL table. Each subclass is for a specific table.
@@ -241,17 +241,24 @@ public abstract class SQLElement<E extends SQLElement<E>> {
* Gets the value of the provided field in this entry.
* @param field the field to get the value from.
* @return the value of the provided field in this entry.
* @throws IllegalArgumentException if the provided field is null or not from the table represented by this class.
* @throws IllegalStateException if the field is not nullable and there is no value set
* @param <T> the Java type of the field.
*/
public <T> T get(SQLField<E, T> field) {
if (field == null) throw new IllegalArgumentException("field can't be null");
if (field == null)
throw new IllegalArgumentException("field can't be null");
if (!fields.containsKey(field.getName()) || !fields.get(field.getName()).equals(field))
throw new IllegalArgumentException("The provided field " + field + " is not from this table " + getClass().getName());
if (values.containsKey(field)) {
@SuppressWarnings("unchecked")
T val = (T) values.get(field);
return val;
}
throw new IllegalArgumentException("The field '" + field.getName() + "' in this instance of " + getClass().getName()
+ " does not exist or is not set");
if (field.nullable)
return null;
throw new IllegalStateException("The non-nullable field '" + field.getName() + "' in this instance of " + getClass().getName()
+ " is not set");
}
/**
@@ -354,7 +361,7 @@ public abstract class SQLElement<E extends SQLElement<E>> {
first = false;
concatValues.append(" ? ");
concatFields.append("`").append(entry.getKey().getName()).append("`");
addValueToSQLObjectList(psValues, entry.getKey(), entry.getValue());
psValues.add(entry.getKey().fromJavaTypeToJDBCType(entry.getValue()));
}
try (Connection c = db.getConnection();
@@ -382,20 +389,6 @@ public abstract class SQLElement<E extends SQLElement<E>> {
return (E) this;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
/* package */ static <E extends SQLElement<E>> void addValueToSQLObjectList(List<Object> list, SQLField<E, ?> field, Object jValue) throws DBException {
if (jValue != null && field.type instanceof SQLCustomType) {
try {
jValue = ((SQLCustomType)field.type).javaToDbConv.apply(jValue);
} catch (Exception e) {
throw new DBException("Error while converting value of field '"+field.getName()+"' with SQLCustomType from "+field.type.getJavaType()
+"(java source) to "+((SQLCustomType<?, ?>)field.type).intermediateJavaType+"(jdbc destination). The original value is '"+jValue+"'", e);
}
}
list.add(jValue);
}
/**
* Tells if this entry is currently stored in DB or not.
* @return true if this entry is currently stored in DB, or false otherwise.

View File

@@ -16,6 +16,11 @@ import java.util.stream.Collectors;
*/
public class SQLElementList<E extends SQLElement<E>> extends ArrayList<E> {
/**
* Creates an empty list of sql elements.
*/
public SQLElementList() {}
/**
* Stores all the values modified by {@link #setCommon(SQLField, Object)}.
*/

View File

@@ -1,6 +1,6 @@
package fr.pandacube.lib.db;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
/**
* A foreign key field in a SQL table.

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import java.io.PrintStream;
import java.net.InetAddress;
import java.net.Socket;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
public abstract class AbstractRequestExecutor {

View File

@@ -1,6 +1,6 @@
package fr.pandacube.lib.netapi.server;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
import java.io.IOException;
import java.net.InetAddress;

View File

@@ -5,7 +5,7 @@ import java.io.PrintStream;
import java.net.Socket;
import fr.pandacube.lib.netapi.server.RequestAnalyser.BadRequestException;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
/**
* Prends en charge un socket client et le transmet au gestionnaire de paquet

View File

@@ -16,7 +16,7 @@
<repositories>
<repository>
<id>papermc</id>
<url>https://papermc.io/repo/repository/maven-public/</url>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
<!-- WorldEdit -->
@@ -77,7 +77,7 @@
<dependency>
<groupId>com.sk89q.worldedit</groupId>
<artifactId>worldedit-bukkit</artifactId>
<version>7.2.9</version>
<version>7.2.19</version>
<scope>provided</scope>
<exclusions>
<exclusion>

View File

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

View File

@@ -6,7 +6,7 @@ import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import fr.pandacube.lib.permissions.Permissions;
import fr.pandacube.lib.reflect.Reflect;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.log.Log;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.entity.Player;

View File

@@ -2,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;
@@ -10,28 +12,55 @@ import org.bukkit.plugin.ServicePriority;
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();
}

View File

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

View File

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

View File

@@ -1,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() {}
}

View File

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

View File

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

View File

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

View File

@@ -3,8 +3,15 @@ package fr.pandacube.lib.paper.backup;
import java.io.File;
import java.util.function.BiPredicate;
/**
* A backup process with specific logic around Paper server working directory.
*/
public class PaperWorkdirProcess extends PaperBackupProcess {
/**
* Instantiates a new backup process for the paper server working directory.
* @param bm the associated backup manager.
*/
protected PaperWorkdirProcess(PaperBackupManager bm) {
super(bm, "workdir");
}

View File

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

View File

@@ -1,8 +1,8 @@
package fr.pandacube.lib.paper.commands;
import com.destroystokyo.paper.brigadier.BukkitBrigadierCommandSource;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.SuggestionProvider;
@@ -10,118 +10,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.util.ThrowableUtil.wrapEx;
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 {
wrapEx(PandalibPaperReflect::init);
vanillaCommandDispatcher = ReflectWrapper.wrapTyped(Bukkit.getServer(), CraftServer.class)
.getServer()
.vanillaCommandDispatcher();
nmsDispatcher = vanillaCommandDispatcher.dispatcher();
/**
* Gets the Brigadier dispatcher provided by paper API during {@link LifecycleEvents#COMMANDS}.
* <p>
* This Dispatcher is not the vanilla one. Instead, Paper implementation wraps the vanilla one to handle proper registration
* of commands from plugins.
* @return the Brigadier dispatcher.
*/
public static CommandDispatcher<CommandSourceStack> getVanillaPaperDispatcher() {
return vanillaPaperDispatcher;
}
/**
* Gets the root node of the dispatcher from {@link #getVanillaPaperDispatcher()}.
* @return the root node, or null if {@link #getVanillaPaperDispatcher()} is also null.
*/
public static RootCommandNode<CommandSourceStack> getRootNode() {
return vanillaPaperDispatcher == null ? null : vanillaPaperDispatcher.getRoot();
}
private static void updateVanillaPaperDispatcher(CommandDispatcher<CommandSourceStack> newDispatcher) {
if (vanillaPaperDispatcher == null || newDispatcher != vanillaPaperDispatcher) {
vanillaPaperDispatcher = newDispatcher;
// vanillaPaperDispatcher.getRoot() is not the real root but a wrapped root. Trying to map the fake root with the real one to trick the Paper/Brigadier (un)wrapper
RootCommandNode<CommandSourceStack> wrappedRoot = vanillaPaperDispatcher.getRoot();
ReflectClass<?> apiMirrorRootNodeClass = Reflect.ofClassOfInstance(wrappedRoot);
try {
RootCommandNode<?> unwrappedRoot = ((CommandDispatcher<?>) apiMirrorRootNodeClass.method("getDispatcher").invoke(wrappedRoot)).getRoot();
Reflect.ofClass(CommandNode.class).field("unwrappedCached").setValue(wrappedRoot, unwrappedRoot);
Reflect.ofClass(CommandNode.class).field("wrappedCached").setValue(unwrappedRoot, wrappedRoot);
} catch (InvocationTargetException|IllegalAccessException|NoSuchMethodException|NoSuchFieldException e) {
Log.severe("Unable to trick the Paper/Brigadier unwrapper to properly handle commands redirecting to root command node.", e);
}
}
}
/**
* Removes a plugin command that overrides a vanilla command, so the vanilla command functionalities are fully
* restored (so, not only the usage, but also the suggestions and the command structure sent to the client).
* @param name the name of the command to restore.
*/
public static void restoreVanillaCommand(String name) {
CommandMap bukkitCmdMap = Bukkit.getCommandMap();
Command bukkitCommand = bukkitCmdMap.getCommand(name);
if (bukkitCommand != null) {
if (VanillaCommandWrapper.REFLECT.get().isInstance(bukkitCommand)) {
//Log.info("Command /" + name + " is already a vanilla command.");
PandaLibPaper.getPlugin().getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS,
event -> updateVanillaPaperDispatcher(event.registrar().getDispatcher()));
Bukkit.getServer().getScheduler().runTask(PandaLibPaper.getPlugin(), () -> {
if (vanillaPaperDispatcher == null)
return;
CommandNode<CommandSourceStack> targetCommand = vanillaPaperDispatcher.getRoot().getChild("minecraft:" + name);
if (targetCommand == null) {
Log.warning("There is no vanilla command '" + name + "' to restore.");
return;
}
Log.info("Removing Bukkit command /" + name + " (" + getCommandIdentity(bukkitCommand) + ")");
bukkitCmdMap.getKnownCommands().remove(name.toLowerCase(java.util.Locale.ENGLISH));
bukkitCommand.unregister(bukkitCmdMap);
LiteralCommandNode<BukkitBrigadierCommandSource> node = (LiteralCommandNode<BukkitBrigadierCommandSource>) getRootNode().getChild(name);
Command newCommand = new VanillaCommandWrapper(vanillaCommandDispatcher, node).__getRuntimeInstance();
bukkitCmdMap.getKnownCommands().put(name.toLowerCase(), newCommand);
newCommand.register(bukkitCmdMap);
}
CommandNode<CommandSourceStack> eventuallyBadCommandToReplace = vanillaPaperDispatcher.getRoot().getChild(name);
Boolean isPluginCommand = isPluginCommand(eventuallyBadCommandToReplace);
if (isPluginCommand != null && isPluginCommand) {
Log.info(getCommandIdentity(eventuallyBadCommandToReplace) + " found in the dispatcher. Restoring the vanilla command.");
vanillaPaperDispatcher.getRoot().getChildren().removeIf(c -> c.getName().equals(name));
vanillaPaperDispatcher.getRoot().addChild(getAliasNode(targetCommand, name));
}
/*else if (isPluginCommand == null) {
Log.info(getCommandIdentity(eventuallyBadCommandToReplace) + " found in the dispatcher. Unsure if we restore the vanilla command.");
}*/
});
}
/**
* Returns the vanilla instance of the Brigadier dispatcher.
* @return the vanilla instance of the Brigadier dispatcher.
*/
public static CommandDispatcher<BukkitBrigadierCommandSource> getNMSDispatcher() {
return nmsDispatcher;
}
/**
* Returns the root command node of the Brigadier dispatcher.
* @return the root command node of the Brigadier dispatcher.
*/
protected static RootCommandNode<BukkitBrigadierCommandSource> getRootNode() {
return nmsDispatcher.getRoot();
}
@@ -130,7 +140,16 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
/**
* The command node of this command.
*/
protected final LiteralCommandNode<BukkitBrigadierCommandSource> commandNode;
protected LiteralCommandNode<CommandSourceStack> commandNode;
/**
* The command requested aliases.
*/
protected final String[] aliases;
/**
* The command description.
*/
protected final String description;
private final RegistrationPolicy registrationPolicy;
@@ -145,13 +164,13 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
public PaperBrigadierCommand(Plugin pl, RegistrationPolicy regPolicy) {
plugin = pl;
registrationPolicy = regPolicy;
commandNode = buildCommand().build();
postBuildCommand(commandNode);
String[] aliasesTmp = getAliases();
aliases = aliasesTmp == null ? new String[0] : aliasesTmp;
description = getDescription();
register();
Bukkit.getPluginManager().registerEvents(this, plugin);
try {
PandalibPaperPermissions.addPermissionMapping("minecraft.command." + commandNode.getLiteral().toLowerCase(), getTargetPermission().toLowerCase());
} catch (NoClassDefFoundError ignored) { }
//try {
// PandalibPaperPermissions.addPermissionMapping("minecraft.command." + commandNode.getLiteral().toLowerCase(), getTargetPermission().toLowerCase());
//} catch (NoClassDefFoundError ignored) { }
}
/**
@@ -166,163 +185,179 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
private void register() {
plugin.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event -> {
updateVanillaPaperDispatcher(event.registrar().getDispatcher());
String[] aliases = getAliases();
if (aliases == null)
aliases = new String[0];
commandNode = buildCommand().build();
postBuildCommand(commandNode);
String pluginName = plugin.getName().toLowerCase();
if (vanillaPaperDispatcher.getRoot().getChild(commandNode.getName()) != null) {
Log.info("Command /" + commandNode.getName() + " found in the vanilla dispatcher during initial command registration. Replacing it by force.");
vanillaPaperDispatcher.getRoot().getChildren().removeIf(c -> c.getName().equals(commandNode.getName()));
}
registeredAliases = new HashSet<>();
registerNode(commandNode, false);
registerAlias(pluginName + ":" + commandNode.getLiteral(), true);
registeredAliases = new HashSet<>(event.registrar().register(commandNode, description, List.of(aliases)));
doPostRegistrationFixes();
for (String alias : aliases) {
registerAlias(alias, false);
registerAlias(pluginName + ":" + alias, true);
if (registrationPolicy == RegistrationPolicy.ALL) {
// enforce registration of aliases
for (String alias : aliases) {
if (!registeredAliases.contains(alias)) {
Log.info("Command /" + commandNode.getName() + ": forcing registration of alias " + alias);
registeredAliases.addAll(event.registrar().register(getAliasNode(commandNode, alias), description));
}
}
}
Bukkit.getServer().getScheduler().runTask(plugin, () -> {
if (vanillaPaperDispatcher == null)
return;
Set<String> forceRegistrationAgain = new HashSet<>();
forceRegistrationAgain.add(commandNode.getName());
if (registrationPolicy == RegistrationPolicy.ALL)
forceRegistrationAgain.addAll(List.of(aliases));
for (String aliasToForce : forceRegistrationAgain) {
CommandNode<CommandSourceStack> actualNode = vanillaPaperDispatcher.getRoot().getChild(aliasToForce);
if (actualNode != null) {
//Log.info("Forcing registration of alias /" + aliasToForce + " for command /" + commandNode.getName() + ": replacing " + getCommandIdentity(actualNode) + "?");
if (PluginCommandNode.REFLECT.get().isInstance(actualNode)) {
PluginCommandNode pcn = wrap(actualNode, PluginCommandNode.class);
if (pcn.getPlugin().equals(plugin))
return;
}
else if (BukkitCommandNode.REFLECT.get().isInstance(actualNode)) {
BukkitCommandNode bcn = wrap(actualNode, BukkitCommandNode.class);
if (bcn.getBukkitCommand() instanceof PluginCommand pc && pc.getPlugin().equals(plugin))
return;
}
vanillaPaperDispatcher.getRoot().getChildren().removeIf(c -> c.getName().equals(aliasToForce));
}
/*else {
Log.info("Forcing registration of alias /" + aliasToForce + " for command /" + commandNode.getName() + ": no command found for alias. Adding alias.");
}*/
LiteralCommandNode<CommandSourceStack> newPCN = unwrap(new PluginCommandNode(aliasToForce, plugin.getPluginMeta(), commandNode, description));
vanillaPaperDispatcher.getRoot().addChild(newPCN);
}
});
});
}
private void doPostRegistrationFixes() {
postRegistrationFixNode(new HashSet<>(), commandNode);
}
private void postRegistrationFixNode(Set<CommandNode<CommandSourceStack>> fixedNodes, CommandNode<CommandSourceStack> originalNode) {
if (originalNode instanceof RootCommandNode)
return;
if (fixedNodes.contains(originalNode))
return;
fixedNodes.add(originalNode);
if (originalNode.getRedirect() != null) {
try {
@SuppressWarnings("rawtypes")
ReflectClass<CommandNode> cmdNodeClass = Reflect.ofClass(CommandNode.class);
@SuppressWarnings("unchecked")
CommandNode<CommandSourceStack> unwrappedNode = (CommandNode<CommandSourceStack>) cmdNodeClass.field("unwrappedCached").getValue(originalNode);
if (unwrappedNode != null) {
cmdNodeClass.field("modifier").setValue(unwrappedNode, cmdNodeClass.field("modifier").getValue(originalNode));
cmdNodeClass.field("forks").setValue(unwrappedNode, cmdNodeClass.field("forks").getValue(originalNode));
}
} catch (IllegalAccessException | NoSuchFieldException e) {
throw new RuntimeException(e);
}
postRegistrationFixNode(fixedNodes, originalNode.getRedirect());
}
else {
try {
for (CommandNode<CommandSourceStack> child : originalNode.getChildren())
postRegistrationFixNode(fixedNodes, child);
} catch (UnsupportedOperationException ignored) {
// in case getChildren is not possible (vanilla commands are wrapped by Paper API)
}
}
}
private void registerAlias(String alias, boolean prefixed) {
LiteralCommandNode<BukkitBrigadierCommandSource> node = literal(alias)
private static LiteralCommandNode<CommandSourceStack> getAliasNode(CommandNode<CommandSourceStack> commandNode, String alias) {
return LiteralArgumentBuilder.<CommandSourceStack>literal(alias)
.requires(commandNode.getRequirement())
.executes(commandNode.getCommand())
.redirect(commandNode)
.build();
registerNode(node, prefixed);
}
private void registerNode(LiteralCommandNode<BukkitBrigadierCommandSource> node, boolean prefixed) {
RootCommandNode<BukkitBrigadierCommandSource> root = getRootNode();
String name = node.getLiteral();
boolean isAlias = node.getRedirect() == commandNode;
boolean forceRegistration = switch (registrationPolicy) {
case NONE -> false;
case ONLY_BASE_COMMAND -> prefixed || !isAlias;
case ALL -> true;
};
// nmsDispatcher integration and conflit resolution
boolean nmsRegister = false, nmsRegistered = false;
CommandNode<BukkitBrigadierCommandSource> nmsConflicted = root.getChild(name);
if (nmsConflicted != null) {
if (isFromThisCommand(nmsConflicted)) {
// this command is already registered in NMS. Dont need to register again
nmsRegistered = true;
private static String getCommandIdentity(CommandNode<CommandSourceStack> command) {
if (PluginCommandNode.REFLECT.get().isInstance(command)) {
PluginCommandNode wrappedPCN = wrap(command, PluginCommandNode.class);
return "Node /" + command.getName() + " from plugin " + wrappedPCN.getPlugin().getName();
}
else if (BukkitCommandNode.REFLECT.get().isInstance(command)) {
BukkitCommandNode wrappedBCN = wrap(command, BukkitCommandNode.class);
Command bukkitCmd = wrappedBCN.getBukkitCommand();
if (bukkitCmd instanceof PluginCommand cmd) {
return "Node /" + command.getName() + " wrapping Bukkit command /" + bukkitCmd.getName() + " from plugin " + cmd.getPlugin().getName();
}
else if (forceRegistration) {
nmsRegister = true;
Log.info("Overwriting Brigadier command /" + name);
}
else if (prefixed || !isAlias) {
Log.severe("/" + name + " already in NMS Brigadier instance."
+ " Wont replace it because registration is not forced for prefixed or initial name of a command.");
}
else { // conflict, won't replace, not forced but only an alias anyway
Log.info("/" + name + " already in NMS Brigadier instance."
+ " Wont replace it because registration is not forced for a non-prefixed alias.");
else if (VanillaCommandWrapper.REFLECT.get().isInstance(bukkitCmd)) {
VanillaCommandWrapper vcw = wrap(bukkitCmd, VanillaCommandWrapper.class);
CommandNode<CommandSourceStack> vanillaCmd = vcw.vanillaCommand();
if (vanillaCmd != command)
return "Node /" + command.getName() + " wrapping non-plugin command /" + bukkitCmd.getName() + " wrapping: " + getCommandIdentity(vcw.vanillaCommand());
else
return "Node /" + command.getName() + " wrapping non-plugin command /" + bukkitCmd.getName() + " wrapping back the node (risk of StackOverflow?)";
}
else
return "Node /" + command.getName() + " wrapping " + bukkitCmd.getClass().getName() + " /" + bukkitCmd.getName();
}
else {
nmsRegister = true;
return "Node /" + command.getName() + " (unspecific)";
}
}
if (nmsRegister) {
@SuppressWarnings("unchecked")
var rCommandNode = ReflectWrapper.wrapTyped(root, fr.pandacube.lib.paper.reflect.wrapper.brigadier.CommandNode.class);
rCommandNode.removeCommand(name);
root.addChild(node);
nmsRegistered = true;
private static Boolean isPluginCommand(CommandNode<CommandSourceStack> command) {
if (PluginCommandNode.REFLECT.get().isInstance(command)) {
return true;
}
if (!nmsRegistered) {
return;
}
registeredAliases.add(name);
// bukkit dispatcher conflict resolution
boolean bukkitRegister = false;
CommandMap bukkitCmdMap = Bukkit.getCommandMap();
Command bukkitConflicted = bukkitCmdMap.getCommand(name);
if (bukkitConflicted != null) {
if (!isFromThisCommand(bukkitConflicted)) {
if (forceRegistration) {
bukkitRegister = true;
Log.info("Overwriting Bukkit command /" + name
+ " (" + getCommandIdentity(bukkitConflicted) + ")");
}
else if (prefixed || !isAlias) {
Log.severe("/" + name + " already in Bukkit dispatcher (" + getCommandIdentity(bukkitConflicted) + ")." +
" Wont replace it because registration is not forced for prefixed or initial name of a command.");
}
else {
Log.info("/" + name + " already in Bukkit dispatcher (" + getCommandIdentity(bukkitConflicted) + ")." +
" Wont replace it because registration is not forced for a non-prefixed alias.");
}
else if (BukkitCommandNode.REFLECT.get().isInstance(command)) {
BukkitCommandNode wrappedBCN = wrap(command, BukkitCommandNode.class);
Command bukkitCmd = wrappedBCN.getBukkitCommand();
if (bukkitCmd instanceof PluginCommand) {
return true;
}
else if (VanillaCommandWrapper.REFLECT.get().isInstance(bukkitCmd)) {
VanillaCommandWrapper vcw = wrap(bukkitCmd, VanillaCommandWrapper.class);
CommandNode<CommandSourceStack> vanillaCmd = vcw.vanillaCommand();
if (vanillaCmd != command)
return isPluginCommand(vcw.vanillaCommand());
else
return false;
}
else
return null;
}
else {
bukkitRegister = true;
return false;
}
if (bukkitRegister) {
bukkitCmdMap.getKnownCommands().remove(name.toLowerCase());
if (bukkitConflicted != null)
bukkitConflicted.unregister(bukkitCmdMap);
Command newCommand = new VanillaCommandWrapper(vanillaCommandDispatcher, node).__getRuntimeInstance();
bukkitCmdMap.getKnownCommands().put(name.toLowerCase(), newCommand);
newCommand.register(bukkitCmdMap);
}
}
private boolean isFromThisCommand(CommandNode<BukkitBrigadierCommandSource> node) {
return node == commandNode || node.getRedirect() == commandNode;
}
private boolean isFromThisCommand(Command bukkitCmd) {
if (VanillaCommandWrapper.REFLECT.get().isInstance(bukkitCmd)) {
return isFromThisCommand(ReflectWrapper.wrapTyped((BukkitCommand) bukkitCmd, VanillaCommandWrapper.class).vanillaCommand());
}
return false;
}
private static String getCommandIdentity(Command bukkitCmd) {
if (bukkitCmd instanceof PluginCommand cmd) {
return "Bukkit command: /" + cmd.getName() + " from plugin " + cmd.getPlugin().getName();
}
else if (VanillaCommandWrapper.REFLECT.get().isInstance(bukkitCmd)) {
return "Vanilla command: /" + bukkitCmd.getName();
}
else
return bukkitCmd.getClass().getName() + ": /" + bukkitCmd.getName();
}
/**
* Player command sender event handler.
* @param event the event.
* Gets the aliases that are actually registered in the server.
* @return the actually registered aliases.
*/
@EventHandler
public void onPlayerCommandSend(PlayerCommandSendEvent event) {
event.getCommands().removeAll(registeredAliases.stream().map(s -> "minecraft:" + s).toList());
protected Set<String> getRegisteredAliases() {
return Set.copyOf(registeredAliases);
}
/**
* Server load event handler.
* @param event the event.
*/
@EventHandler
public void onServerLoad(ServerLoadEvent event) {
register();
}
@@ -341,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();
}
@@ -354,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);
}
@@ -394,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());
}
@@ -403,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();
}
/**
@@ -412,8 +460,8 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
* @param sender the command sender.
* @return a new instance of a command sender wrapper for the provided command sender.
*/
public static BukkitBrigadierCommandSource getBrigadierCommandSource(CommandSender sender) {
return VanillaCommandWrapper.getListener(sender);
public static CommandSourceStack getBrigadierCommandSource(CommandSender sender) {
throw new UnsupportedOperationException("The 1.20.6 Paper API update uses a different wrapper for Brigadier command sender.");
}
@@ -442,7 +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);
}
@@ -455,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));
@@ -480,117 +531,6 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
* Minecraft's argument type
*/
/**
* Creates a new instance of the Brigadier argument type {@code minecraft:entity}.
* @param singleTarget if this argument takes only a single target.
* @param playersOnly if this argument takes players only.
* @return the {@code minecraft:entity} argument type with the specified parameters.
*/
public static ArgumentType<Object> argumentMinecraftEntity(boolean singleTarget, boolean playersOnly) {
if (playersOnly) {
return singleTarget ? EntityArgument.player() : EntityArgument.players();
}
else {
return singleTarget ? EntityArgument.entity() : EntityArgument.entities();
}
}
/**
* Gets the value of the provided argument of type {@code minecraft:entity} (list of entities), from the provided context.
* @param context the command execution context.
* @param argument the argument name.
* @return the value of the argument, or null if not found.
*/
public List<Entity> tryGetMinecraftEntityArgument(CommandContext<BukkitBrigadierCommandSource> context, String argument) {
EntitySelector es = ReflectWrapper.wrap(tryGetArgument(context, argument, EntitySelector.MAPPING.runtimeClass()), EntitySelector.class);
if (es == null)
return null;
List<fr.pandacube.lib.paper.reflect.wrapper.minecraft.world.Entity> nmsEntityList = es.findEntities(context.getSource());
List<Entity> entityList = new ArrayList<>(nmsEntityList.size());
for (fr.pandacube.lib.paper.reflect.wrapper.minecraft.world.Entity nmsEntity : nmsEntityList) {
entityList.add(nmsEntity.getBukkitEntity());
}
return entityList;
}
/**
* Gets the value of the provided argument of type {@code minecraft:entity} (list of players), from the provided context.
* @param context the command execution context.
* @param argument the argument name.
* @return the value of the argument, or null if not found.
*/
public List<Player> tryGetMinecraftEntityArgumentPlayers(CommandContext<BukkitBrigadierCommandSource> context, String argument) {
EntitySelector es = ReflectWrapper.wrap(tryGetArgument(context, argument, EntitySelector.MAPPING.runtimeClass()), EntitySelector.class);
if (es == null)
return null;
List<ServerPlayer> nmsPlayerList = es.findPlayers(context.getSource());
List<Player> playerList = new ArrayList<>(nmsPlayerList.size());
for (ServerPlayer nmsPlayer : nmsPlayerList) {
playerList.add(nmsPlayer.getBukkitEntity());
}
return playerList;
}
/**
* Gets the value of the provided argument of type {@code minecraft:entity} (one entity), from the provided context.
* @param context the command execution context.
* @param argument the argument name.
* @return the value of the argument, or null if not found.
*/
public Entity tryGetMinecraftEntityArgumentOneEntity(CommandContext<BukkitBrigadierCommandSource> context, String argument) {
EntitySelector es = ReflectWrapper.wrap(tryGetArgument(context, argument, EntitySelector.MAPPING.runtimeClass()), EntitySelector.class);
if (es == null)
return null;
fr.pandacube.lib.paper.reflect.wrapper.minecraft.world.Entity nmsEntity = es.findSingleEntity(context.getSource());
return nmsEntity == null ? null : nmsEntity.getBukkitEntity();
}
/**
* Gets the value of the provided argument of type {@code minecraft:entity} (one player), from the provided context.
* @param context the command execution context.
* @param argument the argument name.
* @return the value of the argument, or null if not found.
*/
public Player tryGetMinecraftEntityArgumentOnePlayer(CommandContext<BukkitBrigadierCommandSource> context, String argument) {
EntitySelector es = ReflectWrapper.wrap(tryGetArgument(context, argument, EntitySelector.MAPPING.runtimeClass()), EntitySelector.class);
if (es == null)
return null;
ServerPlayer nmsPlayer = es.findSinglePlayer(context.getSource());
return nmsPlayer == null ? null : nmsPlayer.getBukkitEntity();
}
/**
* Creates a new instance of the Brigadier argument type {@code minecraft:block_pos}.
* @return the {@code minecraft:block_pos} argument type.
*/
public static ArgumentType<Object> argumentMinecraftBlockPosition() {
return BlockPosArgument.blockPos();
}
/**
* Gets the value of the provided argument of type {@code minecraft:block_pos}, from the provided context.
* @param context the command execution context.
* @param argument the argument name.
* @param deflt a default value if the argument is not found.
* @return the value of the argument.
*/
public BlockVector tryGetMinecraftBlockPositionArgument(CommandContext<BukkitBrigadierCommandSource> context,
String argument, BlockVector deflt) {
return tryGetArgument(context, argument, Coordinates.MAPPING.runtimeClass(), nmsCoordinate -> {
BlockPos bp = ReflectWrapper.wrap(nmsCoordinate, Coordinates.class).getBlockPos(context.getSource());
return new BlockVector(bp.getX(), bp.getY(), bp.getZ());
}, deflt);
}
/**
* Creates a new instance of the Brigadier argument type {@code minecraft:vec3}.
* @return the {@code minecraft:vec3} argument type.
@@ -606,11 +546,11 @@ public abstract class PaperBrigadierCommand extends BrigadierCommand<BukkitBriga
* @param deflt a default value if the argument is not found.
* @return the value of the argument.
*/
public Vector tryGetMinecraftVec3Argument(CommandContext<BukkitBrigadierCommandSource> context, String argument,
public Vector tryGetMinecraftVec3Argument(CommandContext<CommandSourceStack> context, String argument,
Vector deflt) {
return tryGetArgument(context, argument, Coordinates.MAPPING.runtimeClass(),
return tryGetArgument(context, argument, Coordinates.REFLECT.get(),
nmsCoordinate -> CraftVector.toBukkit(
ReflectWrapper.wrap(nmsCoordinate, Coordinates.class).getPosition(context.getSource())
wrap(nmsCoordinate, Coordinates.class).getPosition(context.getSource())
),
deflt);
}
@@ -618,44 +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 default value if the argument is not found.
* @return the value of the argument.
*/
public Component tryGetMinecraftChatComponentArgument(CommandContext<BukkitBrigadierCommandSource> context,
String argument, Component deflt) {
return tryGetArgument(context, argument,
fr.pandacube.lib.paper.reflect.wrapper.minecraft.network.chat.Component.MAPPING.runtimeClass(),
nmsComp -> PaperAdventure.asAdventure(
ReflectWrapper.wrap(nmsComp,
fr.pandacube.lib.paper.reflect.wrapper.minecraft.network.chat.Component.class)
),
deflt);
}
/**
* All possible choices on how to force the registration of a command, based on certain conditions.
*/
public enum RegistrationPolicy {
/**
* Do not force to register a command node or an alias if there is already a command with that name in the
* vanilla Brigadier dispatcher.
* Note that all plugin-name-prefixed aliases will be registered anyway.
*/
NONE,
/**
* Force only the base command (but not the aliases) to be registered, even if a command with that name already
* exists in the vanilla Brigadier dispatcher.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,8 @@
package fr.pandacube.lib.paper.gui;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Consumer;
import com.google.common.collect.ImmutableMap;
import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.paper.PandaLibPaper;
import net.kyori.adventure.text.ComponentLike;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.HandlerList;
@@ -21,20 +12,15 @@ import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.paper.util.ItemStackBuilder;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
/**
* An inventory based GUI.
*/
public class GUIInventory implements Listener {
/**
* Used as parameter of {@link #buildButton(ItemStack, Integer, ComponentLike, List, Map)} to indicate that a button should
* shine like an enchanted object, without showing enchant information in the hover text.
*/
public static final Map<Enchantment, Integer> FAKE_ENCHANT = ImmutableMap.of(Enchantment.DURABILITY, 1);
private final Player player;
private final Inventory inv;
private Consumer<InventoryCloseEvent> onCloseEvent;
@@ -52,7 +38,7 @@ public class GUIInventory implements Listener {
if (title == null)
inv = Bukkit.createInventory(null, nbLines * 9);
else
inv = Bukkit.createInventory(null, nbLines * 9, title.getAdv());
inv = Bukkit.createInventory(null, nbLines * 9, title.get());
setCloseEvent(closeEventAction);
@@ -63,7 +49,11 @@ public class GUIInventory implements Listener {
Bukkit.getPluginManager().registerEvents(this, PandaLibPaper.getPlugin());
}
/**
* Sets the action when the player closes the inventory window.
* @param closeEventAction the action to run.
*/
protected void setCloseEvent(Consumer<InventoryCloseEvent> closeEventAction) {
onCloseEvent = closeEventAction;
}
@@ -202,78 +192,13 @@ public class GUIInventory implements Listener {
onCloseEvent.accept(event);
isOpened = false;
}
public static ItemStack buildButton(ItemStack base, Integer amount, ComponentLike displayName,
List<? extends ComponentLike> lores, Map<Enchantment, Integer> enchantments) {
ItemStackBuilder iStackBuilder = ItemStackBuilder.of(base);
if (amount != null)
iStackBuilder.amount(amount);
if (displayName != null)
iStackBuilder.displayName(displayName);
if (lores != null)
iStackBuilder.lore(lores);
if (enchantments != null) {
if (enchantments == FAKE_ENCHANT)
iStackBuilder.fakeEnchant();
else {
for (Entry<Enchantment, Integer> e : enchantments.entrySet()) {
iStackBuilder.enchant(e.getKey(), e.getValue());
}
}
}
return iStackBuilder.build();
}
public static ItemStack buildButton(ItemStack base, ComponentLike displayName, List<? extends ComponentLike> lores, Map<Enchantment, Integer> enchantments) {
return buildButton(base, null, displayName, lores, enchantments);
}
public static ItemStack buildButton(ItemStack base, ComponentLike displayName, Map<Enchantment, Integer> enchantments) {
return buildButton(base, null, displayName, null, enchantments);
}
public static ItemStack buildButton(ItemStack base, Integer amount, ComponentLike displayName, List<? extends ComponentLike> lores) {
return buildButton(base, amount, displayName, lores, null);
}
public static ItemStack buildButton(ItemStack base, ComponentLike displayName, List<? extends ComponentLike> lores) {
return buildButton(base, null, displayName, lores, null);
}
public static ItemStack buildButton(ItemStack base, ComponentLike displayName) {
return buildButton(base, null, displayName, null, null);
}
public static ItemStack buildButton(Material m, ComponentLike displayName, List<? extends ComponentLike> lores, Map<Enchantment, Integer> enchantments) {
return buildButton(new ItemStack(m), null, displayName, lores, enchantments);
}
public static ItemStack buildButton(Material m, ComponentLike displayName, Map<Enchantment, Integer> enchantments) {
return buildButton(new ItemStack(m), null, displayName, null, enchantments);
}
public static ItemStack buildButton(Material m, ComponentLike displayName, List<? extends ComponentLike> lores) {
return buildButton(new ItemStack(m), null, displayName, lores, null);
}
public static ItemStack buildButton(Material m, ComponentLike displayName) {
return buildButton(new ItemStack(m), null, displayName, null, null);
}
/**
* Tells the number of inventory line needed to show the provided number of slot.
* @param nb the number of slot.
* @return the number of line.
*/
public static int nbLineForNbElements(int nb) {
return nb / 9 + ((nb % 9 == 0) ? 0 : 1);
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,77 @@
package fr.pandacube.lib.paper.json;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.bind.TreeTypeAdapter;
import com.google.gson.reflect.TypeToken;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.bukkit.configuration.serialization.ConfigurationSerialization;
import org.bukkit.inventory.ItemStack;
import org.yaml.snakeyaml.Yaml;
import java.lang.reflect.Type;
import java.util.Map;
/**
* Gson adapter for ConfigurationSerializable, an interface implemented by several classes in the Bukkit API to ease
* serialization to YAML.
*
* To not reinvent the wheel, this class uses the Bukkits Yaml API to convert the objects from/to json.
*/
/* package */ class ConfigurationSerializableAdapter implements JsonSerializer<ConfigurationSerializable>, JsonDeserializer<ConfigurationSerializable> {
public static final TypeAdapterFactory FACTORY = TreeTypeAdapter.newTypeHierarchyFactory(ConfigurationSerializable.class, new ConfigurationSerializableAdapter());
private static final TypeToken<Map<String, Object>> MAP_STR_OBJ_TYPE = new TypeToken<>() { };
private boolean isItemStack(Map<String, Object> deserializedMap) {
return deserializedMap.containsKey(ConfigurationSerialization.SERIALIZED_TYPE_KEY)
&& deserializedMap.get(ConfigurationSerialization.SERIALIZED_TYPE_KEY) instanceof String serializedType
&& ItemStack.class.equals(ConfigurationSerialization.getClassByAlias(serializedType));
}
@Override
public ConfigurationSerializable deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (!(json instanceof JsonObject jsonObj) || !jsonObj.has(ConfigurationSerialization.SERIALIZED_TYPE_KEY))
throw new JsonParseException("Unable to deserialize a ConfigurationSerializable from the provided json structure.");
Map<String, Object> map = context.deserialize(jsonObj, MAP_STR_OBJ_TYPE.getType());
if (isItemStack(map)) {
ItemStackAdapter.fixDeserializationVersion(map);
}
String yaml = new Yaml().dump(Map.of("obj", map));
YamlConfiguration cfg = new YamlConfiguration();
try {
cfg.loadFromString(yaml);
} catch (InvalidConfigurationException e) {
throw new JsonParseException("Unable t deserialize a ConfigurationSerializable from the provided json structure.", e);
}
return cfg.getSerializable("obj", ConfigurationSerializable.class);
}
@Override
public JsonElement serialize(ConfigurationSerializable src, Type typeOfSrc, JsonSerializationContext context) {
YamlConfiguration cfg = new YamlConfiguration();
cfg.set("obj", src);
Map<String, Object> map = new Yaml().load(cfg.saveToString());
return context.serialize(map.get("obj"), MAP_STR_OBJ_TYPE.getType());
}
/*public static void main(String[] args) {
PaperJson.init();
BlockVector bv = new BlockVector(12, 24, 48);
String json = Json.gson.toJson(bv);
System.out.println(json);
BlockVector bv2 = Json.gson.fromJson(json, BlockVector.class);
System.out.println(bv.equals(bv2));
}*/
}

View File

@@ -1,8 +1,11 @@
package fr.pandacube.lib.paper.json;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
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;
@@ -10,42 +13,76 @@ import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.bind.TreeTypeAdapter;
import com.google.gson.reflect.TypeToken;
import org.bukkit.Bukkit;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.bukkit.configuration.serialization.ConfigurationSerialization;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.lang.reflect.Type;
import java.util.Map;
/* package */ class ItemStackAdapter implements JsonSerializer<ItemStack>, JsonDeserializer<ItemStack> {
private static final TypeToken<ItemStack> ITEMSTACK_TYPE = TypeToken.get(ItemStack.class);
public static final TypeAdapterFactory FACTORY = TreeTypeAdapter.newFactoryWithMatchRawType(ITEMSTACK_TYPE, new ItemStackAdapter());
public static final TypeAdapterFactory FACTORY = TreeTypeAdapter.newTypeHierarchyFactory(ItemStack.class, new ItemStackAdapter());
private static final TypeToken<Map<String, Object>> MAP_STR_OBJ_TYPE = new TypeToken<>() { };
/** Gson instance with no custom type adapter */
private static final Gson vanillaGson = new GsonBuilder().setLenient().create();
@Override
public ItemStack deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
Map<String, Object> deserializedMap = context.deserialize(json, MAP_STR_OBJ_TYPE.getType());
int itemStackVersion = deserializedMap.containsKey("v") ? ((Number)deserializedMap.get("v")).intValue() : -1;
if (itemStackVersion >= 0) {
@SuppressWarnings("deprecation")
int currentDataVersion = Bukkit.getUnsafe().getDataVersion();
if (itemStackVersion > currentDataVersion) {
/* The itemStack we are deserializing is from a newer MC version, so Bukkit will refuse it.
* We decide to ignore the provided version and consider that the received item stack is from current
* version. We let Bukkit handles the deserialization with the data it can interpret, throwing an error
* only if it can't.
*/
deserializedMap.put("v", currentDataVersion);
return ItemStack.deserialize(deserializedMap);
}
if (!(json instanceof JsonObject jsonObj))
throw new JsonParseException("Unable to deserialize a ConfigurationSerializable from the provided json structure.");
if (jsonObj.has(ConfigurationSerialization.SERIALIZED_TYPE_KEY))
return context.deserialize(jsonObj, ConfigurationSerializable.class);
if (jsonObj.has("meta")
&& jsonObj.get("meta") instanceof JsonObject metaJson
&& !metaJson.has(ConfigurationSerialization.SERIALIZED_TYPE_KEY)) {
// item meta was serialized using GSON reflection serializer, instead of proper serialization using
// ConfigurationSerializable interface. So we try to deserialize it the same way.
Map<String, Object> map = context.deserialize(jsonObj, MAP_STR_OBJ_TYPE.getType());
fixDeserializationVersion(map);
map.remove("meta");
ItemStack is = ItemStack.deserialize(map);
Class<? extends ItemMeta> metaClass = is.getItemMeta().getClass();
ItemMeta meta = vanillaGson.fromJson(jsonObj.get("meta"), metaClass);
is.setItemMeta(meta);
return is;
}
return ItemStack.deserialize(deserializedMap);
// deserialize using ConfigurationSerializableAdapter
jsonObj.addProperty(ConfigurationSerialization.SERIALIZED_TYPE_KEY,
ConfigurationSerialization.getAlias(ItemStack.class));
return context.deserialize(jsonObj, ConfigurationSerializable.class);
}
@Override
public JsonElement serialize(ItemStack src, Type typeOfSrc, JsonSerializationContext context) {
return context.serialize(src.serialize(), MAP_STR_OBJ_TYPE.getType());
}
/* package */ static void fixDeserializationVersion(Map<String, Object> deserializedMap) {
if (!deserializedMap.containsKey("v"))
return;
int itemStackVersion = ((Number)deserializedMap.get("v")).intValue();
if (itemStackVersion >= 0) {
@SuppressWarnings("deprecation")
int currentDataVersion = Bukkit.getUnsafe().getDataVersion();
if (itemStackVersion > currentDataVersion) {
/* Here, the itemStack we are deserializing is from a newer MC version, so Bukkit will refuse it.
* We decide to ignore the provided version and consider that the received item stack is from current
* version. We let Bukkit handles the deserialization with the data it can interpret, throwing an error
* only if it can't.
*/
deserializedMap.put("v", currentDataVersion);
}
}
}
}

View File

@@ -12,5 +12,9 @@ public class PaperJson {
*/
public static void init() {
Json.registerTypeAdapterFactory(ItemStackAdapter.FACTORY);
Json.registerTypeAdapterFactory(ConfigurationSerializableAdapter.FACTORY);
}
private PaperJson() {}
}

View File

@@ -4,7 +4,6 @@ import com.destroystokyo.paper.event.server.ServerTickEndEvent;
import com.destroystokyo.paper.event.server.ServerTickStartEvent;
import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.chat.ChatColorGradient;
import fr.pandacube.lib.chat.ChatColorUtil;
import fr.pandacube.lib.chat.ChatConfig.PandaTheme;
import fr.pandacube.lib.paper.PandaLibPaper;
import fr.pandacube.lib.paper.players.PaperOffPlayer;
@@ -13,16 +12,15 @@ import fr.pandacube.lib.paper.scheduler.SchedulerUtil;
import fr.pandacube.lib.paper.util.AutoUpdatedBossBar;
import fr.pandacube.lib.paper.util.AutoUpdatedBossBar.BarUpdater;
import fr.pandacube.lib.players.standalone.AbstractPlayerManager;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.util.MemoryUtil;
import fr.pandacube.lib.util.MemoryUtil.MemoryUnit;
import fr.pandacube.lib.util.TimeUtil;
import fr.pandacube.lib.util.log.Log;
import net.kyori.adventure.bossbar.BossBar;
import net.kyori.adventure.bossbar.BossBar.Color;
import net.kyori.adventure.bossbar.BossBar.Overlay;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
@@ -45,10 +43,17 @@ import static fr.pandacube.lib.chat.ChatStatic.infoText;
import static fr.pandacube.lib.chat.ChatStatic.successText;
import static fr.pandacube.lib.chat.ChatStatic.text;
/**
* Various tools to supervise the JVM RAM and the CPU usage of the main server thread.
*/
public class PerformanceAnalysisManager implements Listener {
private static PerformanceAnalysisManager instance;
/**
* Gets the instance of {@link PerformanceAnalysisManager}.
* @return the instance of {@link PerformanceAnalysisManager}.
*/
public static synchronized PerformanceAnalysisManager getInstance() {
if (instance == null)
instance = new PerformanceAnalysisManager();
@@ -77,40 +82,33 @@ public class PerformanceAnalysisManager implements Listener {
private final LinkedList<Long> interTPSDurations = new LinkedList<>();
/**
* The boss bar that shows in real time the CPU performance of the main server thread.
*/
public final AutoUpdatedBossBar tpsBar;
/**
* The boss bar that shows in real time the JVM RAM usage.
*/
public final AutoUpdatedBossBar memoryBar;
private final List<Player> barPlayers = new ArrayList<>();
private final List<BossBar> relatedBossBars = new ArrayList<>();
/**
* The gradient of color covering the common range of TPS values.
*/
public final ChatColorGradient tps1sGradient = new ChatColorGradient()
.add(0, NamedTextColor.BLACK)
.add(1, NamedTextColor.DARK_RED)
.add(5, NamedTextColor.RED)
.add(10, NamedTextColor.GOLD)
.add(14, NamedTextColor.YELLOW)
.add(20, PandaTheme.CHAT_GREEN_1_NORMAL)
.add(19, PandaTheme.CHAT_GREEN_1_NORMAL)
.add(21, PandaTheme.CHAT_GREEN_1_NORMAL)
.add(26, NamedTextColor.BLUE);
public final ChatColorGradient tps10sGradient = new ChatColorGradient()
.add(0, NamedTextColor.DARK_RED)
.add(5, NamedTextColor.RED)
.add(10, NamedTextColor.GOLD)
.add(14, NamedTextColor.YELLOW)
.add(18, PandaTheme.CHAT_GREEN_1_NORMAL);
public final ChatColorGradient tps1mGradient = new ChatColorGradient()
.add(0, NamedTextColor.DARK_RED)
.add(8, NamedTextColor.RED)
.add(12, NamedTextColor.GOLD)
.add(16, NamedTextColor.YELLOW)
.add(20, PandaTheme.CHAT_GREEN_1_NORMAL);
public final ChatColorGradient memoryUsageGradient = new ChatColorGradient()
private final ChatColorGradient memoryUsageGradient = new ChatColorGradient()
.add(.60f, PandaTheme.CHAT_GREEN_1_NORMAL)
.add(.70f, NamedTextColor.YELLOW)
.add(.80f, NamedTextColor.GOLD)
@@ -132,10 +130,19 @@ public class PerformanceAnalysisManager implements Listener {
}
/**
* Tells if the provided players is seeing the performance boss bars.
* @param p the player to verify.
* @return true if the provided players is seeing the performance boss bars, false otherwise.
*/
public boolean barsContainsPlayer(Player p) {
return barPlayers.contains(p);
}
/**
* Shows the performance boss bars to the provided player.
* @param p the player.
*/
public synchronized void addPlayerToBars(Player p) {
barPlayers.add(p);
p.showBossBar(tpsBar.bar);
@@ -143,7 +150,11 @@ public class PerformanceAnalysisManager implements Listener {
for (BossBar bar : relatedBossBars)
p.showBossBar(bar);
}
/**
* Hides the performance boss bars from the provided player.
* @param p the player.
*/
public synchronized void removePlayerToBars(Player p) {
p.hideBossBar(tpsBar.bar);
p.hideBossBar(memoryBar.bar);
@@ -151,7 +162,11 @@ public class PerformanceAnalysisManager implements Listener {
p.hideBossBar(bar);
barPlayers.remove(p);
}
/**
* Show an additional boss bar to the players currently seeing the performance ones.
* @param bar the new bar to show.
*/
public synchronized void addBossBar(BossBar bar) {
if (relatedBossBars.contains(bar))
return;
@@ -159,7 +174,11 @@ public class PerformanceAnalysisManager implements Listener {
for (Player p : barPlayers)
p.showBossBar(bar);
}
/**
* Hides an additional boss bar from the players currently seeing the performance ones.
* @param bar the additional bar to hide.
*/
public synchronized void removeBossBar(BossBar bar) {
if (!relatedBossBars.contains(bar))
return;
@@ -167,8 +186,11 @@ public class PerformanceAnalysisManager implements Listener {
for (Player p : barPlayers)
p.hideBossBar(bar);
}
public synchronized void cancelInternalBossBar() {
/**
* De-initialize the performance analyzer.
*/
public synchronized void deinit() {
tpsBar.cancel();
memoryBar.cancel();
}
@@ -178,7 +200,7 @@ public class PerformanceAnalysisManager implements Listener {
@EventHandler
public synchronized void onTickStart(ServerTickStartEvent event) {
synchronized void onTickStart(ServerTickStartEvent event) {
tickStartNanoTime = System.nanoTime();
tickStartCPUTime = threadMXBean.isThreadCpuTimeSupported() ? threadMXBean.getCurrentThreadCpuTime() : 0;
@@ -186,7 +208,7 @@ public class PerformanceAnalysisManager implements Listener {
}
@EventHandler
public synchronized void onTickEnd(ServerTickEndEvent event) {
synchronized void onTickEnd(ServerTickEndEvent event) {
tickEndNanoTime = System.nanoTime();
long tickEndCPUTime = threadMXBean.isThreadCpuTimeSupported() ? threadMXBean.getCurrentThreadCpuTime() : 0;
@@ -213,7 +235,7 @@ public class PerformanceAnalysisManager implements Listener {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
void onPlayerJoin(PlayerJoinEvent event) {
plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {
@SuppressWarnings("unchecked")
AbstractPlayerManager<PaperOnlinePlayer, PaperOffPlayer> playerManager = (AbstractPlayerManager<PaperOnlinePlayer, PaperOffPlayer>) AbstractPlayerManager.getInstance();
@@ -233,7 +255,7 @@ public class PerformanceAnalysisManager implements Listener {
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
void onPlayerQuit(PlayerQuitEvent event) {
removePlayerToBars(event.getPlayer());
}
@@ -298,15 +320,17 @@ public class PerformanceAnalysisManager implements Listener {
// keep the legacy text when generating the bar to save space when converting to component
StringBuilder s = new StringBuilder();
ChatColor prevC = ChatColor.RESET;
TextColor prevC = null;
for (int i = 58; i >= 0; i--) {
int t = tpsHistory[i];
ChatColor newC = ChatColorUtil.toBungee(tps1sGradient.pickColorAt(t));
if (newC != prevC) {
s.append(newC);
TextColor newC = tps1sGradient.pickColorAt(t);
if (!newC.equals(prevC)) {
s.append(text("|").color(newC).getLegacyText());
prevC = newC;
}
s.append("|");
else {
s.append("|");
}
}
@@ -346,19 +370,19 @@ public class PerformanceAnalysisManager implements Listener {
: (avgTickDuration1s < 46) ? NamedTextColor.GOLD
: NamedTextColor.RED;
timings = text("(Tr:")
.then(text(Math.round(avgTickCPUTime1s) + "ms").color(avgTickCPUTime1sColor))
.thenText(" Tw:")
.then(text(Math.round(avgTickWaitingTime1s) + "ms").color(avgTickWaitingTime1sColor))
.thenText(" S:")
.then(text(Math.round(avgInterTickDuration1s) + "ms").color(avgInterTickDuration1sColor))
.thenText(")");
timings = text("(R/W/S:")
.then(text(Math.round(avgTickCPUTime1s)).color(avgTickCPUTime1sColor))
.thenText("/")
.then(text(Math.round(avgTickWaitingTime1s)).color(avgTickWaitingTime1sColor))
.thenText("/")
.then(text(Math.round(avgInterTickDuration1s)).color(avgInterTickDuration1sColor))
.thenText("ms)");
}
title = infoText("TPS [")
.thenLegacyText(s.toString())
.thenText("] ")
.then(text(tps1sDisplay+"/20 ").color(tps1sGradient.pickColorAt(tps1s)))
.then(text(tps1sDisplay + "/" + getTargetTickRate() + " ").color(tps1sGradient.pickColorAt(tps1s)))
.then(timings);
}
@@ -373,29 +397,36 @@ public class PerformanceAnalysisManager implements Listener {
}
private Chat alteredTPSTitle = null;
/**
* Temporary change the title of the TPS boss bar.
* @param title the title override. null to restore to the normal TPS title.
*/
public synchronized void setAlteredTPSTitle(Chat title) {
alteredTPSTitle = title;
}
// special case where the getTPS method always returns a whole number when retrieving the TPS for 1 sec
/**
* Gets the number of tick in the last second.
* @return the number of tick in the last second.
*/
public int getTPS1s() {
return (int) getTPS(1_000);
}
/**
*
* @param nbTicks number of ticks when the avg value is computed from history
* @return the avg number of TPS in the interval
*/
public synchronized float getAvgNano(List<Long> data, int nbTicks) {
if (data.size() <= 0) return 0;
private synchronized float getAvgNano(List<Long> data, int nbTicks) {
if (data.isEmpty())
return 0;
if (nbTicks > data.size()) nbTicks = data.size();
@@ -407,12 +438,13 @@ public class PerformanceAnalysisManager implements Listener {
}
/**
*
* Gets the average number of tick per second in the n last milliseconds.
* @param nbMillis number of milliseconds when the avg TPS is computed from history
* @return the avg number of TPS in the interval
*/
public synchronized float getTPS(long nbMillis) {
if (tpsTimes.size() == 0) return 0;
if (tpsTimes.isEmpty())
return 0;
long currentMillis = System.currentTimeMillis();
@@ -425,8 +457,12 @@ public class PerformanceAnalysisManager implements Listener {
return count * (1000 / (float) nbMillis);
}
/**
* Gets the history of TPS performance.
* @return an array of TPS values from the last minute. The value at 0 is in the last second (current second on the clock - 1), the value at index 1 is now - 2, ...
*/
public synchronized int[] getTPSHistory() {
int[] history = new int[60];
@@ -442,11 +478,24 @@ public class PerformanceAnalysisManager implements Listener {
return history;
}
/**
* Gets the current server's target tick rate.
* Usually 20 but the server can be configured to tick at a different rate.
* @return the current server's target tick rate.
*/
public static int getTargetTickRate() {
return Math.round(Bukkit.getServerTickManager().getTickRate());
}
/**
* Runs the garbage collector on the server.
* Depending on the server load and the used memory, this can freeze the server for a second.
* @param sender the command sender that triggers the garbase collector. Can be null (the report will be sent to the
* console)
*/
public static void gc(CommandSender sender) {
long t1 = System.currentTimeMillis();
long alloc1 = Runtime.getRuntime().totalMemory();
@@ -468,7 +517,7 @@ public class PerformanceAnalysisManager implements Listener {
Log.info(finalMessage.getLegacyText());
}
public static String displayRound10(double val) {
private static String displayRound10(double val) {
long v = (long) Math.ceil(val * 10);
return "" + (v / 10f);
}

View File

@@ -1,14 +1,11 @@
package fr.pandacube.lib.paper.players;
import fr.pandacube.lib.paper.reflect.util.PrimaryWorlds;
import fr.pandacube.lib.paper.world.PrimaryWorlds;
import fr.pandacube.lib.paper.reflect.wrapper.craftbukkit.CraftServer;
import fr.pandacube.lib.paper.reflect.wrapper.dataconverter.MCDataConverter;
import fr.pandacube.lib.paper.reflect.wrapper.dataconverter.MCTypeRegistry;
import fr.pandacube.lib.paper.reflect.wrapper.minecraft.SharedConstants;
import fr.pandacube.lib.paper.reflect.wrapper.minecraft.nbt.CompoundTag;
import fr.pandacube.lib.paper.reflect.wrapper.minecraft.nbt.NbtIo;
import fr.pandacube.lib.paper.util.PlayerDataWrapper;
import fr.pandacube.lib.paper.util.WorldUtil;
import fr.pandacube.lib.paper.players.PlayerDataWrapper.PlayerDataLoadException;
import fr.pandacube.lib.paper.world.WorldUtil;
import fr.pandacube.lib.players.standalone.AbstractOffPlayer;
import fr.pandacube.lib.reflect.wrapper.ReflectWrapper;
import org.bukkit.Bukkit;
@@ -118,26 +115,31 @@ public interface PaperOffPlayer extends AbstractOffPlayer {
* Player config
*/
@SuppressWarnings("RedundantThrows") // may be thrown by concrete implementation
@Override
default String getConfig(String key) throws Exception {
return PaperPlayerConfigStorage.get(getUniqueId(), key);
}
@SuppressWarnings("RedundantThrows") // may be thrown by concrete implementation
@Override
default String getConfig(String key, String deflt) throws Exception {
return PaperPlayerConfigStorage.get(getUniqueId(), key, deflt);
}
@SuppressWarnings("RedundantThrows") // may be thrown by concrete implementation
@Override
default void setConfig(String key, String value) throws Exception {
PaperPlayerConfigStorage.set(getUniqueId(), key, value);
}
@SuppressWarnings("RedundantThrows") // may be thrown by concrete implementation
@Override
default void updateConfig(String key, String deflt, UnaryOperator<String> updater) throws Exception {
PaperPlayerConfigStorage.update(getUniqueId(), key, deflt, updater);
}
@SuppressWarnings("RedundantThrows") // may be thrown by concrete implementation
@Override
default void unsetConfig(String key) throws Exception {
PaperPlayerConfigStorage.unset(getUniqueId(), key);
@@ -153,23 +155,21 @@ public interface PaperOffPlayer extends AbstractOffPlayer {
/**
* Gets the NBT data from the player-data file.
* It will not work if the player is online, because the data on the file are not synchronized with real-time values.
* @param convertTag true to convert the data to the current MC version, false to keep the saved version
* @return the NBT data from the player-data file.
* @return the NBT data from the player-data file, or null if the file does not exist.
* @throws IllegalStateException if the player is online.
*/
default CompoundTag getPlayerData(boolean convertTag) {
default CompoundTag getPlayerData() {
if (isOnline())
throw new IllegalStateException("Cannot access data file of " + getName() + " because theyre online.");
CompoundTag data = ReflectWrapper.wrapTyped(Bukkit.getServer(), CraftServer.class)
.getServer()
.getPlayerList()
.playerIo()
.getPlayerData(getUniqueId().toString());
if (convertTag) {
int i = data.contains("DataVersion", 3) ? data.getInt("DataVersion") : -1;
data = MCDataConverter.convertTag(MCTypeRegistry.PLAYER(), data, i, SharedConstants.getCurrentVersion().getDataVersion().getVersion());
throw new IllegalStateException("Cannot access data file of " + getName() + " because they're online.");
try {
return ReflectWrapper.wrapTyped(Bukkit.getServer(), CraftServer.class)
.getServer()
.getPlayerList()
.playerIo()
.load(getName(), getUniqueId().toString()).orElse(null);
} catch (Exception|LinkageError e) {
throw new PlayerDataLoadException(getName(), getUniqueId(), e);
}
return data;
}
/**
@@ -179,7 +179,7 @@ public interface PaperOffPlayer extends AbstractOffPlayer {
* @throws IllegalStateException if the player is online.
*/
default PlayerDataWrapper getPlayerDataWrapper() {
return new PlayerDataWrapper(getPlayerData(true));
return new PlayerDataWrapper(getPlayerData());
}
/**
@@ -196,7 +196,7 @@ public interface PaperOffPlayer extends AbstractOffPlayer {
File old = getPlayerDataFile(true);
old.delete();
Files.move(file.toPath(), old.toPath());
NbtIo.writeCompressed(data.data, file);
NbtIo.writeCompressed(data.data(), file.toPath());
}
/**
@@ -205,7 +205,7 @@ public interface PaperOffPlayer extends AbstractOffPlayer {
* @return the file where the player-data is stored.
*/
default File getPlayerDataFile(boolean old) {
File playerDataDir = new File(WorldUtil.worldDir(PrimaryWorlds.PRIMARY_WORLDS.get(0)), "playerdata");
File playerDataDir = new File(WorldUtil.worldDir(PrimaryWorlds.PRIMARY_WORLDS.getFirst()), "playerdata");
return new File(playerDataDir, getUniqueId() + (old ? ".dat_old" : ".dat"));
}

View File

@@ -3,10 +3,11 @@ package fr.pandacube.lib.paper.players;
import com.destroystokyo.paper.ClientOption;
import com.destroystokyo.paper.ClientOption.ChatVisibility;
import com.destroystokyo.paper.SkinParts;
import fr.pandacube.lib.paper.players.PlayerNonPersistentConfig.Expiration;
import fr.pandacube.lib.paper.players.PlayerNonPersistentConfig.ExpirationPolicy;
import fr.pandacube.lib.paper.reflect.wrapper.craftbukkit.CraftPlayer;
import fr.pandacube.lib.players.standalone.AbstractOnlinePlayer;
import fr.pandacube.lib.reflect.wrapper.ReflectWrapper;
import io.papermc.paper.entity.TeleportFlag.Relative;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.title.Title;
import net.kyori.adventure.title.Title.Times;
@@ -14,7 +15,7 @@ import net.kyori.adventure.util.Ticks;
import org.bukkit.Location;
import org.bukkit.OfflinePlayer;
import org.bukkit.Sound;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.MainHand;
import org.bukkit.inventory.PlayerInventory;
@@ -26,6 +27,17 @@ import java.util.Locale;
*/
public interface PaperOnlinePlayer extends PaperOffPlayer, AbstractOnlinePlayer {
/**
* The default value for {@link Player#getWalkSpeed()}.
*/
float BUKKIT_DEFAULT_WALK_SPEED = 0.2f;
/**
* The default value for {@link Player#getFlySpeed()}.
*/
float BUKKIT_DEFAULT_FLY_SPEED = 0.1f;
/*
* General data and state
*/
@@ -123,12 +135,14 @@ public interface PaperOnlinePlayer extends PaperOffPlayer, AbstractOnlinePlayer
*/
@Override
PaperClientOptions getClientOptions();
default PaperClientOptions getClientOptions() {
return new PaperClientOptions(this);
}
/**
* Provides various configuration values of the Minecraft client.
*/
abstract class PaperClientOptions implements AbstractOnlinePlayer.ClientOptions {
class PaperClientOptions implements AbstractOnlinePlayer.ClientOptions {
private final PaperOnlinePlayer op;
@@ -156,7 +170,7 @@ public interface PaperOnlinePlayer extends PaperOffPlayer, AbstractOnlinePlayer
@Override
public boolean isChatFullyVisible() {
ChatVisibility v = getChatVisibility();
return v == ChatVisibility.FULL || v == ChatVisibility.UNKNOWN;
return v == ChatVisibility.FULL;
}
@Override
@@ -198,8 +212,8 @@ public interface PaperOnlinePlayer extends PaperOffPlayer, AbstractOnlinePlayer
}
@Override
public boolean isTextFilteringEnabled() { // needs reflection to get the actual value
return false;
public boolean isTextFilteringEnabled() {
return op.getBukkitPlayer().getClientOption(ClientOption.TEXT_FILTERING_ENABLED);
}
@Override
@@ -254,25 +268,30 @@ public interface PaperOnlinePlayer extends PaperOffPlayer, AbstractOnlinePlayer
/*
* Custom damage
* Relative teleport
*/
/**
* Deals damages to this player.
* @param amount the amount of damage to deal.
* Teleports this player to the specified relative location, using the {@link Relative} flags.
* @param relX the relative x coordinate.
* @param relY the relative y coordinate.
* @param relZ the relative z coordinate.
*/
default void damage(double amount) {
getBukkitPlayer().damage(amount); // uses DamageSource.GENERIC
default void teleportRelatively(float relX, float relY, float relZ) {
getBukkitPlayer().teleport(getBukkitPlayer().getLocation().add(relX, relY, relZ), Relative.VELOCITY_X, Relative.VELOCITY_Y, Relative.VELOCITY_Z, Relative.VELOCITY_ROTATION);
}
/**
* Deals damages to this player, from the provided entity.
* @param amount the amount of damage to deal.
* @param source the entity from which the damage comes from.
* Teleports this player to the specified location, using the {@link Relative} flags.
* @param destination the destination.
*/
default void damage(double amount, LivingEntity source) {
getBukkitPlayer().damage(amount, source); // uses appropriate DamageSource according to provided player or entity
default void teleportRelatively(Location destination) {
getBukkitPlayer().teleport(destination, Relative.VELOCITY_X, Relative.VELOCITY_Y, Relative.VELOCITY_Z, Relative.VELOCITY_ROTATION);
}
@@ -285,18 +304,39 @@ public interface PaperOnlinePlayer extends PaperOffPlayer, AbstractOnlinePlayer
* Player config
*/
/**
* Gets the non-persistent value of the provided configuration key of this player.
* @param key the configuration key.
* @return the value of the configuration, or null if the configuration is not set.
*/
default String getNonPersistentConfig(String key) {
return PlayerNonPersistentConfig.getData(getUniqueId(), key);
}
/**
* Gets the non-persistent value of the provided configuration key of this player.
* @param key the configuration key.
* @param deflt the default value if the configuration is not set.
* @return the value of the configuration, or {@code deflt} if the configuration is not set.
*/
default String getNonPersistentConfig(String key, String deflt) {
return PlayerNonPersistentConfig.getData(getUniqueId(), key);
}
default void setNonPersistentConfig(String key, String value, Expiration expiration) {
PlayerNonPersistentConfig.setData(getUniqueId(), key, value, expiration);
/**
* Sets the non-persistent value of the provided configuration key for this player.
* @param key the configuration key to set.
* @param value the new value.
* @param expirationPolicy the expiration policy.
*/
default void setNonPersistentConfig(String key, String value, ExpirationPolicy expirationPolicy) {
PlayerNonPersistentConfig.setData(getUniqueId(), key, value, expirationPolicy);
}
/**
* Unsets the non-persistent value of the provided configuration key for this player.
* @param key the configuration key to update.
*/
default void unsetNonPersistentConfig(String key) {
PlayerNonPersistentConfig.unsetData(getUniqueId(), key);
}

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