diff --git a/extension/src/main/java/de/jeremystartm/pickshadow/extension/BuildOptions.java b/extension/src/main/java/de/jeremystartm/pickshadow/extension/BuildOptions.java
index a18a2fc..84ac322 100644
--- a/extension/src/main/java/de/jeremystartm/pickshadow/extension/BuildOptions.java
+++ b/extension/src/main/java/de/jeremystartm/pickshadow/extension/BuildOptions.java
@@ -19,6 +19,8 @@
package de.jeremystartm.pickshadow.extension;
+import de.jeremystartm.pickshadow.extension.misc.TabListHandler;
+import net.luckperms.api.util.Tristate;
import org.bukkit.potion.PotionEffectType;
import org.jetbrains.annotations.NotNull;
@@ -36,10 +38,58 @@ import java.util.List;
* @since v1-release0
*/
public final class BuildOptions {
+ // -----> Links
+ /**
+ * Contains the link to the repository
+ * hosting PickShadow's server-side code.
+ *
+ * @since v1-release0
+ */
+ public static final @NotNull String LINKS_PICKSHADOW_WEBSITE = "";
+
+ /**
+ * Contains the link to the repository
+ * hosting PickShadow's server-side code.
+ *
+ * @since v1-release0
+ */
+ public static final @NotNull String LINKS_PICKSHADOW_FORUM = "";
+
+ /**
+ * Contains the link to the repository
+ * hosting PickShadow's server-side code.
+ *
+ * @since v1-release0
+ */
+ public static final @NotNull String LINKS_PICKSHADOW_REPOSITORY = "https://git.staropensource.de/JeremyStarTM/PickShadow";
+
+ /**
+ * Contains the link to LuckPerms' website.
+ *
+ * @since v1-release0
+ */
+ public static final @NotNull String LINKS_LUCKPERMS = "https://luckperms.net";
+
+ /**
+ * Contains the link to ViaVersions' website.
+ *
+ * @since v1-release0
+ */
+ public static final @NotNull String LINKS_VIAVERSION = "https://viaversion.com";
+
+ /**
+ * Contains the link to FreedomChat's Modrinth site.
+ *
+ * @since v1-release0
+ */
+ public static final @NotNull String LINKS_FREEDOMCHAT = "https://modrinth.com/plugin/freedomchat";
+
+
// -----> Settings
/**
- * Contains an array of all bad effects.
+ * An array containing all bad effects.
*
+ * @see PotionEffectType
* @since v1-release0
*/
public static final @NotNull List<@NotNull PotionEffectType> SETTINGS_EFFECTS_BAD = List.of(
@@ -58,9 +108,11 @@ public final class BuildOptions {
PotionEffectType.RAID_OMEN,
PotionEffectType.INFESTED
);
+
/**
- * Contains an array of all damaging effects.
+ * An array containing all damaging effects.
*
+ * @see PotionEffectType
* @since v1-release0
*/
public static final @NotNull List<@NotNull PotionEffectType> SETTINGS_EFFECTS_DAMAGING = List.of(
@@ -69,6 +121,15 @@ public final class BuildOptions {
PotionEffectType.POISON
);
+ /**
+ * The rate at which the tab list shall be updated.
+ *
+ * @see TabListHandler
+ * @since v1-release0
+ */
+ public static final long SETTINGS_TABLIST_UPDATERATE = 20L;
+
+
// -----> Fixes and unfixes
/**
* Unfixes MC-212 (fixed in 24w45a),
@@ -82,6 +143,7 @@ public final class BuildOptions {
*/
public static final boolean UNFIX_FALLDAMAGE_CANCELLING = true;
+
// -----> Small stuff
/**
* Hides all messages starting with {@code #}.
@@ -105,4 +167,24 @@ public final class BuildOptions {
* @since v1-release0
*/
public static final boolean SMALLSTUFF_STONECUTTER_DAMAGE = true;
+
+ /**
+ * Determines if and how
+ * bats shall be killed.
+ *
+ * {@link Tristate#TRUE} causes all bats to be killed.
+ * {@link Tristate#UNDEFINED} will kill all naturally spawned bats.
+ * {@link Tristate#FALSE} disables this setting.
+ *
+ * @since v1-release0
+ */
+ public static final @NotNull Tristate SMALLSTUFF_KILL_BATS = Tristate.UNDEFINED;
+
+ /**
+ * Enables a faster walking
+ * if walking on a path block.
+ *
+ * @since v1-release0
+ */
+ public static final boolean SMALLSTUFF_FASTER_PATH_WALKING = true;
}
diff --git a/extension/src/main/java/de/jeremystartm/pickshadow/extension/Extension.java b/extension/src/main/java/de/jeremystartm/pickshadow/extension/Extension.java
index 5c48bbe..21882f9 100644
--- a/extension/src/main/java/de/jeremystartm/pickshadow/extension/Extension.java
+++ b/extension/src/main/java/de/jeremystartm/pickshadow/extension/Extension.java
@@ -20,7 +20,6 @@
package de.jeremystartm.pickshadow.extension;
import de.jeremystartm.pickshadow.common.CommonLibrary;
-import de.jeremystartm.pickshadow.extension.api.entity.player.PlayerDataFactory;
import de.jeremystartm.pickshadow.extension.api.translation.TranslationManager;
import de.jeremystartm.pickshadow.extension.command.general.AnnounceCommand;
import de.jeremystartm.pickshadow.extension.command.general.replacement.GamemodeCommand;
@@ -97,7 +96,6 @@ public final class Extension extends JavaPlugin {
TranslationManager.loadTranslations();
TranslationManager.processTranslations();
//TabListHandler.initialize();
- PlayerDataFactory.initialize();
Logger.info("Bootstrapped in " + Miscellaneous.measureExecutionTime(() -> {}) + "ms");
}
@@ -132,7 +130,9 @@ public final class Extension extends JavaPlugin {
new ClearChatCommand();
new ExtensionCommand();
new LanguageCommand();
+ new LegacyExtensionCommand();
new LinkCommand();
+ new SpeedCommand();
new TrollCommand();
@@ -150,8 +150,8 @@ public final class Extension extends JavaPlugin {
Logger.verb("Starting schedulers");
Bukkit.getServer().getGlobalRegionScheduler().runAtFixedRate(Extension.getInstance(), Scheduler::server, 1L, 1L);
}) + "ms");
- } catch (Exception exception) {
- Logger.crash("Initialization failed", exception);
+ } catch (Throwable throwable) {
+ Logger.crash("Initialization failed", throwable);
}
}
diff --git a/extension/src/main/java/de/jeremystartm/pickshadow/extension/api/command/Command.java b/extension/src/main/java/de/jeremystartm/pickshadow/extension/api/command/Command.java
new file mode 100644
index 0000000..c39820f
--- /dev/null
+++ b/extension/src/main/java/de/jeremystartm/pickshadow/extension/api/command/Command.java
@@ -0,0 +1,453 @@
+/*
+ * PICKSHADOW SERVER KIT SOURCE FILE
+ * Copyright (c) 2024 The PickShadow Server Kit authors
+ * Licensed under the GNU Affero General Public License v3
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.jeremystartm.pickshadow.extension.api.command;
+
+import de.jeremystartm.pickshadow.extension.ExtensionConfiguration;
+import de.jeremystartm.pickshadow.extension.api.entity.player.PSPlayer;
+import de.jeremystartm.pickshadow.extension.api.entity.player.PSPlayerFactory;
+import de.jeremystartm.pickshadow.extension.api.translation.LanguageString;
+import de.jeremystartm.pickshadow.extension.api.translation.TranslationManager;
+import de.staropensource.engine.base.logging.Logger;
+import lombok.Getter;
+import org.bukkit.Bukkit;
+import org.bukkit.command.*;
+import org.bukkit.craftbukkit.command.ServerCommandSender;
+import org.bukkit.entity.Player;
+import org.bukkit.util.StringUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.annotations.Range;
+
+import java.util.*;
+
+import static java.util.Map.entry;
+
+/**
+ * Abstract class for implementing commands.
+ *
+ * @see CommandForced
+ * @since v1-release0
+ */
+@Getter
+@SuppressWarnings({ "JavadocDeclaration" })
+public abstract class Command implements CommandExecutor {
+ /**
+ * Contains a list of all registered commands.
+ *
+ * @since v1-release0
+ */
+ private static final @NotNull List<@NotNull Command> REGISTERED = new ArrayList<>();
+
+ /**
+ * Contains a {@link CommandExecutor} implementation
+ * for disabled commands (caused by a disabled mode).
+ *
+ * @see ExtensionConfiguration#getEnabledModes()
+ * @since v1-release0
+ */
+ public static final @NotNull CommandExecutor disallowedExecutor = (sender, command, alias, arguments) -> {
+ sender.sendRichMessage(TranslationManager.get(LanguageString.ERROR_INVALID_MODE, sender, true));
+ return true;
+ };
+
+ /**
+ * Contains information about this command.
+ *
+ * @since v1-release0
+ * -- GETTER --
+ * Returns information about this command.
+ *
+ * @return command information
+ * @since v1-release0
+ */
+ private final @NotNull Command.Information information;
+
+ // -----> Constructors
+ /**
+ * Initializes this abstract class
+ * and registers the command.
+ *
+ * @param information information about the command
+ * @param commands all commands this class should handle
+ * @throws IllegalArgumentException if a command does not exist
+ * @since v1-release0
+ */
+ public Command(@NotNull Command.Information information, @NotNull String... commands) throws IllegalArgumentException {
+ boolean disallowedByMode = !Arrays.stream(ExtensionConfiguration.getInstance().getEnabledModes()).toList().contains(information.mode());
+ this.information = information;
+
+ for (String command : commands) {
+ PluginCommand pluginCommand = Bukkit.getPluginCommand(command);
+ if (pluginCommand == null)
+ throw new IllegalArgumentException("Command registration failed: The command \"" + command + "\" does not exist");
+
+ if (disallowedByMode) {
+ pluginCommand.setExecutor(disallowedExecutor);
+ pluginCommand.setTabCompleter(null);
+ } else {
+ pluginCommand.setExecutor(this);
+ pluginCommand.setTabCompleter(new TabCompleter() {
+ @Override
+ public @NotNull List onTabComplete(@NotNull CommandSender sender, @NotNull org.bukkit.command.Command command, @NotNull String label, @NotNull String[] args) {
+ return StringUtil.copyPartialMatches(args[args.length - 1], getCompletion().complete(sender, label, args), new ArrayList<>());
+ }
+ });
+ }
+ }
+
+ REGISTERED.add(this);
+ }
+
+ /**
+ * Initializes this abstract class
+ * and registers the command.
+ *
+ * Using this constructor instead of
+ * {@link #Command(Information, String...)}
+ * causes the creation of a dummy command
+ * which must be registered manually.
+ * Not recommended, use only when needed.
+ *
+ * @since v1-release0
+ */
+ public Command(@NotNull Command.Information information) {
+ this.information = information;
+ REGISTERED.add(this);
+ }
+
+
+ // -----> Static methods
+ /**
+ * Returns a list of all registered commands.
+ *
+ * @return list of registered commands
+ * @since v1-release0
+ */
+ public static List<@NotNull Command> getREGISTERED() {
+ return Collections.unmodifiableList(REGISTERED);
+ }
+
+
+ // -----> Getters
+ /**
+ * Provides tab completions for this command.
+ *
+ * @return completion
+ * @since v1-release0
+ */
+ public abstract @NotNull TabCompletion getCompletion();
+
+
+ // -----> Command invocation
+ /**
+ * Required by the {@link CommandExecutor} interface.
+ *
+ * @deprecated use {@link #invoke(CommandSender, String, String[])} instead
+ * @see #invoke(CommandSender, String, String[])
+ * @since v1-release0
+ */
+ @Deprecated
+ @Override
+ public final boolean onCommand(@NotNull CommandSender sender, @NotNull org.bukkit.command.Command command, @NotNull String alias, @NotNull String @NotNull [] arguments) {
+ invoke(sender, alias, arguments);
+ return true;
+ }
+
+ /**
+ * Executes this command.
+ *
+ * @since v1-release0
+ */
+ public final void invoke(@NotNull CommandSender sender, @NotNull String alias, @NotNull String @NotNull [] arguments) {
+ try {
+ invokeAll(sender, alias, arguments);
+
+ if (sender instanceof Player player)
+ invokePlayer(PSPlayerFactory.get(player), alias, arguments);
+ else if (sender instanceof ServerCommandSender consoleSender)
+ invokeConsole(consoleSender, alias, arguments);
+ else
+ throw new IllegalStateException("The command was neither executed by a player nor the server console");
+ } catch (Exception exception) {
+ sender.sendRichMessage(TranslationManager.get(LanguageString.ERROR_UNKNOWN, sender, true));
+ Logger.crash(
+ "Command /"
+ + information.name()
+ + " (under alias /"
+ + alias
+ + ") failed for sender "
+ + sender.getName()
+ + " (console="
+ + (sender instanceof ConsoleCommandSender)
+ + ") with the following arguments:\n"
+ + Arrays.toString(arguments),
+ exception,
+ false);
+ }
+ }
+
+ /**
+ * Command handler, regardless of whether the
+ * caller is a player or the server console.
+ *
+ * @param sender command sender which invoked this command
+ * @param alias alias used
+ * @param arguments command arguments
+ * @throws Exception on error
+ * @since v1-release0
+ */
+ @SuppressWarnings("RedundantThrows")
+ protected void invokeAll(@NotNull CommandSender sender, @NotNull String alias, @NotNull String @NotNull [] arguments) throws Exception {}
+
+ /**
+ * Command handler, invoked if the command
+ * has been invoked by the server console.
+ *
+ * @param console console command sender
+ * @param alias alias used
+ * @param arguments command arguments
+ * @throws Exception on error
+ * @since v1-release0
+ */
+ @SuppressWarnings("RedundantThrows")
+ protected void invokeConsole(@NotNull ServerCommandSender console, @NotNull String alias, @NotNull String @NotNull [] arguments) throws Exception {
+ if (information.executionTarget == ExecutionTarget.PLAYERS_ONLY)
+ console.sendRichMessage(TranslationManager.get(LanguageString.ERROR_NOT_A_PLAYER, console, true));
+ }
+
+ /**
+ * Command handler, invoked if the
+ * command has been invoked by a player.
+ *
+ * @param player player which invoked this command
+ * @param alias alias used
+ * @param arguments command arguments
+ * @throws Exception on error
+ * @since v1-release0
+ */
+ @SuppressWarnings("RedundantThrows")
+ protected void invokePlayer(@NotNull PSPlayer player, @NotNull String alias, @NotNull String @NotNull [] arguments) throws Exception {
+ if (information.executionTarget == ExecutionTarget.CONSOLE_ONLY)
+ player.messageTranslatable(LanguageString.ERROR_NOT_SERVER_CONSOLE, true);
+ }
+
+ // -----> Utility methods
+ /**
+ * Performs a permission check, sends the sender
+ * an error message and then returns.
+ * Useful for easy permission checks.
+ *
+ * @param sender sender to check
+ * @param permission permission to check for
+ * @return {@code true} if the permission is missing, {@code false} otherwise
+ * @since v1-release0
+ */
+ protected static boolean checkPermission(@NotNull CommandSender sender, @NotNull String permission) {
+ if (sender instanceof ServerCommandSender)
+ return false;
+
+ if (!sender.hasPermission(permission)) {
+ sender.sendRichMessage(
+ TranslationManager.get(LanguageString.ERROR_MISSING_PERM, sender, true)
+ .replace("%permission%", permission)
+ );
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Performs a permission check, sends the sender
+ * an error message and then returns.
+ * Useful for easy permission checks.
+ *
+ * @param player {@link PSPlayer} to check
+ * @param permission permission to check for
+ * @return {@code true} if the permission is missing, {@code false} otherwise
+ * @since v1-release0
+ */
+ protected static boolean checkPermission(@NotNull PSPlayer player, @NotNull String permission) {
+ if (!player.hasPermission(permission)) {
+ player.messageTranslatable(
+ LanguageString.ERROR_MISSING_PERM,
+ true,
+ entry("permission", permission)
+ );
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Determines the target player.
+ *
+ * @param sender command sender
+ * @param arguments array of arguments
+ * @param index index of the player name to check at
+ * @return target player and if the supplied command sender was used
+ * @throws IndexOutOfBoundsException if the size of {@code arguments} is smaller than the specified {@code index}
+ * @throws IllegalCallerException if no target was specified in {@code arguments} and the sender is the server console
+ * @throws IllegalArgumentException if the specified target in {@code arguments} is invalid or does not exist
+ * @since v1-release0
+ */
+ public static @NotNull Map.Entry<@NotNull PSPlayer, @NotNull Boolean> determineTarget(@NotNull CommandSender sender, @NotNull String @NotNull [] arguments, int index) throws IndexOutOfBoundsException, IllegalCallerException, IllegalArgumentException {
+ if (arguments.length == index) {
+ if (sender instanceof Player bukkitPlayer)
+ return entry(PSPlayerFactory.get(bukkitPlayer), true);
+ else {
+ sender.sendRichMessage(TranslationManager.get(LanguageString.ERROR_NOT_A_PLAYER, sender, true));
+ throw new IllegalCallerException("No target player was specified and the command sender is the server console");
+ }
+ } else if (arguments.length > index) {
+ Player bukkitPlayer = Bukkit.getPlayer(arguments[index]);
+ PSPlayer player;
+
+ if (bukkitPlayer == null) {
+ sender.sendRichMessage(TranslationManager.get(
+ LanguageString.ERROR_PLAYER_NOT_FOUND,
+ sender,
+ true,
+ entry("player", arguments[index])
+ ));
+ throw new IllegalArgumentException("The specified target player '" + arguments[index] + "' isn't online or is invalid");
+ } else
+ player = PSPlayerFactory.get(bukkitPlayer);
+
+ return entry(player, player.getUsername().equals(sender.getName()));
+ } else {
+ throw new IndexOutOfBoundsException("Size of 'arguments' is smaller than the specified 'index'");
+ }
+ }
+
+ // -----> Inner classes
+ /**
+ * Represents by whom a command can be used.
+ *
+ * @since v1-release0
+ */
+ public enum ExecutionTarget {
+ /**
+ * Restricts the command to players only.
+ *
+ * @since v1-release0
+ */
+ PLAYERS_ONLY,
+
+ /**
+ * Restricts the command to the server console only.
+ *
+ * @since v1-release0
+ */
+ CONSOLE_ONLY,
+
+ /**
+ * Restricts certain actions of the command to players only.
+ *
+ * @since v1-release0
+ */
+ CONSOLE_PARTIAL,
+
+ /**
+ * Restricts certain actions of the command to the server console only.
+ *
+ * @since v1-release0
+ */
+ PLAYERS_PARTIAL,
+
+ /**
+ * Allows the command to be executed by players and the server console.
+ *
+ * @since v1-release0
+ */
+ ALL
+ }
+
+ /**
+ * Provides information about a command.
+ *
+ * @param name Name of the command
+ * @param aliases Array of acceptable aliases
+ * @param description Description of the command
+ * @param syntax Syntax of the command
+ * @param mode Plugin mode of the command
+ * @param executionOrder Execution order in which the three execution methods shall be invoked.
+ *
+ * Must contain exactly three elements, representing this order:
+ *
+ *
+ * Values are required to be between {@code 0} and {@code 2}, with
+ * each value being used only once.
+ *
+ * Examples:
+ *
+ *
{@code new int[]{ 0, 2, 1 }}: 1. all, 2. player, 3. console
+ *
{@code new int[]{ 2, 1, 0 }}: 1. player, 2. console, 3. all
+ *
{@code new int[]{ 1, 2, 0 }}: 1. player, 2. all, 3. console
+ *
+ *
+ * If set to {@code null}, {@code new int[]{ 0, 1, 2 }} will be used.
+ * @param executionTarget information about who can execute the command
+ * @since v1-release0
+ */
+ public record Information (
+ @NotNull String name,
+ @NotNull String @NotNull [] aliases,
+ @NotNull String description,
+ @NotNull String syntax,
+ @NotNull String mode,
+ @Range(from = 0, to = 2) int @Nullable [] executionOrder,
+ @NotNull Command.ExecutionTarget executionTarget
+ ) {
+ /**
+ * Creates and initializes an
+ * instance of this record.
+ */
+ public Information {
+ if (executionOrder == null)
+ executionOrder = new int[]{ 0, 1, 2 };
+
+ // Verify 'executionOrder'
+ // -> Amount of items
+ if (executionOrder.length != 3)
+ throw new IllegalStateException("'executionOrder' does not contain exactly three items");
+
+ // -> Bounds
+ if (
+ (executionOrder[0] < 0 || executionOrder[0] > 2)
+ || (executionOrder[1] < 0 || executionOrder[1] > 2)
+ || (executionOrder[2] < 0 || executionOrder[2] > 2)
+ )
+ throw new IllegalStateException("Some item in 'executionOrder' is either smaller than '0' or bigger than '2'");
+
+ // -> Duplicate values
+ if (
+ executionOrder[0] == executionOrder[1]
+ || executionOrder[0] == executionOrder[2]
+ || executionOrder[1] == executionOrder[2]
+ )
+ throw new IllegalStateException("Two or more items in 'executionOrder' have the same value");
+ }
+ }
+}
diff --git a/extension/src/main/java/de/jeremystartm/pickshadow/extension/api/command/CommandBase.java b/extension/src/main/java/de/jeremystartm/pickshadow/extension/api/command/CommandBase.java
deleted file mode 100644
index dc7b836..0000000
--- a/extension/src/main/java/de/jeremystartm/pickshadow/extension/api/command/CommandBase.java
+++ /dev/null
@@ -1,198 +0,0 @@
-/*
- * PICKSHADOW SERVER KIT SOURCE FILE
- * Copyright (c) 2024 The PickShadow Server Kit authors
- * Licensed under the GNU Affero General Public License v3
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-
-package de.jeremystartm.pickshadow.extension.api.command;
-
-import de.jeremystartm.pickshadow.extension.ExtensionConfiguration;
-import de.jeremystartm.pickshadow.extension.api.translation.LanguageString;
-import de.jeremystartm.pickshadow.extension.api.translation.TranslationManager;
-import de.staropensource.engine.base.logging.Logger;
-import lombok.Getter;
-import org.bukkit.Bukkit;
-import org.bukkit.command.*;
-import org.bukkit.craftbukkit.command.ServerCommandSender;
-import org.bukkit.util.StringUtil;
-import org.jetbrains.annotations.NotNull;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Abstract class for implementing commands.
- *
- * @since v1-release0
- */
-@Getter
-public abstract class CommandBase implements CommandExecutor {
- /**
- * Contains a list of all registered commands.
- *
- * @since v1-release0
- */
- private static final @NotNull List<@NotNull CommandBase> REGISTERED = new ArrayList<>();
-
- /**
- * Contains a {@link CommandExecutor} implementation
- * for disabled commands (caused by a disabled mode).
- *
- * @see ExtensionConfiguration#getEnabledModes()
- * @since v1-release0
- */
- public static final @NotNull CommandExecutor disallowedExecutor = (sender, command, alias, arguments) -> {
- sender.sendRichMessage(TranslationManager.get(LanguageString.ERROR_INVALID_MODE, sender, true));
- return true;
- };
-
- /**
- * Contains a list of all {@link Command}s this
- * command has been registered for.
- *
- * @since v1-release0
- */
- private final @NotNull List<@NotNull PluginCommand> commands = new ArrayList<>();
-
- /**
- * Initializes this abstract class
- * and registers the command.
- *
- * @param mode mode to register this command in
- * @param commands all commands this class should handle
- * @throws IllegalArgumentException if a command does not exist
- * @since v1-release0
- */
- public CommandBase(@NotNull String mode, @NotNull String... commands) throws IllegalArgumentException {
- boolean disallowedByMode = !Arrays.stream(ExtensionConfiguration.getInstance().getEnabledModes()).toList().contains(mode);
-
- for (String command : commands) {
- PluginCommand pluginCommand = Bukkit.getPluginCommand(command);
- if (pluginCommand == null)
- throw new IllegalArgumentException("Command registration failed: The command \"" + command + "\" does not exist");
-
- if (disallowedByMode) {
- pluginCommand.setExecutor(disallowedExecutor);
- pluginCommand.setTabCompleter(null);
- } else {
- pluginCommand.setExecutor(this);
- pluginCommand.setTabCompleter(new TabCompleter() {
- @Override
- public @NotNull List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
- return StringUtil.copyPartialMatches(args[args.length - 1], getCompletion().complete(sender, label, args), new ArrayList<>());
- }
- });
- }
-
- this.commands.add(pluginCommand);
- }
-
- REGISTERED.add(this);
- }
-
- /**
- * Initializes this abstract class
- * and registers the command.
- *
- * Using this constructor instead of
- * {@link #CommandBase(String, String...)}
- * causes the creation of a dummy command
- * which must be registered manually.
- * Not recommended, use only when needed.
- *
- * @since v1-release0
- */
- public CommandBase() {
- REGISTERED.add(this);
- }
-
- /** {@inheritDoc} */
- @Override
- public final boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
- try {
- invoke(sender, command, alias, args);
- } catch (Exception exception) {
- Logger.crash(
- "Command /"
- + command.getName()
- + " (under alias /"
- + alias
- + ") failed for sender "
- + sender.getName()
- + " (console="
- + (sender instanceof ConsoleCommandSender)
- + ") with the following arguments:\n"
- + Arrays.toString(args),
- exception,
- false);
- sender.sendRichMessage(TranslationManager.get(LanguageString.ERROR_UNKNOWN, sender, true));
- }
- return true;
- }
-
- /**
- * Executes this command.
- *
- * @since v1-release0
- */
- @SuppressWarnings("NullableProblems") // intentional, see CommandBaseWithNull
- public abstract void invoke(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] arguments);
-
- /**
- * Provides tab completions for this command.
- *
- * @return completion
- * @since v1-release0
- */
- public abstract @NotNull TabCompletion getCompletion();
-
- /**
- * Performs a permission check, sends the sender
- * an error message and then returns.
- * Useful for easy permission checks.
- *
- * @param sender sender to check
- * @param permission permission to check for
- * @return {@code true} if the permission is missing, {@code false} otherwise
- * @since v1-release0
- */
- protected static boolean checkPermission(@NotNull CommandSender sender, @NotNull String permission) {
- if (sender instanceof ServerCommandSender)
- return false;
-
- if (!sender.hasPermission(permission)) {
- sender.sendRichMessage(
- TranslationManager.get(LanguageString.ERROR_MISSING_PERM, sender, true)
- .replace("%permission%", permission)
- );
- return true;
- }
-
- return false;
- }
-
- /**
- * Returns a list of all registered commands.
- *
- * @return list of registered commands
- * @since v1-release0
- */
- public static List<@NotNull CommandBase> getREGISTERED() {
- return Collections.unmodifiableList(REGISTERED);
- }
-}
diff --git a/extension/src/main/java/de/jeremystartm/pickshadow/extension/api/command/CommandForced.java b/extension/src/main/java/de/jeremystartm/pickshadow/extension/api/command/CommandForced.java
new file mode 100644
index 0000000..56a72fc
--- /dev/null
+++ b/extension/src/main/java/de/jeremystartm/pickshadow/extension/api/command/CommandForced.java
@@ -0,0 +1,83 @@
+/*
+ * PICKSHADOW SERVER KIT SOURCE FILE
+ * Copyright (c) 2024 The PickShadow Server Kit authors
+ * Licensed under the GNU Affero General Public License v3
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.jeremystartm.pickshadow.extension.api.command;
+
+import de.jeremystartm.pickshadow.extension.api.entity.player.PSPlayer;
+import lombok.Getter;
+import org.bukkit.command.*;
+import org.bukkit.craftbukkit.command.ServerCommandSender;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Abstract class for implementing commands.
+ *
+ * Forces {@link #invokeAll(CommandSender, String, String[])},
+ * {@link #invokeConsole(ServerCommandSender, String, String[])}
+ * and {@link #invokePlayer(PSPlayer, String, String[])}
+ * on the implementation class.
+ *
+ * Very useful during development.
+ *
+ * @see Command
+ * @since v1-release0
+ */
+@Getter
+public abstract class CommandForced extends Command {
+ /**
+ * Initializes this abstract class
+ * and registers the command.
+ *
+ * @param information information about the command
+ * @param commands all commands this class should handle
+ * @throws IllegalArgumentException if a command does not exist
+ * @since v1-release0
+ */
+ public CommandForced(@NotNull Command.Information information, @NotNull String... commands) throws IllegalArgumentException {
+ super(information, commands);
+ }
+
+ /**
+ * Initializes this abstract class
+ * and registers the command.
+ *
+ * Using this constructor instead of
+ * {@link #CommandForced(Information, String...)}
+ * causes the creation of a dummy command
+ * which must be registered manually.
+ * Not recommended, use only when needed.
+ *
+ * @since v1-release0
+ */
+ public CommandForced(@NotNull Command.Information information) {
+ super(information);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected abstract void invokeAll(@NotNull CommandSender sender, @NotNull String alias, @NotNull String[] arguments) throws Exception;
+
+ /** {@inheritDoc} */
+ @Override
+ protected abstract void invokeConsole(@NotNull ServerCommandSender console, @NotNull String alias, @NotNull String[] arguments) throws Exception;
+
+ /** {@inheritDoc} */
+ @Override
+ protected abstract void invokePlayer(@NotNull PSPlayer player, @NotNull String alias, @NotNull String[] arguments) throws Exception;
+}
diff --git a/extension/src/main/java/de/jeremystartm/pickshadow/extension/api/entity/player/PSPlayer.java b/extension/src/main/java/de/jeremystartm/pickshadow/extension/api/entity/player/PSPlayer.java
new file mode 100644
index 0000000..4c1f8ec
--- /dev/null
+++ b/extension/src/main/java/de/jeremystartm/pickshadow/extension/api/entity/player/PSPlayer.java
@@ -0,0 +1,1005 @@
+/*
+ * PICKSHADOW SERVER KIT SOURCE FILE
+ * Copyright (c) 2024 The PickShadow Server Kit authors
+ * Licensed under the GNU Affero General Public License v3
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.jeremystartm.pickshadow.extension.api.entity.player;
+
+import com.google.gson.*;
+import com.google.gson.reflect.TypeToken;
+import de.jeremystartm.pickshadow.extension.Extension;
+import de.jeremystartm.pickshadow.extension.api.translation.LanguageString;
+import de.jeremystartm.pickshadow.extension.api.translation.TranslationManager;
+import de.jeremystartm.pickshadow.extension.api.type.PlayerAttribute;
+import de.jeremystartm.pickshadow.extension.api.type.PlayerDamageState;
+import de.staropensource.engine.base.logging.Logger;
+import fr.mrmicky.fastboard.adventure.FastBoard;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.Setter;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.minimessage.MiniMessage;
+import net.minecraft.server.MinecraftServer;
+import org.bukkit.Bukkit;
+import org.bukkit.GameMode;
+import org.bukkit.Location;
+import org.bukkit.World;
+import org.bukkit.damage.DamageSource;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.Player;
+import org.bukkit.event.player.PlayerKickEvent;
+import org.bukkit.permissions.Permission;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+import static java.util.Map.entry;
+
+/**
+ * Bukkit's {@link Player} class,
+ * but it's a lot nicer to work with.
+ *
+ * @since v1-release0
+ */
+@Getter
+@SuppressWarnings({ "UnusedReturnValue", "unused" })
+public final class PSPlayer {
+ /**
+ * Contains the {@link Data} instance
+ * associated to this player.
+ *
+ * @since v1-release0
+ * -- GETTER --
+ * Returns the {@link Data} instance
+ * associated to this player.
+ *
+ * @since v1-release0
+ */
+ private final @NotNull PSPlayer.Data data;
+
+ /**
+ * Contains the {@link Player} instance
+ * associated to this player.
+ *
+ * @since v1-release0
+ * -- GETTER --
+ * Returns the {@link Player} instance
+ * associated to this player.
+ *
+ * @since v1-release0
+ */
+ @ApiStatus.Obsolete
+ private final @NotNull Player bukkitPlayer;
+
+ /**
+ * Creates and initializes an
+ * instance of this class.
+ *
+ * @param player Bukkit {@link Player}
+ * @since v1-release0
+ */
+ PSPlayer(@NotNull Player player) {
+ // Set final variables
+ bukkitPlayer = player;
+ data = new Data(this);
+ }
+
+
+ // -----> Static
+ /**
+ * Returns an array of all online players.
+ *
+ * @return online players array
+ * @since v1-release0
+ */
+ public static @NotNull PSPlayer[] getOnline() {
+ List players = new ArrayList<>();
+
+ for (Player player : Bukkit.getOnlinePlayers())
+ try {
+ players.add(PSPlayerFactory.get(player));
+ } catch (NullPointerException ignored) {}
+
+ return players.toArray(new PSPlayer[0]);
+ }
+
+ /**
+ * Broadcasts the specified message
+ * to all online players.
+ *
+ * This method will run the specified
+ * message through MiniMessage.
+ *
+ * @param message message to broadcast
+ * @since v1-release0
+ */
+ public static void broadcast(@NotNull String message) {
+ Component messageFormatted = MiniMessage.miniMessage().deserialize(message);
+
+ for (PSPlayer player : getOnline())
+ player.messageRaw(message);
+ }
+
+ /**
+ * Broadcasts the specified message
+ * to all online players.
+ *
+ * This method will automatically translate
+ * the specified {@link LanguageString}.
+ *
+ * @param languageString language string to get
+ * @param includePrefix if {@link LanguageString#PREFIX} should be prepended
+ * @param placeholders placeholders
+ * @since v1-release0
+ */
+ @SafeVarargs
+ public static void broadcastTranslatable(@NotNull LanguageString languageString, boolean includePrefix, @NotNull Map.Entry<@NotNull String, @NotNull String> @Nullable ... placeholders) {
+ for (PSPlayer player : getOnline())
+ player.messageTranslatable(languageString, includePrefix, placeholders);
+ }
+
+ /**
+ * Broadcasts the specified message
+ * to all online players.
+ *
+ * This method causes the message
+ * to be sent directly, without
+ * any MiniMessage formatting.
+ *
+ * @param message message to broadcast
+ * @since v1-release0
+ */
+ public static void broadcastRaw(@NotNull String message) {
+ for (PSPlayer player : getOnline())
+ player.messageRaw(message);
+ }
+
+
+ // -----> Information
+ /**
+ * Returns if this player is online.
+ *
+ * This method will pretty much
+ * always return {@code true}.
+ *
+ * @return online?
+ * @since v1-release0
+ */
+ public boolean isOnline() {
+ return bukkitPlayer.isOnline();
+ }
+
+ /**
+ * Returns this player's {@link UUID}.
+ *
+ * @return player {@link UUID}
+ * @since v1-release0
+ */
+ public @NotNull UUID getUUID() {
+ return bukkitPlayer.getUniqueId();
+ }
+
+ /**
+ * Returns this player's username.
+ *
+ * @return username
+ * @since v1-release0
+ */
+ public @NotNull String getUsername() {
+ return bukkitPlayer.getName();
+ }
+
+ /**
+ * Returns the full player identification
+ * for use in log messages and other strings.
+ *
+ * @return human-friendly player identification string
+ * @since v1-release0
+ */
+ public @NotNull String getIdentificationString() {
+ return getUsername() + " [" + getUUID() + "]";
+ }
+
+ /**
+ * Returns the position of this player.
+ *
+ * @return position
+ * @since v1-release0
+ */
+ public @NotNull Location getPosition() {
+ return bukkitPlayer.getLocation();
+ }
+
+ /**
+ * Returns the world this player is in.
+ *
+ * @return world
+ * @since v1-release0
+ */
+ public @NotNull World getWorld() {
+ return bukkitPlayer.getWorld();
+ }
+
+
+ // -----> Connection
+ /**
+ * Terminates the connection to the player.
+ *
+ * This will instantaneously remove the player
+ * from the game without informing them. As if
+ * their internet connection died.
+ *
+ * @since v1-release0
+ */
+ public void terminate() {
+ Objects.requireNonNull(MinecraftServer.getServer().getPlayerList().getPlayer(bukkitPlayer.getUniqueId())).disconnect();
+ }
+
+ /**
+ * Kicks the player.
+ *
+ * This method will run the specified
+ * kick reason through MiniMessage.
+ *
+ * @param reason kick reason
+ * @since v1-release0
+ */
+ public void kick(@NotNull String reason) {
+ bukkitPlayer.kick(MiniMessage.miniMessage().deserialize(reason));
+ }
+
+ /**
+ * Kicks the player.
+ *
+ * This method will run the specified
+ * kick reason through MiniMessage.
+ *
+ * @param reason kick reason
+ * @param cause cause of the kick
+ * @since v1-release0
+ */
+ public void kick(@NotNull String reason, @NotNull PlayerKickEvent.Cause cause) {
+ bukkitPlayer.kick(MiniMessage.miniMessage().deserialize(reason), cause);
+ }
+
+ /**
+ * Kicks the player.
+ *
+ * This method will automatically translate
+ * the specified {@link LanguageString}.
+ *
+ * @param languageString language string to get
+ * @param placeholders placeholders
+ * @since v1-release0
+ */
+ @SafeVarargs
+ public final void kickTranslatable(@NotNull LanguageString languageString, @NotNull Map.Entry<@NotNull String, @NotNull String> @Nullable ... placeholders) {
+ bukkitPlayer.kick(MiniMessage.miniMessage().deserialize(TranslationManager.get(languageString, this, false, placeholders)));
+ }
+ /**
+ * Kicks the player.
+ *
+ * This method will automatically translate
+ * the specified {@link LanguageString}.
+ *
+ * @param cause cause of the kick
+ * @param languageString language string to get
+ * @param placeholders placeholders
+ * @since v1-release0
+ */
+ @SafeVarargs
+ public final void kickTranslatable(@NotNull PlayerKickEvent.Cause cause, @NotNull LanguageString languageString, @NotNull Map.Entry<@NotNull String, @NotNull String> @Nullable ... placeholders) {
+ bukkitPlayer.kick(MiniMessage.miniMessage().deserialize(TranslationManager.get(languageString, this, false, placeholders)), cause);
+ }
+
+ /**
+ * Kicks the player.
+ *
+ * This method causes the message
+ * to be sent directly, without
+ * any MiniMessage formatting.
+ *
+ * @param reason kick reason
+ * @since v1-release0
+ */
+ public void kickRaw(@NotNull String reason) {
+ bukkitPlayer.kick(Component.text(reason));
+ }
+
+ /**
+ * Kicks the player.
+ *
+ * This method causes the message
+ * to be sent directly, without
+ * any MiniMessage formatting.
+ *
+ * @param reason kick reason
+ * @param cause cause of the kick
+ * @since v1-release0
+ */
+ public void kickRaw(@NotNull String reason, @NotNull PlayerKickEvent.Cause cause) {
+ bukkitPlayer.kick(Component.text(reason), cause);
+ }
+
+
+ // -----> Messaging
+ /**
+ * Messages this player.
+ *
+ * This method will run the specified
+ * message through MiniMessage.
+ *
+ * @param message message to send
+ * @return this instance
+ * @since v1-release0
+ */
+ public @NotNull PSPlayer message(@NotNull String message) {
+ bukkitPlayer.sendRichMessage(message);
+ return this;
+ }
+
+ /**
+ * Messages this player.
+ *
+ * This method will automatically translate
+ * the specified {@link LanguageString}.
+ *
+ * @param languageString language string to get
+ * @param includePrefix if {@link LanguageString#PREFIX} should be prepended
+ * @param placeholders placeholders
+ * @return this instance
+ * @since v1-release0
+ */
+ @SafeVarargs
+ public final @NotNull PSPlayer messageTranslatable(@NotNull LanguageString languageString, boolean includePrefix, @NotNull Map.Entry<@NotNull String, @NotNull String> @Nullable ... placeholders) {
+ message(TranslationManager.get(languageString, this, includePrefix, placeholders));
+ return this;
+ }
+
+ /**
+ * Messages the player.
+ *
+ * This method causes the message
+ * to be sent directly, without
+ * any MiniMessage formatting.
+ *
+ * @param message message to send
+ * @return this instance
+ * @since v1-release0
+ */
+ public @NotNull PSPlayer messageRaw(@NotNull String message) {
+ bukkitPlayer.sendPlainMessage(message);
+ return this;
+ }
+
+
+ // -----> Scheduling
+ /**
+ * Schedules an action.
+ *
+ * @param task {@link Runnable} to invoke after the set delay
+ * @param delay delay in ticks; values lower than {@code 1} will be treated as {@code 0}
+ * @since v1-release0
+ */
+ public void schedule(@NotNull Runnable task, long delay) {
+ if (delay < 1L)
+ delay = 1L;
+
+ bukkitPlayer.getScheduler().execute(Extension.getInstance(), task, null, delay);
+ }
+
+ /**
+ * Schedules an action.
+ *
+ * @param task {@link Runnable} to invoke after the set delay
+ * @param retired {@link Runnable} to invoke if the player disconnects during the delay
+ * @param delay delay in ticks; values lower than {@code 1} will be treated as {@code 0}
+ * @since v1-release0
+ */
+ public void schedule(@NotNull Runnable task, @NotNull Runnable retired, long delay) {
+ if (delay < 1L)
+ delay = 1L;
+
+ bukkitPlayer.getScheduler().execute(Extension.getInstance(), task, retired, delay);
+ }
+
+ /**
+ * Schedules an action to execute repeatedly
+ * until this player disconnects.
+ *
+ * @param task {@link Runnable} to invoke after the set delay
+ * @param delay delay in ticks; values lower than {@code 1} will be treated as {@code 0}
+ * @since v1-release0
+ */
+ public void scheduleRepeatedly(@NotNull Runnable task, long delay) {
+ if (delay < 1L)
+ delay = 1L;
+
+ long finalDelay = delay;
+ bukkitPlayer.getScheduler().execute(Extension.getInstance(), () -> {
+ task.run();
+
+ scheduleRepeatedly(task, finalDelay);
+ }, null, delay);
+ }
+
+
+ // -----> Miscellaneous
+ /**
+ * Checks whether the specified permission is set.
+ *
+ * @param permission permission to check
+ * @return set?
+ * @since v1-release0
+ */
+ public boolean hasPermission(@NotNull Permission permission) {
+ return bukkitPlayer.hasPermission(permission);
+ }
+ /**
+ * Checks whether the specified permission is set.
+ *
+ * @param permission permission to check
+ * @return set?
+ * @since v1-release0
+ */
+ public boolean hasPermission(@NotNull String permission) {
+ return bukkitPlayer.hasPermission(permission);
+ }
+
+ /**
+ * Returns the amount of height accumulated during a fall.
+ *
+ * @return fall distance
+ * @since v1-release0
+ */
+ public float getFallDistance() {
+ return bukkitPlayer.getFallDistance();
+ }
+
+ /**
+ * Sets the amount of height accumulated during a fall.
+ *
+ * @param fallDistance new fall distance
+ * @return this instance
+ * @since v1-release0
+ */
+ public @NotNull PSPlayer setFallDistance(float fallDistance) {
+ bukkitPlayer.setFallDistance(fallDistance);
+ return this;
+ }
+
+ /**
+ * Returns this player's gamemode.
+ *
+ * @return gamemode
+ * @since v1-release0
+ */
+ public @NotNull GameMode getGamemode() {
+ return bukkitPlayer.getGameMode();
+ }
+
+ /**
+ * Sets this player's gamemode.
+ *
+ * @param gamemode new gamemode
+ * @return this instance
+ * @since v1-release0
+ */
+ public @NotNull PSPlayer setGamemode(@NotNull GameMode gamemode) {
+ bukkitPlayer.setGameMode(gamemode);
+ return this;
+ }
+
+ /**
+ * Damages this player.
+ *
+ * @param healthPoints amount of damage to deal in health points
+ * @return this instance
+ * @since v1-release0
+ */
+ public @NotNull PSPlayer damage(double healthPoints) {
+ bukkitPlayer.damage(healthPoints);
+ return this;
+ }
+
+ /**
+ * Damages this player.
+ *
+ * @param healthPoints amount of damage to deal in health points
+ * @param entity entity which dealt the damage
+ * @return this instance
+ * @since v1-release0
+ */
+ public @NotNull PSPlayer damage(double healthPoints, @NotNull Entity entity) {
+ bukkitPlayer.damage(healthPoints, entity);
+ return this;
+ }
+
+ /**
+ * Damages this player.
+ *
+ * @param healthPoints amount of damage to deal in health points
+ * @param damageSource information about what the damage caused
+ * @return this instance
+ * @since v1-release0
+ */
+ @SuppressWarnings("UnstableApiUsage")
+ public @NotNull PSPlayer damage(double healthPoints, @NotNull DamageSource damageSource) {
+ bukkitPlayer.damage(healthPoints, damageSource);
+ return this;
+ }
+
+
+ // -----> Data
+ /**
+ * Applies all player attributes.
+ *
+ * @return this instance
+ * @since v1-release0
+ */
+ public @NotNull PSPlayer applyPlayerAttributes() {
+ for (PlayerAttribute attribute : PlayerAttribute.values())
+ switch (attribute) {
+ case WALKING_SPEED, FLY_SPEED -> {
+ // Calculate
+ float speed = (float) data.getPlayerAttribute(attribute) + (float) data.getPlayerAttributeOverride(attribute);
+
+ // Keep within bounds
+ if (speed > 1f)
+ speed = 1f;
+ if (speed < 0f)
+ speed = 0f;
+
+ // Apply
+ if (attribute == PlayerAttribute.WALKING_SPEED)
+ bukkitPlayer.setWalkSpeed(speed);
+ else
+ bukkitPlayer.setFlySpeed(speed);
+ }
+ }
+
+ return this;
+ }
+
+ @Setter
+ @Getter
+ @SuppressWarnings({ "JavadocDeclaration" })
+ public static final class Data {
+ /**
+ * Contains a static {@link Gson} instance
+ * used for (de-)serializing {@link Data}
+ * instances using JSON.
+ *
+ * @since v1-release0
+ */
+ private static final @NotNull Gson gson = new GsonBuilder()
+ .disableJdkUnsafe()
+ //.generateNonExecutableJson()
+ .serializeNulls()
+ .setDateFormat("dd.MM.yyyy HH:mm:ss:SSSS Z")
+ .setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
+ //.setFormattingStyle(FormattingStyle.COMPACT)
+ .setLongSerializationPolicy(LongSerializationPolicy.STRING)
+ //.setStrictness(Strictness.STRICT)
+ .create();
+
+ /**
+ * Contains the associated {@link PSPlayer} instance.
+ *
+ * Will be saved.
+ *
+ * @since v1-release0
+ * -- GETTER --
+ * Returns the associated {@link PSPlayer} instance.
+ *
+ * Will be saved.
+ *
+ * @return associated {@link PSPlayer} instance
+ * @since v1-release0
+ */
+ @Setter(value = AccessLevel.NONE)
+ private final PSPlayer player;
+
+ /**
+ * Contains the player list scoreboard.
+ *
+ * Will not be saved.
+ *
+ * @since v1-release0
+ * -- GETTER --
+ * Returns the player list scoreboard.
+ *
+ * Will not be saved.
+ *
+ * @return player list scoreboard
+ * @since v1-release0
+ * -- SETTER --
+ * Sets the player list scoreboard.
+ *
+ * Will not be saved.
+ *
+ * @param playerListScoreboard new player list scoreboard
+ * @since v1-release0
+ */
+ @ApiStatus.Experimental
+ private @NotNull FastBoard playerListScoreboard;
+
+ /**
+ * Contains the preferred language
+ * the player has set.
+ *
+ * Will be saved.
+ *
+ * @since v1-release0
+ * -- GETTER --
+ * Returns the preferred language
+ * the player has set.
+ *
+ * Will be saved.
+ *
+ * @return preferred language
+ * @since v1-release0
+ * -- SETTER --
+ * Sets the preferred language
+ * the player has set.
+ *
+ * Will be saved.
+ *
+ * @param language new preferred language
+ * @since v1-release0
+ */
+ private String language;
+
+ /**
+ * Contains when the player has
+ * first been seen on the server.
+ *
+ * Will be saved.
+ *
+ * @since v1-release0
+ * -- GETTER --
+ * Returns when the player has
+ * first been seen on the server.
+ *
+ * Will be saved.
+ *
+ * @return first played date and time
+ * @since v1-release0
+ */
+ private ZonedDateTime firstSeen;
+
+ /**
+ * Contains the UUID of the player
+ * this player has messaged last.
+ *
+ * Will be saved.
+ *
+ * @since v1-release0
+ * -- GETTER --
+ * Returns the UUID of the player
+ * this player has messaged last.
+ *
+ * Will be saved.
+ *
+ * @return UUID of the player last messaged
+ * @since v1-release0
+ */
+ private UUID lastMessaged;
+
+ /**
+ * Contains a map of attributes.
+ *
+ * @see #playerAttributeOverrides
+ * @since v1-release0
+ */
+ @Getter(value = AccessLevel.NONE)
+ @Setter(value = AccessLevel.NONE)
+ private @NotNull Map<@NotNull PlayerAttribute, @NotNull Object> playerAttributes = Collections.synchronizedMap(new HashMap<>());
+
+ /**
+ * Contains a map of attributes overrides.
+ *
+ * Note: This map doesn't serve actual
+ * overrides. Instead, numbered attributes
+ * are either added or subtracted by the
+ * number present in this map.
+ *
+ * @see #playerAttributes
+ * @since v1-release0
+ */
+ @Getter(value = AccessLevel.NONE)
+ @Setter(value = AccessLevel.NONE)
+ private @NotNull Map<@NotNull PlayerAttribute, @NotNull Object> playerAttributeOverrides = Collections.synchronizedMap(new HashMap<>());
+
+ /**
+ * Contains a map of damage states.
+ *
+ * @since v1-release0
+ */
+ @Getter(value = AccessLevel.NONE)
+ @Setter(value = AccessLevel.NONE)
+ private @NotNull Map<@NotNull PlayerDamageState, @NotNull Integer> playerDamageStates = Collections.synchronizedMap(new HashMap<>());
+
+
+ // -----> Constructors
+ /**
+ * Creates and initializes an instance of this class.
+ *
+ * This constructor creates a completely
+ * new instance without any data.
+ *
+ * @param player {@link PSPlayer} instance to associate with
+ * @since v1-release0
+ */
+ Data(@NotNull PSPlayer player) {
+ Logger.info("Initializing fresh player data for player " + player.getIdentificationString());
+
+ this.player = player;
+ //TabListHandler.getInstance().initializeTabList(this);
+ language = "en";
+ firstSeen = ZonedDateTime.now();
+ lastMessaged = null;
+ }
+
+ /**
+ * Creates and initializes an instance of this class.
+ *
+ * This constructor creates a new instance
+ * from already existing player data.
+ *
+ * @param player {@link PSPlayer} instance to associate with
+ * @param firstSeen when the player was first seen
+ * @since v1-release0
+ */
+ private Data(
+ @NotNull PSPlayer player,
+ @NotNull String language,
+ @NotNull ZonedDateTime firstSeen,
+ @Nullable UUID lastMessaged,
+ @NotNull Map<@NotNull PlayerAttribute, @NotNull Object> playerAttributes,
+ @NotNull Map<@NotNull PlayerAttribute, @NotNull Object> playerAttributeOverrides
+ ) {
+ Logger.info("Initializing player data from existing data for player " + player.getIdentificationString());
+
+ this.player = player;
+ //TabListHandler.getInstance().initializeTabList(this);
+ this.language = language;
+ this.firstSeen = firstSeen;
+ this.lastMessaged = lastMessaged;
+ this.playerAttributes = playerAttributes;
+ this.playerAttributeOverrides = playerAttributeOverrides;
+ }
+
+
+ // -----> Attributes
+ /**
+ * Returns the override for the specified player attribute.
+ *
+ * @param value type
+ * @param attribute attribute to return
+ * @return attribute value
+ * @since v1-release0
+ */
+ public @NotNull T getPlayerAttribute(@NotNull PlayerAttribute attribute) {
+ return (T) playerAttributes.getOrDefault(attribute, attribute.getDefault());
+ }
+
+ /**
+ * Updates the specified player attribute.
+ *
+ * @param value type
+ * @param attribute attribute to update
+ * @param value value to set the attribute to
+ * @since v1-release0
+ */
+ public void setPlayerAttribute(@NotNull PlayerAttribute attribute, @Nullable T value) {
+ if (value == null || attribute.getDefault().equals(value))
+ playerAttributes.remove(attribute);
+ else
+ playerAttributes.put(attribute, value);
+ }
+
+ /**
+ * Nullifies and resets the specified
+ * player attribute to it's default value.
+ *
+ * @param attribute attribute to nullify
+ * @since v1-release0
+ */
+ public void nullifyPlayerAttribute(@NotNull PlayerAttribute attribute) {
+ playerAttributes.remove(attribute);
+ }
+
+ /**
+ * Returns if a override for the
+ * specified player attribute exists.
+ *
+ * @param attribute attribute to check
+ * @return overridden?
+ * @since v1-release0
+ */
+ public boolean isPlayerAttributeOverrideSet(@NotNull PlayerAttribute attribute) {
+ return playerAttributeOverrides.containsKey(attribute);
+ }
+
+ /**
+ * Returns the overridden value for
+ * the specified player attribute.
+ *
+ * @param value type
+ * @param attribute attribute to return
+ * @return overridden attribute value
+ * @since v1-release0
+ */
+ public @NotNull T getPlayerAttributeOverride(@NotNull PlayerAttribute attribute) {
+ return (T) playerAttributeOverrides.getOrDefault(attribute, attribute.getZero());
+ }
+
+ /**
+ * Updates the override for the
+ * specified player attribute.
+ *
+ * @param value type
+ * @param attribute attribute to update
+ * @param value value to set the override to
+ * @since v1-release0
+ */
+ public void setPlayerAttributeOverride(@NotNull PlayerAttribute attribute, @Nullable T value) {
+ if (
+ value == null
+ || (value instanceof Byte && (Byte) value == 0)
+ || (value instanceof Short && (Short) value == 0)
+ || (value instanceof Integer && (Integer) value == 0)
+ || (value instanceof Float && (Float) value == 0f)
+ || (value instanceof Double && (Double) value == 0d)
+ )
+ playerAttributeOverrides.remove(attribute);
+ else
+ playerAttributeOverrides.put(attribute, value);
+ }
+
+ /**
+ * Nullifies and removes the override
+ * for the specified player attribute.
+ *
+ * @param attribute attribute to nullify
+ * @since v1-release0
+ */
+ public void nullifyPlayerAttributeOverride(@NotNull PlayerAttribute attribute) {
+ playerAttributeOverrides.remove(attribute);
+ }
+
+
+ // -----> Damage states
+ /**
+ * Returns the specified damage state.
+ *
+ * @param damageState damage state to return
+ * @return damage state
+ * @since v1-release0
+ */
+ public int getDamageState(@NotNull PlayerDamageState damageState) {
+ return playerDamageStates.getOrDefault(damageState, damageState.getDefault());
+ }
+
+ /**
+ * Updates the specified damage state.
+ *
+ * @param damageState damageState to update
+ * @param value value to set the damageState to
+ * @since v1-release0
+ */
+ public void setDamageState(@NotNull PlayerDamageState damageState, @Nullable Integer value) {
+ if (value == null || damageState.getDefault() == value)
+ playerDamageStates.remove(damageState);
+ else
+ playerDamageStates.put(damageState, value);
+ }
+
+ /**
+ * Increases the specified damage state by one.
+ *
+ * @param damageState damageState to increase
+ * @since v1-release0
+ */
+ public void increaseDamageState(@NotNull PlayerDamageState damageState) {
+ setDamageState(damageState, getDamageState(damageState) + 1);
+ }
+
+ /**
+ * Nullifies and resets the specified
+ * damage state to it's default value.
+ *
+ * @param damageState damageState to nullify
+ * @since v1-release0
+ */
+ public void nullifyDamageState(@NotNull PlayerDamageState damageState) {
+ playerDamageStates.remove(damageState);
+ }
+
+
+ // -----> Serialization
+ /**
+ * Deserializes a JSON object and creates
+ * a new {@link Data} instance.
+ *
+ * @param jsonString JSON string
+ * @throws JsonSyntaxException on invalid JSON
+ * @throws RuntimeException on invalid PlayerData serialization
+ * @since v1-release0
+ */
+ public static @NotNull PSPlayer.Data fromJSON(@NotNull String jsonString) throws JsonSyntaxException, NullPointerException {
+ try {
+ // Parse data
+ Map<@NotNull String, @NotNull String> data = gson.fromJson(jsonString, new TypeToken