diff --git a/pandalib-paper-players/pom.xml b/pandalib-paper-players/pom.xml
deleted file mode 100644
index 3900bf6..0000000
--- a/pandalib-paper-players/pom.xml
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
-
- pandalib-parent
- fr.pandacube.lib
- 1.0-SNAPSHOT
- ../pom.xml
-
- 4.0.0
-
- pandalib-paper-players
- jar
-
-
-
- papermc
- https://papermc.io/repo/repository/maven-public/
-
-
-
-
-
- fr.pandacube.lib
- pandalib-players
- ${project.version}
-
-
-
-
-
- io.papermc.paper
- paper-api
- ${paper.version}-SNAPSHOT
- provided
-
-
- io.papermc.paper
- paper-mojangapi
- ${paper.version}-SNAPSHOT
- provided
-
-
-
-
\ No newline at end of file
diff --git a/pandalib-paper-players/src/main/java/fr/pandacube/lib/paper/players/PaperOffPlayer.java b/pandalib-paper/src/main/java/fr/pandacube/lib/paper/players/PaperOffPlayer.java
similarity index 76%
rename from pandalib-paper-players/src/main/java/fr/pandacube/lib/paper/players/PaperOffPlayer.java
rename to pandalib-paper/src/main/java/fr/pandacube/lib/paper/players/PaperOffPlayer.java
index d6f99f1..5f026ac 100644
--- a/pandalib-paper-players/src/main/java/fr/pandacube/lib/paper/players/PaperOffPlayer.java
+++ b/pandalib-paper/src/main/java/fr/pandacube/lib/paper/players/PaperOffPlayer.java
@@ -7,6 +7,8 @@ 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.
*/
@@ -94,4 +96,37 @@ public interface PaperOffPlayer extends AbstractOffPlayer {
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 updater) throws Exception {
+ PaperPlayerConfigStorage.update(getUniqueId(), key, deflt, updater);
+ }
+
+ @Override
+ default void unsetConfig(String key) throws Exception {
+ PaperPlayerConfigStorage.unset(getUniqueId(), key);
+ }
+
}
diff --git a/pandalib-paper-players/src/main/java/fr/pandacube/lib/paper/players/PaperOnlinePlayer.java b/pandalib-paper/src/main/java/fr/pandacube/lib/paper/players/PaperOnlinePlayer.java
similarity index 100%
rename from pandalib-paper-players/src/main/java/fr/pandacube/lib/paper/players/PaperOnlinePlayer.java
rename to pandalib-paper/src/main/java/fr/pandacube/lib/paper/players/PaperOnlinePlayer.java
diff --git a/pandalib-paper/src/main/java/fr/pandacube/lib/paper/players/PaperPlayerConfigStorage.java b/pandalib-paper/src/main/java/fr/pandacube/lib/paper/players/PaperPlayerConfigStorage.java
new file mode 100644
index 0000000..cc6d662
--- /dev/null
+++ b/pandalib-paper/src/main/java/fr/pandacube/lib/paper/players/PaperPlayerConfigStorage.java
@@ -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 data = new LinkedHashMap<>();
+ static LinkedHashMap> playerSortedData = new LinkedHashMap<>();
+ static LinkedHashMap> 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 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 getAllFromPlayer(UUID p) {
+ initIfNeeded();
+ return new LinkedHashSet<>(playerSortedData.getOrDefault(p, new LinkedHashSet<>()));
+ }
+
+ public static LinkedHashSet getAllWithKeys(String key) {
+ initIfNeeded();
+ return new LinkedHashSet<>(keySortedData.getOrDefault(key, new LinkedHashSet<>()));
+ }
+
+ public static LinkedHashSet 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);
+ }
+ }
+}
diff --git a/pandalib-players/src/main/java/fr/pandacube/lib/players/standalone/AbstractOffPlayer.java b/pandalib-players/src/main/java/fr/pandacube/lib/players/standalone/AbstractOffPlayer.java
index 04d80e9..864bcd5 100644
--- a/pandalib-players/src/main/java/fr/pandacube/lib/players/standalone/AbstractOffPlayer.java
+++ b/pandalib-players/src/main/java/fr/pandacube/lib/players/standalone/AbstractOffPlayer.java
@@ -1,6 +1,7 @@
package fr.pandacube.lib.players.standalone;
import java.util.UUID;
+import java.util.function.UnaryOperator;
/**
* Represents any player, either offline or online.
@@ -65,6 +66,24 @@ public interface AbstractOffPlayer {
*/
String getDisplayName();
+
+
+
+
+ /*
+ * Player config
+ */
+
+ String getConfig(String key) throws Exception;
+
+ String getConfig(String key, String deflt) throws Exception;
+
+ void setConfig(String key, String value) throws Exception;
+
+ void updateConfig(String key, String deflt, UnaryOperator updater) throws Exception;
+
+ void unsetConfig(String key) throws Exception;
+