renamed modules dir

This commit is contained in:
2022-07-20 13:28:01 +02:00
parent 7dcd92f72d
commit d2ca501203
205 changed files with 12 additions and 12 deletions

View File

@@ -0,0 +1,40 @@
package fr.pandacube.lib.core.commands;
import java.util.Arrays;
import fr.pandacube.lib.chat.ChatStatic;
public class AbstractCommand extends ChatStatic {
public final String commandName;
public AbstractCommand(String cmdName) {
commandName = cmdName;
}
/**
* <p>
* Concatène les chaines de caractères passés dans <code>args</code> (avec
* <code>" "</code> comme séparateur), en ommettant
* celles qui se trouvent avant <code>index</code>.<br/>
* Par exemple :
* </p>
* <code>
* getLastParams(new String[] {"test", "bouya", "chaka", "bukkit"}, 1);
* </code>
* <p>
* retournera la chaine "bouya chaka bukkit"
*
* @param args liste des arguments d'une commandes.<br/>
* Le premier élément est l'argument qui suit le nom de la commande.
* Usuellement, ce paramètre correspond au paramètre
* <code>args</code> de la méthode onCommand
*/
public static String getLastParams(String[] args, int index) {
return String.join(" ", Arrays.copyOfRange(args, index, args.length));
}
}

View File

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

View File

@@ -0,0 +1,382 @@
package fr.pandacube.lib.core.commands;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.LongStream;
import java.util.stream.Stream;
import fr.pandacube.lib.util.ListUtil;
import fr.pandacube.lib.util.TimeUtil;
@FunctionalInterface
public interface SuggestionsSupplier<S> {
/**
* Number of suggestion visible at once without having to scroll
*/
int VISIBLE_SUGGESTION_COUNT = 10;
List<String> getSuggestions(S sender, int tokenIndex, String token, String[] args);
static Predicate<String> filter(String token) {
return suggestion -> suggestion != null && suggestion.toLowerCase().startsWith(token.toLowerCase());
}
/**
* Filter the provided {@link Stream} of string according to the provided token, using the filter returned by {@link #filter(String)},
* then returns the strings collected into a {@link List}.
*
* This methods consume the provided stream, so will not be usable anymore.
*/
static List<String> collectFilteredStream(Stream<String> stream, String token) {
return stream.filter(filter(token)).sorted().collect(Collectors.toList());
}
static <S> SuggestionsSupplier<S> empty() { return (s, ti, t, a) -> Collections.emptyList(); }
static <S> SuggestionsSupplier<S> fromCollectionsSupplier(Supplier<Collection<String>> streamSupplier) {
return (s, ti, token, a) -> collectFilteredStream(streamSupplier.get().stream(), token);
}
static <S> SuggestionsSupplier<S> fromStreamSupplier(Supplier<Stream<String>> streamSupplier) {
return (s, ti, token, a) -> collectFilteredStream(streamSupplier.get(), token);
}
static <S> SuggestionsSupplier<S> fromCollection(Collection<String> suggestions) {
return fromStreamSupplier(suggestions::stream);
}
static <S> SuggestionsSupplier<S> fromArray(String... suggestions) {
return fromStreamSupplier(() -> Arrays.stream(suggestions));
}
static <E extends Enum<E>, S> SuggestionsSupplier<S> fromEnum(Class<E> enumClass) {
return fromEnumValues(enumClass.getEnumConstants());
}
static <E extends Enum<E>, S> SuggestionsSupplier<S> fromEnum(Class<E> enumClass, boolean lowerCase) {
return fromEnumValues(lowerCase, enumClass.getEnumConstants());
}
@SafeVarargs
static <E extends Enum<E>, S> SuggestionsSupplier<S> fromEnumValues(E... enumValues) {
return fromEnumValues(false, enumValues);
}
@SafeVarargs
static <E extends Enum<E>, S> SuggestionsSupplier<S> fromEnumValues(boolean lowerCase, E... enumValues) {
return (s, ti, token, a) -> {
Stream<String> st = Arrays.stream(enumValues).map(Enum::name);
if (lowerCase)
st = st.map(String::toLowerCase);
return collectFilteredStream(st, token);
};
}
static <S> SuggestionsSupplier<S> booleanValues() {
return fromCollection(Arrays.asList("true", "false"));
}
/**
* Create a {@link SuggestionsSupplier} that suggest numbers according to the provided range.
*
* The current implementation only support range that include either -1 or 1.
*/
static <S> SuggestionsSupplier<S> fromIntRange(int min, int max, boolean compact) {
return fromLongRange(min, max, compact);
}
/**
* Create a {@link SuggestionsSupplier} that suggest numbers according to the provided range.
*
* The current implementation only support range that include either -1 or 1.
*/
static <S> SuggestionsSupplier<S> fromLongRange(long min, long max, boolean compact) {
if (max < min) {
throw new IllegalArgumentException("min should be less or equals than max");
}
if (compact) {
return (s, ti, token, a) -> {
try {
List<Long> proposedValues = new ArrayList<>();
if (token.length() == 0) {
long start = Math.max(Math.max(Math.min(-4, max - 9), min), -9);
long end = Math.min(Math.min(start + 9, max), 9);
ListUtil.addLongRangeToList(proposedValues, start, end);
}
else if (token.length() == 1) {
if (token.charAt(0) == '0') {
if (min > 0 || max < 0) {
return Collections.emptyList();
}
else
return Collections.singletonList(token);
}
else if (token.charAt(0) == '-') {
ListUtil.addLongRangeToList(proposedValues, Math.max(-9, min), -1);
}
else {
long lToken = Long.parseLong(token);
if (lToken > max) {
return Collections.emptyList();
}
lToken *= 10;
if (lToken > max) {
return Collections.singletonList(token);
}
ListUtil.addLongRangeToList(proposedValues, lToken, Math.min(lToken + 9, max));
}
}
else {
long lToken = Long.parseLong(token);
if (lToken < min || lToken > max) {
return Collections.emptyList();
}
lToken *= 10;
if (lToken < min || lToken > max) {
return Collections.singletonList(token);
}
if (lToken < 0) {
ListUtil.addLongRangeToList(proposedValues, Math.max(lToken - 9, min), lToken);
}
else {
ListUtil.addLongRangeToList(proposedValues, lToken, Math.min(lToken + 9, max));
}
}
return collectFilteredStream(proposedValues.stream().map(Object::toString), token);
} catch (NumberFormatException e) {
return Collections.emptyList();
}
};
}
else {
return (s, ti, token, a) -> collectFilteredStream(LongStream.rangeClosed(min, max).mapToObj(Long::toString), token);
}
}
public static <S> SuggestionsSupplier<S> suggestDuration() {
final List<String> emptyTokenSuggestions = TimeUtil.DURATION_SUFFIXES.stream().map(p -> "1" + p).collect(Collectors.toList());
return (s, ti, token, args) -> {
if (token.isEmpty()) {
return emptyTokenSuggestions;
}
List<String> remainingSuffixes = new ArrayList<>(TimeUtil.DURATION_SUFFIXES);
char[] tokenChars = token.toCharArray();
String accSuffix = "";
for (char c : tokenChars) {
if (Character.isDigit(c)) {
scanAndRemovePastSuffixes(remainingSuffixes, accSuffix);
accSuffix = "";
} else if (Character.isLetter(c)) {
accSuffix += c;
} else
return Collections.emptyList();
}
String prefixToken = token.substring(0, token.length() - accSuffix.length());
return SuggestionsSupplier.collectFilteredStream(remainingSuffixes.stream(), accSuffix)
.stream()
.map(str -> prefixToken + str)
.collect(Collectors.toList());
};
}
private static void scanAndRemovePastSuffixes(List<String> suffixes, String foundSuffix) {
for (int i = 0; i < suffixes.size(); i++) {
if (foundSuffix.startsWith(suffixes.get(i))) {
suffixes.subList(0, i + 1).clear();
return;
}
}
}
/**
* Create a {@link SuggestionsSupplier} that support greedy strings argument using the suggestion from this {@link SuggestionsSupplier}.
* @param index the index of the first argument of the greedy string argument
*/
default SuggestionsSupplier<S> greedyString(int index) {
return (s, ti, token, args) -> {
if (ti < index)
return Collections.emptyList();
String gToken = AbstractCommand.getLastParams(args, index);
String[] splitGToken = gToken.split(" ", -1);
int currentTokenPosition = splitGToken.length - 1;
String[] prevWordsGToken = Arrays.copyOf(splitGToken, currentTokenPosition);
String[] argsWithMergedGreedyToken = Arrays.copyOf(args, index + 1);
argsWithMergedGreedyToken[index] = gToken;
List<String> currentTokenProposal = new ArrayList<>();
for (String suggestion : getSuggestions(s, index, gToken, argsWithMergedGreedyToken)) {
String[] splitSuggestion = suggestion.split(" ", -1);
if (splitSuggestion.length <= currentTokenPosition)
continue;
if (!Arrays.equals(Arrays.copyOf(splitGToken, currentTokenPosition), prevWordsGToken))
continue;
if (splitSuggestion[currentTokenPosition].isEmpty())
continue;
currentTokenProposal.add(splitSuggestion[currentTokenPosition]);
}
return currentTokenProposal;
};
}
default SuggestionsSupplier<S> quotableString() {
return (s, ti, token, a) -> {
boolean startWithQuote = token.length() > 0 && (token.charAt(0) == '"' || token.charAt(0) == '\'');
String realToken = startWithQuote ? unescapeBrigadierQuotable(token.substring(1), token.charAt(0)) : token;
String[] argsCopy = Arrays.copyOf(a, a.length);
argsCopy[a.length - 1] = realToken;
List<String> rawResults = getSuggestions(s, ti, realToken, argsCopy);
boolean needsQuotes = false;
for (String res : rawResults) {
if (!isAllowedInBrigadierUnquotedString(res)) {
needsQuotes = true;
break;
}
}
return needsQuotes
? rawResults.stream().map(SuggestionsSupplier::escapeBrigadierQuotable).collect(Collectors.toList())
: rawResults;
};
}
// inspired from com.mojang.brigadier.StringReader#readQuotedString()
static String unescapeBrigadierQuotable(String input, char quote) {
StringBuilder builder = new StringBuilder(input.length());
boolean escaped = false;
for (char c : input.toCharArray()) {
if (escaped) {
if (c == quote || c == '\\') {
escaped = false;
} else {
builder.append('\\');
}
builder.append(c);
} else if (c == '\\') {
escaped = true;
} else if (c == quote) {
return builder.toString();
} else {
builder.append(c);
}
}
return builder.toString();
}
// from com.mojang.brigadier.StringReader#isAllowedInUnquotedString(char)
static boolean isAllowedInBrigadierUnquotedString(char c) {
return c >= '0' && c <= '9' || c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z'
|| c == '_' || c == '-' || c == '.' || c == '+';
}
static boolean isAllowedInBrigadierUnquotedString(String s) {
for (char c : s.toCharArray())
if (!isAllowedInBrigadierUnquotedString(c))
return false;
return true;
}
static String escapeBrigadierQuotable(final String input) {
final StringBuilder result = new StringBuilder("\"");
for (int i = 0; i < input.length(); i++) {
final char c = input.charAt(i);
if (c == '\\' || c == '"') {
result.append('\\');
}
result.append(c);
}
result.append("\"");
return result.toString();
}
default SuggestionsSupplier<S> requires(Predicate<S> check) {
return (s, ti, to, a) -> check.test(s) ? getSuggestions(s, ti, to, a) : Collections.emptyList();
}
/**
* Returns a new {@link SuggestionsSupplier} containing all the element of this instance then the element of the provided one,
* with all duplicated values removed using {@link Stream#distinct()}.
*/
default SuggestionsSupplier<S> merge(SuggestionsSupplier<S> other) {
return (s, ti, to, a) -> {
List<String> l1 = getSuggestions(s, ti, to, a);
List<String> l2 = other.getSuggestions(s, ti, to, a);
return Stream.concat(l1.stream(), l2.stream())
.distinct()
.collect(Collectors.toList());
};
}
/**
* Returns a new {@link SuggestionsSupplier} containing all the suggestions of this instance,
* but if this list is still empty, returns the suggestions from the provided one.
*/
default SuggestionsSupplier<S> orIfEmpty(SuggestionsSupplier<S> other) {
return (s, ti, to, a) -> {
List<String> l1 = getSuggestions(s, ti, to, a);
return !l1.isEmpty() ? l1 : other.getSuggestions(s, ti, to, a);
};
}
}

View File

@@ -0,0 +1,143 @@
package fr.pandacube.lib.core.config;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import fr.pandacube.lib.chat.ChatColorUtil;
import fr.pandacube.lib.util.Log;
/**
* Class tht loads a specific config file or directory
*
*/
public abstract class AbstractConfig {
/**
* Correspond au dossier ou au fichier de configuration traité par la sous-classe
* courante de {@link AbstractConfig}
*/
protected final File configFile;
/**
* @param configDir the parent directory
* @param fileOrDirName The name of the config file or folder
* @param type if the provided name is a file or a directory
* @throws IOException if we cannot create the file
*/
public AbstractConfig(File configDir, String fileOrDirName, FileType type) throws IOException {
configFile = new File(configDir, fileOrDirName);
if (type == FileType.DIR)
configFile.mkdir();
else
configFile.createNewFile();
}
/**
* Gets the lines from the config file
* @param ignoreEmpty <code>true</code> if we ignore the empty lines
* @param ignoreHashtagComment <code>true</code> if we ignore the comment lines (starting with {@code #})
* @param trimOutput <code>true</code> if we want to trim all lines using {@link String#trim()}
* @param f the file to read
* @return the list of lines, filtered according to the parameters
*/
protected List<String> getFileLines(boolean ignoreEmpty, boolean ignoreHashtagComment, boolean trimOutput, File f) throws IOException {
if (!f.isFile())
return null;
BufferedReader reader = new BufferedReader(new FileReader(f));
List<String> lines = new ArrayList<>();
String line;
while ((line = reader.readLine()) != null) {
String trimmedLine = line.trim();
if (ignoreEmpty && trimmedLine.equals(""))
continue;
if (ignoreHashtagComment && trimmedLine.startsWith("#"))
continue;
if (trimOutput)
lines.add(trimmedLine);
else
lines.add(line);
}
reader.close();
return lines;
}
/**
* Retourne toutes les lignes du fichier de configuration
* @param ignoreEmpty <code>true</code> si on doit ignorer les lignes vides
* @param ignoreHashtagComment <code>true</code> si on doit ignorer les lignes commentés (commençant par un #)
* @param trimOutput <code>true</code> si on doit appeller la méthode String.trim() sur chaque ligne retournée
* @return la liste des lignes utiles
*/
protected List<String> getFileLines(boolean ignoreEmpty, boolean ignoreHashtagComment, boolean trimOutput) throws IOException {
return getFileLines(ignoreEmpty, ignoreHashtagComment, trimOutput, configFile);
}
protected List<File> getFileList() {
if (!configFile.isDirectory())
return null;
return Arrays.asList(configFile.listFiles());
}
/**
* Découpe une chaine de caractère contenant une série de noeuds
* de permissions séparés par des point-virgules et la retourne sous forme d'une liste.
* @param perms la chaine de permissions à traiter
* @return <code>null</code> si le paramètre est nulle ou si <code>perms.equals("*")</code>, ou alors la chaine splittée.
*/
public static List<String> splitPermissionsString(String perms) {
if (perms == null || perms.equals("*"))
return null;
return getSplittedString(perms, ";");
}
public static List<String> getSplittedString(String value, String split) {
return List.of(value.split(split));
}
public static String getTranslatedColorCode(String string) {
return ChatColorUtil.translateAlternateColorCodes('&', string);
}
protected void warning(String message) {
Log.warning("Erreur dans la configuration de '"+configFile.getName()+"' : "+message);
}
protected enum FileType {
FILE, DIR
}
}

View File

@@ -0,0 +1,37 @@
package fr.pandacube.lib.core.config;
import java.io.File;
import java.io.IOException;
public abstract class AbstractConfigManager {
protected final File configDir;
public AbstractConfigManager(File configD) throws IOException {
configDir = configD;
configDir.mkdirs();
init();
}
/**
* Implementation must close all closeable configuration (saving for example)
*/
public abstract void close() throws IOException;
/**
* Implementation must init all config data
*/
public abstract void init() throws IOException;
public synchronized void reloadConfig() throws IOException {
close();
init();
}
}

View File

@@ -0,0 +1,460 @@
package fr.pandacube.lib.core.players;
import java.util.Calendar;
import java.util.OptionalLong;
import java.util.UUID;
import java.util.stream.LongStream;
import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.chat.ChatColorUtil;
import fr.pandacube.lib.db.DBException;
import fr.pandacube.lib.permissions.PermPlayer;
import fr.pandacube.lib.permissions.Permissions;
import fr.pandacube.lib.util.Log;
import static fr.pandacube.lib.chat.ChatStatic.dataText;
import static fr.pandacube.lib.chat.ChatStatic.successText;
import static fr.pandacube.lib.chat.ChatStatic.text;
import static fr.pandacube.lib.chat.ChatStatic.warningText;
public interface IOffPlayer {
/** From how long the last web activity should be before considering the user offline (in ms)? */
long TIMEOUT_WEB_SESSION = 10000; // msec
/*
* General data and state
*/
/**
* Return the ID of the minecraft account.
*
* @return the id of the player
*/
UUID getUniqueId();
/**
* Tells if the current account is an alt account generated by Pandacube.
*
* An alt account uses a specific bit in the UUID to distinguish themselves from the original account.
*/
default boolean isAltAccount() {
return (getUniqueId().getMostSignificantBits() & 0x8000L) == 0x8000L;
}
/**
* Gets the index of the current alt account generated by Pandacube.
*
* The first generated alt account will be numbered 1, the second 2, ...
*
* This method will return undetermined value if {@link #isAltAccount()} is false.
*/
default int getAltIndex() {
return (int) (getUniqueId().getMostSignificantBits() >> 8) & 0xF;
}
/**
* @return the last known player name of this player, or null if this player never joined the network.
*/
default String getName() {
return PlayerFinder.getLastKnownName(getUniqueId());
}
/**
* 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();
/**
* Provides informations of the online status of the player:
* online in game, online on the website, or offline.
* If the player is online in game, it provides the current server they are
* connected.
*/
default PlayerStatusOnServer getPlayerStatus() {
IOnlinePlayer op = getOnlineInstance();
if (op != null && !op.isVanished())
return new PlayerStatusOnServer(PlayerStatusOnServer.PlayerStatus.ONLINE_IG, op.getServerName());
try {
SQLPlayer webSession = getDbPlayer();
if (webSession != null) {
long lastWebActivity = webSession.get(SQLPlayer.lastWebActivity);
if (System.currentTimeMillis() - lastWebActivity < TIMEOUT_WEB_SESSION)
return new PlayerStatusOnServer(PlayerStatusOnServer.PlayerStatus.ONLINE_WEB, null);
}
} catch (Exception e) {
Log.severe(e);
}
return new PlayerStatusOnServer(PlayerStatusOnServer.PlayerStatus.OFFLINE, null);
}
record PlayerStatusOnServer(PlayerStatus status, String server) {
public Chat toComponent() {
if (status == PlayerStatus.ONLINE_IG)
return successText("En ligne, " + server);
if (status == PlayerStatus.ONLINE_WEB)
return warningText("En ligne, web");
if (status == PlayerStatus.OFFLINE)
return dataText("Hors ligne");
return text("N/A");
}
public enum PlayerStatus {
ONLINE_IG, ONLINE_WEB, OFFLINE
}
}
/*
* Floodgate related stuff
*/
default boolean isBedrockAccount() {
int v = getUniqueId().version();
return v == 0 || v == 8; // also 8 if one day we supports alt accounts for floodgate players
}
default boolean isJavaAccount() {
return !isBedrockAccount();
}
/*
* Related class instances
*/
/**
* Return the online instance of this player, if any exists.
* May return itself if the current instance already represent an online player.
*/
IOnlinePlayer getOnlineInstance();
/**
* Get the database entry of this player, or null if the player never joined the network.
*/
default SQLPlayer getDbPlayer() throws DBException {
return SQLPlayer.getPlayerFromUUID(getUniqueId());
}
/**
* Get the permission instance of this player. This will never return null.
* @return the permission instance of this player
*/
default PermPlayer getPermissionUser() {
return Permissions.getPlayer(getUniqueId());
}
/*
* Display name
*/
/**
* 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
*/
String getDisplayName();
/**
* 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 §}.
*/
default String getDisplayNameFromPermissionSystem() {
PermPlayer permU = getPermissionUser();
return ChatColorUtil.translateAlternateColorCodes('&',
permU.getPrefix() + getName() + permU.getSuffix());
}
/*
* Permissions and groups
*/
/**
* Tells if this player has the specified permission.
* If the player is online, this will redirect the
* method call to the {@link IOnlinePlayer} instance,
* that MUST override this current method to avoid recussive
* loop.
* If the player is offline, it just call the Pandacube
* permission system.
* @param permission the permission node to test
* @return whether this player has the provided permission
*/
default boolean hasPermission(String permission) {
IOnlinePlayer online = getOnlineInstance();
if (online != null)
return online.hasPermission(permission);
// at this point, the player is offline
return getPermissionUser().hasPermissionOr(permission, null, null, false);
}
/**
* Tells if this player has the permission resulted from the provided expression.
* If the player is online, this will redirect the
* method call to the {@link IOnlinePlayer} instance,
* that MUST override this current method to avoid recussive
* loop.
* If the player is offline, it just call the Pandacube
* permission system.
* @param permissionExpression the permission node to test
* @return whether this player has the provided permission
*/
default boolean hasPermissionExpression(String permissionExpression) {
IOnlinePlayer online = getOnlineInstance();
if (online != null)
return online.hasPermissionExpression(permissionExpression);
// at this point, the player is offline
return getPermissionUser().hasPermissionExpression(permissionExpression, null, null);
}
/**
* Lists all the values for a set of permission indicating an integer in a range.
* <p>
* A permission range is used to easily attribute a number to a group or player,
* like the maximum number of homes allowed. For instance, if the player has the permission
* {@code essentials.home.12}, this method would return a stream containing the value 12,
* if the parameter {@code permissionPrefix} is {@code "essentials.home."}.
* <p>
* The use of a stream allow the caller to get either the maximum, the minimum, or do any
* other treatment to the values.
* @param permissionPrefix the permission prefix to search for.
* @return a LongStream containing all the values found for the specified permission prefix.
*/
default LongStream getPermissionRangeValues(String permissionPrefix) {
IOnlinePlayer online = getOnlineInstance();
if (online != null)
return online.getPermissionRangeValues(permissionPrefix);
// at this point, the player is offline
return getPermissionUser().getPermissionRangeValues(permissionPrefix, null, null);
}
/**
* Returns the maximum value returned by {@link IOffPlayer#getPermissionRangeValues(String)}.
*/
default OptionalLong getPermissionRangeMax(String permissionPrefix) {
IOnlinePlayer online = getOnlineInstance();
if (online != null)
return online.getPermissionRangeMax(permissionPrefix);
// at this point, the player is offline
return getPermissionUser().getPermissionRangeMax(permissionPrefix, null, null);
}
/**
* 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
*/
default boolean isInGroup(String group) {
return getPermissionUser().isInGroup(group);
}
/**
* Tells if this player is part of the staff, based on permission groups
*/
default boolean isInStaff() {
return getPermissionUser().inheritsFromGroup("staff-base", true);
}
/*
* Ignore
*/
/**
* Tells if this player have the right to ignore the provided player
* @param ignored the player that is potentially ignored by this player.
* If this parameter is null, this method returns false.
*/
default boolean canIgnore(IOffPlayer ignored) {
if (ignored == null)
return false;
if (equals(ignored))
return false;
if (!isInStaff() && !ignored.isInStaff())
return true;
return hasPermission("pandacube.ignore.bypassfor." + ignored.getUniqueId());
}
/**
* Tells if the provided player have the right to ignore this player
* @param ignorer the player that potentially ignore this player
* If this parameter is null, this method returns false.
* @implNote the default implementation just calls {@link #canIgnore(IOffPlayer) ignorer.canIgnore(this)}.
*/
default boolean canBeIgnoredBy(IOffPlayer ignorer) {
if (ignorer == null)
return false;
return ignorer.canIgnore(this);
}
/**
* Determine if this player ignore the provided player.
* @param ignored the player that is potentially ignored by this player.
* If this parameter is null, this method returns false.
* @return true if this player have to right to ignore the provided player and is actually ignoring him.
*/
default boolean isIgnoring(IOffPlayer ignored) {
if (!canIgnore(ignored))
return false;
try {
return SQLPlayerIgnore.isPlayerIgnoringPlayer(getUniqueId(), ignored.getUniqueId());
} catch (DBException e) {
Log.severe("Can't determine if a player ignore another player, because we can't access to the database", e);
return false;
}
}
/**
* Determine if the provided player ignore this player, taking into account the exception permissions.
* @param ignorer the player that potentially ignore this player
* If this parameter is null, this method returns false.
* @return true if the provided player have to right to ignore this player and is actually ignoring him.
* @implNote the default implementation just calls {@link #isIgnoring(IOffPlayer) ignorer.isIgnoring(this)}.
*/
default boolean isIgnoredBy(IOffPlayer ignorer) {
return ignorer.isIgnoring(this);
}
/*
* Modération
*/
/**
* Retrieve the time when the player will be unmuted, or null if the player is not muted.
* @return the timestamp in millisecond of when the player will be unmuted
*/
default Long getMuteTimeout() {
try {
Long muteTimeout = getDbPlayer().get(SQLPlayer.muteTimeout);
if (muteTimeout == null || muteTimeout <= System.currentTimeMillis())
return null;
return muteTimeout;
} catch (DBException e) {
Log.severe(e);
return null;
}
}
/**
* Tells if the player is currently muted, meaning that they cannot communicate
* through the chat or private messages.
*/
default boolean isMuted() {
return getMuteTimeout() != null;
}
/*
* Birthday
*/
default void setBirthday(int day, int month, Integer year) {
try {
SQLPlayer dbPlayer = getDbPlayer();
dbPlayer.setBirthday(day, month, year);
dbPlayer.save();
} catch (DBException e) {
throw new RuntimeException(e);
}
}
default Calendar getBirthday() {
try {
return getDbPlayer().getBirthday();
} catch (DBException e) {
throw new RuntimeException(e);
}
}
/*
* Player config
*/
default String getConfig(String key) throws DBException {
return SQLPlayerConfig.get(getUniqueId(), key);
}
default String getConfig(String key, String deflt) throws DBException {
return SQLPlayerConfig.get(getUniqueId(), key, deflt);
}
default void setConfig(String key, String value) throws DBException {
SQLPlayerConfig.set(getUniqueId(), key, value);
}
default void unsetConfig(String key) throws DBException {
SQLPlayerConfig.unset(getUniqueId(), key);
}
default boolean isWelcomeQuizzDone() {
try {
return Boolean.parseBoolean(getConfig("welcome.quizz.done", "false"));
} catch (DBException e) {
Log.severe("Error knowing if player has already done the quizz. Assuming they did for now.", e);
return true;
}
}
}

View File

@@ -0,0 +1,402 @@
package fr.pandacube.lib.core.players;
import java.util.Locale;
import java.util.OptionalLong;
import java.util.UUID;
import java.util.stream.LongStream;
import org.geysermc.floodgate.api.FloodgateApi;
import org.geysermc.floodgate.api.player.FloodgatePlayer;
import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.db.DBException;
import net.kyori.adventure.identity.Identified;
import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
public interface IOnlinePlayer extends IOffPlayer {
/*
* General data and state
*/
/**
* @return The current name of this player
* @implSpec The implementation is expected to call the environment API
* (Bukkit/Bungee) to get the name of the player.
*/
String getName();
String getServerName();
String getWorldName();
/*
* Floodgate related
*/
default boolean isBedrockClient() {
try {
return FloodgateApi.getInstance().isFloodgatePlayer(getUniqueId());
} catch (NoClassDefFoundError e) {
return false;
}
}
default FloodgatePlayer getBedrockClient() {
return FloodgateApi.getInstance().getPlayer(getUniqueId());
}
default boolean isJavaClient() {
return !isBedrockClient();
}
/*
* Related class instances
*/
/**
* @throws IllegalStateException if the player was not found in the database (should never happen)
* @throws DBException if a database access error occurs
*/
@Override
default SQLPlayer getDbPlayer() throws DBException {
SQLPlayer p = SQLPlayer.getPlayerFromUUID(getUniqueId());
if (p == null)
throw new IllegalStateException("The player was not found in the database: " + getUniqueId());
return p;
}
/*
* Permissions and groups
*/
/**
* Tells if this online player has the specified permission.
* @implSpec the implementation of this method must not directly or
* indirectly call the method {@link IOffPlayer#hasPermission(String)},
* or it may result in a {@link StackOverflowError}.
*/
boolean hasPermission(String permission);
/**
* Tells if this online player has the permission resulted from the provided expression.
* @implSpec the implementation of this method must not directly or
* indirectly call the method {@link IOffPlayer#hasPermissionExpression(String)},
* or it may result in a {@link StackOverflowError}.
*/
boolean hasPermissionExpression(String permission);
/**
* Lists all the values for a set of permission indicating an integer in a range.
* <p>
* A permission range is used to easily attribute a number to a group or player,
* like the maximum number of homes allowed. For instance, if the player has the permission
* {@code essentials.home.12}, this method would return a stream containing the value 12,
* if the parameter {@code permissionPrefix} is {@code "essentials.home."}.
* <p>
* The use of a stream allow the caller to get either the maximum, the minimum, or do any
* other treatment to the values.
* @param permissionPrefix the permission prefix to search for.
* @return a LongStream containing all the values found for the specified permission prefix.
*/
LongStream getPermissionRangeValues(String permissionPrefix);
/**
* Returns the maximum value returned by {@link IOffPlayer#getPermissionRangeValues(String)}.
*/
OptionalLong getPermissionRangeMax(String permissionPrefix);
/*
* Vanish
*/
boolean isVanished();
default boolean isVanishedFor(IOffPlayer other) {
if (!isVanished())
return false; // can see unvanished
if (getUniqueId().equals(other.getUniqueId()))
return false; // can see themself
if (!isInStaff() && other.isInStaff())
return false; // can see non-staff as a staff
if (other.hasPermission("pandacube.vanish.see." + getUniqueId()))
return false; // can see if has a specific permission
return true;
}
/*
* Sending packet and stuff to player
*/
/**
* 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.
* @param message the message to display.
*/
default void sendMessage(ComponentLike message) {
sendMessage(message.asComponent());
}
/**
* Display the provided message in the players chat, if
* the chat is activated
* @param message the message to display
*/
default void sendMessage(Chat message) {
sendMessage(message.getAdv());
}
/**
* 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.
* @implNote implementation of this method should not filter the send of the message, based on
* the sender. This parameter is only there to be transmitted to the client, so client side filtering can
* be processed.
*/
default void sendMessage(Component message, UUID sender) {
sendMessage(message, () -> sender == null ? Identity.nil() : Identity.identity(sender));
}
/**
* 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.
* @implNote implementation of this method should not filter the send of the message, based on
* the sender. This parameter is only there to be transmitted to the client, so client side filtering can
* be processed.
*/
void sendMessage(Component message, Identified sender);
/**
* 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.
*/
default void sendMessage(ComponentLike message, UUID sender) {
sendMessage(message.asComponent(), sender);
}
/**
* 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.
*/
default void sendMessage(Chat message, UUID sender) {
sendMessage(message.getAdv(), sender);
}
/**
* 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(Component message) {
sendMessage(IPlayerManager.prefixedAndColored(message));
}
/**
* 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(Chat message) {
sendPrefixedMessage(message.getAdv());
}
/**
* Display a title in the middle of the screen.
* @param title The big text
* @param subtitle The less big text
* @param fadeIn Fade in time in tick
* @param stay Stay time in tick
* @param fadeOut Fade out time in tick
*/
void sendTitle(Component title, Component subtitle, int fadeIn, int stay, int fadeOut);
/**
* Display a title in the middle of the screen.
* @param title The big text
* @param subtitle The less big text
* @param fadeIn Fade in time in tick
* @param stay Stay time in tick
* @param fadeOut Fade out time in tick
*/
default void sendTitle(Chat title, Chat subtitle, int fadeIn, int stay, int fadeOut) {
sendTitle(title.getAdv(), subtitle.getAdv(), fadeIn, stay, fadeOut);
}
/**
* 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.
* @param brand the server brand to send to the client.
*/
void sendServerBrand(String brand);
/*
* Client options
*/
ClientOptions getClientOptions();
interface ClientOptions {
Locale getLocale();
int getViewDistance();
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.
*/
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.
*/
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.
*/
boolean isChatOnlyDisplayingSystemMessages();
/**
* 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.
*/
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.
*/
boolean isRightHanded();
/**
* Tells if the client has enabled the filtering of texts on sign and book titles.
* Always false as of MC 1.18.
*/
boolean isTextFilteringEnabled();
/**
* Tells if the client allows the server to list their player name in the
* multiplayer menu.
*/
boolean allowsServerListing();
boolean hasSkinCapeEnabled();
boolean hasSkinJacketEnabled();
boolean hasSkinLeftSleeveEnabled();
boolean hasSkinRightSleeveEnabled();
boolean hasSkinLeftPantsEnabled();
boolean hasSkinRightPantsEnabled();
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
*/
default boolean canChat() {
return getClientOptions().isChatFullyVisible();
}
}

View File

@@ -0,0 +1,376 @@
package fr.pandacube.lib.core.players;
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.TimeUnit;
import java.util.stream.Collectors;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.md_5.bungee.api.chat.BaseComponent;
import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.db.DB;
import fr.pandacube.lib.db.DBInitTableException;
import fr.pandacube.lib.util.Log;
public abstract class IPlayerManager<OP extends IOnlinePlayer, OF extends IOffPlayer> {
private static IPlayerManager<?, ?> instance;
public static synchronized IPlayerManager<?, ?> getInstance() {
if (instance == null) {
try {
new StandalonePlayerManager(); // will set the instance value itself (see IPlayerManager constructor)
} catch (DBInitTableException e) {
throw new RuntimeException(e);
}
}
return instance;
}
private static synchronized void setInstance(IPlayerManager<?, ?> newInstance) {
if (instance != null && !(instance instanceof StandalonePlayerManager)) {
throw new IllegalStateException("only one instance of playerManager is possible");
}
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 IPlayerManager() throws DBInitTableException {
setInstance(this);
DB.initTable(SQLPlayer.class);
DB.initTable(SQLPlayerConfig.class);
DB.initTable(SQLPlayerIgnore.class);
DB.initTable(SQLPlayerNameHistory.class);
}
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 List<OP> getAllNotVanished() {
List<OP> players = getAll();
players.removeIf(IOnlinePlayer::isVanished);
return players;
}
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 (Exception e) {
Log.severe("Cannot cache Offline player instance", e);
return newOffPlayerInstance(p);
}
}
public List<OP> getOnlyVisibleFor(OF viewer) {
List<OP> players = getAll();
if (viewer != null)
players.removeIf(op -> op.isVanishedFor(viewer));
return players;
}
public List<OP> getOnlyVisibleFor(OP viewer, boolean sameServerOnly) {
if (sameServerOnly && (viewer == null || viewer.getServerName() == null))
return Collections.emptyList();
@SuppressWarnings("unchecked")
List<OP> players = getOnlyVisibleFor((OF)viewer);
if (sameServerOnly)
players.removeIf(op -> !viewer.getServerName().equals(op.getServerName()));
return players;
}
public List<String> getNamesOnlyVisibleFor(OP viewer, boolean sameServerOnly) {
return getOnlyVisibleFor(viewer, sameServerOnly).stream()
.map(IOnlinePlayer::getName)
.collect(Collectors.toList());
}
public List<String> getNamesOnlyVisibleFor(UUID viewer, boolean sameServerOnly) {
return getNamesOnlyVisibleFor(get(viewer), sameServerOnly);
}
protected abstract OF newOffPlayerInstance(UUID p);
protected abstract void sendMessageToConsole(Component message);
@Deprecated
public static BaseComponent prefixedAndColored(BaseComponent message) {
return prefixedAndColored(Chat.chatComponent(message)).get();
}
public static Component prefixedAndColored(Component message) {
return prefixedAndColored(Chat.chatComponent(message)).getAdv();
}
public static Chat prefixedAndColored(Chat message) {
return Chat.chat()
.broadcastColor()
.then(Chat.getConfig().prefix.get())
.then(message);
}
/*
* 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.
*
* @throws IllegalArgumentException if message is null.
*/
public static void broadcast(ComponentLike message, boolean prefix, boolean console, String permission, UUID sourcePlayer) {
Objects.requireNonNull(message, "message cannot be null");
IOffPlayer oSourcePlayer = getInstance().getOffline(sourcePlayer);
if (prefix)
message = prefixedAndColored(message.asComponent());
for (IOnlinePlayer op : getInstance().getAll()) {
if (permission != null && !(op.hasPermission(permission))) continue;
if (sourcePlayer != null && op.isIgnoring(oSourcePlayer))
continue;
if (sourcePlayer != null) {
if (op.canIgnore(oSourcePlayer)) {
op.sendMessage(message, sourcePlayer); // CHAT message with UUID
}
else {
op.sendMessage(message, new UUID(0, 0)); // CHAT message without UUID
}
}
else
op.sendMessage(message); // SYSTEM message
}
if (console)
getInstance().sendMessageToConsole(message.asComponent());
}
/**
* 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 all players, and eventually to the console.
* <p>
* This method does not restrict the reception of the message to a specific permission. If you
* want to specify a permission, 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.
* @throws IllegalArgumentException if message is null.
*/
public static void broadcast(ComponentLike message, boolean prefix, boolean console, UUID sourcePlayer) {
broadcast(message, prefix, console, null, sourcePlayer);
}
/**
* Broadcast a message to all players, and eventually to the console.
* <p>
* This method does not restrict the reception of the message to a specific permission. If you
* want to specify a permission, use {@link #broadcast(ComponentLike, boolean, boolean, String)}.
* <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, 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 does not restrict the reception of the message to a specific permission. If you
* want to specify a permission, use {@link #broadcast(ComponentLike, boolean, String, UUID)}.
* <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.
* @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 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.
* @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 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 does not restrict the reception of the message to a specific permission. If you
* want to specify a permission, use {@link #broadcast(ComponentLike, boolean, String)}.
* <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, null);
}
}

View File

@@ -0,0 +1,343 @@
package fr.pandacube.lib.core.players;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.ToIntBiFunction;
import java.util.stream.Collectors;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.UncheckedExecutionException;
import fr.pandacube.lib.core.commands.SuggestionsSupplier;
import fr.pandacube.lib.db.DB;
import fr.pandacube.lib.db.DBException;
import fr.pandacube.lib.db.SQLOrderBy;
import fr.pandacube.lib.util.LevenshteinDistance;
import fr.pandacube.lib.util.Log;
/*
* Etape de recherche de joueur :
* utiliser directement la table pandacube_player
* chercher dans l'historique de login
*/
public class PlayerFinder {
private static final Cache<UUID, String> playerLastKnownName = CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(1000)
.build();
record PlayerIdCacheKey(String pName, boolean old) { }
private static final Cache<PlayerIdCacheKey, UUID> playerId = CacheBuilder.newBuilder()
.expireAfterWrite(2, TimeUnit.MINUTES)
.maximumSize(1000)
.build();
public static void clearCacheEntry(UUID pId, String pName) {
playerLastKnownName.invalidate(pId);
playerId.invalidate(new PlayerIdCacheKey(pName.toLowerCase(), true));
playerId.invalidate(new PlayerIdCacheKey(pName.toLowerCase(), false));
}
public static String getLastKnownName(UUID id) {
if (id == null) return null;
try {
return playerLastKnownName.get(id, () -> {
try {
return getDBPlayer(id).get(SQLPlayer.playerName); // eventual NPE will be ignored
} catch (NullPointerException|DBException e) {
Log.severe("Can't search for player name from uuid in database", e);
throw e;
}
});
} catch (ExecutionException e) {
// ignored (ORM Exception)
} catch (UncheckedExecutionException e) {
Log.severe("Cant retrieve player last known name of " + id, e);
}
return null;
}
/**
* Cherche un UUID de compte en se basant sur le pseudo passé en
* paramètre. La méthode
* cherchera d'abord dans les derniers pseudos connus. Puis, cherchera la
* dernière personne à
* s'être connecté avec ce pseudo sur le serveur.
*
* @param exactName le pseudo complet, insensible à la casse, et dans un
* format de pseudo valide
* @param old si on doit chercher dans les anciens pseudos de joueurs
* @return l'UUID du joueur si trouvé, null sinon
*/
public static UUID getPlayerId(String exactName, boolean old) {
if (!isValidPlayerName(exactName))
return null; // évite une recherche inutile dans la base de donnée
try {
return playerId.get(new PlayerIdCacheKey(exactName.toLowerCase(), old), () -> {
try {
SQLPlayer el = DB.getFirst(SQLPlayer.class,
SQLPlayer.playerName.like(exactName.replace("_", "\\_")),
SQLOrderBy.desc(SQLPlayer.lastTimeInGame));
/*
* Si il n'y a pas 1 élément, alors soit le pseudo n'a jamais été attribué
* soit il a été changé, et nous avons l'ancien possesseur et le nouveau possesseur du pseudo.
*/
if (el != null)
return el.get(SQLPlayer.playerId);
} catch (Exception e) {
Log.severe("Can't search for uuid from player name in database", e);
}
if (old) {
try {
SQLPlayerNameHistory el = DB.getFirst(SQLPlayerNameHistory.class,
SQLPlayerNameHistory.playerName.like(exactName.replace("_", "\\_")),
SQLOrderBy.desc(SQLPlayerNameHistory.timeChanged));
if (el != null) return el.get(SQLPlayerNameHistory.playerId);
} catch (Exception e) {
Log.severe("Can't search for uuid from old player name in database", e);
}
}
throw new Exception(); // ignored
});
} catch (ExecutionException e) {
// ignored
}
return null;
}
/**
* Parse a player name or a player ID from the provided string, and returns the UUID of the player, if found.
* @param nameOrId a valid player name, or a UUID in the format of {@link UUID#toString()}
* @return the id of the player, or null if not found or if the input is invalid.
*/
public static UUID parsePlayer(String nameOrId) {
if (nameOrId == null)
return null;
if (isValidPlayerName(nameOrId))
return getPlayerId(nameOrId, true);
try {
return UUID.fromString(nameOrId);
} catch (Exception e) {
return null;
}
}
public static boolean isValidPlayerName(String name) {
if (name == null) return false;
return name.matches("[\\da-zA-Z_.]{2,20}");
}
public static SQLPlayer getDBPlayer(UUID id) throws DBException {
if (id == null) return null;
return SQLPlayer.getPlayerFromUUID(id);
}
private static final SuggestionsSupplier<?> TAB_PLAYER_OFFLINE = (sender, tokenIndex, token, args) -> {
if (token.length() < 3) {
return Collections.emptyList();
}
List<SearchResponseProfile> list = findPlayer(token, 10).profiles;
if (!list.isEmpty() && list.get(0).d == 0)
return Collections.singletonList(list.get(0).name);
return list.stream().map(p -> p.name).collect(Collectors.toList());
};
@SuppressWarnings("unchecked")
public static <S> SuggestionsSupplier<S> TAB_PLAYER_OFFLINE() {
return (SuggestionsSupplier<S>) TAB_PLAYER_OFFLINE;
}
public static SearchResponse findPlayer(String query, int resultsCount) {
SearchResponse cacheData = searchCache.getUnchecked(query.toLowerCase());
cacheData = new SearchResponse(cacheData.profiles.subList(0, Math.min(resultsCount, cacheData.profiles.size())));
return cacheData;
}
public static int SEARCH_MAX_DISTANCE = 20;
public static int MISSING_CHAR_DISTANCE = 1;
public static int SURPLUS_CHAR_DISTANCE = 8;
public static int DIFF_CHAR_DISTANCE = 8;
public static int CLOSE_CHAR_DISTANCE = 4;
public static int OLD_NICK_MULTIPLIER = 2;
private static final List<List<Character>> CONFUSABLE_CHARACTERS = ImmutableList.of(
ImmutableList.of('o', '0'),
ImmutableList.of('i', '1', 'l'),
ImmutableList.of('b', '8')
);
private static final ToIntBiFunction<Character, Character> CHAR_DISTANCE = (c1, c2) -> {
if (c1.equals(c2))
return 0;
for (List<Character> charTab : CONFUSABLE_CHARACTERS) {
if (charTab.contains(c1) && charTab.contains(c2))
return CLOSE_CHAR_DISTANCE;
}
return DIFF_CHAR_DISTANCE;
};
record NamesCacheResult(String name, String lowercaseName, UUID id) { } // Java 16
private static final LoadingCache<String, List<NamesCacheResult>> namesCache = CacheBuilder.newBuilder()
.expireAfterWrite(2, TimeUnit.MINUTES)
.maximumSize(1)
.build(CacheLoader.from((String k) -> {
List<NamesCacheResult> cached = new ArrayList<>();
try {
DB.forEach(SQLPlayerNameHistory.class, el -> {
String name = el.get(SQLPlayerNameHistory.playerName);
cached.add(new NamesCacheResult(name, name.toLowerCase(), el.get(SQLPlayerNameHistory.playerId)));
});
} catch (DBException e) {
throw new RuntimeException(e);
}
return cached;
}));
private static final LoadingCache<String, SearchResponse> searchCache = CacheBuilder.newBuilder()
.expireAfterWrite(2, TimeUnit.MINUTES)
.maximumSize(100)
.build(CacheLoader.from((String query) -> {
List<FoundName> foundNames = new ArrayList<>();
try {
namesCache.get("").forEach(el -> {
int dist = new LevenshteinDistance(el.lowercaseName(), query, SURPLUS_CHAR_DISTANCE, MISSING_CHAR_DISTANCE, CHAR_DISTANCE).getCurrentDistance();
if (dist <= SEARCH_MAX_DISTANCE) {
FoundName n = new FoundName();
n.dist = dist;
n.id = el.id();
n.name = el.name();
foundNames.add(n);
}
});
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
Map<UUID, SearchResponseProfile> profiles = new HashMap<>();
foundNames.forEach(foundName -> {
SearchResponseProfile profile = profiles.getOrDefault(foundName.id, new SearchResponseProfile());
if (profile.id == null) {
profile.id = foundName.id.toString();
profile.names = new ArrayList<>();
profiles.put(foundName.id, profile);
}
profile.names.add(foundName);
});
try {
DB.forEach(SQLPlayer.class, SQLPlayer.playerId.in(profiles.keySet()), el -> {
SearchResponseProfile profile = profiles.get(el.get(SQLPlayer.playerId));
if (profile == null)
return;
profile.displayName = el.get(SQLPlayer.playerDisplayName);
profile.name = el.get(SQLPlayer.playerName);
FoundName currentName = null;
for (FoundName foundName : profile.names) {
if (foundName.name.equals(profile.name)) {
currentName = foundName;
profile.d = foundName.dist;
break;
}
}
if (currentName != null) {
profile.names.remove(currentName);
}
else {
int min = Integer.MAX_VALUE;
for (FoundName foundName : profile.names) {
if (foundName.dist < min) {
min = foundName.dist;
}
}
profile.d = min * OLD_NICK_MULTIPLIER + 1;
if (profile.d > SEARCH_MAX_DISTANCE)
profiles.remove(el.get(SQLPlayer.playerId));
}
// unset id field in old names entries to save memory and network activity
profile.names.forEach(n -> n.id = null);
});
} catch (DBException e) {
throw new RuntimeException(e);
}
List<SearchResponseProfile> searchResponseList = new ArrayList<>(profiles.values());
searchResponseList.sort(null);
searchResponseList.removeIf(p -> {
if (p.name == null) { // if the current name was not found in the database
Log.warning("[PlayerFinder] Name found in history table for id " + p.id + " but the current name was not found in the player table.");
return true;
}
return false;
});
return new SearchResponse(searchResponseList);
}));
public static class SearchResponseProfile implements Comparable<SearchResponseProfile> {
public int d;
public String id;
public String name;
public String displayName;
public List<FoundName> names;
@Override
public int compareTo(SearchResponseProfile o) {
return Integer.compare(d, o.d);
}
}
private static class FoundName {
public UUID id;
public String name;
public int dist;
}
public static class SearchResponse {
public final List<SearchResponseProfile> profiles;
private SearchResponse(List<SearchResponseProfile> p) {
profiles = p;
}
}
}

View File

@@ -0,0 +1,124 @@
package fr.pandacube.lib.core.players;
import java.sql.Date;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Set;
import java.util.UUID;
import fr.pandacube.lib.db.DB;
import fr.pandacube.lib.db.DBException;
import fr.pandacube.lib.db.SQLElement;
import fr.pandacube.lib.db.SQLElementList;
import fr.pandacube.lib.db.SQLField;
import fr.pandacube.lib.util.Log;
public class SQLPlayer extends SQLElement<SQLPlayer> {
/** If the player birth year is internally 1800, it is considered as a non disclosed age.
* All player with an age below 13 should have their age automatically hidden.
*/
public static final int UNDISCLOSED_AGE_YEAR = 1800;
public SQLPlayer() {
super();
}
public SQLPlayer(int id) {
super(id);
}
/*
* Nom de la table
*/
@Override
protected String tableName() {
return "player";
}
/*
* Champs de la table
*/
public static final SQLField<SQLPlayer, UUID> playerId = field(CHAR36_UUID, false);
public static final SQLField<SQLPlayer, String> playerName = field(VARCHAR(16), false);
public static final SQLField<SQLPlayer, String> token = field(CHAR(36), true);
public static final SQLField<SQLPlayer, String> mailCheck = field(VARCHAR(255), true);
public static final SQLField<SQLPlayer, String> password = field(VARCHAR(255), true);
public static final SQLField<SQLPlayer, String> mail = field(VARCHAR(255), true);
public static final SQLField<SQLPlayer, String> playerDisplayName = field(VARCHAR(255),
false);
public static final SQLField<SQLPlayer, Long> firstTimeInGame = field(BIGINT, false, 0L);
public static final SQLField<SQLPlayer, Long> timeWebRegister = field(BIGINT, true);
public static final SQLField<SQLPlayer, Long> lastTimeInGame = field(BIGINT, true);
public static final SQLField<SQLPlayer, Long> lastWebActivity = field(BIGINT, false, 0L);
public static final SQLField<SQLPlayer, String> onlineInServer = field(VARCHAR(32), true);
public static final SQLField<SQLPlayer, String> skinURL = field(VARCHAR(255), true);
public static final SQLField<SQLPlayer, Boolean> isVanish = field(BOOLEAN, false,
(Boolean) false);
public static final SQLField<SQLPlayer, Date> birthday = field(DATE, true);
public static final SQLField<SQLPlayer, Integer> lastYearCelebratedBirthday = field(INT,
false, 0);
public static final SQLField<SQLPlayer, Long> banTimeout = field(BIGINT, true);
public static final SQLField<SQLPlayer, Long> muteTimeout = field(BIGINT, true);
public static final SQLField<SQLPlayer, Boolean> isWhitelisted = field(BOOLEAN, false,
(Boolean) false);
public static final SQLField<SQLPlayer, Long> bambou = field(BIGINT, false, 0L);
public static final SQLField<SQLPlayer, String> grade = field(VARCHAR(36), false, "default");
public Calendar getBirthday() {
try {
Date birthday = get(SQLPlayer.birthday);
if (birthday == null) // le joueur n'a pas de date d'anniversaire
return null;
GregorianCalendar cal = new GregorianCalendar();
cal.setTime(birthday);
return cal;
} catch (Exception e) {
Log.severe(e);
return null;
}
}
public void setBirthday(int day, int month, Integer year) {
if (year == null)
year = UNDISCLOSED_AGE_YEAR;
GregorianCalendar cal = new GregorianCalendar();
cal.set(year, month, day, 0, 0, 0);
set(SQLPlayer.birthday, new java.sql.Date(cal.getTimeInMillis()));
}
public boolean isWebRegistered() {
return get(password) != null;
}
public static SQLPlayer getPlayerFromUUID(UUID pId) throws DBException {
if (pId == null)
return null;
return DB.getFirst(SQLPlayer.class, playerId.eq(pId));
}
public static SQLElementList<SQLPlayer> getPlayersFromUUIDs(Set<UUID> playerIds) throws DBException {
if (playerIds == null || playerIds.isEmpty()) {
return new SQLElementList<>();
}
return DB.getAll(SQLPlayer.class, playerId.in(playerIds));
}
}

View File

@@ -0,0 +1,90 @@
package fr.pandacube.lib.core.players;
import java.util.UUID;
import fr.pandacube.lib.db.DB;
import fr.pandacube.lib.db.DBException;
import fr.pandacube.lib.db.SQLElement;
import fr.pandacube.lib.db.SQLElementList;
import fr.pandacube.lib.db.SQLFKField;
import fr.pandacube.lib.db.SQLField;
public class SQLPlayerConfig extends SQLElement<SQLPlayerConfig> {
public SQLPlayerConfig() {
super();
}
public SQLPlayerConfig(int id) {
super(id);
}
/*
* Nom de la table
*/
@Override
protected String tableName() {
return "player_config";
}
/*
* Champs de la table
*/
public static final SQLFKField<SQLPlayerConfig, UUID, SQLPlayer> playerId = foreignKey(false, SQLPlayer.class, SQLPlayer.playerId);
public static final SQLField<SQLPlayerConfig, String> key = field(VARCHAR(255), false);
public static final SQLField<SQLPlayerConfig, String> value = field(VARCHAR(8192), false);
public static String get(UUID p, String k, String deflt) throws DBException {
SQLPlayerConfig res = DB.getFirst(SQLPlayerConfig.class, playerId.eq(p).and(key.eq(k)));
return res == null ? deflt : res.get(value);
}
public static String get(UUID p, String k) throws DBException {
return get(p, k, null);
}
public static void set(UUID p, String k, String v) throws DBException {
if (v == null) {
unset(p, k);
return;
}
SQLPlayerConfig entry = DB.getFirst(SQLPlayerConfig.class, playerId.eq(p).and(key.eq(k)));
if (entry == null) {
entry = new SQLPlayerConfig();
entry.set(playerId, p);
entry.set(key, k);
}
entry.set(value, v);
entry.save();
}
public static void unset(UUID p, String k) throws DBException {
SQLPlayerConfig entry = DB.getFirst(SQLPlayerConfig.class, playerId.eq(p).and(key.eq(k)));
if (entry != null)
entry.delete();
}
public static SQLElementList<SQLPlayerConfig> getAllFromPlayer(UUID p, String likeQuery) throws DBException {
return DB.getAll(SQLPlayerConfig.class, playerId.eq(p).and(key.like(likeQuery)));
}
public static SQLElementList<SQLPlayerConfig> getAllWithKeys(String likeQuery) throws DBException {
return DB.getAll(SQLPlayerConfig.class, key.like(likeQuery));
}
public static SQLElementList<SQLPlayerConfig> getAllWithKeyValue(String k, String v) throws DBException {
return DB.getAll(SQLPlayerConfig.class, key.eq(k).and(value.eq(v)));
}
}

View File

@@ -0,0 +1,58 @@
package fr.pandacube.lib.core.players;
import java.util.Map;
import java.util.UUID;
import fr.pandacube.lib.db.DB;
import fr.pandacube.lib.db.DBException;
import fr.pandacube.lib.db.SQLElement;
import fr.pandacube.lib.db.SQLFKField;
public class SQLPlayerIgnore extends SQLElement<SQLPlayerIgnore> {
public SQLPlayerIgnore() {
super();
}
public SQLPlayerIgnore(int id) {
super(id);
}
@Override
protected String tableName() {
return "player_ignore";
}
public static final SQLFKField<SQLPlayerIgnore, UUID, SQLPlayer> ignorer = foreignKey(false, SQLPlayer.class, SQLPlayer.playerId);
public static final SQLFKField<SQLPlayerIgnore, UUID, SQLPlayer> ignored = foreignKey(false, SQLPlayer.class, SQLPlayer.playerId);
public static SQLPlayerIgnore getPlayerIgnoringPlayer(UUID ignorer, UUID ignored) throws DBException {
return DB.getFirst(SQLPlayerIgnore.class, SQLPlayerIgnore.ignorer.eq(ignorer).and(SQLPlayerIgnore.ignored.eq(ignored)));
}
public static boolean isPlayerIgnoringPlayer(UUID ignorer, UUID ignored) throws DBException {
return getPlayerIgnoringPlayer(ignorer, ignored) != null;
}
public static void setPlayerIgnorePlayer(UUID ignorer, UUID ignored, boolean newIgnoreState) throws DBException {
SQLPlayerIgnore el = getPlayerIgnoringPlayer(ignorer, ignored);
if (el == null && newIgnoreState) {
el = new SQLPlayerIgnore();
el.set(SQLPlayerIgnore.ignorer, ignorer);
el.set(SQLPlayerIgnore.ignored, ignored);
el.save();
return;
}
if (el != null && !newIgnoreState) {
el.delete();
}
}
public static Map<UUID, SQLPlayer> getIgnoredPlayer(UUID ignorer) throws DBException {
return DB.getAll(SQLPlayerIgnore.class, SQLPlayerIgnore.ignorer.eq(ignorer))
.getReferencedEntriesInGroups(SQLPlayerIgnore.ignored);
}
}

View File

@@ -0,0 +1,53 @@
package fr.pandacube.lib.core.players;
import java.util.UUID;
import fr.pandacube.lib.db.DB;
import fr.pandacube.lib.db.DBException;
import fr.pandacube.lib.db.SQLElement;
import fr.pandacube.lib.db.SQLFKField;
import fr.pandacube.lib.db.SQLField;
import fr.pandacube.lib.util.Log;
public class SQLPlayerNameHistory extends SQLElement<SQLPlayerNameHistory> {
public SQLPlayerNameHistory() {
super();
}
public SQLPlayerNameHistory(int id) {
super(id);
}
@Override
protected String tableName() {
return "player_name_history";
}
public static final SQLFKField<SQLPlayerNameHistory, UUID, SQLPlayer> playerId = foreignKey(false, SQLPlayer.class, SQLPlayer.playerId);
public static final SQLField<SQLPlayerNameHistory, String> playerName = field(VARCHAR(16), false);
public static final SQLField<SQLPlayerNameHistory, Long> timeChanged = field(BIGINT, true);
public static void updateIfNeeded(UUID player, String name, long time) {
SQLPlayerNameHistory histEl;
try {
histEl = DB.getFirst(SQLPlayerNameHistory.class, playerId.eq(player).and(playerName.eq(name)));
if (histEl == null) {
histEl = new SQLPlayerNameHistory();
histEl.set(playerId, player);
histEl.set(playerName, name);
histEl.set(timeChanged, time);
histEl.save();
}
else if (time < histEl.get(timeChanged)) {
histEl.set(timeChanged, time);
histEl.save();
}
} catch (DBException e) {
Log.severe(e);
}
}
}

View File

@@ -0,0 +1,38 @@
package fr.pandacube.lib.core.players;
import java.util.UUID;
/* package */ class StandaloneOffPlayer implements IOffPlayer {
private final UUID uniqueId;
public StandaloneOffPlayer(UUID id) {
if (id == null) throw new IllegalArgumentException("id cannot be null");
uniqueId = id;
}
@Override
public UUID getUniqueId() {
return uniqueId;
}
@Override
public boolean isOnline() {
return false;
}
@Override
public IOnlinePlayer getOnlineInstance() {
return null;
}
private String displayName = null;
@Override
public String getDisplayName() {
if (displayName == null)
displayName = getDisplayNameFromPermissionSystem();
return displayName;
}
}

View File

@@ -0,0 +1,32 @@
package fr.pandacube.lib.core.players;
import java.util.UUID;
import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.db.DBInitTableException;
import fr.pandacube.lib.util.Log;
import net.kyori.adventure.text.Component;
/**
* A standalone player manager, using an implementation of {@link IPlayerManager}
* that does not manage online players. This is used to ease access to players data
* on standalone applications (web api, discord bot, ...)
*
*/
/* package */ class StandalonePlayerManager extends IPlayerManager<IOnlinePlayer, StandaloneOffPlayer> {
public StandalonePlayerManager() throws DBInitTableException {
super();
}
@Override
protected StandaloneOffPlayer newOffPlayerInstance(UUID p) {
return new StandaloneOffPlayer(p);
}
@Override
protected void sendMessageToConsole(Component message) {
Log.info(Chat.chatComponent(message).getLegacyText());
}
}

View File

@@ -0,0 +1,185 @@
package fr.pandacube.lib.core.search;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import fr.pandacube.lib.util.Log;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
/**
* Utility class to manage searching among a set of
* SearchResult instances, using case insensitive
* keywords.
*/
public class SearchEngine<R extends SearchResult> {
private final Map<String, Set<R>> searchKeywordsResultMap = new HashMap<>();
private final Map<R, Set<String>> resultsSearchKeywordsMap = new HashMap<>();
private final Map<String, Set<R>> suggestionsKeywordsResultMap = new HashMap<>();
private final Map<R, Set<String>> resultsSuggestionsKeywordsMap = new HashMap<>();
private final Set<R> resultSet = new HashSet<>();
private final Cache<Set<String>, List<String>> suggestionsCache;
public SearchEngine(int suggestionsCacheSize) {
suggestionsCache = CacheBuilder.newBuilder()
.maximumSize(suggestionsCacheSize)
.build();
}
public synchronized void addResult(R result) {
if (result == null)
throw new IllegalArgumentException("Provided result cannot be null.");
if (resultSet.contains(result))
return;
Set<String> searchKw;
try {
searchKw = result.getSearchKeywords();
Objects.requireNonNull(searchKw, "SearchResult instance must provide a non null set of search keywords");
searchKw = searchKw.stream()
.filter(Objects::nonNull)
.map(String::toLowerCase)
.collect(Collectors.toSet());
} catch (Exception e) {
Log.severe(e);
return;
}
Set<String> suggestsKw;
try {
suggestsKw = result.getSuggestionKeywords();
Objects.requireNonNull(suggestsKw, "SearchResult instance must provide a non null set of suggestions keywords");
suggestsKw = new HashSet<>(suggestsKw);
suggestsKw.removeIf(Objects::isNull);
} catch (Exception e) {
Log.severe(e);
return;
}
resultSet.add(result);
for (String skw : searchKw) {
searchKeywordsResultMap.computeIfAbsent(skw, s -> new HashSet<>()).add(result);
}
resultsSearchKeywordsMap.put(result, searchKw);
resultsSuggestionsKeywordsMap.put(result, suggestsKw);
for (String skw : suggestsKw) {
suggestionsKeywordsResultMap.computeIfAbsent(skw, s -> new HashSet<>()).add(result);
}
suggestionsCache.invalidateAll();
}
public synchronized void removeResult(R result) {
if (result == null || !resultSet.contains(result))
return;
resultSet.remove(result);
Set<String> searchKw = resultsSearchKeywordsMap.remove(result);
if (searchKw != null) {
for (String skw : searchKw) {
Set<R> set = searchKeywordsResultMap.get(skw);
if (set == null)
continue;
set.remove(result);
if (set.isEmpty())
searchKeywordsResultMap.remove(skw);
}
}
Set<String> suggestsKw = resultsSearchKeywordsMap.remove(result);
if (suggestsKw != null) {
for (String skw : suggestsKw) {
Set<R> set = suggestionsKeywordsResultMap.get(skw);
if (set == null)
continue;
set.remove(result);
if (set.isEmpty())
suggestionsKeywordsResultMap.remove(skw);
}
}
resultsSuggestionsKeywordsMap.remove(result);
suggestionsCache.invalidateAll();
}
public synchronized Set<R> search(Set<String> searchTerms) {
if (searchTerms == null)
searchTerms = new HashSet<>();
Set<R> retainedResults = new HashSet<>(resultSet);
for (String term : searchTerms) {
retainedResults.retainAll(search(term));
}
return retainedResults;
}
public synchronized Set<R> search(String searchTerm) {
if (searchTerm == null || searchTerm.isEmpty()) {
return new HashSet<>(resultSet);
}
searchTerm = searchTerm.toLowerCase();
Set<R> retainedResults = new HashSet<>();
for (String skw : searchKeywordsResultMap.keySet()) {
if (skw.contains(searchTerm)) {
retainedResults.addAll(new ArrayList<>(searchKeywordsResultMap.get(skw)));
}
}
return retainedResults;
}
public synchronized List<String> suggestKeywords(List<String> prevSearchTerms) {
if (prevSearchTerms == null || prevSearchTerms.isEmpty()) {
return new ArrayList<>(suggestionsKeywordsResultMap.keySet());
}
Set<String> lowerCaseSearchTerm = prevSearchTerms.stream()
.map(String::toLowerCase)
.collect(Collectors.toSet());
try {
return suggestionsCache.get(lowerCaseSearchTerm, () -> {
Set<R> prevResults = search(lowerCaseSearchTerm);
Set<String> suggestions = new HashSet<>();
for (R prevRes : prevResults) {
suggestions.addAll(new ArrayList<>(resultsSuggestionsKeywordsMap.get(prevRes)));
}
suggestions.removeIf(s -> {
for (String st : lowerCaseSearchTerm)
if (s.contains(st))
return true;
return false;
});
return new ArrayList<>(suggestions);
});
} catch (ExecutionException e) {
Log.severe(e);
return new ArrayList<>(suggestionsKeywordsResultMap.keySet());
}
}
// TODO sort results
}

View File

@@ -0,0 +1,11 @@
package fr.pandacube.lib.core.search;
import java.util.Set;
public interface SearchResult {
Set<String> getSearchKeywords();
Set<String> getSuggestionKeywords();
}

View File

@@ -0,0 +1,104 @@
package fr.pandacube.lib.core.util;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.RecordComponent;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
public class Json {
public static final Gson gson = build(Function.identity());
public static final Gson gsonPrettyPrinting = build(GsonBuilder::setPrettyPrinting);
public static final Gson gsonSerializeNulls = build(GsonBuilder::serializeNulls);
public static final Gson gsonSerializeNullsPrettyPrinting = build(b -> b.serializeNulls().setPrettyPrinting());
private static Gson build(Function<GsonBuilder, GsonBuilder> builderModifier) {
return builderModifier
.apply(new GsonBuilder().registerTypeAdapterFactory(new RecordAdapterFactory()).setLenient()).create();
}
// from https://github.com/google/gson/issues/1794#issuecomment-812964421
private static class RecordAdapterFactory implements TypeAdapterFactory {
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
@SuppressWarnings("unchecked")
Class<T> clazz = (Class<T>) type.getRawType();
if (!clazz.isRecord() || clazz == Record.class) {
return null;
}
return new RecordTypeAdapter<>(gson, this, type);
}
}
private static class RecordTypeAdapter<T> extends TypeAdapter<T> {
private final Gson gson;
private final TypeAdapterFactory factory;
private final TypeToken<T> type;
public RecordTypeAdapter(Gson gson, TypeAdapterFactory factory, TypeToken<T> type) {
this.gson = gson;
this.factory = factory;
this.type = type;
}
@Override
public void write(JsonWriter out, T value) throws IOException {
gson.getDelegateAdapter(factory, type).write(out, value);
}
@Override
public T read(JsonReader reader) throws IOException {
if (reader.peek() == JsonToken.NULL) {
reader.nextNull();
return null;
} else {
@SuppressWarnings("unchecked")
Class<T> clazz = (Class<T>) type.getRawType();
RecordComponent[] recordComponents = clazz.getRecordComponents();
Map<String, TypeToken<?>> typeMap = new HashMap<>();
for (RecordComponent recordComponent : recordComponents) {
typeMap.put(recordComponent.getName(), TypeToken.get(recordComponent.getGenericType()));
}
var argsMap = new HashMap<String, Object>();
reader.beginObject();
while (reader.hasNext()) {
String name = reader.nextName();
argsMap.put(name, gson.getAdapter(typeMap.get(name)).read(reader));
}
reader.endObject();
var argTypes = new Class<?>[recordComponents.length];
var args = new Object[recordComponents.length];
for (int i = 0; i < recordComponents.length; i++) {
argTypes[i] = recordComponents[i].getType();
args[i] = argsMap.get(recordComponents[i].getName());
}
Constructor<T> constructor;
try {
constructor = clazz.getDeclaredConstructor(argTypes);
constructor.setAccessible(true);
return constructor.newInstance(args);
} catch (NoSuchMethodException | InstantiationException | SecurityException | IllegalAccessException
| IllegalArgumentException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
}
}

View File

@@ -0,0 +1,129 @@
package fr.pandacube.lib.core.util;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Objects;
import com.google.gson.JsonSyntaxException;
import fr.pandacube.lib.util.Log;
public class ServerPropertyFile {
private final transient File file;
private String name = "default_name";
private String memory = "512M";
private String javaArgs = "";
private String MinecraftArgs = "";
private String jarFile = "";
private long startupDelay = 0;
private boolean run = true;
public ServerPropertyFile(File f) {
file = Objects.requireNonNull(f, "f");
}
/**
* Charge le fichier de configuration dans cette instance de la classe
*
* @return true si le chargement a réussi, false sinon
*/
public boolean loadFromFile() {
try (BufferedReader in = new BufferedReader(new FileReader(file))) {
ServerPropertyFile dataFile = Json.gsonPrettyPrinting.fromJson(in, getClass());
name = dataFile.name;
memory = dataFile.memory;
javaArgs = dataFile.javaArgs;
MinecraftArgs = dataFile.MinecraftArgs;
jarFile = dataFile.jarFile;
run = dataFile.run;
startupDelay = dataFile.startupDelay;
return true;
} catch(JsonSyntaxException e) {
Log.severe("Error in config file " + file + ": backed up and creating a new one from previous or default values.", e);
return save();
} catch (IOException e) {
Log.severe(e);
return false;
}
}
public boolean save() {
try (BufferedWriter out = new BufferedWriter(new FileWriter(file, false))) {
Json.gsonPrettyPrinting.toJson(this, out);
out.flush();
return true;
} catch (IOException e) {
Log.severe(e);
}
return false;
}
public String getName() {
return name;
}
public String getMemory() {
return memory;
}
public String getJavaArgs() {
return javaArgs;
}
public String getMinecraftArgs() {
return MinecraftArgs;
}
public String getJarFile() {
return jarFile;
}
public boolean isRun() {
return run;
}
public long getStartupDelay() {
return startupDelay;
}
public void setName(String n) {
if (n == null || !n.matches("^[a-zA-Z]$")) throw new IllegalArgumentException();
name = n;
}
public void setMemory(String m) {
if (m == null || !m.matches("^\\d+[mgMG]$")) throw new IllegalArgumentException();
memory = m;
}
public void setJavaArgs(String ja) {
if (ja == null) throw new IllegalArgumentException();
javaArgs = ja;
}
public void setMinecraftArgs(String ma) {
if (ma == null) throw new IllegalArgumentException();
MinecraftArgs = ma;
}
public void setJarFile(String j) {
if (j == null) throw new IllegalArgumentException();
jarFile = j;
}
public void setRun(boolean r) {
run = r;
}
}

View File

@@ -0,0 +1,213 @@
package fr.pandacube.lib.core.util;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.google.gson.JsonElement;
/**
* Utility for conversion of basic Java types and JsonElements types
* @author Marc
*
*/
public class TypeConverter {
public static Integer toInteger(Object o) {
if (o == null) {
return null;
}
if (o instanceof JsonElement) {
try {
return ((JsonElement)o).getAsInt();
} catch(UnsupportedOperationException e) {
throw new ConvertionException(e);
}
}
if (o instanceof Number) {
return ((Number)o).intValue();
}
if (o instanceof String) {
try {
return Integer.parseInt((String)o);
} catch (NumberFormatException e) {
throw new ConvertionException(e);
}
}
if (o instanceof Boolean) {
return ((Boolean)o) ? 1 : 0;
}
throw new ConvertionException("No integer convertion available for an instance of "+o.getClass());
}
public static int toPrimInt(Object o) {
Integer val = toInteger(o);
if (val == null)
throw new ConvertionException("null values can't be converted to primitive int");
return val;
}
public static Double toDouble(Object o) {
if (o == null) {
return null;
}
if (o instanceof JsonElement) {
try {
return ((JsonElement)o).getAsDouble();
} catch(UnsupportedOperationException e) {
throw new ConvertionException(e);
}
}
if (o instanceof Number) {
return ((Number)o).doubleValue();
}
if (o instanceof String) {
try {
return Double.parseDouble((String)o);
} catch (NumberFormatException e) {
throw new ConvertionException(e);
}
}
if (o instanceof Boolean) {
return ((Boolean)o) ? 1d : 0d;
}
throw new ConvertionException("No double convertion available for an instance of "+o.getClass());
}
public static double toPrimDouble(Object o) {
Double val = toDouble(o);
if (val == null)
throw new ConvertionException("null values can't converted to primitive int");
return val;
}
public static String toString(Object o) {
if (o == null) {
return null;
}
if (o instanceof JsonElement) {
try {
return ((JsonElement)o).getAsString();
} catch(UnsupportedOperationException e) {
throw new ConvertionException(e);
}
}
if (o instanceof Number || o instanceof String || o instanceof Boolean || o instanceof Character) {
return o.toString();
}
throw new ConvertionException("No string convertion available for an instance of "+o.getClass());
}
/**
*
* @param o the object to convert to good type
* @param mapIntKeys if the String key representing an int should be duplicated as integer type,
* which map to the same value as the original String key. For example, if a key is "12" and map
* to the object <i>o</i>, an integer key 12 will be added and map to the same object <i>o</i>
*/
@SuppressWarnings("unchecked")
public static Map<Object, Object> toMap(Object o, boolean mapIntKeys) {
if (o == null) {
return null;
}
if (o instanceof JsonElement) {
o = Json.gson.fromJson((JsonElement)o, Object.class);
}
if (o instanceof Map) {
Map<Object, Object> currMap = (Map<Object, Object>) o;
if (mapIntKeys) {
Map<Integer, Object> newEntries = new HashMap<>();
for (Map.Entry<Object, Object> entry : currMap.entrySet()) {
if (entry.getKey() instanceof String) {
try {
int intKey = Integer.parseInt((String)entry.getKey());
newEntries.put(intKey, entry.getValue());
} catch (NumberFormatException ignored) { }
}
}
if (!newEntries.isEmpty()) {
currMap = new HashMap<>(currMap);
currMap.putAll(newEntries);
}
}
return currMap;
}
if (o instanceof List<?> list) {
Map<Object, Object> map = new HashMap<>();
for(int i = 0; i < list.size(); i++) {
map.put(Integer.toString(i), list.get(i));
map.put(i, list.get(i));
}
return map;
}
throw new ConvertionException("No Map convertion available for an instance of "+o.getClass());
}
@SuppressWarnings("unchecked")
public static List<Object> toList(Object o) {
if (o == null) {
return null;
}
if (o instanceof JsonElement) {
o = Json.gson.fromJson((JsonElement)o, Object.class);
}
if (o instanceof List) {
return (List<Object>) o;
}
if (o instanceof Map) {
return new ArrayList<>(((Map<?, ?>)o).values());
}
throw new ConvertionException("No Map convertion available for an instance of "+o.getClass());
}
public static class ConvertionException extends RuntimeException {
public ConvertionException(String m) {
super(m);
}
public ConvertionException(Throwable t) {
super(t);
}
public ConvertionException(String m, Throwable t) {
super(m, t);
}
}
}