renamed modules dir
This commit is contained in:
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -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
|
||||
}
|
||||
|
||||
}
|
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -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 permission’s prefix(es) and suffix(es) of the player,
|
||||
* and with color codes translated to Minecraft’s 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -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 player’s chat, if
|
||||
* the chat is activated.
|
||||
* @param message the message to display.
|
||||
*/
|
||||
void sendMessage(Component message);
|
||||
|
||||
/**
|
||||
* Display the provided message in the player’s 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 player’s 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 player’s 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 don’t 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 player’s 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 don’t 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 player’s 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 don’t 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 player’s 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 don’t 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 player’s 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 player’s 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 can’t 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();
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
@@ -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("Can’t 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
@@ -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));
|
||||
|
||||
}
|
||||
}
|
@@ -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)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -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());
|
||||
}
|
||||
|
||||
}
|
@@ -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
|
||||
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
package fr.pandacube.lib.core.search;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public interface SearchResult {
|
||||
|
||||
Set<String> getSearchKeywords();
|
||||
|
||||
Set<String> getSuggestionKeywords();
|
||||
|
||||
}
|
104
pandalib-core/src/main/java/fr/pandacube/lib/core/util/Json.java
Normal file
104
pandalib-core/src/main/java/fr/pandacube/lib/core/util/Json.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user