Refactor PlayerManager API + little fixes in Chat API

This commit is contained in:
Marc Baloup 2022-07-21 02:19:28 +02:00
parent d4471f2845
commit f4d436671c
23 changed files with 781 additions and 1824 deletions

View File

@ -20,12 +20,6 @@
</repositories> </repositories>
<dependencies> <dependencies>
<dependency>
<groupId>fr.pandacube.lib</groupId>
<artifactId>pandalib-util</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency> <dependency>

View File

@ -38,8 +38,7 @@ public abstract sealed class Chat extends ChatStatic implements HoverEventSource
protected boolean console = false; protected boolean console = false;
/* package */ Chat(ComponentBuilder<?, ?> b) { /* package */ Chat(ComponentBuilder<?, ?> b) {
Objects.requireNonNull(b, "Provided component builder must not be null"); builder = Objects.requireNonNull(b, "Provided component builder must not be null");
builder = b;
} }

View File

@ -3,18 +3,18 @@ package fr.pandacube.lib.chat;
import java.util.Objects; import java.util.Objects;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.BaseComponent;
import fr.pandacube.lib.chat.Chat.FormatableChat; import fr.pandacube.lib.chat.Chat.FormatableChat;
import fr.pandacube.lib.util.Log;
public abstract class ChatStatic { public abstract class ChatStatic {
public static FormatableChat chatComponent(Component c) { private static FormatableChat chatComponent(Component c) {
return new FormatableChat(Chat.componentToBuilder(c)); return new FormatableChat(Chat.componentToBuilder(c));
} }
@ -22,8 +22,8 @@ public abstract class ChatStatic {
return new FormatableChat(Chat.componentToBuilder(Chat.toAdventure(c))); return new FormatableChat(Chat.componentToBuilder(Chat.toAdventure(c)));
} }
public static FormatableChat chatComponent(Chat c) { public static FormatableChat chatComponent(ComponentLike c) {
return chatComponent(c.getAdv()); return chatComponent(c.asComponent());
} }
public static FormatableChat chat() { public static FormatableChat chat() {
@ -34,26 +34,38 @@ public abstract class ChatStatic {
return chatComponent(Chat.toAdventure(c)); return chatComponent(Chat.toAdventure(c));
} }
/**
* Create a Chat instance with the provided plain text as its main text content.
*
* @param plainText the text to use as he content of the new Chat instance.
* @return a Chat instance with the provided text as its main text content.
*
* @throws IllegalArgumentException If the {@code plainText} parameter is instance of {@link Chat} or
* {@link Component}. The caller should use {@link #chatComponent(ComponentLike)}
* instead.
*/
public static FormatableChat text(Object plainText) { public static FormatableChat text(Object plainText) {
if (plainText instanceof Chat) { if (plainText instanceof ComponentLike) {
Log.warning("Using Chat instance as plain text. Please use proper API method. Ill properly use your Chat instance this time...", new Throwable()); throw new IllegalArgumentException("Expected any object except instance of " + ComponentLike.class + ". Received " + plainText + ". Please use ChatStatic.chatComponent(ComponentLike) instead.");
return (FormatableChat) plainText;
}
if (plainText instanceof Component) {
Log.warning("Using Component instance as plain text. Please use proper API method. Ill properly use your Component this time...", new Throwable());
return chatComponent((Component) plainText);
} }
return new FormatableChat(Component.text().content(Objects.toString(plainText))); return new FormatableChat(Component.text().content(Objects.toString(plainText)));
} }
/**
* Create a Chat instance with the provided legacy text as its main text content.
*
* @param legacyText the text to use as he content of the new Chat instance.
* @return a Chat instance with the provided text as its main text content.
*
* @throws IllegalArgumentException If the {@code plainText} parameter is instance of {@link Chat} or
* {@link Component}. The caller should use {@link #chatComponent(ComponentLike)}
* instead.
*/
public static FormatableChat legacyText(Object legacyText) { public static FormatableChat legacyText(Object legacyText) {
if (legacyText instanceof Chat) { if (legacyText instanceof ComponentLike) {
Log.warning("Using Chat instance as legacy text. Please use proper API method. Ill properly use your Chat instance this time...", new Throwable()); throw new IllegalArgumentException("Expected any object except instance of " + ComponentLike.class + ". Received " + legacyText + ". Please use ChatStatic.chatComponent(ComponentLike) instead.");
return (FormatableChat) legacyText;
}
if (legacyText instanceof Component) {
Log.warning("Using Component instance as legacy text. Please use proper API method. Ill properly use your Component this time...", new Throwable());
return chatComponent((Component) legacyText);
} }
return chatComponent(LegacyComponentSerializer.legacySection().deserialize(Objects.toString(legacyText))); return chatComponent(LegacyComponentSerializer.legacySection().deserialize(Objects.toString(legacyText)));
} }
@ -116,4 +128,19 @@ public abstract class ChatStatic {
public static Component prefixedAndColored(ComponentLike message) {
return prefixedAndColored(Chat.chatComponent(message)).getAdv();
}
public static Chat prefixedAndColored(Chat message) {
return Chat.chat()
.broadcastColor()
.then(Chat.getConfig().prefix.get())
.then(message);
}
} }

View File

@ -56,19 +56,11 @@
<dependency> <dependency>
<groupId>fr.pandacube.lib</groupId> <groupId>fr.pandacube.lib</groupId>
<artifactId>pandalib-network-api</artifactId> <artifactId>pandalib-players-standalone</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.fathzer</groupId>
<artifactId>javaluator</artifactId>
<version>3.0.3</version>
</dependency>
<dependency> <dependency>
<groupId>org.geysermc.floodgate</groupId> <groupId>org.geysermc.floodgate</groupId>
<artifactId>api</artifactId> <artifactId>api</artifactId>

View File

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

View File

@ -1,376 +0,0 @@
package fr.pandacube.lib.core.players;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.md_5.bungee.api.chat.BaseComponent;
import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.db.DB;
import fr.pandacube.lib.db.DBInitTableException;
import fr.pandacube.lib.util.Log;
public abstract class IPlayerManager<OP extends IOnlinePlayer, OF extends IOffPlayer> {
private static IPlayerManager<?, ?> instance;
public static synchronized IPlayerManager<?, ?> getInstance() {
if (instance == null) {
try {
new StandalonePlayerManager(); // will set the instance value itself (see IPlayerManager constructor)
} catch (DBInitTableException e) {
throw new RuntimeException(e);
}
}
return instance;
}
private static synchronized void setInstance(IPlayerManager<?, ?> newInstance) {
if (instance != null && !(instance instanceof StandalonePlayerManager)) {
throw new IllegalStateException("only one instance of playerManager is possible");
}
instance = newInstance;
}
private final Map<UUID, OP> onlinePlayers = Collections.synchronizedMap(new HashMap<>());
private final LoadingCache<UUID, OF> offlinePlayers = CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(CacheLoader.from(this::newOffPlayerInstance));
public IPlayerManager() throws DBInitTableException {
setInstance(this);
DB.initTable(SQLPlayer.class);
DB.initTable(SQLPlayerConfig.class);
DB.initTable(SQLPlayerIgnore.class);
DB.initTable(SQLPlayerNameHistory.class);
}
protected void addPlayer(OP p) {
onlinePlayers.put(p.getUniqueId(), p);
offlinePlayers.invalidate(p.getUniqueId());
}
protected OP removePlayer(UUID p) {
return onlinePlayers.remove(p);
}
public OP get(UUID p) {
return onlinePlayers.get(p);
}
public boolean isOnline(UUID p) {
return onlinePlayers.containsKey(p);
}
public int getPlayerCount() {
return onlinePlayers.size();
}
public List<OP> getAll() {
return new ArrayList<>(onlinePlayers.values());
}
public List<OP> getAllNotVanished() {
List<OP> players = getAll();
players.removeIf(IOnlinePlayer::isVanished);
return players;
}
public OF getOffline(UUID p) {
if (p == null)
return null;
OP online = get(p);
if (online != null) {
offlinePlayers.invalidate(p);
@SuppressWarnings("unchecked")
OF ret = (OF) online;
return ret;
}
// if not online
try {
return offlinePlayers.get(p); // load and cache new instance if necessary
} catch (Exception e) {
Log.severe("Cannot cache Offline player instance", e);
return newOffPlayerInstance(p);
}
}
public List<OP> getOnlyVisibleFor(OF viewer) {
List<OP> players = getAll();
if (viewer != null)
players.removeIf(op -> op.isVanishedFor(viewer));
return players;
}
public List<OP> getOnlyVisibleFor(OP viewer, boolean sameServerOnly) {
if (sameServerOnly && (viewer == null || viewer.getServerName() == null))
return Collections.emptyList();
@SuppressWarnings("unchecked")
List<OP> players = getOnlyVisibleFor((OF)viewer);
if (sameServerOnly)
players.removeIf(op -> !viewer.getServerName().equals(op.getServerName()));
return players;
}
public List<String> getNamesOnlyVisibleFor(OP viewer, boolean sameServerOnly) {
return getOnlyVisibleFor(viewer, sameServerOnly).stream()
.map(IOnlinePlayer::getName)
.collect(Collectors.toList());
}
public List<String> getNamesOnlyVisibleFor(UUID viewer, boolean sameServerOnly) {
return getNamesOnlyVisibleFor(get(viewer), sameServerOnly);
}
protected abstract OF newOffPlayerInstance(UUID p);
protected abstract void sendMessageToConsole(Component message);
@Deprecated
public static BaseComponent prefixedAndColored(BaseComponent message) {
return prefixedAndColored(Chat.chatComponent(message)).get();
}
public static Component prefixedAndColored(Component message) {
return prefixedAndColored(Chat.chatComponent(message)).getAdv();
}
public static Chat prefixedAndColored(Chat message) {
return Chat.chat()
.broadcastColor()
.then(Chat.getConfig().prefix.get())
.then(message);
}
/*
* Message broadcasting
*/
// ComponentLike message
// boolean prefix
// boolean console = (permission == null)
// String permission = null
// UUID sourcePlayer = null
/**
* Broadcast a message to some or all players, and eventually to the console.
*
* @param message the message to send.
* @param prefix if the server prefix will be prepended to the message.
* @param console if the message must be displayed in the console.
* @param permission if not null, the message is only sent to player with this permission.
* @param sourcePlayer specifiy the eventual player that is the source of the message.
* If null, the message will be sent as a SYSTEM chat message.
* If not null, the message will be sent as a CHAT message, and will not be sent
* to players ignoring the provided player.
*
* @throws IllegalArgumentException if message is null.
*/
public static void broadcast(ComponentLike message, boolean prefix, boolean console, String permission, UUID sourcePlayer) {
Objects.requireNonNull(message, "message cannot be null");
IOffPlayer oSourcePlayer = getInstance().getOffline(sourcePlayer);
if (prefix)
message = prefixedAndColored(message.asComponent());
for (IOnlinePlayer op : getInstance().getAll()) {
if (permission != null && !(op.hasPermission(permission))) continue;
if (sourcePlayer != null && op.isIgnoring(oSourcePlayer))
continue;
if (sourcePlayer != null) {
if (op.canIgnore(oSourcePlayer)) {
op.sendMessage(message, sourcePlayer); // CHAT message with UUID
}
else {
op.sendMessage(message, new UUID(0, 0)); // CHAT message without UUID
}
}
else
op.sendMessage(message); // SYSTEM message
}
if (console)
getInstance().sendMessageToConsole(message.asComponent());
}
/**
* Broadcast a message to some or all players, and eventually to the console.
* <p>
* This method assumes this message is not caused by a specific player. To specify the source player, use
* {@link #broadcast(ComponentLike, boolean, boolean, String, UUID)}.
*
* @param message the message to send.
* @param prefix if the server prefix will be prepended to the message.
* @param console if the message must be displayed in the console.
* @param permission if not null, the message is only sent to player with this permission.
* @throws IllegalArgumentException if message is null.
*/
public static void broadcast(ComponentLike message, boolean prefix, boolean console, String permission) {
broadcast(message, prefix, console, permission, null);
}
/**
* Broadcast a message to all players, and eventually to the console.
* <p>
* This method does not restrict the reception of the message to a specific permission. If you
* want to specify a permission, use {@link #broadcast(ComponentLike, boolean, boolean, String, UUID)}.
*
* @param message the message to send.
* @param prefix if the server prefix will be prepended to the message.
* @param console if the message must be displayed in the console.
* @param sourcePlayer specifiy the eventual player that is the source of the message.
* If null, the message will be sent as a SYSTEM chat message.
* If not null, the message will be sent as a CHAT message, and will not be sent
* to players ignoring the provided player.
* @throws IllegalArgumentException if message is null.
*/
public static void broadcast(ComponentLike message, boolean prefix, boolean console, UUID sourcePlayer) {
broadcast(message, prefix, console, null, sourcePlayer);
}
/**
* Broadcast a message to all players, and eventually to the console.
* <p>
* This method does not restrict the reception of the message to a specific permission. If you
* want to specify a permission, use {@link #broadcast(ComponentLike, boolean, boolean, String)}.
* <p>
* This method assumes this message is not caused by a specific player. To specify the source player, use
* {@link #broadcast(ComponentLike, boolean, boolean, UUID)}.
*
* @param message the message to send.
* @param prefix if the server prefix will be prepended to the message.
* @param console if the message must be displayed in the console.
* @throws IllegalArgumentException if message is null.
*/
public static void broadcast(ComponentLike message, boolean prefix, boolean console) {
broadcast(message, prefix, console, null, null);
}
/**
* Broadcast a message to some or all players, and eventually to the console.
* <p>
* This method assumes this message is not caused by a specific player. To specify the source player, use
* {@link #broadcast(ComponentLike, boolean, String, UUID)}.
* <p>
* This method decides to send the message to the console depending on whether {@code permission}
* is null (will send to console) or not (will not send to console). To specify this behaviour, use
* {@link #broadcast(ComponentLike, boolean, boolean, String)}.
*
* @param message the message to send.
* @param prefix if the server prefix will be prepended to the message.
* @param permission if not null, the message is only sent to player with this permission (but not to console).
* If null, the message will be sent to all players and to console.
* @throws IllegalArgumentException if message is null.
*/
public static void broadcast(ComponentLike message, boolean prefix, String permission) {
broadcast(message, prefix, (permission == null), permission, null);
}
/**
* Broadcast a message to all players, and to the console.
* <p>
* This method does not restrict the reception of the message to a specific permission. If you
* want to specify a permission, use {@link #broadcast(ComponentLike, boolean, String, UUID)}.
* <p>
* This method sends the message to the console. To change this behaviour, use
* {@link #broadcast(ComponentLike, boolean, boolean, UUID)}.
*
* @param message the message to send.
* @param prefix if the server prefix will be prepended to the message.
* @param sourcePlayer specifiy the eventual player that is the source of the message.
* If null, the message will be sent as a SYSTEM chat message.
* If not null, the message will be sent as a CHAT message, and will not be sent
* to players ignoring the provided player.
* @throws IllegalArgumentException if message is null.
*/
public static void broadcast(ComponentLike message, boolean prefix, UUID sourcePlayer) {
broadcast(message, prefix, true, null, sourcePlayer);
}
/**
* Broadcast a message to all players, and to the console.
* <p>
* This method sends the message to the console. To change this behaviour, use
* {@link #broadcast(ComponentLike, boolean, boolean, String, UUID)}.
*
* @param message the message to send.
* @param prefix if the server prefix will be prepended to the message.
* @param permission if not null, the message is only sent to player with this permission (but not to console).
* If null, the message will be sent to all players and to console.
* @param sourcePlayer specifiy the eventual player that is the source of the message.
* If null, the message will be sent as a SYSTEM chat message.
* If not null, the message will be sent as a CHAT message, and will not be sent
* to players ignoring the provided player.
* @throws IllegalArgumentException if message is null.
*/
public static void broadcast(ComponentLike message, boolean prefix, String permission, UUID sourcePlayer) {
broadcast(message, prefix, true, permission, sourcePlayer);
}
/**
* Broadcast a message to all players, and to the console.
* <p>
* This method assumes this message is not caused by a specific player. To specify the source player, use
* {@link #broadcast(ComponentLike, boolean, UUID)}.
* <p>
* This method does not restrict the reception of the message to a specific permission. If you
* want to specify a permission, use {@link #broadcast(ComponentLike, boolean, String)}.
* <p>
* This method sends the message to the console. To change this behaviour, use
* {@link #broadcast(ComponentLike, boolean, boolean)}.
*
* @param message the message to send.
* @param prefix if the server prefix will be prepended to the message.
* @throws IllegalArgumentException if message is null.
*/
public static void broadcast(ComponentLike message, boolean prefix) {
broadcast(message, prefix, true, null, null);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.function.Consumer; import java.util.function.Consumer;
import com.google.common.collect.ImmutableMap;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.enchantments.Enchantment; import org.bukkit.enchantments.Enchantment;
@ -19,11 +20,7 @@ import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin; import org.bukkit.plugin.Plugin;
import com.google.common.collect.ImmutableMap;
import fr.pandacube.lib.chat.Chat; import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.core.players.IPlayerManager;
import fr.pandacube.lib.util.Log;
import fr.pandacube.lib.paper.util.ItemStackBuilder; import fr.pandacube.lib.paper.util.ItemStackBuilder;
public class GUIInventory implements Listener { public class GUIInventory implements Listener {
@ -43,10 +40,6 @@ public class GUIInventory implements Listener {
else else
inv = Bukkit.createInventory(null, nbLines * 9, title.getAdv()); inv = Bukkit.createInventory(null, nbLines * 9, title.getAdv());
if (IPlayerManager.getInstance().get(p.getUniqueId()).isBedrockClient()) {
Log.warning("Opening GUI inventory for player on Bedrock client " + p.getName() + " (" + p.getUniqueId() + "). Please use a Form instead.", new Throwable());
}
setCloseEvent(closeEventAction); setCloseEvent(closeEventAction);
onClickEvents = new HashMap<>(); onClickEvents = new HashMap<>();

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>pandalib-parent</artifactId>
<groupId>fr.pandacube.lib</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>pandalib-players-permissible</artifactId>
<dependencies>
<dependency>
<groupId>fr.pandacube.lib</groupId>
<artifactId>pandalib-permissions</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>fr.pandacube.lib</groupId>
<artifactId>pandalib-players-standalone</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,153 @@
package fr.pandacube.lib.players.permissible;
import java.util.OptionalLong;
import java.util.stream.LongStream;
import fr.pandacube.lib.chat.ChatColorUtil;
import fr.pandacube.lib.permissions.PermPlayer;
import fr.pandacube.lib.permissions.Permissions;
import fr.pandacube.lib.players.standalone.StandaloneOffPlayer;
public interface PermissibleOffPlayer extends StandaloneOffPlayer {
/*
* 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.
*/
PermissibleOnlinePlayer getOnlineInstance();
/**
* 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
*/
/**
* Get an updated display name of the user,
* generated using eventual permissions prefix(es) and suffix(es) of the player,
* and with color codes translated to Minecrafts native {@code §}.
*/
default String getDisplayNameFromPermissionSystem() {
PermPlayer permU = getPermissionUser();
return ChatColorUtil.translateAlternateColorCodes('&',
permU.getPrefix() + getName() + permU.getSuffix());
}
/*
* Permissions and groups
*/
/**
* Tells if this player has the specified permission.
* If the player is online, this will redirect the
* method call to the {@link PermissibleOnlinePlayer} 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) {
PermissibleOnlinePlayer 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 PermissibleOnlinePlayer} 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) {
PermissibleOnlinePlayer 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) {
PermissibleOnlinePlayer 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 PermissibleOffPlayer#getPermissionRangeValues(String)}.
*/
default OptionalLong getPermissionRangeMax(String permissionPrefix) {
PermissibleOnlinePlayer 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);
}
}

View File

@ -0,0 +1,72 @@
package fr.pandacube.lib.players.permissible;
import java.util.OptionalLong;
import java.util.stream.LongStream;
import fr.pandacube.lib.players.standalone.StandaloneOnlinePlayer;
public interface PermissibleOnlinePlayer extends PermissibleOffPlayer, StandaloneOnlinePlayer {
/*
* 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();
/*
* 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 PermissibleOffPlayer#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 PermissibleOffPlayer#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 PermissibleOffPlayer#getPermissionRangeValues(String)}.
*/
OptionalLong getPermissionRangeMax(String permissionPrefix);
}

View File

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

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>pandalib-parent</artifactId>
<groupId>fr.pandacube.lib</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>pandalib-players-standalone</artifactId>
<dependencies>
<dependency>
<groupId>fr.pandacube.lib</groupId>
<artifactId>pandalib-chat</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>fr.pandacube.lib</groupId>
<artifactId>pandalib-util</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,70 @@
package fr.pandacube.lib.players.standalone;
import java.util.UUID;
public interface StandaloneOffPlayer {
/*
* General data and state
*/
/**
* Return the ID of the minecraft account.
*
* @return the id of the player
*/
UUID getUniqueId();
/**
* @return the last known player name of this player, or null if this player never joined the network.
*/
String getName();
/**
* Indicate if this player is connected to the current node (server or proxy, depending on interface implementation)
* @return wether the player is online or not
*/
boolean isOnline();
/*
* 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.
*/
StandaloneOnlinePlayer getOnlineInstance();
/*
* 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();
}

View File

@ -1,21 +1,17 @@
package fr.pandacube.lib.core.players; package fr.pandacube.lib.players.standalone;
import java.util.Locale; import java.util.Locale;
import java.util.OptionalLong;
import java.util.UUID; 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.Identified;
import net.kyori.adventure.identity.Identity; import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike; import net.kyori.adventure.text.ComponentLike;
public interface IOnlinePlayer extends IOffPlayer { import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.chat.ChatStatic;
public interface StandaloneOnlinePlayer extends StandaloneOffPlayer {
@ -26,10 +22,9 @@ public interface IOnlinePlayer extends IOffPlayer {
/** /**
* @return The current name of this player * @return The current name of this player
* @implSpec The implementation is expected to call the environment API * @apiNote The implementation is expected to call the environment API
* (Bukkit/Bungee) to get the name of the player. * (Bukkit/Bungee) to get the name of the player.
*/ */
String getName();
String getServerName(); String getServerName();
@ -38,122 +33,10 @@ public interface IOnlinePlayer extends IOffPlayer {
/*
* 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;
}
@ -179,29 +62,6 @@ public interface IOnlinePlayer extends IOffPlayer {
sendMessage(message.asComponent()); sendMessage(message.asComponent());
} }
/**
* Display the provided message in the players chat, if
* the chat is activated
* @param message the message to display
*/
default void sendMessage(Chat message) {
sendMessage(message.getAdv());
}
/**
* Display the provided message in the players chat, if
* they allows to display CHAT messages
* @param message the message to display.
* @param sender the player causing the send of this message. Client side filtering may occur.
* May be null if we dont want client filtering, but still consider the message as CHAT message.
* @implNote implementation of this method should not filter the send of the message, based on
* the sender. This parameter is only there to be transmitted to the client, so client side filtering can
* be processed.
*/
default void sendMessage(Component message, UUID sender) {
sendMessage(message, () -> sender == null ? Identity.nil() : Identity.identity(sender));
}
/** /**
* Display the provided message in the players chat, if * Display the provided message in the players chat, if
* they allows to display CHAT messages * they allows to display CHAT messages
@ -220,20 +80,12 @@ public interface IOnlinePlayer extends IOffPlayer {
* @param message the message to display * @param message the message to display
* @param sender the player causing the send of this message. Client side filtering may occur. * @param sender the player causing the send of this message. Client side filtering may occur.
* May be null if we dont want client filtering, but still consider the message as CHAT message. * May be null if we dont want client filtering, but still consider the message as CHAT message.
* @implNote implementation of this method should not filter the send of the message, based on
* the sender. This parameter is only there to be transmitted to the client, so client side filtering can
* be processed.
*/ */
default void sendMessage(ComponentLike message, UUID sender) { default void sendMessage(ComponentLike message, UUID sender) {
sendMessage(message.asComponent(), sender); sendMessage(message.asComponent(), () -> sender == null ? Identity.nil() : Identity.identity(sender));
}
/**
* Display the provided message in the players chat, if
* they allows to display CHAT messages
* @param message the message to display
* @param sender the player causing the send of this message. Client side filtering may occur.
* May be null if we dont want client filtering, but still consider the message as CHAT message.
*/
default void sendMessage(Chat message, UUID sender) {
sendMessage(message.getAdv(), sender);
} }
/** /**
@ -241,17 +93,8 @@ public interface IOnlinePlayer extends IOffPlayer {
* activated, prepended with the server prefix. * activated, prepended with the server prefix.
* @param message the message to display * @param message the message to display
*/ */
default void sendPrefixedMessage(Component message) { default void sendPrefixedMessage(ComponentLike message) {
sendMessage(IPlayerManager.prefixedAndColored(message)); sendMessage(ChatStatic.prefixedAndColored(message));
}
/**
* Display the provided message in the players chat, if the chat is
* activated, prepended with the server prefix.
* @param message the message to display
*/
default void sendPrefixedMessage(Chat message) {
sendPrefixedMessage(message.getAdv());
} }
/** /**
@ -272,8 +115,8 @@ public interface IOnlinePlayer extends IOffPlayer {
* @param stay Stay time in tick * @param stay Stay time in tick
* @param fadeOut Fade out time in tick * @param fadeOut Fade out time in tick
*/ */
default void sendTitle(Chat title, Chat subtitle, int fadeIn, int stay, int fadeOut) { default void sendTitle(ComponentLike title, ComponentLike subtitle, int fadeIn, int stay, int fadeOut) {
sendTitle(title.getAdv(), subtitle.getAdv(), fadeIn, stay, fadeOut); sendTitle(title.asComponent(), subtitle.asComponent(), fadeIn, stay, fadeOut);
} }
/** /**

View File

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

36
pom.xml
View File

@ -53,10 +53,12 @@
<module>pandalib-net</module> <module>pandalib-net</module>
<module>pandalib-netapi</module> <module>pandalib-netapi</module>
<module>pandalib-paper</module> <module>pandalib-paper</module>
<module>pandalib-paper-reflect</module>
<module>pandalib-permissions</module> <module>pandalib-permissions</module>
<module>pandalib-players-standalone</module>
<module>pandalib-reflect</module> <module>pandalib-reflect</module>
<module>pandalib-util</module> <module>pandalib-util</module>
<module>pandalib-paper-reflect</module> <module>pandalib-players-permissible</module>
</modules> </modules>
<dependencies> <dependencies>
@ -69,39 +71,7 @@
</dependencies> </dependencies>
<build> <build>
<finalName>${project.name}-${build.number}</finalName>
<plugins> <plugins>
<plugin>
<groupId>net.md-5</groupId>
<artifactId>scriptus</artifactId>
<version>0.4.1</version>
<configuration>
<format>git:${project.name}:${project.version}:%s:${build.number}</format>
<override>true</override> <!-- Hide warnings about parameters already set -->
</configuration>
<executions>
<execution>
<phase>initialize</phase>
<goals>
<goal>describe</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.2</version>
<configuration>
<archive>
<manifestEntries>
<!--suppress UnresolvedMavenProperty -->
<Implementation-Version>${describe}</Implementation-Version>
<Specification-Version>${maven.build.timestamp}</Specification-Version>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId> <artifactId>maven-source-plugin</artifactId>