Player configuration API with default implementation on paper server.
Also merging pandalib-paper-player into pandalib-paper due to unavoidable circular dependency
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
package fr.pandacube.lib.paper.players;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scoreboard.Team;
|
||||
|
||||
import fr.pandacube.lib.players.standalone.AbstractOffPlayer;
|
||||
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
/**
|
||||
* Represents any player on a paper server, either offline or online.
|
||||
*/
|
||||
public interface PaperOffPlayer extends AbstractOffPlayer {
|
||||
|
||||
/*
|
||||
* General data and state
|
||||
*/
|
||||
|
||||
@Override
|
||||
default boolean isOnline() {
|
||||
return getBukkitPlayer() != null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Related class instances
|
||||
*/
|
||||
|
||||
@Override
|
||||
PaperOnlinePlayer getOnlineInstance();
|
||||
|
||||
/**
|
||||
* Returns the Bukkit online {@link Player} instance of this player, or null if not available (offline).
|
||||
* @return the Bukkit online {@link Player} instance of this player, or null if not available (offline).
|
||||
*/
|
||||
default Player getBukkitPlayer() {
|
||||
return Bukkit.getPlayer(getUniqueId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Bukkit’s {@link OfflinePlayer} instance for this player.
|
||||
* May represent a player that never joined the server before.
|
||||
* @return an instance of {@link OfflinePlayer} for this player.
|
||||
*/
|
||||
default OfflinePlayer getBukkitOfflinePlayer() {
|
||||
return Bukkit.getOfflinePlayer(getUniqueId());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Display name
|
||||
*/
|
||||
/**
|
||||
* Get the display name of the user, in legacy format.
|
||||
* @return the display name of the player.
|
||||
*
|
||||
* @implNote This default implementation gets the display name from bukkit (if the player is online).
|
||||
* If its different to the player name, it returns it. Otherwise, it tries to generate the team displayname with {@link #getTeamDisplayName()}.
|
||||
* If the player is not in a team, then the player name is used.
|
||||
*/
|
||||
@Override
|
||||
default String getDisplayName() {
|
||||
String name = getName();
|
||||
Player p = getBukkitPlayer();
|
||||
@SuppressWarnings("deprecation")
|
||||
String bukkitDispName = p != null ? p.getDisplayName() : name;
|
||||
if (!name.equals(bukkitDispName))
|
||||
return bukkitDispName;
|
||||
String teamDispName = getTeamDisplayName();
|
||||
return teamDispName == null ? name : teamDispName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes and returns the the name of the player with the prefix, suffix and color of the team the player is in.
|
||||
* @return The legacy formated player display name, if he is in a {@link Team}, or null otherwise.
|
||||
*/
|
||||
default String getTeamDisplayName() {
|
||||
String name = getName();
|
||||
Team team = Bukkit.getScoreboardManager().getMainScoreboard().getEntryTeam(name);
|
||||
if (team == null)
|
||||
return null;
|
||||
@SuppressWarnings("deprecation")
|
||||
String teamPrefix = team.getPrefix();
|
||||
@SuppressWarnings("deprecation")
|
||||
String teamSuffix = team.getSuffix();
|
||||
@SuppressWarnings("deprecation")
|
||||
String teamColor = team.getColor().toString();
|
||||
|
||||
return teamPrefix + teamColor + name + teamSuffix;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Player config
|
||||
*/
|
||||
|
||||
@Override
|
||||
default String getConfig(String key) throws Exception {
|
||||
return PaperPlayerConfigStorage.get(getUniqueId(), key);
|
||||
}
|
||||
|
||||
@Override
|
||||
default String getConfig(String key, String deflt) throws Exception {
|
||||
return PaperPlayerConfigStorage.get(getUniqueId(), key, deflt);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setConfig(String key, String value) throws Exception {
|
||||
PaperPlayerConfigStorage.set(getUniqueId(), key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void updateConfig(String key, String deflt, UnaryOperator<String> updater) throws Exception {
|
||||
PaperPlayerConfigStorage.update(getUniqueId(), key, deflt, updater);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void unsetConfig(String key) throws Exception {
|
||||
PaperPlayerConfigStorage.unset(getUniqueId(), key);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,281 @@
|
||||
package fr.pandacube.lib.paper.players;
|
||||
|
||||
import com.destroystokyo.paper.ClientOption;
|
||||
import com.destroystokyo.paper.ClientOption.ChatVisibility;
|
||||
import com.destroystokyo.paper.SkinParts;
|
||||
import fr.pandacube.lib.players.standalone.AbstractOnlinePlayer;
|
||||
import net.kyori.adventure.audience.MessageType;
|
||||
import net.kyori.adventure.identity.Identified;
|
||||
import net.kyori.adventure.identity.Identity;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.ComponentLike;
|
||||
import net.kyori.adventure.title.Title;
|
||||
import net.kyori.adventure.title.Title.Times;
|
||||
import net.kyori.adventure.util.Ticks;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.inventory.MainHand;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Represents any online player on a paper server.
|
||||
*/
|
||||
public interface PaperOnlinePlayer extends PaperOffPlayer, AbstractOnlinePlayer {
|
||||
|
||||
/*
|
||||
* General data and state
|
||||
*/
|
||||
|
||||
@Override
|
||||
default String getName() {
|
||||
return getBukkitPlayer().getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
default String getWorldName() {
|
||||
return getBukkitPlayer().getWorld().getName();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Related class instances
|
||||
*/
|
||||
|
||||
@Override
|
||||
default OfflinePlayer getBukkitOfflinePlayer() {
|
||||
return getBukkitPlayer();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Permissions and groups
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tells if this online player has the specified permission.
|
||||
*/
|
||||
default boolean hasPermission(String permission) {
|
||||
return getBukkitPlayer().hasPermission(permission);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Sending packet and stuff to player
|
||||
*/
|
||||
|
||||
@Override
|
||||
default void sendMessage(Component message) {
|
||||
getBukkitPlayer().sendMessage(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void sendMessage(ComponentLike message, UUID sender) {
|
||||
getBukkitPlayer().sendMessage(sender == null ? Identity.nil() : Identity.identity(sender), message, MessageType.CHAT);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void sendMessage(Component message, Identified sender) {
|
||||
getBukkitPlayer().sendMessage(sender == null ? Identity.nil() : sender.identity(), message, MessageType.CHAT);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void sendTitle(Component title, Component subtitle, int fadeIn, int stay, int fadeOut) {
|
||||
getBukkitPlayer().showTitle(Title.title(title, subtitle, Times.times(Ticks.duration(fadeIn), Ticks.duration(stay), Ticks.duration(fadeOut))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Play a sound on this player’s client, sourced at this player’s location.
|
||||
* @param sound the sound to play
|
||||
* @param volume the volume of the sound.
|
||||
* @param pitch the pich in which the sound is played.
|
||||
*/
|
||||
default void playSound(Sound sound, float volume, float pitch) {
|
||||
playSound(sound, getBukkitPlayer().getLocation(), volume, pitch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Play a sound on this player’s client.
|
||||
* @param sound the sound to play
|
||||
* @param location the source location of the sound.
|
||||
* @param volume the volume of the sound.
|
||||
* @param pitch the pich in which the sound is played.
|
||||
*/
|
||||
default void playSound(Sound sound, Location location, float volume, float pitch) {
|
||||
getBukkitPlayer().playSound(location, sound, volume, pitch);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Client options
|
||||
*/
|
||||
|
||||
@Override
|
||||
PaperClientOptions getClientOptions();
|
||||
|
||||
/**
|
||||
* Provides various configuration values of the Minecraft client.
|
||||
*/
|
||||
abstract class PaperClientOptions implements AbstractOnlinePlayer.ClientOptions {
|
||||
|
||||
private final PaperOnlinePlayer op;
|
||||
|
||||
/**
|
||||
* Create a new instance of {@link PaperClientOptions}.
|
||||
* @param op the {@link PaperOnlinePlayer} instance.
|
||||
*/
|
||||
protected PaperClientOptions(PaperOnlinePlayer op) {
|
||||
this.op = op;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasChatColorEnabled() {
|
||||
return op.getBukkitPlayer().getClientOption(ClientOption.CHAT_COLORS_ENABLED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the chat visibility configuration.
|
||||
* @return the chat visibility configuration.
|
||||
*/
|
||||
public ChatVisibility getChatVisibility() {
|
||||
return op.getBukkitPlayer().getClientOption(ClientOption.CHAT_VISIBILITY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChatFullyVisible() {
|
||||
ChatVisibility v = getChatVisibility();
|
||||
return v == ChatVisibility.FULL || v == ChatVisibility.UNKNOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChatOnlyDisplayingSystemMessages() {
|
||||
return getChatVisibility() == ChatVisibility.SYSTEM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChatHidden() {
|
||||
return getChatVisibility() == ChatVisibility.HIDDEN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Locale getLocale() {
|
||||
return op.getBukkitPlayer().locale();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewDistance() {
|
||||
return op.getBukkitPlayer().getClientViewDistance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the player’s main hand.
|
||||
* @return the player’s main hand.
|
||||
*/
|
||||
public MainHand getMainHand() {
|
||||
return op.getBukkitPlayer().getMainHand();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLeftHanded() {
|
||||
return getMainHand() == MainHand.LEFT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRightHanded() {
|
||||
return getMainHand() == MainHand.RIGHT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTextFilteringEnabled() { // needs reflection to get the actual value
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allowsServerListing() {
|
||||
return op.getBukkitPlayer().isAllowingServerListings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the player’s skin configuration.
|
||||
* @return the player’s skin configuration.
|
||||
*/
|
||||
public SkinParts getSkinParts() {
|
||||
return op.getBukkitPlayer().getClientOption(ClientOption.SKIN_PARTS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSkinCapeEnabled() {
|
||||
return getSkinParts().hasCapeEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSkinJacketEnabled() {
|
||||
return getSkinParts().hasJacketEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSkinLeftSleeveEnabled() {
|
||||
return getSkinParts().hasLeftSleeveEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSkinRightSleeveEnabled() {
|
||||
return getSkinParts().hasRightSleeveEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSkinLeftPantsEnabled() {
|
||||
return getSkinParts().hasLeftPantsEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSkinRightPantsEnabled() {
|
||||
return getSkinParts().hasRightPantsEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSkinHatsEnabled() {
|
||||
return getSkinParts().hasHatsEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Custom damage
|
||||
*/
|
||||
|
||||
/**
|
||||
* Deals damages to this player.
|
||||
* @param amount the amount of damage to deal.
|
||||
*/
|
||||
default void damage(double amount) {
|
||||
getBukkitPlayer().damage(amount); // uses DamageSource.GENERIC
|
||||
}
|
||||
|
||||
/**
|
||||
* Deals damages to this player, from the provided entity.
|
||||
* @param amount the amount of damage to deal.
|
||||
* @param source the entity from which the damage comes from.
|
||||
*/
|
||||
default void damage(double amount, LivingEntity source) {
|
||||
getBukkitPlayer().damage(amount, source); // uses appropriate DamageSource according to provided player or entity
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -0,0 +1,213 @@
|
||||
package fr.pandacube.lib.paper.players;
|
||||
|
||||
import fr.pandacube.lib.paper.PandaLibPaper;
|
||||
import fr.pandacube.lib.util.Log;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.InvalidConfigurationException;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.function.UnaryOperator;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class PaperPlayerConfigStorage {
|
||||
|
||||
static File storageFile = new File(PandaLibPaper.getPlugin().getDataFolder(), "playerdata.yml");
|
||||
static boolean initialized = false;
|
||||
|
||||
static LinkedHashMap<ConfigKey, ConfigEntry> data = new LinkedHashMap<>();
|
||||
static LinkedHashMap<UUID, LinkedHashSet<ConfigEntry>> playerSortedData = new LinkedHashMap<>();
|
||||
static LinkedHashMap<String, LinkedHashSet<ConfigEntry>> keySortedData = new LinkedHashMap<>();
|
||||
static boolean changed = false;
|
||||
|
||||
|
||||
|
||||
private static synchronized void initIfNeeded() {
|
||||
if (initialized)
|
||||
return;
|
||||
|
||||
try {
|
||||
load();
|
||||
} catch (InvalidConfigurationException|IOException e) {
|
||||
throw new RuntimeException("Unable to load the player data file.", e);
|
||||
}
|
||||
|
||||
// auto-save every 30 seconds
|
||||
Bukkit.getScheduler().runTaskTimerAsynchronously(PandaLibPaper.getPlugin(), PaperPlayerConfigStorage::save, 600, 600);
|
||||
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private static synchronized void load() throws IOException, InvalidConfigurationException {
|
||||
YamlConfiguration config = new YamlConfiguration();
|
||||
config.load(storageFile);
|
||||
data.clear();
|
||||
playerSortedData.clear();
|
||||
keySortedData.clear();
|
||||
for (String pIdStr : config.getKeys(false)) {
|
||||
UUID pId;
|
||||
try {
|
||||
pId = UUID.fromString(pIdStr);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.severe("Invalid player UUID: '" + pIdStr + "'", e);
|
||||
continue;
|
||||
}
|
||||
ConfigurationSection sec = config.getConfigurationSection(pIdStr);
|
||||
for (String key : sec.getKeys(false)) {
|
||||
String value = sec.getString(key);
|
||||
create(pId, key, value);
|
||||
}
|
||||
}
|
||||
changed = false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static synchronized void save() {
|
||||
YamlConfiguration config = new YamlConfiguration();
|
||||
for (UUID pId : playerSortedData.keySet()) {
|
||||
String pIdStr = pId.toString();
|
||||
ConfigurationSection sec = new YamlConfiguration();
|
||||
for (ConfigEntry e : playerSortedData.get(pId)) {
|
||||
sec.set(e.key, e.value);
|
||||
}
|
||||
config.set(pIdStr, sec);
|
||||
}
|
||||
try {
|
||||
config.save(storageFile);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Unable to save the player data file.", e);
|
||||
}
|
||||
changed = false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private static synchronized void create(UUID player, String key, String newValue) {
|
||||
ConfigKey cKey = new ConfigKey(player, key);
|
||||
ConfigEntry e = new ConfigEntry(player, key, newValue);
|
||||
data.put(cKey, e);
|
||||
playerSortedData.computeIfAbsent(player, p -> new LinkedHashSet<>()).add(e);
|
||||
keySortedData.computeIfAbsent(key, p -> new LinkedHashSet<>()).add(e);
|
||||
}
|
||||
|
||||
|
||||
public static synchronized void set(UUID player, String key, String newValue) {
|
||||
initIfNeeded();
|
||||
ConfigKey cKey = new ConfigKey(player, key);
|
||||
ConfigEntry e = data.get(cKey);
|
||||
if (e != null && newValue == null) { // delete
|
||||
data.remove(cKey);
|
||||
if (playerSortedData.containsKey(player))
|
||||
playerSortedData.get(player).remove(e);
|
||||
if (keySortedData.containsKey(key))
|
||||
keySortedData.get(key).remove(e);
|
||||
changed = true;
|
||||
}
|
||||
else if (e == null && newValue != null) { // create
|
||||
create(player, key, newValue);
|
||||
changed = true;
|
||||
}
|
||||
else if (e != null && !newValue.equals(e.value)) { // update
|
||||
e.value = newValue;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized String get(UUID player, String key) {
|
||||
initIfNeeded();
|
||||
ConfigEntry e = data.get(new ConfigKey(player, key));
|
||||
return e != null ? e.value : null;
|
||||
}
|
||||
|
||||
public static String get(UUID p, String k, String deflt) {
|
||||
String value = get(p, k);
|
||||
return value == null ? deflt : value;
|
||||
}
|
||||
|
||||
public static synchronized void update(UUID p, String k, String deflt, UnaryOperator<String> updater) {
|
||||
String oldValue = get(p, k, deflt);
|
||||
set(p, k, updater.apply(oldValue));
|
||||
}
|
||||
|
||||
public static void unset(UUID p, String k) {
|
||||
set(p, k, null);
|
||||
}
|
||||
|
||||
|
||||
public static LinkedHashSet<ConfigEntry> getAllFromPlayer(UUID p) {
|
||||
initIfNeeded();
|
||||
return new LinkedHashSet<>(playerSortedData.getOrDefault(p, new LinkedHashSet<>()));
|
||||
}
|
||||
|
||||
public static LinkedHashSet<ConfigEntry> getAllWithKeys(String key) {
|
||||
initIfNeeded();
|
||||
return new LinkedHashSet<>(keySortedData.getOrDefault(key, new LinkedHashSet<>()));
|
||||
}
|
||||
|
||||
public static LinkedHashSet<ConfigEntry> getAllWithKeyValue(String k, String v) {
|
||||
initIfNeeded();
|
||||
return getAllWithKeys(k).stream()
|
||||
.filter(c -> c.value.equals(v))
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
}
|
||||
|
||||
|
||||
|
||||
private record ConfigKey(UUID playerId, String key) { }
|
||||
|
||||
public static class ConfigEntry {
|
||||
private final UUID playerId;
|
||||
private final String key;
|
||||
private String value;
|
||||
|
||||
private ConfigEntry(UUID playerId, String key, String value) {
|
||||
this.playerId = playerId;
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public UUID getPlayerId() {
|
||||
return playerId;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
private void setValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(playerId, key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return obj instanceof ConfigEntry o
|
||||
&& Objects.equals(playerId, o.playerId)
|
||||
&& Objects.equals(key, o.key);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user