Mostly javadoc, and also some fixes there and there

This commit is contained in:
Marc Baloup 2022-08-10 03:04:12 +02:00
parent f976350ee1
commit 54bc8ab99a
Signed by: marcbal
GPG Key ID: BBC0FE3ABC30B893
42 changed files with 1671 additions and 733 deletions

View File

@ -1,16 +1,12 @@
package fr.pandacube.lib.bungee.commands;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.context.ParsedCommandNode;
import com.mojang.brigadier.context.StringRange;
import com.mojang.brigadier.suggestion.Suggestion;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import com.mojang.brigadier.tree.LiteralCommandNode;
import fr.pandacube.lib.commands.BrigadierCommand;
import fr.pandacube.lib.commands.SuggestionsSupplier;
import fr.pandacube.lib.reflect.Reflect;
import fr.pandacube.lib.util.Log;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.ProxyServer;
@ -18,18 +14,26 @@ import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.plugin.Command;
import net.md_5.bungee.api.plugin.TabExecutor;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* Abstract class that holds the logic of a specific command to be registered in {@link BungeeBrigadierDispatcher} and
* BungeeCord command API.
*/
public abstract class BungeeBrigadierCommand extends BrigadierCommand<CommandSender> {
/**
* The command dispatcher.
*/
protected BungeeBrigadierDispatcher dispatcher;
/**
* Instanciate this command isntance.
* @param d the dispatcher in which to register this command.
*/
public BungeeBrigadierCommand(BungeeBrigadierDispatcher d) {
if (d == null) {
throw new IllegalStateException("BungeeBrigadierDispatcher not provided.");
@ -62,12 +66,14 @@ public abstract class BungeeBrigadierCommand extends BrigadierCommand<CommandSen
.redirect(commandNode)
.build()
);
ProxyServer.getInstance().getPluginManager().registerCommand(dispatcher.plugin, new CommandRelay(alias));
}
}
private class CommandRelay extends Command implements TabExecutor {
private final String alias;
public CommandRelay(String alias) {
@ -117,9 +123,11 @@ public abstract class BungeeBrigadierCommand extends BrigadierCommand<CommandSen
}
/**
* Wraps the provided {@link SuggestionsSupplier} into a Brigadiers {@link SuggestionProvider}.
* @param suggestions the suggestions to wrap.
* @return a {@link SuggestionProvider} generating the suggestions from the provided {@link SuggestionsSupplier}.
*/
protected SuggestionProvider<CommandSender> wrapSuggestions(SuggestionsSupplier<CommandSender> suggestions) {
return wrapSuggestions(suggestions, Function.identity());
}

View File

@ -1,21 +1,7 @@
package fr.pandacube.lib.bungee.commands;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.ParseResults;
import com.mojang.brigadier.context.CommandContextBuilder;
import com.mojang.brigadier.context.StringRange;
import com.mojang.brigadier.context.SuggestionContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.Suggestion;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import com.mojang.brigadier.tree.CommandNode;
import com.mojang.brigadier.tree.LiteralCommandNode;
import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.commands.BrigadierCommand;
import fr.pandacube.lib.commands.BrigadierDispatcher;
import fr.pandacube.lib.commands.BrigadierSuggestionsUtil;
import fr.pandacube.lib.util.Log;
import net.kyori.adventure.text.ComponentLike;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.ProxyServer;
@ -25,11 +11,10 @@ import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.event.EventHandler;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* Implementation of {@link BrigadierDispatcher} that integrates the commands into BungeeCord API, so the players and
* the console can actually execute them.
*/
public class BungeeBrigadierDispatcher extends BrigadierDispatcher<CommandSender> implements Listener {
@ -37,15 +22,20 @@ public class BungeeBrigadierDispatcher extends BrigadierDispatcher<CommandSender
/* package */ final Plugin plugin;
/**
* Create a new instance of {@link BungeeBrigadierDispatcher}.
* @param pl the plugin that creates this dispatcher.
*/
public BungeeBrigadierDispatcher(Plugin pl) {
plugin = pl;
ProxyServer.getInstance().getPluginManager().registerListener(plugin, this);
}
/**
* Called when a player sends a chat message. Used to gets the typed command and execute it.
* @param event the event.
*/
@EventHandler
public void onChat(ChatEvent event) {
if (!event.getMessage().startsWith("/"))

View File

@ -1,8 +1,10 @@
package fr.pandacube.lib.bungee.permissions;
import fr.pandacube.lib.db.DB;
import fr.pandacube.lib.db.DBConnection;
import fr.pandacube.lib.permissions.Permissions;
import fr.pandacube.lib.players.standalone.StandaloneOnlinePlayer;
import fr.pandacube.lib.players.standalone.StandalonePlayerManager;
import fr.pandacube.lib.players.standalone.AbstractOnlinePlayer;
import fr.pandacube.lib.players.standalone.AbstractPlayerManager;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.connection.ProxiedPlayer;
@ -12,13 +14,31 @@ import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.event.EventHandler;
import java.util.function.Function;
/**
* Class that integrates the {@code pandalib-permissions} system into a BungeeCord instance.
* To register the event listener into BungeeCord, use {@link #init(Plugin)}.
* 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.
*/
public class PandalibBungeePermissions implements Listener {
/**
* Registers event listener to redirect permission checks to {@code pandalib-permissions}.
* @param bungeePlugin a BungeeCord plugin.
*/
public static void init(Plugin bungeePlugin) {
ProxyServer.getInstance().getPluginManager().registerListener(bungeePlugin, new PandalibBungeePermissions());
}
/**
* Event handler called when a plugin asks if a player has a permission.
* @param event the permission check event.
*/
@EventHandler(priority = Byte.MAX_VALUE)
public void onPermissionCheck(PermissionCheckEvent event)
{
@ -37,9 +57,9 @@ public class PandalibBungeePermissions implements Listener {
String world = null;
if (tryPermPlayerManager) {
try {
StandalonePlayerManager<?, ?> pm = StandalonePlayerManager.getInstance();
AbstractPlayerManager<?, ?> pm = AbstractPlayerManager.getInstance();
if (pm != null) {
StandaloneOnlinePlayer op = pm.get(p.getUniqueId());
AbstractOnlinePlayer op = pm.get(p.getUniqueId());
if (op != null) {
world = op.getWorldName();
}

View File

@ -3,9 +3,9 @@ package fr.pandacube.lib.bungee.players;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import fr.pandacube.lib.players.standalone.StandaloneOffPlayer;
import fr.pandacube.lib.players.standalone.AbstractOffPlayer;
public interface BungeeOffPlayer extends StandaloneOffPlayer {
public interface BungeeOffPlayer extends AbstractOffPlayer {
/*
* Related class instances

View File

@ -19,11 +19,11 @@ import net.md_5.bungee.protocol.packet.ClientSettings;
import net.md_5.bungee.protocol.packet.PluginMessage;
import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.players.standalone.StandaloneOnlinePlayer;
import fr.pandacube.lib.players.standalone.AbstractOnlinePlayer;
import fr.pandacube.lib.reflect.Reflect;
import fr.pandacube.lib.util.MinecraftVersion;
public interface BungeeOnlinePlayer extends BungeeOffPlayer, StandaloneOnlinePlayer {
public interface BungeeOnlinePlayer extends BungeeOffPlayer, AbstractOnlinePlayer {
/*
* General data and state
@ -134,7 +134,7 @@ public interface BungeeOnlinePlayer extends BungeeOffPlayer, StandaloneOnlinePla
return new BungeeClientOptions(this);
}
class BungeeClientOptions implements StandaloneOnlinePlayer.ClientOptions {
class BungeeClientOptions implements AbstractOnlinePlayer.ClientOptions {
private final BungeeOnlinePlayer op;
public BungeeClientOptions(BungeeOnlinePlayer op) {

View File

@ -4,7 +4,7 @@ import java.util.ArrayList;
import java.util.List;
/**
* A tree structure of {@link Chat} component intended to be rendered using {@link #render(boolean)}.
* A tree structure of {@link Chat} component intended to be rendered in chat using {@link #render(boolean)}.
*/
public class ChatTreeNode {

View File

@ -6,91 +6,111 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.context.ParsedCommandNode;
import com.mojang.brigadier.context.StringRange;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
import com.mojang.brigadier.suggestion.Suggestion;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import com.mojang.brigadier.tree.LiteralCommandNode;
import fr.pandacube.lib.reflect.Reflect;
import fr.pandacube.lib.util.Log;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* Abstract class that holds the logic of a specific command to be integrated in a Brigadier command dispatcher.
* Subclasses may use any mechanism to integrate this command in the environments Brigadier instance, during the
* instantiation of this object.
* Subclasses may use any mechanism to integrate this command in the environments Brigadier instance (or in a
* {@link BrigadierDispatcher} instance), during the instantiation of this object.
* @param <S> the command source (or command sender) type.
*/
public abstract class BrigadierCommand<S> {
/**
* Returns a builder for this command.
* Concrete class should include any element in the builder that is needed to build the command (sub-commands and
* arguments, requirements, redirection, ...).
* If any of the sub-commands and arguments needs to know the {@link LiteralCommandNode} builded from the returned
* {@link LiteralArgumentBuilder}, this can be done by overriding {@link #postBuildCommand(LiteralCommandNode)}.
* @return a builder for this command.
*/
protected abstract LiteralArgumentBuilder<S> buildCommand();
/**
* Method to implement if the reference to the command node has to be known when building the subcommands.
* Method to override if the reference to the command node has to be known when building the subcommands.
* @param commandNode the command node builded from {@link #buildCommand()}.
*/
protected void postBuildCommand(LiteralCommandNode<S> commandNode) {
// default implementation does nothing.
}
/**
* Method to override if this command have any aliases.
* @return an array of string corresponding to the aliases. This must not include the orignal command name (that
* is the name of the literal command node builded from {@link #buildCommand()}).
*/
protected String[] getAliases() {
return new String[0];
}
/**
* Creates a new {@link LiteralArgumentBuilder} that has the provided name.
* @param name the name of the command node.
* @return a new {@link LiteralArgumentBuilder} that has the provided name.
*/
public LiteralArgumentBuilder<S> literal(String name) {
return LiteralArgumentBuilder.literal(name);
}
/**
* Creates a new {@link RequiredArgumentBuilder} that has the provided name.
* @param name the name of the command node.
* @param type the type of the argument.
* @param <T> the argument type.
* @return a new {@link RequiredArgumentBuilder} that has the provided name.
*/
public <T> RequiredArgumentBuilder<S, T> argument(String name, ArgumentType<T> type) {
return RequiredArgumentBuilder.argument(name, type);
}
/**
* Tells if the provided command sender is a player.
* @param sender the sender to test if its a player or not.
* @return true if the sender is a player, false otherwise.
*/
public abstract boolean isPlayer(S sender);
/**
* Tells if the provided command sender is the console.
* @param sender the sender to test if its the console or not.
* @return true if the sender is the console, false otherwise.
*/
public abstract boolean isConsole(S sender);
/**
* Provides a {@link Predicate} that tests if the command sender is a player.
* @return a {@link Predicate} that tests if the command sender is a player.
* @see #isPlayer(Object)
*/
public Predicate<S> isPlayer() {
return this::isPlayer;
}
/**
* Provides a {@link Predicate} that tests if the command sender is the console.
* @return a {@link Predicate} that tests if the command sender is the console.
* @see #isConsole(Object)
*/
public Predicate<S> isConsole() {
return this::isConsole;
}
/**
* Provides a {@link Predicate} that tests if the command sender has the provided permission.
* @param permission the permission tested by the returned predicate on the command sender.
* @return a {@link Predicate} that tests if the command sender has the provided permission.
*/
public abstract Predicate<S> hasPermission(String permission);
@ -99,8 +119,15 @@ public abstract class BrigadierCommand<S> {
/**
* Determines if the literal node is found in the command.
* <p>
* <b>Be aware that this method search even beyond any fork or redirection, so it may encounter literal nodes that
* have the provided name but belong to other commands, so it can produce a false positive.</b>
* @param context the context of the command execution.
* @param literal the literal command node to search for in the typed command.
* @return true if the provided literal is in the typed command, false otherwise.
*/
public static boolean isLiteralParsed(CommandContext<?> context, String literal) {
for (ParsedCommandNode<?> node : context.getNodes()) {
if (node.getNode() instanceof LiteralCommandNode<?> lNode
@ -111,24 +138,60 @@ public abstract class BrigadierCommand<S> {
}
/**
* Gets the argument value from the provided context, silently returning null (instead of throwing an exception)
* if the argument is not found.
* @param context the context of the command execution.
* @param argument the argument to search for.
* @param type the type of the argument.
* @param <T> the argument type.
* @return the value of the argument, or null if not found.
*/
public static <T> T tryGetArgument(CommandContext<?> context, String argument, Class<T> type) {
return tryGetArgument(context, argument, type, Function.identity(), null);
}
public static <ST, T> T tryGetArgument(CommandContext<?> context, String argument, Class<ST> sourceType, Function<ST, T> transformIfFound) {
return tryGetArgument(context, argument, sourceType, transformIfFound, null);
}
/**
* Gets the argument value from the provided context, silently returning a default value (instead of throwing an
* exception) if the argument is not found.
* @param context the context of the command execution.
* @param argument the argument to search for.
* @param type the type of the argument.
* @param deflt the default value if not found.
* @param <T> the argument type.
* @return the value of the argument, or {@code deflt} if not found.
*/
public static <T> T tryGetArgument(CommandContext<?> context, String argument, Class<T> type, T deflt) {
return tryGetArgument(context, argument, type, Function.identity(), deflt);
}
/**
* Gets the argument value from the provided context and transform it using the provided function, or silently
* returning null (instead of throwing an exception) if the argument is not found.
* @param context the context of the command execution.
* @param argument the argument to search for.
* @param sourceType the type of the argument in the command context.
* @param transformIfFound the function to transform the argument value before returning.
* @param <ST> the argument type in the command context.
* @param <T> the returned type.
* @return the value of the argument, transformed by {@code transformIfFound}, or null if not found.
*/
public static <ST, T> T tryGetArgument(CommandContext<?> context, String argument, Class<ST> sourceType, Function<ST, T> transformIfFound) {
return tryGetArgument(context, argument, sourceType, transformIfFound, null);
}
/**
* Gets the argument value from the provided context and transform it using the provided function, or silently
* returning a default value (instead of throwing an exception) if the argument is not found.
* @param context the context of the command execution.
* @param argument the argument to search for.
* @param sourceType the type of the argument in the command context.
* @param transformIfFound the function to transform the argument value before returning.
* @param deflt the default value if not found.
* @param <ST> the argument type in the command context.
* @param <T> the returned type.
* @return the value of the argument, transformed by {@code transformIfFound}, or {@code deflt} if not found.
*/
public static <ST, T> T tryGetArgument(CommandContext<?> context, String argument, Class<ST> sourceType, Function<ST, T> transformIfFound, T deflt) {
ST sourceValue;
try {
@ -140,25 +203,25 @@ public abstract class BrigadierCommand<S> {
}
/**
* Creates a new instance of {@link CommandSyntaxException} with the provided message.
* @param message the exception message.
* @return a new instance of {@link CommandSyntaxException} with the provided message.
*/
public static CommandSyntaxException newCommandException(String message) {
return new SimpleCommandExceptionType(new LiteralMessage(message)).create();
}
/**
* Wraps the provided {@link SuggestionsSupplier} into a Brigadiers {@link SuggestionProvider}.
* @param suggestions the suggestions to wrap.
* @param senderUnwrapper function to convert the command sender provided by brigadier into the command sender
* supported by {@link SuggestionsSupplier}.
* @return a {@link SuggestionProvider} generating the suggestions from the provided {@link SuggestionsSupplier}.
* @param <AS> the type of command sender supported by the {@link SuggestionsSupplier}.
*/
protected <AS> SuggestionProvider<S> wrapSuggestions(SuggestionsSupplier<AS> suggestions, Function<S, AS> senderUnwrapper) {
return (context, builder) -> {
AS sender = senderUnwrapper.apply(context.getSource());

View File

@ -11,28 +11,41 @@ import net.kyori.adventure.text.ComponentLike;
import java.util.concurrent.CompletableFuture;
/**
* Abstract class that holds a Brigadier {@link CommandDispatcher} instance.
* Subclasses contains logic to integrate this commands dispatcher into their environment (like Bungee or CLI app).
* @param <S> the command source (or command sender) type.
*/
public abstract class BrigadierDispatcher<S> {
private final CommandDispatcher<S> dispatcher = new CommandDispatcher<>();
/**
* Registers the provided command node into this dispatcher.
* @param node the node to register.
*/
public void register(LiteralCommandNode<S> node) {
dispatcher.getRoot().addChild(node);
}
/**
* Returns the Brigadier dispatcher.
* @return the Brigadier dispatcher.
*/
public CommandDispatcher<S> getDispatcher() {
return dispatcher;
}
/**
* Executes the provided command as the provided sender.
* @param sender the command sender.
* @param commandWithoutSlash the command, without the eventual slash at the begining.
* @return the value returned by the executed command.
*/
public int execute(S sender, String commandWithoutSlash) {
ParseResults<S> parsed = dispatcher.parse(commandWithoutSlash, sender);
@ -46,11 +59,15 @@ public abstract class BrigadierDispatcher<S> {
Log.severe(e);
return 0;
}
}
/**
* Gets the suggestions for the currenlty being typed command.
* @param sender the command sender.
* @param buffer the command that is being typed.
* @return the suggestions for the currenlty being typed command.
*/
public Suggestions getSuggestions(S sender, String buffer) {
ParseResults<S> parsed = dispatcher.parse(buffer, sender);
try {
@ -64,10 +81,11 @@ public abstract class BrigadierDispatcher<S> {
}
/**
* Sends the provided message to the sender.
* @param sender the sender to send the message to.
* @param message the message to send.
*/
protected abstract void sendSenderMessage(S sender, ComponentLike message);
}

View File

@ -1,5 +1,6 @@
package fr.pandacube.lib.commands;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.ParseResults;
import com.mojang.brigadier.context.CommandContextBuilder;
import com.mojang.brigadier.context.StringRange;
@ -22,7 +23,16 @@ import java.util.concurrent.CompletableFuture;
public class BrigadierSuggestionsUtil {
/**
* Gets suggestions with the provided parsed command.
* <p>
* This is a reimplementation of {@link CommandDispatcher#getCompletionSuggestions(ParseResults, int)} that
* keeps the original ordering of the suggestions.
* @param parsed the parsed command.
* @return the suggestions.
* @param <S> the sender type.
* @see CommandDispatcher#getCompletionSuggestions(ParseResults, int)
*/
public static <S> CompletableFuture<Suggestions> buildSuggestionBrigadier(ParseResults<S> parsed) {
int cursor = parsed.getReader().getTotalLength();
final CommandContextBuilder<S> context = parsed.getContext();
@ -55,7 +65,17 @@ public class BrigadierSuggestionsUtil {
return result;
}
// inspired from com.mojang.brigadier.suggestion.Suggestions#merge, but without the sorting part
/**
* Merges the provided {@link Suggestions}.
* <p>
* This is a reimplementation of {@link Suggestions#merge(String, Collection)} that keeps the original ordering of
* the suggestions.
* @param input the command on which are based the suggestions.
* @param suggestions the {@link Suggestions} to merge.
* @return a {@link Suggestions}.
* @see Suggestions#create(String, Collection)
*/
public static Suggestions mergeSuggestionsOriginalOrdering(String input, Collection<Suggestions> suggestions) {
if (suggestions.isEmpty()) {
return new Suggestions(StringRange.at(0), new ArrayList<>(0));
@ -72,8 +92,16 @@ public class BrigadierSuggestionsUtil {
// inspired from com.mojang.brigadier.suggestion.Suggestions#create, but without the sorting part
/**
* Creates a {@link Suggestions} from the provided Collection of {@link Suggestion}.
* <p>
* This is a reimplementation of {@link Suggestions#create(String, Collection)} that keeps the original ordering of
* the suggestions.
* @param command the command on which are based the suggestions.
* @param suggestions the Collection of {@link Suggestion}.
* @return a {@link Suggestions}.
* @see Suggestions#create(String, Collection)
*/
public static Suggestions createSuggestionsOriginalOrdering(String command, Collection<Suggestion> suggestions) {
if (suggestions.isEmpty()) {
return new Suggestions(StringRange.at(0), new ArrayList<>(0));
@ -96,7 +124,7 @@ public class BrigadierSuggestionsUtil {
public static CompletableFuture<Suggestions> completableFutureSuggestionsKeepsOriginalOrdering(SuggestionsBuilder builder) {
/* package */ static CompletableFuture<Suggestions> completableFutureSuggestionsKeepsOriginalOrdering(SuggestionsBuilder builder) {
return CompletableFuture.completedFuture(
createSuggestionsOriginalOrdering(builder.getInput(), getSuggestionsFromSuggestionsBuilder(builder))
);

View File

@ -19,6 +19,11 @@
<artifactId>pandalib-util</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>fr.pandacube.lib</groupId>
<artifactId>pandalib-reflect</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>

View File

@ -14,6 +14,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import fr.pandacube.lib.reflect.Reflect;
import fr.pandacube.lib.util.Log;
/**
@ -576,7 +577,7 @@ public final class DB {
@SuppressWarnings("unchecked")
private static <E extends SQLElement<E>> E getElementInstance(ResultSet set, Class<E> elemClass) throws DBException {
try {
E instance = elemClass.getConstructor(int.class).newInstance(set.getInt("id"));
E instance = Reflect.ofClass(elemClass).constructor(int.class).instanciate(set.getInt("id"));
int fieldCount = set.getMetaData().getColumnCount();

View File

@ -8,6 +8,8 @@ import java.util.concurrent.ExecutionException;
import java.util.function.Function;
import java.util.function.Predicate;
import fr.pandacube.lib.db.DB;
import fr.pandacube.lib.db.DBConnection;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.event.EventHandler;
@ -23,12 +25,26 @@ import org.bukkit.plugin.java.JavaPlugin;
import fr.pandacube.lib.permissions.Permissions;
import fr.pandacube.lib.util.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 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.
*/
public class PandalibPaperPermissions implements Listener {
/* package */ static JavaPlugin plugin;
/* package */ static String serverName;
/* package */ static final Map<String, String> permissionMap = new HashMap<>();
/**
* Integrates the {@code pandalib-permissions} system into the Bukkit server.
* @param plugin a Bukkit plugin.
* @param serverName the name of the current server, used to fetch server specific permissions. Cannot be null.
* If this server in not in a multi-server configuration, use a dummy server name, like
* {@code ""} (empty string).
*/
public static void init(JavaPlugin plugin, String serverName) {
PandalibPaperPermissions.plugin = plugin;
PandalibPaperPermissions.serverName = serverName;
@ -58,6 +74,10 @@ public class PandalibPaperPermissions implements Listener {
}
}
/**
* Player login event handler.
* @param event the event.
*/
@EventHandler(priority = EventPriority.LOWEST)
public void onPlayerLogin(PlayerLoginEvent event) {
Permissions.clearPlayerCache(event.getPlayer().getUniqueId());
@ -67,6 +87,10 @@ public class PandalibPaperPermissions implements Listener {
}
/**
* Player quit event handler.
* @param event the event.
*/
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerQuit(PlayerQuitEvent event) {
PermissionsInjectorBukkit.uninject(event.getPlayer());

View File

@ -29,7 +29,7 @@ import fr.pandacube.lib.permissions.Permissions;
import fr.pandacube.lib.reflect.Reflect;
import fr.pandacube.lib.util.Log;
public class PermissionsInjectorBukkit
/* package */ class PermissionsInjectorBukkit
{
// to be called : onEnable for console, onPlayerLogin (not Join) (Priority LOWEST) for players

View File

@ -10,7 +10,7 @@ import fr.pandacube.lib.permissions.PermGroup;
import fr.pandacube.lib.permissions.Permissions;
import fr.pandacube.lib.util.Log;
public class PermissionsInjectorVault {
/* package */ class PermissionsInjectorVault {
public static PandaVaultPermission permInstance;
@ -27,12 +27,12 @@ public class PermissionsInjectorVault {
Log.warning("Vault plugin not detected. Not using it to provide permissions and prefix/suffix." + e.getMessage());
}
}
public static class PandaVaultPermission extends net.milkbowl.vault.permission.Permission {
/* package */ static class PandaVaultPermission extends net.milkbowl.vault.permission.Permission {
private PandaVaultPermission() { }

View File

@ -13,13 +13,11 @@ import fr.pandacube.lib.permissions.PermPlayer;
import fr.pandacube.lib.permissions.Permissions;
import fr.pandacube.lib.util.Log;
public class PermissionsInjectorWEPIF {
public static PandaWEPIFPermissionsProvider permInstance;
/* package */ class PermissionsInjectorWEPIF {
public static void inject() {
try {
permInstance = new PandaWEPIFPermissionsProvider();
PandaWEPIFPermissionsProvider permInstance = new PandaWEPIFPermissionsProvider();
Bukkit.getServicesManager().register(com.sk89q.wepif.PermissionsProvider.class, permInstance,
PandalibPaperPermissions.plugin, ServicePriority.Highest);
Log.info("Providing permissions through WEPIF");
@ -32,11 +30,11 @@ public class PermissionsInjectorWEPIF {
}
}
public static class PandaWEPIFPermissionsProvider implements com.sk89q.wepif.PermissionsProvider {
/* package */ static class PandaWEPIFPermissionsProvider implements com.sk89q.wepif.PermissionsProvider {
private PandaWEPIFPermissionsProvider() { }
private PermPlayer getPlayer(OfflinePlayer player) {

View File

@ -5,9 +5,9 @@ import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.bukkit.scoreboard.Team;
import fr.pandacube.lib.players.standalone.StandaloneOffPlayer;
import fr.pandacube.lib.players.standalone.AbstractOffPlayer;
public interface PaperOffPlayer extends StandaloneOffPlayer {
public interface PaperOffPlayer extends AbstractOffPlayer {
/*
* General data and state

View File

@ -21,9 +21,9 @@ import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.MainHand;
import fr.pandacube.lib.players.standalone.StandaloneOnlinePlayer;
import fr.pandacube.lib.players.standalone.AbstractOnlinePlayer;
public interface PaperOnlinePlayer extends PaperOffPlayer, StandaloneOnlinePlayer {
public interface PaperOnlinePlayer extends PaperOffPlayer, AbstractOnlinePlayer {
/*
* General data and state
@ -119,7 +119,7 @@ public interface PaperOnlinePlayer extends PaperOffPlayer, StandaloneOnlinePlaye
@Override
PaperClientOptions getClientOptions();
abstract class PaperClientOptions implements StandaloneOnlinePlayer.ClientOptions {
abstract class PaperClientOptions implements AbstractOnlinePlayer.ClientOptions {
private final PaperOnlinePlayer op;

View File

@ -16,6 +16,6 @@ public class ServerChunkCache extends ReflectWrapper {
protected ServerChunkCache(Object obj) {
super(obj);
chunkMap = wrap(wrapReflectEx(() -> FIELD_chunkMap.getValue(obj)));
chunkMap = wrap(wrapReflectEx(() -> FIELD_chunkMap.getValue(obj)), ChunkMap.class);
}
}

View File

@ -65,13 +65,14 @@
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
<exclude>META-INF/MANIFEDT.MF</exclude>
</excludes>
</filter>
</filters>
<relocations>
<relocation>
<pattern>com.fathzer.soft.javaluator</pattern>
<shadedPattern>fr.pandacube.shaded.javaluator</shadedPattern>
<shadedPattern>fr.pandacube.lib.permissions.shaded.javaluator</shadedPattern>
</relocation>
</relocations>
</configuration>

View File

@ -8,98 +8,219 @@ import java.util.Set;
import java.util.stream.LongStream;
import fr.pandacube.lib.chat.ChatTreeNode;
import fr.pandacube.lib.permissions.PermissionExpressionParser.LitteralPermissionTester;
import fr.pandacube.lib.permissions.PermissionsCachedBackendReader.CachedEntity;
import fr.pandacube.lib.permissions.SQLPermissions.EntityType;
import fr.pandacube.lib.util.Log;
public abstract class PermEntity {
protected final String name;
protected final EntityType type;
protected PermEntity(String n, EntityType t) {
/**
* Represents an entity in the permission system, either a group or a player.
*/
public sealed abstract class PermEntity permits PermPlayer, PermGroup {
/* package */ final String name;
/* package */ final EntityType type;
/* package */ PermEntity(String n, EntityType t) {
name = n; type = t;
}
protected abstract CachedEntity getBackendEntity();
/* package */ abstract CachedEntity getBackendEntity();
/**
* Gets all the groups this entity inherits from.
* @return a list of all the groups this entity inherits from.
*/
public abstract List<PermGroup> getInheritances();
/**
* Gets all the group names this entity inherits from.
* @return a list of all the group names this entity inherits from.
*/
public abstract List<String> getInheritancesString();
/**
* Gets the name of this entity.
* @return the name of this entity.
*/
public abstract String getName();
/**
* Gets the name of this entity, as it is stored in the database.
* @return the name of this entity, as it is stored in the database.
*/
public String getInternalName() {
return name;
}
/**
* Tells if the current entity inherits directly or indirectly from the specified group
* Tells if the current entity inherits directly or indirectly from the specified group.
* @param group the group to search for
* @param recursive true to search in the inheritance tree, or false to search only in the inheritance list of the current entity.
* @return true if the current entity inherits directly or indirectly from the specified group, false otherwise.
*/
public boolean inheritsFromGroup(String group, boolean recursive) {
if (group == null)
return false;
return getInheritances().stream().anyMatch(g -> g.name.equals(group) || (recursive && g.inheritsFromGroup(group, true)));
}
/**
* Gets the effective prefix of this entity.
* It is either the prefix defined directly for this entity, or from inheritance.
* @return the effective prefix of this entity.
*/
public String getPrefix() {
return Permissions.resolver.getEffectivePrefix(name, type);
}
/**
* Gets the prefix defined directly for this entity.
* @return the prefix defined directly for this entity.
*/
public String getSelfPrefix() {
return getBackendEntity().getSelfPrefix();
}
/**
* Provides informations on how the effective prefix of this entity is determined.
* @return a {@link ChatTreeNode} providing informations on how the effective prefix of this entity is determined.
*/
public ChatTreeNode debugPrefix() {
return Permissions.resolver.debugPrefix(name, type);
}
/**
* Sets the prefix of this entity.
* @param prefix the prefix for this entity.
*/
public void setSelfPrefix(String prefix) {
Permissions.backendWriter.setSelfPrefix(name, type, prefix);
}
/**
* Gets the effective suffix of this entity.
* It is either the suffix defined directly for this entity, or from inheritance.
* @return the effective suffix of this entity.
*/
public String getSuffix() {
return Permissions.resolver.getEffectiveSuffix(name, type);
}
/**
* Gets the suffix defined directly for this entity.
* @return the suffix defined directly for this entity.
*/
public String getSelfSuffix() {
return getBackendEntity().getSelfSuffix();
}
/**
* Provides informations on how the effective suffix of this entity is determined.
* @return a {@link ChatTreeNode} providing informations on how the effective suffix of this entity is determined.
*/
public ChatTreeNode debugSuffix() {
return Permissions.resolver.debugSuffix(name, type);
}
/**
* Sets the suffix of this entity.
* @param suffix the suffix for this entity.
*/
public void setSelfSuffix(String suffix) {
Permissions.backendWriter.setSelfSuffix(name, type, suffix);
}
/**
* Gets the effective list of permissions that applies to this entity out of a specific server and world.
* It is either the permissions defined directly for this entity, or from inheritance as long as they are not
* overriden.
* @return the effective list of permissions that applies to this entity out of a specific server and world.
*/
public Map<String, Boolean> listEffectivePermissions() {
return listEffectivePermissions(null, null);
}
/**
* Gets the effective list of permissions that applies to this entity on a specific server.
* It is either the permissions defined directly for this entity, or from inheritance as long as they are not
* overriden.
* @param server the server where the returned permissions apply for this entity.
* @return the effective list of permissions that applies to this entity on a specific server.
*/
public Map<String, Boolean> listEffectivePermissions(String server) {
return listEffectivePermissions(server, null);
}
/**
* Gets the effective list of permissions that applies to this entity on a specific server and world.
* It is either the permissions defined directly for this entity, or from inheritance as long as they are not
* overriden.
* @param server the server containing the world where the returned permissions apply for this entity.
* @param world the world in the server where the returned permissions apply for this entity.
* @return the effective list of permissions that applies to this entity on a specific server and world.
*/
public Map<String, Boolean> listEffectivePermissions(String server, String world) {
return Permissions.resolver.getEffectivePermissionList(name, type, server, world);
}
/**
* Gets the effective values of the provided permission range prefix that applies to this entity out of a specific
* server and world.
* It is either the range values defined directly for this entity, or from inheritance as long as they are not
* overriden.
* @param permissionPrefix the permission range prefix.
* @return the effective values of the provided permission range prefix that applies to this entity out of a
* specific server and world.
*/
public LongStream getPermissionRangeValues(String permissionPrefix) {
return getPermissionRangeValues(permissionPrefix, null, null);
}
/**
* Gets the effective values of the provided permission range prefix that applies to this entity on a specific
* server.
* It is either the range values defined directly for this entity, or from inheritance as long as they are not
* overriden.
* @param permissionPrefix the permission range prefix.
* @param server the server where the returned values apply for this entity.
* @return the effective values of the provided permission range prefix that applies to this entity on a specific
* server.
*/
public LongStream getPermissionRangeValues(String permissionPrefix, String server) {
return getPermissionRangeValues(permissionPrefix, server, null);
}
/**
* Gets the effective values of the provided permission range prefix that applies to this entity on a specific
* server and world.
* It is either the range values defined directly for this entity, or from inheritance as long as they are not
* overriden.
* @param permissionPrefix the permission range prefix.
* @param server the server containing the world where the returned values apply for this entity.
* @param world the world in the server where the returned values apply for this entity.
* @return the effective values of the provided permission range prefix that applies to this entity on a specific
* server and world.
*/
public LongStream getPermissionRangeValues(String permissionPrefix, String server, String world) {
String prefixWithEndingDot = permissionPrefix.endsWith(".") ? permissionPrefix : (permissionPrefix + ".");
int prefixLength = prefixWithEndingDot.length();
@ -120,106 +241,299 @@ public abstract class PermEntity {
.mapToLong(longSuffix -> longSuffix)
.sorted();
}
/**
* Gets the maximum effective value of the provided permission range prefix that applies to this entity out of a
* specific server and world.
* It is either the range values defined directly for this entity, or from inheritance as long as they are not
* overriden.
* @param permissionPrefix the permission range prefix.
* @return the maximum effective value of the provided permission range prefix that applies to this entity out of a
* specific server and world.
*/
public OptionalLong getPermissionRangeMax(String permissionPrefix) {
return getPermissionRangeMax(permissionPrefix, null, null);
}
/**
* Gets the maximum effective value of the provided permission range prefix that applies to this entity on a
* specific server.
* It is either the range values defined directly for this entity, or from inheritance as long as they are not
* overriden.
* @param permissionPrefix the permission range prefix.
* @param server the server where the returned value applies for this entity.
* @return the maximum effective value of the provided permission range prefix that applies to this entity on a
* specific server.
*/
public OptionalLong getPermissionRangeMax(String permissionPrefix, String server) {
return getPermissionRangeMax(permissionPrefix, server, null);
}
/**
* Gets the maximum effective value of the provided permission range prefix that applies to this entity on a
* specific server and world.
* It is either the range values defined directly for this entity, or from inheritance as long as they are not
* overriden.
* @param permissionPrefix the permission range prefix.
* @param server the server containing the world where the returned value applies for this entity.
* @param world the world in the server where the returned value applies for this entity.
* @return the maximum effective value of the provided permission range prefix that applies to this entity on a
* specific server and world.
*/
public OptionalLong getPermissionRangeMax(String permissionPrefix, String server, String world) {
return getPermissionRangeValues(permissionPrefix, server, world).max();
}
/**
* Tells if this entity has the provided permission out of a specific server and world.
* It is either based on the permissions defined directly for this entity, or from inheritance as long as they are
* not overriden.
* @param permission the permission to ckeck on this entity.
* @return true if this entity has the permission, false if it is negated, or null if not known.
*/
public Boolean hasPermission(String permission) {
return hasPermission(permission, null, null);
}
/**
* Tells if this entity has the provided permission on a specitif server.
* It is either based on the permissions defined directly for this entity, or from inheritance as long as they are
* not overriden. It also consider permissions that apply on any server.
* @param permission the permission to ckeck on this entity.
* @param server the server in which to test the permission for this entity.
* @return true if this entity has the permission, false if it is negated, or null if not known.
*/
public Boolean hasPermission(String permission, String server) {
return hasPermission(permission, server, null);
}
/**
* Tells if this entity has the provided permission on a specitif server and world.
* It is either based on the permissions defined directly for this entity, or from inheritance as long as they are
* not overriden. It also consider permissions that apply on any world of that server, and then any server.
* @param permission the permission to ckeck on this entity.
* @param server the server in which to test the permission for this entity.
* @param world the world in which to test the permission for this entity.
* @return true if this entity has the permission, false if it is negated, or null if not known.
*/
public Boolean hasPermission(String permission, String server, String world) {
Boolean ret = Permissions.resolver.getEffectivePermission(name, type, permission, server, world);
Log.debug("[Perm] For " + type.toString().toLowerCase() + " " + getName() + ", '" + permission + "' is " + ret);
return ret;
}
/**
* Tells if this entity has the provided permission on a specitif server and world.
* It is either based on the permissions defined directly for this entity, or from inheritance as long as they are
* not overriden.
* @param permission the permission to ckeck on this entity.
* @param server the server in which to test the permission for this entity.
* @param world the world in which to test the permission for this entity.
* @param deflt the default value is the permission is undefined for this entity.
* @return true if this entity has the permission, false if it is negated, or {@code deflt} if not known.
*/
public boolean hasPermissionOr(String permission, String server, String world, boolean deflt) {
Boolean ret = hasPermission(permission, server, world);
return ret != null ? ret : deflt;
}
/**
* Evaluates the provided permission expression for this entity.
* It uses {@link #hasPermissionOr(String, String, String, boolean)} with {@code false} as a default value, to check
* each permission nodes individualy.
* @param permExpression the permission expression to evaluate on this entity.
* @param server the server in which to test the permission expression for this entity.
* @param world the world in which to test the permission expression for this entity.
* @return true if this the permission expression evaluates to true, false otherwise.
* @see PermissionExpressionParser#evaluate(String, LitteralPermissionTester)
*/
public boolean hasPermissionExpression(String permExpression, String server, String world) {
return PermissionExpressionParser.evaluate(permExpression, p -> hasPermissionOr(p, server, world, false));
}
/**
* Provides informations on how the effective permission of this entity on the provided permission node is
* determined.
* @param permission the permission node to debug on this entity.
* @return a {@link ChatTreeNode} providing informations on how the effective permission is determined.
*/
public ChatTreeNode debugPermission(String permission) {
return debugPermission(permission, null, null);
}
/**
* Provides informations on how the effective permission of this entity on the provided permission node is
* determined.
* @param permission the permission node to debug on this entity.
* @param server the server in which to test the permission for this entity.
* @return a {@link ChatTreeNode} providing informations on how the effective permission is determined.
*/
public ChatTreeNode debugPermission(String permission, String server) {
return debugPermission(permission, server, null);
}
/**
* Provides informations on how the effective permission of this entity on the provided permission node is
* determined.
* @param permission the permission node to debug on this entity.
* @param server the server in which to test the permission for this entity.
* @param world the world in which to test the permission for this entity.
* @return a {@link ChatTreeNode} providing informations on how the effective permission is determined.
*/
public ChatTreeNode debugPermission(String permission, String server, String world) {
return Permissions.resolver.debugPermission(name, type, permission, server, world);
}
/**
* Adds the provided permission node to this entity that apply on any server.
* @param permission the permission node to add.
*/
public void addSelfPermission(String permission) {
addSelfPermission(permission, null, null);
}
/**
* Adds the provided permission node to this entity that apply on the provided server.
* @param permission the permission node to add.
* @param server the server in which to apply the permission.
*/
public void addSelfPermission(String permission, String server) {
addSelfPermission(permission, server, null);
}
/**
* Adds the provided permission node to this entity that apply on the provided server and world.
* @param permission the permission node to add.
* @param server the server in which to apply the permission.
* @param world the world in which to apply the permission.
*/
public void addSelfPermission(String permission, String server, String world) {
Permissions.backendWriter.addSelfPermission(name, type, permission, server, world);
}
/**
* Removes the provided permission node from this entity that applied on any server.
* @param permission the permission node to add.
*/
public void removeSelfPermission(String permission) {
removeSelfPermission(permission, null, null);
}
/**
* Removes the provided permission node from this entity that applied on the provided server.
* @param permission the permission node to remove.
* @param server the server from which to remove the permission.
*/
public void removeSelfPermission(String permission, String server) {
removeSelfPermission(permission, server, null);
}
/**
* Removes the provided permission node from this entity that applied on the provided server and world.
* @param permission the permission node to remove.
* @param server the server from which to remove the permission.
* @param world the world from which to remove the permission.
*/
public void removeSelfPermission(String permission, String server, String world) {
Permissions.backendWriter.removeSelfPermission(name, type, permission, server, world);
}
/**
* Counts the number of self permission nodes for this entity.
* @return the number of self permission nodes for this entity.
*/
public int getSelfPermissionsCount() {
return getSelfPermissionsServerWorldKeys().stream()
.mapToInt(key -> getSelfPermissions(key.server, key.world).size())
.mapToInt(key -> getSelfPermissions(key.server(), key.world()).size())
.sum();
}
/**
* Gets all the server/world attribution that have at least one self permission for this entity.
* @return all the server/world attribution that have at least one self permission for this entity.
*/
public Set<ServerWorldKey> getSelfPermissionsServerWorldKeys() {
return getBackendEntity().getSelfPermissionsServerWorldKeys();
}
/**
* Gets all the self permission nodes that apply everywhere for this entity.
* @return all the self permission nodes that apply everywhere for this entity.
*/
public List<String> getSelfPermissions() {
return getSelfPermissions(null, null);
}
/**
* Gets all the self permission nodes that apply on the provided server for this entity.
* @param server the server from which to get the permissions.
* @return all the self permission nodes that apply on the provided server for this entity.
*/
public List<String> getSelfPermissions(String server) {
return getSelfPermissions(server, null);
}
/**
* Gets all the self permission nodes that apply on the provided server and world for this entity.
* @param server the server from which to get the permissions.
* @param world the world from which to get the permissions.
* @return all the self permission nodes that apply on the provided server and world for this entity.
*/
public List<String> getSelfPermissions(String server, String world) {
return getBackendEntity().getSelfPermissions(server, world);
}
@Override
public boolean equals(Object obj) {

View File

@ -6,7 +6,10 @@ import java.util.stream.Collectors;
import fr.pandacube.lib.permissions.PermissionsCachedBackendReader.CachedGroup;
import fr.pandacube.lib.permissions.SQLPermissions.EntityType;
public class PermGroup extends PermEntity {
/**
* Represents an group in the permission system.
*/
public final class PermGroup extends PermEntity {
/* package */ PermGroup(String name) {
super(name, EntityType.Group);
}
@ -31,27 +34,54 @@ public class PermGroup extends PermEntity {
.map(cg -> cg.name)
.collect(Collectors.toList());
}
/**
* Tells if this group is a default group.
* A player inherits all default groups when they dont explicitely inherit from at least one group.
* @return true if this group is a default group, false otherwise.
*/
public boolean isDefault() {
return getBackendEntity().deflt;
}
/**
* Sets this group as a default group or not.
* All players that dont explicitely inherit from at least one group will either start or stop implicitely
* inheriting from this group.
* @param deflt true to set this group as default, false to set is as not default.
*/
public void setDefault(boolean deflt) {
Permissions.backendWriter.setGroupDefault(name, deflt);
}
/**
* Makes this group inherit the provided group.
* @param group the name of the group to inherit from.
*/
public void addInheritance(String group) {
Permissions.backendWriter.addInheritance(name, type, group);
}
/**
* Makes this group inherit the provided group.
* @param group the group to inherit from.
*/
public void addInheritance(PermGroup group) {
addInheritance(group.name);
}
/**
* Makes this group stop inheriting from the provided group.
* @param group the name of the group to stop inheriting from.
*/
public void removeInheritance(String group) {
Permissions.backendWriter.removeInheritance(name, type, group);
}
/**
* Makes this group stop inheriting from the provided group.
* @param group the group to stop inheriting from.
*/
public void removeInheritance(PermGroup group) {
removeInheritance(group.name);
}

View File

@ -7,7 +7,10 @@ import java.util.stream.Collectors;
import fr.pandacube.lib.permissions.PermissionsCachedBackendReader.CachedPlayer;
import fr.pandacube.lib.permissions.SQLPermissions.EntityType;
public class PermPlayer extends PermEntity {
/**
* Represents an player in the permission system.
*/
public final class PermPlayer extends PermEntity {
private final UUID playerId;
/* package */ PermPlayer(UUID id) {
super(id.toString(), EntityType.User);
@ -29,7 +32,11 @@ public class PermPlayer extends PermEntity {
.map(cg -> cg.name)
.collect(Collectors.toList());
}
/**
* Gets the UUID of this player.
* @return the UUID of this player.
*/
public UUID getPlayerId() {
return playerId;
}
@ -48,14 +55,18 @@ public class PermPlayer extends PermEntity {
}
/**
* Gets all the groups this player belongs to.
* Alias for {@link #getInheritances()}.
* @return a list of all the groups this player belongs to.
*/
public List<PermGroup> getGroups() {
return getInheritances();
}
/**
* Gets all the group names this player belongs to.
* Alias for {@link #getInheritances()}.
* @return a list of all the group names this player belongs to.
*/
public List<String> getGroupsString() {
return getInheritancesString();
@ -65,35 +76,67 @@ public class PermPlayer extends PermEntity {
* Tells if the player is directly part of a group.
* This is equivalent to {@link #inheritsFromGroup(String, boolean) inheritsFromGroup(group, false)}
* @param group the group to search for
* @return true if the player is directly part of a group, false otherwise.
*/
public boolean isInGroup(String group) {
return inheritsFromGroup(group, false);
}
/**
* Tells if this player has been assigned to the default groups.
* @return true if this player has been assigned to the default groups, or false if this player belongs explicitely
* to their groups.
*/
public boolean isUsingDefaultGroups() {
return getBackendEntity().usingDefaultGroups;
}
/**
* Sets the group this player will now inheritate, removing all previously inherited groups.
* To keep the other inherited groups, use {@link #addGroup(String)}.
* @param group the name of the group to inherit from.
*/
public void setGroup(String group) {
Permissions.backendWriter.setInheritance(name, type, group);
}
/**
* Sets the group this player will now inheritate, removing all previously inherited groups.
* To keep the other inherited groups, use {@link #addGroup(PermGroup)}.
* @param group the group to inherit from.
*/
public void setGroup(PermGroup group) {
setGroup(group.name);
}
/**
* Makes this player inherit the provided group, keeping the other groups they already inherits from.
* @param group the name of the group to inherit from.
*/
public void addGroup(String group) {
Permissions.backendWriter.addInheritance(name, type, group);
}
/**
* Makes this player inherit the provided group, keeping the other groups they already inherits from.
* @param group the group to inherit from.
*/
public void addGroup(PermGroup group) {
addGroup(group.name);
}
/**
* Makes this player stop inheriting from the provided group.
* @param group the name of the group to stop inheriting from.
*/
public void removeGroup(String group) {
Permissions.backendWriter.removeInheritance(name, type, group);
}
/**
* Makes this player stop inheriting from the provided group.
* @param group the group to stop inheriting from.
*/
public void removeGroup(PermGroup group) {
removeGroup(group.name);
}

View File

@ -10,10 +10,38 @@ import com.fathzer.soft.javaluator.Operator;
import com.fathzer.soft.javaluator.Operator.Associativity;
import com.fathzer.soft.javaluator.Parameters;
/**
* Class that evaluates a permission string as if it was a boolean expression with permission nodes as variables.
* <p>
* A permission expression contains permission nodes, boolean operators ({@code "||"}, {@code "&&"} and {@code "!"}) and
* literal values {@code "true"} and {@code "false"}.
* Here are some example of permission expressions:
* <pre>{@code
* "p1.cmd"
* "!p1.toto"
* "p1.cmd!"
* "p1.cmd || p1.toto"
* "p1.cmd && p1.toto"
* "p1.cmd && !p1.toto "
* "p1.cmd && true"
* "false || p2.cmd"
* }</pre>
* Notice that spaces around permission nodes and operators does not affect the results of the parsing.
*/
public class PermissionExpressionParser {
private static final PermissionEvaluator PERMISSION_EVALUATOR = new PermissionEvaluator();
/**
* Evaluate the provided permission expression, testing each permission with the provided permTester.
*
* @param permString the permission expression to evaluate.
* @param permTester a function that gives the value of the provided permission node. It is usually a method
* reference to the {@code hasPermission(String)} method the player we want to test the
* permissions.
* @throws IllegalArgumentException if the expression is not correct.
* @return the result of the evaluation of the permission expression.
*/
public static boolean evaluate(String permString, LitteralPermissionTester permTester) {
try {
return PERMISSION_EVALUATOR.evaluate(permString, permTester);
@ -22,6 +50,9 @@ public class PermissionExpressionParser {
}
}
/**
* Functional interface that converts a string into a boolean.
*/
public interface LitteralPermissionTester extends Function<String, Boolean> { }
@ -89,41 +120,4 @@ public class PermissionExpressionParser {
}
}
/* TODO move to test code
public static void main(String[] args) {
java.util.List<String> pList = java.util.Arrays.asList("p1.cmd", "p1.toto", "p2.lol");
LitteralPermissionTester tester = p -> pList.contains(p);
for (String permExpr : java.util.Arrays.asList(
"p1.cmd", // true
"p1.notexist", // false
"p2lol.lol", // false
"!p1.notexist", // true
"!p1.cmd", // false
"p1.cmd!", // false
"p1.cmd! p2.lol", // exception
"p1.cmd || p1.toto", // true || true == true
"p1.cmd || p1.notexist", // true || false == true
"p1.fefef || p2.lol", // false || true == true
"p1.fefef || p2.lolilol", // false || false == false
"p1.cmd && p1.toto", // true && true == true
"p1.cmd && p1.notexist", // true && false == false
"p1.fefef && p2.lol", // false && true == false
"p1.fefef && p2.lolilol", // false && false == false
"p1.cmd && !p1.toto ", // true && !true == false
" !p1.cmd && p1.toto", // !true && true == false
"!p1.cmd & p1.toto", // exception
"!p1.cmd | p1.toto", // exception
"p1.not exist" // exception
)) {
try {
System.out.println(permExpr + " -> " + evaluate(permExpr, tester));
} catch (Exception e) {
e.printStackTrace();
}
}
}
*/
}

View File

@ -10,6 +10,13 @@ import fr.pandacube.lib.db.DBConnection;
import fr.pandacube.lib.db.DBException;
import fr.pandacube.lib.util.Log;
/**
* Main class for the Pandalib permission system.
* <p>
* This permission system uses the Pandalib DB API to connect to the database, so the connection to the MySQL must be
* established first, using {@link DB#init(DBConnection, String)}.
* Then, this class must be initialized using {@link #init(Function)}.
*/
public class Permissions {
/* package */ static PermissionsCachedBackendReader backendReader;
@ -20,6 +27,10 @@ public class Permissions {
/**
* Initialize the permission system.
* The connection to the database needs to be initialized first, using {@link DB#init(DBConnection, String)}.
* @param playerNameGetter a function to get the player name associated with a UUID. It is used for
* and to generate {@link PermPlayer#getName()} and for
* {@link PermEntity#debugPermission(String)}.
* @throws DBException if an error occurs when interacting with the database.
*/
public static void init(Function<UUID, String> playerNameGetter) throws DBException {
Permissions.playerNameGetter = playerNameGetter == null ? UUID::toString : playerNameGetter;
@ -37,7 +48,18 @@ public class Permissions {
throw e;
}
}
private static void checkInitialized() {
if (backendReader == null) {
throw new IllegalStateException("Permissions system not initialized. Check the server logs to check if there is an error during the startup, and check if the init() method is called properly.");
}
}
/**
* Adds the provided special permissions to this permission system.
* @param specialPermissions the {@link SpecialPermission}s to add.
* @throws IllegalStateException if the permission system was not initialized properly.
*/
public static void addSpecialPermissions(SpecialPermission... specialPermissions) {
checkInitialized();
if (specialPermissions == null)
@ -45,18 +67,23 @@ public class Permissions {
resolver.specialPermissions.addAll(Arrays.asList(specialPermissions));
}
private static void checkInitialized() {
if (backendReader == null) {
throw new IllegalStateException("Permissions system not initialized. Check the server logs to check if there is an error during the startup, and check if the init() method is called properly.");
}
}
/**
* Clears the cached data of a specific player.
* @param playerId the UUID of the player.
* @throws IllegalStateException if the permission system was not initialized properly.
*/
public static void clearPlayerCache(UUID playerId) {
checkInitialized();
backendReader.clearPlayerCache(playerId);
resolver.clearPlayerFromCache(playerId);
}
/**
* Clears all the cached data (players and groupds) and fetch all the groups data from the database.
* The clearing and fetching of the data is made asynchronously in a new thread.
* @param then the action to perform after the cache has been updated.
* @throws IllegalStateException if the permission system was not initialized properly.
*/
public static void clearCache(Runnable then) {
checkInitialized();
backendReader.clearAndResetCacheAsync(() -> {
@ -65,13 +92,25 @@ public class Permissions {
then.run();
});
}
/**
* Gets the permission player object.
* @param playerId the UUID of the player.
* @return the permission player object.
* @throws IllegalStateException if the permission system was not initialized properly.
*/
public static PermPlayer getPlayer(UUID playerId) {
checkInitialized();
return new PermPlayer(playerId);
}
/**
* Asks the permission system to preventively and asynchronoulsy cache the data of the provided player.
* This can be called as soon as possible when a player connects, so the permission data of the player are
* accessible as soon as possible when they are needed.
* @param playerId the UUID of the player.
* @throws IllegalStateException if the permission system was not initialized properly.
*/
public static void precachePlayerAsync(UUID playerId) {
checkInitialized();
Thread t = new Thread(() -> {
@ -84,23 +123,45 @@ public class Permissions {
t.setDaemon(true);
t.start();
}
/**
* Gets the permission group object.
* @param name the name of the group.
* @return the permission group object.
* @throws IllegalStateException if the permission system was not initialized properly.
*/
public static PermGroup getGroup(String name) {
checkInitialized();
return new PermGroup(name);
}
/**
* Gets all the permission group objects.
* @return all the permission group objects.
* @throws IllegalStateException if the permission system was not initialized properly.
*/
public static List<PermGroup> getGroups() {
checkInitialized();
return PermGroup.fromCachedGroups(backendReader.getGroups());
}
/**
* Gets all the default permission group objects.
* @return all the default permission group objects.
* @throws IllegalStateException if the permission system was not initialized properly.
*/
public static List<PermGroup> getDefaultGroups() {
checkInitialized();
return PermGroup.fromCachedGroups(backendReader.getDefaultGroups());
}
/**
* Gets the full permission list.
* @return the full permission list.
* @throws IllegalStateException if the permission system was not initialized properly.
*/
public static List<String> getFullPermissionsList() {
checkInitialized();
return backendReader.getFullPermissionsList();
}

View File

@ -25,7 +25,7 @@ import fr.pandacube.lib.permissions.PermissionsCachedBackendReader.CachedPlayer;
import fr.pandacube.lib.permissions.SQLPermissions.EntityType;
import fr.pandacube.lib.util.Log;
public class PermissionsResolver {
/* package */ class PermissionsResolver {
private final PermissionsCachedBackendReader backendReader;
@ -400,7 +400,7 @@ public class PermissionsResolver {
ParsedSelfPermission specialPerm = null;
for (SpecialPermission spePerm : specialPermissions) {
if (spePerm.match().match(permission)) {
if (spePerm.matcher().match(permission)) {
boolean res = spePerm.tester().test(permP, permission, server, world);
specialPerm = new ParsedSelfPermission(permission, res, PermType.SPECIAL);
break;

View File

@ -3,13 +3,19 @@ package fr.pandacube.lib.permissions;
import fr.pandacube.lib.db.SQLElement;
import fr.pandacube.lib.db.SQLField;
/**
* SQL Table to store the permissions data.
*/
public class SQLPermissions extends SQLElement<SQLPermissions> {
/**
* Instanciate a new entry in the table.
*/
public SQLPermissions() {
super();
}
public SQLPermissions(int id) {
private SQLPermissions(int id) {
super(id);
}
@ -18,22 +24,45 @@ public class SQLPermissions extends SQLElement<SQLPermissions> {
return "permissions";
}
/** The name of the entity (player id or group name). */
public static final SQLField<SQLPermissions, String> name = field(VARCHAR(64), false);
/** The entity type, based on {@link EntityType}. */
public static final SQLField<SQLPermissions, Integer> type = field(TINYINT, false);
/** The key of the data ("permission", "inheritance", …). */
public static final SQLField<SQLPermissions, String> key = field(VARCHAR(256), false);
/** The data value (permission node, name of inherited group, prefix/suffix, …). */
public static final SQLField<SQLPermissions, String> value = field(VARCHAR(256), false);
/** The server in which the permission apply. */
public static final SQLField<SQLPermissions, String> server = field(VARCHAR(64), true);
/** The world in which the permission apply. */
public static final SQLField<SQLPermissions, String> world = field(VARCHAR(64), true);
/**
* All possible type of entity type.
*/
public enum EntityType {
/**
* User entity type.
*/
User,
/**
* Group entity type.
*/
Group;
/**
* Returns the database value of this entity type.
* @return the database value of this entity type.
*/
public int getCode() {
return ordinal();
}
/**
* Gets the {@link EntityType} corresponding to the database value.
* @param code the database value.
* @return the {@link EntityType} corresponding to the database value.
*/
public static EntityType getByCode(int code) {
if (code >= 0 && code < values().length)
return values()[code];

View File

@ -3,11 +3,14 @@ package fr.pandacube.lib.permissions;
import java.util.Comparator;
import java.util.Objects;
public class ServerWorldKey implements Comparable<ServerWorldKey> {
public final String server, world;
ServerWorldKey(String s, String w) {
server = s; world = w;
}
/**
* A pair of string representing a server and world name, used to organize and filter the permission data of a player or
* group.
* @param server the server name, can be null.
* @param world the world name, can be null.
*/
public record ServerWorldKey(String server, String world) implements Comparable<ServerWorldKey> {
@Override
public boolean equals(Object obj) {
return obj instanceof ServerWorldKey o

View File

@ -2,14 +2,35 @@ package fr.pandacube.lib.permissions;
/**
* Represents a permission node that is based on an arbitrary player state.
* @param matcher predicate that tell if the provided permission is our special permission.
* @param tester predicate that tell the value of this special permission, based on the parameters.
*/
public record SpecialPermission(PermissionMatcher match, PermissionTester tester) {
public record SpecialPermission(PermissionMatcher matcher, PermissionTester tester) {
/**
* Predicate that tell if the provided permission is our special permission.
*/
public interface PermissionMatcher {
/**
* Tells if the provided permission is our special permission.
* @param permission the permission to test.
* @return true if the provided permission is our special permission, false otherwise.
*/
boolean match(String permission);
}
/**
* Predicate that tell the value of this special permission, based on the parameters.
*/
public interface PermissionTester {
/**
* Tells the value of this special permission, based on the parameters.
* @param player the player to test the permission on.
* @param permission the permission to test.
* @param server the server on which the player is.
* @param world the world in which the player is.
* @return the value of this special permission, based on the parameters.
*/
boolean test(PermPlayer player, String permission, String server, String world);
}

View File

@ -0,0 +1,109 @@
package fr.pandacube.lib.permissions;
import fr.pandacube.lib.permissions.PermissionExpressionParser.LitteralPermissionTester;
import org.junit.Before;
import org.junit.Test;
import static fr.pandacube.lib.permissions.PermissionExpressionParser.evaluate;
import static org.junit.Assert.*;
public class PermissionExpressionParserTest {
java.util.List<String> pList = java.util.Arrays.asList("p1.cmd", "p1.toto", "p2.lol");
LitteralPermissionTester tester = p -> pList.contains(p);
@Test
public void evaluateTrue() {
assertTrue(evaluate("p1.cmd", tester));
}
@Test
public void evaluateFalse() {
assertFalse(evaluate("p1.notexist", tester));
}
@Test
public void evaluateNegateFalse() {
assertTrue(evaluate("!p1.notexist", tester));
}
@Test
public void evaluateNegateTrue() {
assertFalse(evaluate("!p1.cmd", tester));
}
@Test
public void evaluateRevNegateTrue() {
assertFalse(evaluate("p1.cmd!", tester));
}
@Test
public void evaluateOrBothTrue() {
assertTrue(evaluate("p1.cmd || p1.toto", tester));
}
@Test
public void evaluateOrTrueFalse() {
assertTrue(evaluate("p1.cmd || p1.notexist", tester));
}
@Test
public void evaluateOrFalseTrue() {
assertTrue(evaluate("p1.fefef || p2.lol", tester));
}
@Test
public void evaluateOrBothFalse() {
assertFalse(evaluate("p1.fefef || p2.lolilol", tester));
}
@Test
public void evaluateAndBothTrue() {
assertTrue(evaluate("p1.cmd && p1.toto", tester));
}
@Test
public void evaluateAndTrueFalse() {
assertFalse(evaluate("p1.cmd && p1.notexist", tester));
}
@Test
public void evaluateAndFalseTrue() {
assertFalse(evaluate("p1.fefef && p2.lol", tester));
}
@Test
public void evaluateAndBothFalse() {
assertFalse(evaluate("p1.fefef && p2.lolilol", tester));
}
@Test
public void evaluateAndTrueNegateTrueWithSomeExtraSpaces() {
assertFalse(evaluate("p1.cmd && !p1.toto ", tester));
}
@Test
public void evaluateAndNegateTrueTrueWithLotOfExtraSpaces() {
assertFalse(evaluate(" !p1.cmd && p1.toto ", tester));
}
@Test(expected = IllegalArgumentException.class)
public void evaluateBadSyntax1() {
evaluate("p1.cmd! p2.lol", tester);
}
@Test(expected = IllegalArgumentException.class)
public void evaluateBadSyntax2() {
evaluate("!p1.cmd & p1.toto", tester);
}
@Test(expected = IllegalArgumentException.class)
public void evaluateBadSyntax3() {
evaluate("!p1.cmd | p1.toto", tester);
}
@Test(expected = IllegalArgumentException.class)
public void evaluateBadSyntax4() {
evaluate("p1.not exist", tester);
}
}

View File

@ -6,9 +6,12 @@ import java.util.stream.LongStream;
import fr.pandacube.lib.chat.ChatColorUtil;
import fr.pandacube.lib.permissions.PermPlayer;
import fr.pandacube.lib.permissions.Permissions;
import fr.pandacube.lib.players.standalone.StandaloneOffPlayer;
import fr.pandacube.lib.players.standalone.AbstractOffPlayer;
public interface PermissibleOffPlayer extends StandaloneOffPlayer {
/**
* Represents a player, either offline or online, with extra methods related to the {@code pandalib-permissions} system.
*/
public interface PermissibleOffPlayer extends AbstractOffPlayer {
@ -44,6 +47,7 @@ public interface PermissibleOffPlayer extends StandaloneOffPlayer {
* Get an updated display name of the user,
* generated using eventual permissions prefix(es) and suffix(es) of the player,
* and with color codes translated to Minecrafts native {@code §}.
* @return the display name of this player, generated by the permission system.
*/
default String getDisplayNameFromPermissionSystem() {
PermPlayer permU = getPermissionUser();
@ -127,6 +131,8 @@ public interface PermissibleOffPlayer extends StandaloneOffPlayer {
/**
* Returns the maximum value returned by {@link PermissibleOffPlayer#getPermissionRangeValues(String)}.
* @param permissionPrefix the permission prefix to search for.
* @return the maximum value for the provided permission range.
*/
default OptionalLong getPermissionRangeMax(String permissionPrefix) {
PermissibleOnlinePlayer online = getOnlineInstance();
@ -139,8 +145,7 @@ public interface PermissibleOffPlayer extends StandaloneOffPlayer {
}
/**
* Tells if the this player is part of the specified group
*
* Tells if the this player is part of the specified group.
* @param group the permissions group
* @return <i>true</i> if this player is part of the group,
* <i>false</i> otherwise

View File

@ -4,9 +4,12 @@ import java.util.OptionalLong;
import java.util.stream.LongStream;
import fr.pandacube.lib.permissions.PermissionExpressionParser;
import fr.pandacube.lib.players.standalone.StandaloneOnlinePlayer;
import fr.pandacube.lib.players.standalone.AbstractOnlinePlayer;
public interface PermissibleOnlinePlayer extends PermissibleOffPlayer, StandaloneOnlinePlayer {
/**
* Represents an online player, with extra methods related to the {@code pandalib-permissions} system.
*/
public interface PermissibleOnlinePlayer extends PermissibleOffPlayer, AbstractOnlinePlayer {
@ -16,7 +19,7 @@ public interface PermissibleOnlinePlayer extends PermissibleOffPlayer, Standalon
*/
/**
* @return The current name of this player
* {@inheritDoc}
* @implSpec The implementation is expected to call the environment API
* (Bukkit/Bungee) to get the name of the player.
*/

View File

@ -1,140 +0,0 @@
package fr.pandacube.lib.players.permissible;
import java.util.Objects;
import java.util.UUID;
import net.kyori.adventure.text.ComponentLike;
import fr.pandacube.lib.chat.ChatStatic;
import fr.pandacube.lib.players.standalone.StandalonePlayerManager;
public abstract class PermissiblePlayerManager<OP extends PermissibleOnlinePlayer, OF extends PermissibleOffPlayer> extends StandalonePlayerManager<OP, OF> {
private static PermissiblePlayerManager<?, ?> instance;
public static synchronized PermissiblePlayerManager<?, ?> getInstance() {
return instance;
}
private static synchronized void setInstance(PermissiblePlayerManager<?, ?> newInstance) {
if (instance != null) {
throw new IllegalStateException("cannot have multiple instance of PlayerManager");
}
instance = newInstance;
}
public PermissiblePlayerManager() {
super();
setInstance(this);
}
@Override
public void broadcastMessage(ComponentLike message, boolean prefix, boolean console, String permission, UUID sourcePlayer) {
Objects.requireNonNull(message, "message cannot be null");
if (prefix)
message = ChatStatic.prefixedAndColored(message.asComponent());
for (PermissibleOnlinePlayer op : getAll()) {
if (permission != null && !(op.hasPermission(permission))) continue;
if (sourcePlayer != null)
op.sendMessage(message, sourcePlayer); // CHAT message with UUID
else
op.sendMessage(message); // SYSTEM message
}
if (console)
sendMessageToConsole(message.asComponent());
}
/*
* Message broadcasting
*/
// ComponentLike message
// boolean prefix
// boolean console = (permission == null)
// String permission = null
// UUID sourcePlayer = null
/**
* Broadcast a message to some or all players, and eventually to the console.
*
* @param message the message to send.
* @param prefix if the server prefix will be prepended to the message.
* @param console if the message must be displayed in the console.
* @param permission if not null, the message is only sent to player with this permission.
* @param sourcePlayer specifiy the eventual player that is the source of the message.
* If null, the message will be sent as a SYSTEM chat message.
* If not null, the message will be sent as a CHAT message, and will not be sent
* to players ignoring the provided player (if implemented).
*
* @throws IllegalArgumentException if message is null.
*/
public static void broadcast(ComponentLike message, boolean prefix, boolean console, String permission, UUID sourcePlayer) {
getInstance().broadcastMessage(message, prefix, console, permission, sourcePlayer);
}
/**
* Broadcast a message to some or all players, and eventually to the console.
* <p>
* This method assumes this message is not caused by a specific player. To specify the source player, use
* {@link #broadcast(ComponentLike, boolean, boolean, String, UUID)}.
*
* @param message the message to send.
* @param prefix if the server prefix will be prepended to the message.
* @param console if the message must be displayed in the console.
* @param permission if not null, the message is only sent to player with this permission.
* @throws IllegalArgumentException if message is null.
*/
public static void broadcast(ComponentLike message, boolean prefix, boolean console, String permission) {
broadcast(message, prefix, console, permission, null);
}
/**
* Broadcast a message to some or all players, and eventually to the console.
* <p>
* This method assumes this message is not caused by a specific player. To specify the source player, use
* {@link #broadcast(ComponentLike, boolean, String, UUID)}.
* <p>
* This method decides to send the message to the console depending on whether {@code permission}
* is null (will send to console) or not (will not send to console). To specify this behaviour, use
* {@link #broadcast(ComponentLike, boolean, boolean, String)}.
*
* @param message the message to send.
* @param prefix if the server prefix will be prepended to the message.
* @param permission if not null, the message is only sent to player with this permission (but not to console).
* If null, the message will be sent to all players and to console.
* @throws IllegalArgumentException if message is null.
*/
public static void broadcast(ComponentLike message, boolean prefix, String permission) {
broadcast(message, prefix, (permission == null), permission, null);
}
/**
* Broadcast a message to all players, and to the console.
* <p>
* This method sends the message to the console. To change this behaviour, use
* {@link #broadcast(ComponentLike, boolean, boolean, String, UUID)}.
*
* @param message the message to send.
* @param prefix if the server prefix will be prepended to the message.
* @param permission if not null, the message is only sent to player with this permission (but not to console).
* If null, the message will be sent to all players and to console.
* @param sourcePlayer specifiy the eventual player that is the source of the message.
* If null, the message will be sent as a SYSTEM chat message.
* If not null, the message will be sent as a CHAT message, and will not be sent
* to players ignoring the provided player (if implemented).
* @throws IllegalArgumentException if message is null.
*/
public static void broadcast(ComponentLike message, boolean prefix, String permission, UUID sourcePlayer) {
broadcast(message, prefix, true, permission, sourcePlayer);
}
}

View File

@ -2,7 +2,10 @@ package fr.pandacube.lib.players.standalone;
import java.util.UUID;
public interface StandaloneOffPlayer {
/**
* Represents any player, either offline or online.
*/
public interface AbstractOffPlayer {
@ -12,20 +15,20 @@ public interface StandaloneOffPlayer {
*/
/**
* Return the ID of the minecraft account.
*
* Returns the {@link UUID} of this player.
* @return the id of the player
*/
UUID getUniqueId();
/**
* @return the last known player name of this player, or null if this player never joined the network.
* The last known player name of this player, or null if it is not known.
* @return the last known player name of this player, or null if it is not known.
*/
String getName();
/**
* Indicate if this player is connected to the current node (server or proxy, depending on interface implementation)
* @return wether the player is online or not
* Indicate if this player is connected to the current node (server or proxy, depending on interface implementation).
* @return wether the player is online or not.
*/
boolean isOnline();
@ -42,8 +45,9 @@ public interface StandaloneOffPlayer {
/**
* Return the online instance of this player, if any exists.
* May return itself if the current instance already represent an online player.
* @return the online instance for this player.
*/
StandaloneOnlinePlayer getOnlineInstance();
AbstractOnlinePlayer getOnlineInstance();
@ -55,9 +59,9 @@ public interface StandaloneOffPlayer {
*/
/**
* Returns the name of the player (if any), with eventual prefix and suffix depending on permission groups
* (and team for bukkit implementation)
* @return the display name of the player
* Returns the display name of the player (if any), with eventual prefix and suffix depending on permission groups
* (and team for bukkit implementation).
* @return the display name of the player.
*/
String getDisplayName();

View File

@ -9,7 +9,10 @@ import net.kyori.adventure.text.ComponentLike;
import java.util.Locale;
import java.util.UUID;
public interface StandaloneOnlinePlayer extends StandaloneOffPlayer {
/**
* Represents any online player.
*/
public interface AbstractOnlinePlayer extends AbstractOffPlayer {
@ -17,15 +20,19 @@ public interface StandaloneOnlinePlayer extends StandaloneOffPlayer {
/*
* General data and state
*/
/**
* @return The current name of this player
* @apiNote The implementation is expected to call the environment API
* (Bukkit/Bungee) to get the name of the player.
* Returns the name of the current server the player is in.
* The returned value is used by the 'pandalib-permissions' system.
* @return the name of the current server the player is in.
*/
String getServerName();
/**
* Returns the name of the current world the player is in.
* The returned value is used by the 'pandalib-permissions' system.
* @return the name of the current world the player is in.
*/
String getWorldName();
@ -40,6 +47,8 @@ public interface StandaloneOnlinePlayer extends StandaloneOffPlayer {
/**
* Tells if this online player has the specified permission.
* @param permission the permission to test on that player.
* @return weither this player has the specified permission or not.
* @implSpec Implementation of this method should call the permission system of their environment (paper/bungee),
* so this method will work independently of the usage of the 'pandalib-permissions' module.
*/
@ -58,15 +67,13 @@ public interface StandaloneOnlinePlayer extends StandaloneOffPlayer {
*/
/**
* Display the provided message in the players chat, if
* the chat is activated.
* Display the provided message in the players chat, if the chat is activated.
* @param message the message to display.
*/
void sendMessage(Component message);
/**
* Display the provided message in the players chat, if
* the chat is activated.
* Display the provided message in the players chat, if the chat is activated.
* @param message the message to display.
*/
default void sendMessage(ComponentLike message) {
@ -74,8 +81,7 @@ public interface StandaloneOnlinePlayer extends StandaloneOffPlayer {
}
/**
* Display the provided message in the players chat, if
* they allows to display CHAT messages
* Display the provided message in the players chat, if they allows to display CHAT messages.
* @param message the message to display.
* @param sender the player causing the send of this message. Client side filtering may occur.
* May be null if we dont want client filtering, but still consider the message as CHAT message.
@ -86,8 +92,7 @@ public interface StandaloneOnlinePlayer extends StandaloneOffPlayer {
void sendMessage(Component message, Identified sender);
/**
* Display the provided message in the players chat, if
* they allows to display CHAT messages
* Display the provided message in the players chat, if they allows to display CHAT messages.
* @param message the message to display
* @param sender the player causing the send of this message. Client side filtering may occur.
* May be null if we dont want client filtering, but still consider the message as CHAT message.
@ -100,8 +105,7 @@ public interface StandaloneOnlinePlayer extends StandaloneOffPlayer {
}
/**
* Display the provided message in the players chat, if the chat is
* activated, prepended with the server prefix.
* Display the provided message in the players chat, if the chat is activated, prepended with the server prefix.
* @param message the message to display
*/
default void sendPrefixedMessage(ComponentLike message) {
@ -131,9 +135,8 @@ public interface StandaloneOnlinePlayer extends StandaloneOffPlayer {
}
/**
* Update the server brand field in the debug menu (F3) of the player
* (third line in 1.15 debug screen). Supports ChatColor codes but no
* line break.
* Update the server brand field in the debug menu (F3) of the player (third line in 1.15 debug screen).
* Supports legacy section format but no line break.
* @param brand the server brand to send to the client.
*/
void sendServerBrand(String brand);
@ -148,47 +151,57 @@ public interface StandaloneOnlinePlayer extends StandaloneOffPlayer {
/*
* Client options
*/
/**
* Gets the current client options of this player.
* @return the current client options of this player.
*/
ClientOptions getClientOptions();
/**
* Interface providing various configuration values of the Minecraft client.
*/
interface ClientOptions {
/**
* The language of the client interface.
* @return language of the client interface.
*/
Locale getLocale();
/**
* The client view distance, in chunks.
* @return client view distance, in chunks.
*/
int getViewDistance();
/**
* If the chat displays the text colors.
* @return true if the chat displays the text colors, false otherwise.
*/
boolean hasChatColorEnabled();
/**
* Tells if the client is configured to completely hide the chat to the
* player. When this is the case, nothing is displayed in the chat box,
* and the player cant send any message or command.
* @implSpec if the value is unknown, it is assumed that the chat is
* fully visible.
* Tells if the client is configured to completely hide the chat to the player. When this is the case, nothing
* is displayed in the chat box, and the player cant send any message or command.
* @return true if the chat is fully hidden, false otherwise.
*/
boolean isChatHidden();
/**
* Tells if the client is configured to display the chat normally.
* When this is the case, chat messages and system messages are
* displayed in the chat box, and the player can send messages and
* commands.
* @implSpec if the value is unknown, it is assumed that the chat is
* fully visible.
* Tells if the client is configured to display the chat normally. When this is the case, chat messages and
* system messages are displayed in the chat box, and the player can send messages and commands.
* @return true if the chat is fully visible, false otherwise.
*/
boolean isChatFullyVisible();
/**
* Tells if the client is configured to only display system messages
* in the chat.
* When this is the case, chat messages are hidden but system messages
* are visible in the chat box, and the player can only send commands.
* @implSpec if the value is unknown, it is assumed that the chat is
* fully visible.
* Tells if the client is configured to only display system messages in the chat.
* When this is the case, chat messages are hidden but system messages are visible in the chat box, and the
* player can only send commands.
* @return true if the chat is visible but only shows system messages, false otherwise.
*/
boolean isChatOnlyDisplayingSystemMessages();
@ -196,57 +209,85 @@ public interface StandaloneOnlinePlayer extends StandaloneOffPlayer {
/**
* Tells if the client has configured the main hand on the left.
* @implSpec if the value is unknown, it is assumed that the main hand
* is on the right.
* @return true if the players character is left handed, false otherwise.
*/
boolean isLeftHanded();
/**
* Tells if the client has configured the main hand on the right.
* @implSpec if the value is unknown, it is assumed that the main hand
* is on the right.
* @return true if the players character is right handed, false otherwise.
*/
boolean isRightHanded();
/**
* Tells if the client has enabled the filtering of texts on sign and book titles.
* Always false as of MC 1.18.
* @return true if the client filters swearing in texts, false otherwise.
*/
boolean isTextFilteringEnabled();
/**
* Tells if the client allows the server to list their player name in the
* multiplayer menu.
* Tells if the client allows the server to list their player name in the multiplayer menu.
* <b>To respect the player privacys configuration, this configuration value must be verified when generating
* custom ping response packet (MOTD in multiplayer servers menu) that includes player names.</b>
* @return true if the client allows the server to list their player name in the multiplayer menu, false
* otherwise.
*/
boolean allowsServerListing();
/**
* Tells if the cape is enabled on the players skin.
* @return true if the cape is enabled on the players skin, false otherwise.
*/
boolean hasSkinCapeEnabled();
/**
* Tells if the jacket is enabled on the players skin.
* @return true if the jacket is enabled on the players skin, false otherwise.
*/
boolean hasSkinJacketEnabled();
/**
* Tells if the left sleeve is enabled on the players skin.
* @return true if the left sleeve is enabled on the players skin, false otherwise.
*/
boolean hasSkinLeftSleeveEnabled();
/**
* Tells if the right sleeve is enabled on the players skin.
* @return true if the right sleeve is enabled on the players skin, false otherwise.
*/
boolean hasSkinRightSleeveEnabled();
/**
* Tells if the left pants is enabled on the players skin.
* @return true if the left pants is enabled on the players skin, false otherwise.
*/
boolean hasSkinLeftPantsEnabled();
/**
* Tells if the right pants is enabled on the players skin.
* @return true if the right pants is enabled on the players skin, false otherwise.
*/
boolean hasSkinRightPantsEnabled();
/**
* Tells if the hat is enabled on the players skin.
* @return true if the hat is enabled on the players skin, false otherwise.
*/
boolean hasSkinHatsEnabled();
}
/**
* Tells if the player can send chat messages or receive chat messages from
* other players, according to their client configuration.
* <br>
* Chat messages represent public communication between players. By default,
* it only include actual chat message. This method may be used in commands
* like /me, /afk or the login/logout broadcasted messages
* Tells if the player can send chat messages or receive chat messages from other players, according to their client
* configuration.
* <p>
* Chat messages represent public communication between players. By default, it only include actual chat message.
* This method may be used in commands like /me, /afk or the login/logout broadcasted messages.
* @return true if the player can send chat messages or receive chat messages from other players, false otherwise.
*/
default boolean canChat() {
return getClientOptions().isChatFullyVisible();

View File

@ -0,0 +1,372 @@
package fr.pandacube.lib.players.standalone;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.UncheckedExecutionException;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import fr.pandacube.lib.chat.ChatStatic;
/**
* Generic implementation of a player manager, handling instances of {@link AbstractOffPlayer} and
* {@link AbstractOnlinePlayer}.
* Subclasses should handle the addition and removal of {@link AbstractOnlinePlayer} instances according to the
* currently online players.
* @param <OP> the type of subclass of {@link AbstractOnlinePlayer}
* @param <OF> the type of subclass of {@link AbstractOffPlayer}
*/
public abstract class AbstractPlayerManager<OP extends AbstractOnlinePlayer, OF extends AbstractOffPlayer> {
private static AbstractPlayerManager<?, ?> instance;
/**
* Gets the current instance of player manager.
* @return the player manager.
*/
public static synchronized AbstractPlayerManager<?, ?> getInstance() {
return instance;
}
private static synchronized void setInstance(AbstractPlayerManager<?, ?> newInstance) {
if (instance != null) {
throw new IllegalStateException("cannot have multiple instance of PlayerManager");
}
instance = newInstance;
}
private final Map<UUID, OP> onlinePlayers = Collections.synchronizedMap(new HashMap<>());
private final LoadingCache<UUID, OF> offlinePlayers = CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(CacheLoader.from(this::newOffPlayerInstance));
/**
* Creates a new instance of player manager.
*/
public AbstractPlayerManager() {
setInstance(this);
}
/**
* Adds the provided player to this player manager.
* Usually this is called by a player join event handler.
* @param p the player to add.
*/
protected void addPlayer(OP p) {
onlinePlayers.put(p.getUniqueId(), p);
offlinePlayers.invalidate(p.getUniqueId());
}
/**
* Removes the player from this player manager.
* Usually this is called by a player quit event handler.
* @param p the UUID of the player to remove.
* @return the player that has been removed.
*/
protected OP removePlayer(UUID p) {
return onlinePlayers.remove(p);
}
/**
* Get the online player that has the provided UUID.
* @param p the UUID of the player.
* @return the online player instance.
*/
public OP get(UUID p) {
return onlinePlayers.get(p);
}
/**
* Tells if the provided player is online.
* @param p the UUID of the player.
* @return true if the provided player is online (that is when this player manager contains an online player
* instance), false otherwise.
*/
public boolean isOnline(UUID p) {
return onlinePlayers.containsKey(p);
}
/**
* Get the number of online player registered in this player manager.
* @return the number of online player registered in this player manager.
*/
public int getPlayerCount() {
return onlinePlayers.size();
}
/**
* Get all the online players is this player manager.
* @return all the online players is this player manager.
*/
public List<OP> getAll() {
return new ArrayList<>(onlinePlayers.values());
}
/**
* Returns an instance of {@link AbstractOffPlayer} corresponding to a player with the provided {@link UUID}.
* @param p the UUID of the player.
* @return an instance of {@link AbstractOffPlayer}. It can be a new instance, an {@link AbstractOnlinePlayer}
* instance if the player is online, or a cached instance of {@link AbstractOffPlayer}.
*/
public OF getOffline(UUID p) {
if (p == null)
return null;
OP online = get(p);
if (online != null) {
offlinePlayers.invalidate(p);
@SuppressWarnings("unchecked")
OF ret = (OF) online;
return ret;
}
// if not online
try {
return offlinePlayers.get(p); // load and cache new instance if necessary
} catch (ExecutionException e) {
throw new UncheckedExecutionException(e.getCause());
}
}
/**
* Create a new instance of the appropriate subclass of {@link AbstractOffPlayer}.
* @param p the uuid of the player.
* @return the new instance of {@link AbstractOffPlayer}.
*/
protected abstract OF newOffPlayerInstance(UUID p);
/**
* Abstract method to implement to send a message to the console.
* @param message the message to send to the console.
*/
protected abstract void sendMessageToConsole(Component message);
/**
* Broadcast a message to some or all players, and eventually to the console.
*
* @param message the message to send.
* @param prefix if the server prefix will be prepended to the message.
* @param console if the message must be displayed in the console.
* @param permission if not null, the message is only sent to player with this permission.
* @param sourcePlayer specifiy the eventual player that is the source of the message.
* If null, the message will be sent as a SYSTEM chat message.
* If not null, the message will be sent as a CHAT message, and will not be sent
* to players ignoring the provided player (if implemented).
* @throws IllegalArgumentException if message is null.
* @implSpec subclasses may override this method, for instance to restrict the players being able to see the message
* (like /ignored players).
*/
public void broadcastMessage(ComponentLike message, boolean prefix, boolean console, String permission, UUID sourcePlayer) {
Objects.requireNonNull(message, "message cannot be null");
if (prefix)
message = ChatStatic.prefixedAndColored(message.asComponent());
for (OP op : getAll()) {
if (permission != null && !(op.hasPermission(permission))) continue;
if (sourcePlayer != null)
op.sendMessage(message, sourcePlayer); // CHAT message with UUID
else
op.sendMessage(message); // SYSTEM message
}
if (console)
sendMessageToConsole(message.asComponent());
}
/*
* Message broadcasting
*/
/**
* Broadcast a message to some or all players, and eventually to the console.
*
* @param message the message to send.
* @param prefix if the server prefix will be prepended to the message.
* @param console if the message must be displayed in the console.
* @param permission if not null, the message is only sent to player with this permission.
* @param sourcePlayer specifiy the eventual player that is the source of the message.
* If null, the message will be sent as a SYSTEM chat message.
* If not null, the message will be sent as a CHAT message, and will not be sent
* to players ignoring the provided player (if implemented).
*
* @throws IllegalArgumentException if message is null.
*/
public static void broadcast(ComponentLike message, boolean prefix, boolean console, String permission, UUID sourcePlayer) {
getInstance().broadcastMessage(message, prefix, console, permission, sourcePlayer);
}
/**
* Broadcast a message to some or all players, and eventually to the console.
* <p>
* This method assumes this message is not caused by a specific player. To specify the source player, use
* {@link #broadcast(ComponentLike, boolean, boolean, String, UUID)}.
*
* @param message the message to send.
* @param prefix if the server prefix will be prepended to the message.
* @param console if the message must be displayed in the console.
* @param permission if not null, the message is only sent to player with this permission.
* @throws IllegalArgumentException if message is null.
*/
public static void broadcast(ComponentLike message, boolean prefix, boolean console, String permission) {
broadcast(message, prefix, console, permission, null);
}
/**
* Broadcast a message to some or all players, and eventually to the console.
* <p>
* This method assumes this message is not caused by a specific player. To specify the source player, use
* {@link #broadcast(ComponentLike, boolean, String, UUID)}.
* <p>
* This method decides to send the message to the console depending on whether {@code permission}
* is null (will send to console) or not (will not send to console). To specify this behaviour, use
* {@link #broadcast(ComponentLike, boolean, boolean, String)}.
*
* @param message the message to send.
* @param prefix if the server prefix will be prepended to the message.
* @param permission if not null, the message is only sent to player with this permission (but not to console).
* If null, the message will be sent to all players and to console.
* @throws IllegalArgumentException if message is null.
*/
public static void broadcast(ComponentLike message, boolean prefix, String permission) {
broadcast(message, prefix, (permission == null), permission, null);
}
/**
* Broadcast a message to all players, and to the console.
* <p>
* This method sends the message to the console. To change this behaviour, use
* {@link #broadcast(ComponentLike, boolean, boolean, String, UUID)}.
*
* @param message the message to send.
* @param prefix if the server prefix will be prepended to the message.
* @param permission if not null, the message is only sent to player with this permission (but not to console).
* If null, the message will be sent to all players and to console.
* @param sourcePlayer specifiy the eventual player that is the source of the message.
* If null, the message will be sent as a SYSTEM chat message.
* If not null, the message will be sent as a CHAT message, and will not be sent
* to players ignoring the provided player (if implemented).
* @throws IllegalArgumentException if message is null.
*/
public static void broadcast(ComponentLike message, boolean prefix, String permission, UUID sourcePlayer) {
broadcast(message, prefix, true, permission, sourcePlayer);
}
/**
* Broadcast a message to some or all players, and eventually to the console.
* <p>
* This method doesnt restrict to a specific permission. To change this behaviour, use
* {@link #broadcast(ComponentLike, boolean, boolean, String, UUID)}.
*
* @param message the message to send.
* @param prefix if the server prefix will be prepended to the message.
* @param console if the message must be displayed in the console.
* @param sourcePlayer specifiy the eventual player that is the source of the message.
* If null, the message will be sent as a SYSTEM chat message.
* If not null, the message will be sent as a CHAT message, and will not be sent
* to players ignoring the provided player (if implemented).
*
* @throws IllegalArgumentException if message is null.
*/
public static void broadcast(ComponentLike message, boolean prefix, boolean console, UUID sourcePlayer) {
getInstance().broadcastMessage(message, prefix, console, null, sourcePlayer);
}
/**
* Broadcast a message to some or all players, and eventually to the console.
* <p>
* This method assumes this message is not caused by a specific player. To specify the source player, use
* {@link #broadcast(ComponentLike, boolean, boolean, UUID)}.
* <p>
* This method doesnt restrict to a specific permission. To change this behaviour, use
* {@link #broadcast(ComponentLike, boolean, boolean, String)}.
*
* @param message the message to send.
* @param prefix if the server prefix will be prepended to the message.
* @param console if the message must be displayed in the console.
* @throws IllegalArgumentException if message is null.
*/
public static void broadcast(ComponentLike message, boolean prefix, boolean console) {
broadcast(message, prefix, console, null, null);
}
/**
* Broadcast a message to all players, and to the console.
* <p>
* This method sends the message to the console. To change this behaviour, use
* {@link #broadcast(ComponentLike, boolean, boolean, UUID)}.
* <p>
* This method doesnt restrict to a specific permission. To change this behaviour, use
* {@link #broadcast(ComponentLike, boolean, String, UUID)}.
*
* @param message the message to send.
* @param prefix if the server prefix will be prepended to the message.
* @param sourcePlayer specifiy the eventual player that is the source of the message.
* If null, the message will be sent as a SYSTEM chat message.
* If not null, the message will be sent as a CHAT message, and will not be sent
* to players ignoring the provided player (if implemented).
* @throws IllegalArgumentException if message is null.
*/
public static void broadcast(ComponentLike message, boolean prefix, UUID sourcePlayer) {
broadcast(message, prefix, true, null, sourcePlayer);
}
/**
* Broadcast a message to all players, and to the console.
* <p>
* This method assumes this message is not caused by a specific player. To specify the source player, use
* {@link #broadcast(ComponentLike, boolean, UUID)}.
* <p>
* This method sends the message to the console. To change this behaviour, use
* {@link #broadcast(ComponentLike, boolean, boolean)}.
* <p>
* This method doesnt restrict to a specific permission. To change this behaviour, use
* {@link #broadcast(ComponentLike, boolean, String)}.
*
* @param message the message to send.
* @param prefix if the server prefix will be prepended to the message.
* @throws IllegalArgumentException if message is null.
*/
public static void broadcast(ComponentLike message, boolean prefix) {
broadcast(message, prefix, true, null, null);
}
}

View File

@ -1,216 +0,0 @@
package fr.pandacube.lib.players.standalone;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.UncheckedExecutionException;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import fr.pandacube.lib.chat.ChatStatic;
public abstract class StandalonePlayerManager<OP extends StandaloneOnlinePlayer, OF extends StandaloneOffPlayer> {
private static StandalonePlayerManager<?, ?> instance;
public static synchronized StandalonePlayerManager<?, ?> getInstance() {
return instance;
}
private static synchronized void setInstance(StandalonePlayerManager<?, ?> newInstance) {
if (instance != null) {
throw new IllegalStateException("cannot have multiple instance of PlayerManager");
}
instance = newInstance;
}
private final Map<UUID, OP> onlinePlayers = Collections.synchronizedMap(new HashMap<>());
private final LoadingCache<UUID, OF> offlinePlayers = CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(CacheLoader.from(this::newOffPlayerInstance));
public StandalonePlayerManager() {
setInstance(this);
}
protected void addPlayer(OP p) {
onlinePlayers.put(p.getUniqueId(), p);
offlinePlayers.invalidate(p.getUniqueId());
}
protected OP removePlayer(UUID p) {
return onlinePlayers.remove(p);
}
public OP get(UUID p) {
return onlinePlayers.get(p);
}
public boolean isOnline(UUID p) {
return onlinePlayers.containsKey(p);
}
public int getPlayerCount() {
return onlinePlayers.size();
}
public List<OP> getAll() {
return new ArrayList<>(onlinePlayers.values());
}
public OF getOffline(UUID p) {
if (p == null)
return null;
OP online = get(p);
if (online != null) {
offlinePlayers.invalidate(p);
@SuppressWarnings("unchecked")
OF ret = (OF) online;
return ret;
}
// if not online
try {
return offlinePlayers.get(p); // load and cache new instance if necessary
} catch (ExecutionException e) {
throw new UncheckedExecutionException(e.getCause());
}
}
protected abstract OF newOffPlayerInstance(UUID p);
protected abstract void sendMessageToConsole(Component message);
public void broadcastMessage(ComponentLike message, boolean prefix, boolean console, String permission, UUID sourcePlayer) {
Objects.requireNonNull(message, "message cannot be null");
if (prefix)
message = ChatStatic.prefixedAndColored(message.asComponent());
for (StandaloneOnlinePlayer op : getAll()) {
if (sourcePlayer != null)
op.sendMessage(message, sourcePlayer); // CHAT message without UUID
else
op.sendMessage(message); // SYSTEM message
}
if (console)
getInstance().sendMessageToConsole(message.asComponent());
}
/*
* Message broadcasting
*/
// ComponentLike message
// boolean prefix
// boolean console = (permission == null)
// UUID sourcePlayer = null
/**
* Broadcast a message to some or all players, and eventually to the console.
*
* @param message the message to send.
* @param prefix if the server prefix will be prepended to the message.
* @param console if the message must be displayed in the console.
* @param sourcePlayer specifiy the eventual player that is the source of the message.
* If null, the message will be sent as a SYSTEM chat message.
* If not null, the message will be sent as a CHAT message, and will not be sent
* to players ignoring the provided player (if implemented).
*
* @throws IllegalArgumentException if message is null.
*/
public static void broadcast(ComponentLike message, boolean prefix, boolean console, UUID sourcePlayer) {
getInstance().broadcastMessage(message, prefix, console, null, sourcePlayer);
}
/**
* Broadcast a message to some or all players, and eventually to the console.
* <p>
* This method assumes this message is not caused by a specific player. To specify the source player, use
* {@link #broadcast(ComponentLike, boolean, boolean, UUID)}.
*
* @param message the message to send.
* @param prefix if the server prefix will be prepended to the message.
* @param console if the message must be displayed in the console.
* @throws IllegalArgumentException if message is null.
*/
public static void broadcast(ComponentLike message, boolean prefix, boolean console) {
broadcast(message, prefix, console, null);
}
/**
* Broadcast a message to all players, and to the console.
* <p>
* This method sends the message to the console. To change this behaviour, use
* {@link #broadcast(ComponentLike, boolean, boolean, UUID)}.
*
* @param message the message to send.
* @param prefix if the server prefix will be prepended to the message.
* @param sourcePlayer specifiy the eventual player that is the source of the message.
* If null, the message will be sent as a SYSTEM chat message.
* If not null, the message will be sent as a CHAT message, and will not be sent
* to players ignoring the provided player (if implemented).
* @throws IllegalArgumentException if message is null.
*/
public static void broadcast(ComponentLike message, boolean prefix, UUID sourcePlayer) {
broadcast(message, prefix, true, sourcePlayer);
}
/**
* Broadcast a message to all players, and to the console.
* <p>
* This method assumes this message is not caused by a specific player. To specify the source player, use
* {@link #broadcast(ComponentLike, boolean, UUID)}.
* <p>
* This method sends the message to the console. To change this behaviour, use
* {@link #broadcast(ComponentLike, boolean, boolean)}.
*
* @param message the message to send.
* @param prefix if the server prefix will be prepended to the message.
* @throws IllegalArgumentException if message is null.
*/
public static void broadcast(ComponentLike message, boolean prefix) {
broadcast(message, prefix, true, null);
}
}

View File

@ -12,5 +12,9 @@ import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ConcreteWrapper {
/**
* The class representing a dummy implementation of the annotated wrapper interface.
* @return the class representing a dummy implementation of the annotated wrapper interface.
*/
Class<? extends ReflectWrapper> value();
}

View File

@ -6,6 +6,11 @@ import java.util.function.Supplier;
import fr.pandacube.lib.util.MappedListView;
/**
* A wrapper for a list of wrapped object. It is an extension of {@link MappedListView} that is used to transparently
* wrap/unwrap the elements of the backend list.
* @param <W> the type of the reflect wrapper for the elements of this list.
*/
public class ReflectListWrapper<W extends ReflectWrapperI> extends MappedListView<Object, W> implements ReflectWrapperTypedI<List<Object>> {
private final Class<W> expectedWrapperClass;
@ -14,11 +19,12 @@ public class ReflectListWrapper<W extends ReflectWrapperI> extends MappedListVie
this(ArrayList::new, expectedWrapperClass);
}
/* package */
@SuppressWarnings("unchecked")
ReflectListWrapper(Supplier<List<?>> listCreator, Class<W> expectedWrapperClass) {
/* package */ ReflectListWrapper(Supplier<List<?>> listCreator, Class<W> expectedWrapperClass) {
this((List<Object>) (listCreator == null ? new ArrayList<>() : listCreator.get()), expectedWrapperClass);
}
/* package */ ReflectListWrapper(List<Object> wrappedList, Class<W> expectedWrapperClass) {
super(wrappedList, el -> ReflectWrapper.wrap(el, expectedWrapperClass), ReflectWrapper::unwrap);
this.expectedWrapperClass = expectedWrapperClass;

View File

@ -10,25 +10,79 @@ import java.util.Objects;
import static fr.pandacube.lib.util.ThrowableUtil.wrapEx;
/**
* Superclass of all reflect wrapper objects.
*/
public abstract class ReflectWrapper implements ReflectWrapperI {
private static final Map<Object, ReflectWrapperI> objectWrapperCache = new MapMaker().weakKeys().makeMap();
/**
* Unwraps the object from the provided reflect wrapper.
* @param wr the reflect wrapper from which to get the object.
* @return the object from the provided reflect wrapper.
*/
public static Object unwrap(ReflectWrapperI wr) {
return wr == null ? null : wr.__getRuntimeInstance();
}
/**
* Unwraps the object from the provided reflect wrapper.
* @param wr the reflect wrapper from which to get the object.
* @param <T> the type of the wrapped object.
* @return the object from the provided reflect wrapper.
*/
public static <T> T unwrap(ReflectWrapperTypedI<T> wr) {
return wr == null ? null : wr.__getRuntimeInstance();
}
public static <W extends ReflectWrapperI> W wrap(Object runtimeObj) {
/**
* Wraps the provided runtime object into a reflect wrapper.
* If a wrapper instance is already known, it will return it instead of instanciating a new one.
* It is better to call {@link #wrap(Object, Class)} if you know the type of wrapper needed.
* @param runtimeObj the object to wrap.
* @return the reflect wrapper wrapping the provided object.
* @throws ClassCastException if the runtime class of the object is not handled by the expected wrapper class or its
* subclasses.
* @throws IllegalArgumentException if the runtime class of the object is not handled by any of the registered
* wrapper classes.
*/
public static ReflectWrapperI wrap(Object runtimeObj) {
return wrap(runtimeObj, null);
}
/**
* Wraps the provided runtime object (with has a known type) into a reflect wrapper.
* If a wrapper instance is already known, it will return it instead of instanciating a new one.
* It is better to call {@link #wrap(Object, Class)} if you know the type of wrapper needed.
* @param runtimeObj the object to wrap.
* @param expectedWrapperClass the reflect wrapper class expected to be returned.
* @param <W> the type of the reflect wrapper.
* @param <T> the type of the wrapped object.
* @return the reflect wrapper wrapping the provided object.
* @throws ClassCastException if the runtime class of the object is not handled by the expected wrapper class or its
* subclasses.
* @throws IllegalArgumentException if the runtime class of the object is not handled by any of the registered
* wrapper classes.
*/
public static <T, W extends ReflectWrapperTypedI<T>> W wrapTyped(T runtimeObj, Class<W> expectedWrapperClass) {
return wrap(runtimeObj, expectedWrapperClass);
}
/**
* Wraps the provided runtime object into a reflect wrapper.
* If a wrapper instance is already known, it will return it instead of instanciating a new one.
* It is better to call {@link #wrap(Object, Class)} if you know the type of wrapper needed.
* @param runtimeObj the object to wrap.
* @param expectedWrapperClass the reflect wrapper class expected to be returned.
* @param <W> the type of the reflect wrapper.
* @return the reflect wrapper wrapping the provided object.
* @throws ClassCastException if the runtime class of the object is not handled by the expected wrapper class or its
* subclasses.
* @throws IllegalArgumentException if the runtime class of the object is not handled by any of the registered
* wrapper classes.
*/
public static <W extends ReflectWrapperI> W wrap(Object runtimeObj, Class<W> expectedWrapperClass) {
if (runtimeObj == null)
return null;
@ -73,16 +127,36 @@ public abstract class ReflectWrapper implements ReflectWrapperI {
}
}
/**
* Wraps the provided runtime list into a reflect list wrapper.
* @param runtimeList the list of runtime object to wrap.
* @param expectedWrapperClass the wrapper class of the objects in this list.
* @param <W> the type of reflect wrapper for the objects in this list.
* @return a reflect list wrapper wrapping the provided list.
*/
public static <W extends ReflectWrapperI> ReflectListWrapper<W> wrapList(List<Object> runtimeList, Class<W> expectedWrapperClass) {
return new ReflectListWrapper<>(runtimeList, expectedWrapperClass);
}
protected final Object reflectObject;
private final Object reflectObject;
/**
* Instanciate this Reflect Wrapper with the provided object.
* Any subclasses should not make their constructor public since the instanciation is managed by {@link #wrap(Object, Class) wrap(...)}.
* @param obj the object to wrap. It must be an instance of the {@link #__getRuntimeClass() runtime class} of this
* wrapper class.
*/
protected ReflectWrapper(Object obj) {
Objects.requireNonNull(obj);
if (!__getRuntimeClass().isInstance(obj)) {

View File

@ -1,11 +1,22 @@
package fr.pandacube.lib.reflect.wrapper;
/**
* Interface implemented by all reflect wrapper objects.
*/
public interface ReflectWrapperI {
/**
* Gets the class of the wrapped object.
* @return the class of the wrapped object.
*/
default Class<?> __getRuntimeClass() {
return WrapperRegistry.getRuntimeClassOfWrapperClass(getClass());
}
/**
* Returns the wrapped object.
* @return the wrapped object.
*/
Object __getRuntimeInstance();

View File

@ -1,7 +1,17 @@
package fr.pandacube.lib.reflect.wrapper;
/**
* Superclass of all reflect wrapper objects which wrapped objet type is statically known.
* @param <T> the type (or supertype) of the wrapped object.
*/
public abstract class ReflectWrapperTyped<T> extends ReflectWrapper implements ReflectWrapperTypedI<T> {
/**
* Instanciate this Reflect Wrapper with the provided object.
* Any subclasses should not make their constructor public since the instanciation is managed by {@link #wrap(Object, Class) wrap(...)}.
* @param obj the object to wrap. It must be an instance of the {@link #__getRuntimeClass() runtime class} of this
* wrapper class.
*/
protected ReflectWrapperTyped(Object obj) {
super(obj);
}

View File

@ -1,5 +1,9 @@
package fr.pandacube.lib.reflect.wrapper;
/**
* Interface implemented by all reflect wrapper objects which wrapped objet type is statically known.
* @param <T> the type (or supertype) of the wrapped object.
*/
public interface ReflectWrapperTypedI<T> extends ReflectWrapperI {
@SuppressWarnings("unchecked")
@Override