PandaLib/Core/src/main/java/fr/pandacube/lib/core/players/PlayerFinder.java

344 lines
11 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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