mirror of
https://github.com/Winds-Studio/Leaf.git
synced 2025-12-19 15:09:25 +00:00
async saving player stats and advancements (#334)
* async saving player stats and advancements * remove thread check * fix interrupt * longer wait IO tasks time * safe replace * delay join while saving player * mark as experimental --------- Co-authored-by: Taiyou06 <kaandindar21@gmail.com>
This commit is contained in:
@@ -32,10 +32,10 @@ index 4f01b53bf801f99253efd27df6216912705d18af..82a1715fea41e6a41c4ff441ea89f424
|
||||
level.addDuringTeleport(this);
|
||||
this.triggerDimensionChangeTriggers(serverLevel);
|
||||
diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java
|
||||
index 75fb49f1596f475278d12c8c7aea9ad4952b6056..b17c8a2f5294ac28cc05fb05c84a041b2c6c8721 100644
|
||||
index 52a0fa425a30caa2e592c0fdda44800da169c2a0..3f5c5b6234eb400838973c37e5a48bb121d1ff16 100644
|
||||
--- a/net/minecraft/server/players/PlayerList.java
|
||||
+++ b/net/minecraft/server/players/PlayerList.java
|
||||
@@ -955,11 +955,11 @@ public abstract class PlayerList {
|
||||
@@ -997,11 +997,11 @@ public abstract class PlayerList {
|
||||
byte b = (byte)(keepInventory ? 1 : 0);
|
||||
ServerLevel serverLevel = serverPlayer.serverLevel();
|
||||
LevelData levelData = serverLevel.getLevelData();
|
||||
|
||||
@@ -6,15 +6,213 @@ Subject: [PATCH] Nitori: Async playerdata saving
|
||||
Original license: GPL v3
|
||||
Original project: https://github.com/Gensokyo-Reimagined/Nitori
|
||||
|
||||
diff --git a/net/minecraft/network/Connection.java b/net/minecraft/network/Connection.java
|
||||
index 00a82873d226f113278632a53c0faca420dd67d4..2c4423eb2d465c2782a8dab851619ce539f69ae8 100644
|
||||
--- a/net/minecraft/network/Connection.java
|
||||
+++ b/net/minecraft/network/Connection.java
|
||||
@@ -586,7 +586,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
|
||||
// Paper end - Optimize network
|
||||
|
||||
private static final int MAX_PER_TICK = io.papermc.paper.configuration.GlobalConfiguration.get().misc.maxJoinsPerTick; // Paper - Buffer joins to world
|
||||
- private static int joinAttemptsThisTick; // Paper - Buffer joins to world
|
||||
+ public static int joinAttemptsThisTick; // Paper - Buffer joins to world // Leaf - Async player IO
|
||||
private static int currTick; // Paper - Buffer joins to world
|
||||
private static int tickSecond; // Purpur - Max joins per second
|
||||
public void tick() {
|
||||
diff --git a/net/minecraft/server/PlayerAdvancements.java b/net/minecraft/server/PlayerAdvancements.java
|
||||
index d2159a747fe42aa95cfc6bca0e55e3f4485847bb..d5196b181a0a633cb04ce18b0471cda2dcaa8816 100644
|
||||
--- a/net/minecraft/server/PlayerAdvancements.java
|
||||
+++ b/net/minecraft/server/PlayerAdvancements.java
|
||||
@@ -111,6 +111,7 @@ public class PlayerAdvancements {
|
||||
}
|
||||
|
||||
private void load(ServerAdvancementManager manager) {
|
||||
+ org.dreeam.leaf.async.storage.AsyncPlayerDataSaving.INSTANCE.blockAdvancements(player.getUUID(), player.getScoreboardName()); // Leaf - Async player IO
|
||||
if (Files.isRegularFile(this.playerSavePath)) {
|
||||
try (JsonReader jsonReader = new JsonReader(Files.newBufferedReader(this.playerSavePath, StandardCharsets.UTF_8))) {
|
||||
jsonReader.setLenient(false);
|
||||
@@ -133,12 +134,18 @@ public class PlayerAdvancements {
|
||||
JsonElement jsonElement = this.codec.encodeStart(JsonOps.INSTANCE, this.asData()).getOrThrow();
|
||||
|
||||
try {
|
||||
- FileUtil.createDirectoriesSafe(this.playerSavePath.getParent());
|
||||
-
|
||||
- try (Writer bufferedWriter = Files.newBufferedWriter(this.playerSavePath, StandardCharsets.UTF_8)) {
|
||||
- GSON.toJson(jsonElement, GSON.newJsonWriter(bufferedWriter));
|
||||
- }
|
||||
- } catch (JsonIOException | IOException var7) {
|
||||
+ // Leaf start - Async player IO
|
||||
+ String content = GSON.toJson(jsonElement);
|
||||
+ final Path path = this.playerSavePath;
|
||||
+ org.dreeam.leaf.async.storage.AsyncPlayerDataSaving.INSTANCE.submitAdvancements(
|
||||
+ this.player.getUUID(),
|
||||
+ this.player.getScoreboardName(),
|
||||
+ () -> {
|
||||
+ org.dreeam.leaf.async.storage.AsyncPlayerDataSaving.safeReplace(path, content);
|
||||
+ return null;
|
||||
+ });
|
||||
+ } catch (JsonIOException /*| IOException*/ var7) {
|
||||
+ // Leaf end - Async player IO
|
||||
LOGGER.error("Couldn't save player advancements to {}", this.playerSavePath, var7);
|
||||
}
|
||||
}
|
||||
diff --git a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
|
||||
index 114b25f933c6a1b011523581a5a02a5a2c1e827e..3da6dad3dd0f4c5750609b382f47a6cd14f18e7a 100644
|
||||
--- a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
|
||||
+++ b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
|
||||
@@ -79,6 +79,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
|
||||
.expireAfterWrite(org.dreeam.leaf.config.modules.misc.Cache.cachePlayerProfileResultTimeout, java.util.concurrent.TimeUnit.MINUTES)
|
||||
.build();
|
||||
// Leaf end - Cache player profileResult
|
||||
+ @Nullable public java.util.UUID[] duplicateDisconnect = null; // Leaf - Async player IO
|
||||
|
||||
public ServerLoginPacketListenerImpl(MinecraftServer server, Connection connection, boolean transferred) {
|
||||
this.server = server;
|
||||
diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java
|
||||
index 75fb49f1596f475278d12c8c7aea9ad4952b6056..52a0fa425a30caa2e592c0fdda44800da169c2a0 100644
|
||||
--- a/net/minecraft/server/players/PlayerList.java
|
||||
+++ b/net/minecraft/server/players/PlayerList.java
|
||||
@@ -782,6 +782,31 @@ public abstract class PlayerList {
|
||||
// UserBanListEntry userBanListEntry = this.bans.get(gameProfile);
|
||||
// Moved from processLogin
|
||||
UUID uuid = gameProfile.getId();
|
||||
+
|
||||
+ // Leaf start - Async player IO
|
||||
+ if (org.dreeam.leaf.config.modules.async.AsyncPlayerDataSave.enabled) {
|
||||
+ if (org.dreeam.leaf.async.storage.AsyncPlayerDataSaving.INSTANCE.isSaving(uuid)) {
|
||||
+ if (Connection.joinAttemptsThisTick != 0) Connection.joinAttemptsThisTick -= 1;
|
||||
+ return null;
|
||||
+ }
|
||||
+ if (loginlistener.duplicateDisconnect != null
|
||||
+ && loginlistener.duplicateDisconnect.length != 0) {
|
||||
+ // check last one
|
||||
+ var last = loginlistener.duplicateDisconnect[loginlistener.duplicateDisconnect.length - 1];
|
||||
+ if (org.dreeam.leaf.async.storage.AsyncPlayerDataSaving.INSTANCE.isSaving(last)) {
|
||||
+ if (Connection.joinAttemptsThisTick != 0) Connection.joinAttemptsThisTick -= 1;
|
||||
+ return null;
|
||||
+ }
|
||||
+ for (UUID uuid1 : loginlistener.duplicateDisconnect) {
|
||||
+ if (org.dreeam.leaf.async.storage.AsyncPlayerDataSaving.INSTANCE.isSaving(uuid1)) {
|
||||
+ if (Connection.joinAttemptsThisTick != 0) Connection.joinAttemptsThisTick -= 1;
|
||||
+ return null;
|
||||
+ }
|
||||
+ }
|
||||
+ loginlistener.duplicateDisconnect = null;
|
||||
+ }
|
||||
+ }
|
||||
+ // Leaf end - Async player IO
|
||||
List<ServerPlayer> list = Lists.newArrayList();
|
||||
|
||||
ServerPlayer entityplayer;
|
||||
@@ -793,6 +818,23 @@ public abstract class PlayerList {
|
||||
}
|
||||
}
|
||||
|
||||
+ // Leaf start - Async player IO
|
||||
+ if (org.dreeam.leaf.config.modules.async.AsyncPlayerDataSave.enabled && !list.isEmpty()) {
|
||||
+ loginlistener.duplicateDisconnect = new UUID[list.size()];
|
||||
+ java.util.Iterator<ServerPlayer> iterator = list.iterator();
|
||||
+
|
||||
+ int index = 0;
|
||||
+ while (iterator.hasNext()) {
|
||||
+ entityplayer = iterator.next();
|
||||
+ // this.save(entityplayer); // CraftBukkit - Force the player's inventory to be saved
|
||||
+ entityplayer.connection.disconnect(Component.translatable("multiplayer.disconnect.duplicate_login"), org.bukkit.event.player.PlayerKickEvent.Cause.DUPLICATE_LOGIN); // Paper - kick event cause
|
||||
+ loginlistener.duplicateDisconnect[index] = entityplayer.getUUID();
|
||||
+ index++;
|
||||
+ }
|
||||
+ if (Connection.joinAttemptsThisTick != 0) Connection.joinAttemptsThisTick -= 1;
|
||||
+ return null;
|
||||
+ }
|
||||
+ // Leaf end - Async player IO
|
||||
java.util.Iterator iterator = list.iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
@@ -1582,7 +1624,7 @@ public abstract class PlayerList {
|
||||
*/
|
||||
// Leaf end - Remove useless creating stats json bases on player name logic
|
||||
|
||||
- serverStatsCounter = new ServerStatsCounter(this.server, file1);
|
||||
+ serverStatsCounter = new ServerStatsCounter(this.server, file1, displayName, uuid);
|
||||
// this.stats.put(uuid, serverStatsCounter); // CraftBukkit
|
||||
}
|
||||
|
||||
diff --git a/net/minecraft/stats/ServerStatsCounter.java b/net/minecraft/stats/ServerStatsCounter.java
|
||||
index b26dbe807e5cb0a42f6c06b933397902310e5616..35ad7f249cfb6f5c779136d96f3698ea4de1eb7c 100644
|
||||
--- a/net/minecraft/stats/ServerStatsCounter.java
|
||||
+++ b/net/minecraft/stats/ServerStatsCounter.java
|
||||
@@ -39,12 +39,23 @@ public class ServerStatsCounter extends StatsCounter {
|
||||
private final File file;
|
||||
private final Set<Stat<?>> dirty = Sets.newHashSet();
|
||||
|
||||
+ // Leaf start - Async player IO
|
||||
+ private final String name;
|
||||
+ private final java.util.UUID uuid;
|
||||
+ @Deprecated(forRemoval = true)
|
||||
public ServerStatsCounter(MinecraftServer server, File file) {
|
||||
+ throw new UnsupportedOperationException();
|
||||
+ }
|
||||
+ public ServerStatsCounter(MinecraftServer server, File file, String name, java.util.UUID uuid) {
|
||||
+ this.name = name;
|
||||
+ this.uuid = uuid;
|
||||
+ org.dreeam.leaf.async.storage.AsyncPlayerDataSaving.INSTANCE.blockStats(uuid, name);
|
||||
+ // Leaf end - Async player IO
|
||||
this.server = server;
|
||||
this.file = file;
|
||||
if (file.isFile()) {
|
||||
try {
|
||||
- this.parseLocal(server.getFixerUpper(), FileUtils.readFileToString(file));
|
||||
+ this.parseLocal(server.getFixerUpper(), FileUtils.readFileToString(file, java.nio.charset.StandardCharsets.UTF_8)); // Leaf - UTF-8
|
||||
} catch (IOException var4) {
|
||||
LOGGER.error("Couldn't read statistics file {}", file, var4);
|
||||
} catch (JsonParseException var5) {
|
||||
@@ -66,11 +77,37 @@ public class ServerStatsCounter extends StatsCounter {
|
||||
|
||||
public void save() {
|
||||
if (org.spigotmc.SpigotConfig.disableStatSaving) return; // Spigot
|
||||
+ // Leaf start - Async player IO
|
||||
+ Map<StatType<?>, JsonObject> map = Maps.newHashMap();
|
||||
+ for (it.unimi.dsi.fastutil.objects.Object2IntMap.Entry<Stat<?>> entry : this.stats.object2IntEntrySet()) {
|
||||
+ Stat<?> stat = entry.getKey();
|
||||
+ map.computeIfAbsent(stat.getType(), type -> new JsonObject()).addProperty(getKey(stat).toString(), entry.getIntValue());
|
||||
+ }
|
||||
+ final File file = this.file;
|
||||
+ org.dreeam.leaf.async.storage.AsyncPlayerDataSaving.INSTANCE.submitStats(
|
||||
+ uuid,
|
||||
+ name,
|
||||
+ () -> {
|
||||
+ JsonObject jsonObject = new JsonObject();
|
||||
+
|
||||
+ for (Entry<StatType<?>, JsonObject> entry1 : map.entrySet()) {
|
||||
+ jsonObject.add(BuiltInRegistries.STAT_TYPE.getKey(entry1.getKey()).toString(), entry1.getValue());
|
||||
+ }
|
||||
+
|
||||
+ JsonObject jsonObject1 = new JsonObject();
|
||||
+ jsonObject1.add("stats", jsonObject);
|
||||
+ jsonObject1.addProperty("DataVersion", SharedConstants.getCurrentVersion().getDataVersion().getVersion());
|
||||
+ org.dreeam.leaf.async.storage.AsyncPlayerDataSaving.safeReplace(file.toPath(), jsonObject1.toString());
|
||||
+ return null;
|
||||
+ });
|
||||
+ /*
|
||||
try {
|
||||
FileUtils.writeStringToFile(this.file, this.toJson());
|
||||
} catch (IOException var2) {
|
||||
LOGGER.error("Couldn't save stats", (Throwable)var2);
|
||||
}
|
||||
+ */
|
||||
+ // Leaf end - Async player IO
|
||||
}
|
||||
|
||||
@Override
|
||||
diff --git a/net/minecraft/world/level/storage/LevelStorageSource.java b/net/minecraft/world/level/storage/LevelStorageSource.java
|
||||
index de43e54698125ce9f319d4889dd49f7029fe95e0..742bd4b60321adc9e63c3de910ea95f4990b618d 100644
|
||||
index de43e54698125ce9f319d4889dd49f7029fe95e0..360e54b87db68fad60cdec63af466765baae0a07 100644
|
||||
--- a/net/minecraft/world/level/storage/LevelStorageSource.java
|
||||
+++ b/net/minecraft/world/level/storage/LevelStorageSource.java
|
||||
@@ -520,15 +520,26 @@ public class LevelStorageSource {
|
||||
@@ -520,15 +520,24 @@ public class LevelStorageSource {
|
||||
private void saveLevelData(CompoundTag tag) {
|
||||
Path path = this.levelDirectory.path();
|
||||
|
||||
+ // Leaf start - Async playerdata saving
|
||||
+ // Leaf start - Async player IO
|
||||
+ // Save level.dat asynchronously
|
||||
+ var nbtBytes = new it.unimi.dsi.fastutil.io.FastByteArrayOutputStream(65536);
|
||||
try {
|
||||
@@ -28,38 +226,36 @@ index de43e54698125ce9f319d4889dd49f7029fe95e0..742bd4b60321adc9e63c3de910ea95f4
|
||||
- LevelStorageSource.LOGGER.error("Failed to save level {}", path, var6);
|
||||
+ LevelStorageSource.LOGGER.error("Failed to encode level {}", path, var6);
|
||||
}
|
||||
+ org.dreeam.leaf.async.AsyncPlayerDataSaving.submit(() -> {
|
||||
+ org.dreeam.leaf.async.storage.AsyncPlayerDataSaving.INSTANCE.saveLevelData(path, () -> {
|
||||
+ try {
|
||||
+ Path path1 = Files.createTempFile(path, "level", ".dat");
|
||||
+ org.apache.commons.io.FileUtils.writeByteArrayToFile(path1.toFile(), nbtBytes.array, 0, nbtBytes.length, false);
|
||||
+ Path path2 = this.levelDirectory.oldDataFile();
|
||||
+ Path path3 = this.levelDirectory.dataFile();
|
||||
+ Util.safeReplaceFile(path3, path1, path2);
|
||||
+ Path old = this.levelDirectory.oldDataFile();
|
||||
+ Path current = this.levelDirectory.dataFile();
|
||||
+ org.dreeam.leaf.async.storage.AsyncPlayerDataSaving.safeReplaceBackup(current, old, nbtBytes.array, 0, nbtBytes.length);
|
||||
+ } catch (Exception var6) {
|
||||
+ LevelStorageSource.LOGGER.error("Failed to save level {}", path, var6);
|
||||
+ LevelStorageSource.LOGGER.error("Failed to save level.dat {}", path, var6);
|
||||
+ }
|
||||
+ });
|
||||
+ // Leaf end - Async playerdata saving
|
||||
+ // Leaf end - Async player IO
|
||||
}
|
||||
|
||||
public Optional<Path> getIconFile() {
|
||||
@@ -645,6 +654,7 @@ public class LevelStorageSource {
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
+ org.dreeam.leaf.async.storage.AsyncPlayerDataSaving.INSTANCE.saveLevelData(this.levelDirectory.path(), null); // Leaf - Async player IO
|
||||
this.lock.close();
|
||||
}
|
||||
|
||||
diff --git a/net/minecraft/world/level/storage/PlayerDataStorage.java b/net/minecraft/world/level/storage/PlayerDataStorage.java
|
||||
index c44110b123ba5912af18faf0065e9ded780da9b7..fd8b4832c8b4a52bd8f9b3ea59111af85127b573 100644
|
||||
index c44110b123ba5912af18faf0065e9ded780da9b7..2eae5ccb37b942b94964c28391b96989ae85b072 100644
|
||||
--- a/net/minecraft/world/level/storage/PlayerDataStorage.java
|
||||
+++ b/net/minecraft/world/level/storage/PlayerDataStorage.java
|
||||
@@ -25,6 +25,7 @@ public class PlayerDataStorage {
|
||||
private final File playerDir;
|
||||
protected final DataFixer fixerUpper;
|
||||
private static final DateTimeFormatter FORMATTER = FileNameDateFormatter.create();
|
||||
+ private final java.util.Map<java.util.UUID, java.util.concurrent.Future<?>> savingLocks = new it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap<>(); // Leaf - Async playerdata saving
|
||||
|
||||
public PlayerDataStorage(LevelStorageSource.LevelStorageAccess levelStorageAccess, DataFixer fixerUpper) {
|
||||
this.fixerUpper = fixerUpper;
|
||||
@@ -34,19 +35,82 @@ public class PlayerDataStorage {
|
||||
@@ -34,19 +34,37 @@ public class PlayerDataStorage {
|
||||
|
||||
public void save(Player player) {
|
||||
if (org.spigotmc.SpigotConfig.disablePlayerDataSaving) return; // Spigot
|
||||
+ // Leaf start - Async playerdata saving
|
||||
+ // Leaf start - Async player IO
|
||||
+ CompoundTag compoundTag;
|
||||
try {
|
||||
- CompoundTag compoundTag = player.saveWithoutId(new CompoundTag());
|
||||
@@ -77,105 +273,60 @@ index c44110b123ba5912af18faf0065e9ded780da9b7..fd8b4832c8b4a52bd8f9b3ea59111af8
|
||||
+ return;
|
||||
}
|
||||
+ save(player.getScoreboardName(), player.getUUID(), player.getStringUUID(), compoundTag);
|
||||
+ // Leaf end - Async playerdata saving
|
||||
+ // Leaf end - Async player IO
|
||||
}
|
||||
|
||||
+ // Leaf start - Async playerdata saving
|
||||
+ public void save(String playerName, java.util.UUID uniqueId, String stringId, CompoundTag compoundTag) {
|
||||
+ // Leaf start - Async player IO
|
||||
+ public void save(String playerName, java.util.UUID uuid, String stringId, CompoundTag compoundTag) {
|
||||
+ final File playerDir = this.playerDir;
|
||||
+ org.dreeam.leaf.async.storage.AsyncPlayerDataSaving.INSTANCE.submitEntity(
|
||||
+ uuid,
|
||||
+ playerName,
|
||||
+ () -> {
|
||||
+ var nbtBytes = new it.unimi.dsi.fastutil.io.FastByteArrayOutputStream(65536);
|
||||
+ try {
|
||||
+ NbtIo.writeCompressed(compoundTag, nbtBytes);
|
||||
+ } catch (Exception exception) {
|
||||
+ LOGGER.warn("Failed to encode player data for {}", stringId, exception);
|
||||
+ }
|
||||
+ lockFor(uniqueId, playerName);
|
||||
+ synchronized (PlayerDataStorage.this) {
|
||||
+ org.dreeam.leaf.async.AsyncPlayerDataSaving.submit(() -> {
|
||||
+ try {
|
||||
+ Path path = this.playerDir.toPath();
|
||||
+ Path path1 = Files.createTempFile(path, stringId + "-", ".dat");
|
||||
+ org.apache.commons.io.FileUtils.writeByteArrayToFile(path1.toFile(), nbtBytes.array, 0, nbtBytes.length, false);
|
||||
+ Path path2 = path.resolve(stringId + ".dat");
|
||||
+ Path path3 = path.resolve(stringId + ".dat_old");
|
||||
+ Util.safeReplaceFile(path2, path1, path3);
|
||||
+ } catch (Exception var7) {
|
||||
+ LOGGER.warn("Failed to save player data for {}", playerName, var7);
|
||||
+ } finally {
|
||||
+ synchronized (PlayerDataStorage.this) {
|
||||
+ savingLocks.remove(uniqueId);
|
||||
+ }
|
||||
+ }
|
||||
+ }).ifPresent(future -> savingLocks.put(uniqueId, future));
|
||||
+ }
|
||||
+ }
|
||||
+ Path path = playerDir.toPath();
|
||||
+
|
||||
+ private void lockFor(java.util.UUID uniqueId, String playerName) {
|
||||
+ java.util.concurrent.Future<?> fut;
|
||||
+ synchronized (this) {
|
||||
+ fut = savingLocks.get(uniqueId);
|
||||
+ Path current = path.resolve(stringId + ".dat");
|
||||
+ Path old = path.resolve(stringId + ".dat_old");
|
||||
+ org.dreeam.leaf.async.storage.AsyncPlayerDataSaving.safeReplaceBackup(current, old, nbtBytes.array, 0, nbtBytes.length);
|
||||
+ return null;
|
||||
+ });
|
||||
+ }
|
||||
+ if (fut == null) {
|
||||
+ return;
|
||||
+ }
|
||||
+ while (true) {
|
||||
+ try {
|
||||
+ fut.get(10_000L, java.util.concurrent.TimeUnit.MILLISECONDS);
|
||||
+ break;
|
||||
+ } catch (InterruptedException ignored) {
|
||||
+ } catch (java.util.concurrent.ExecutionException
|
||||
+ | java.util.concurrent.TimeoutException exception) {
|
||||
+ LOGGER.warn("Failed to save player data for {}", playerName, exception);
|
||||
+
|
||||
+ String threadDump = "";
|
||||
+ var threadMXBean = java.lang.management.ManagementFactory.getThreadMXBean();
|
||||
+ for (var threadInfo : threadMXBean.dumpAllThreads(true, true)) {
|
||||
+ if (threadInfo.getThreadName().equals("Leaf IO Thread")) {
|
||||
+ threadDump = threadInfo.toString();
|
||||
+ break;
|
||||
+ }
|
||||
+ }
|
||||
+ LOGGER.warn(threadDump);
|
||||
+ fut.cancel(true);
|
||||
+ break;
|
||||
+ } finally {
|
||||
+ savingLocks.remove(uniqueId);
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ // Leaf end - Async playerdata saving
|
||||
+ // Leaf end - Async player IO
|
||||
+
|
||||
private void backup(String name, String stringUuid, String suffix) { // CraftBukkit
|
||||
Path path = this.playerDir.toPath();
|
||||
Path path1 = path.resolve(stringUuid + suffix); // CraftBukkit
|
||||
@@ -60,7 +124,13 @@ public class PlayerDataStorage {
|
||||
@@ -60,7 +78,13 @@ public class PlayerDataStorage {
|
||||
}
|
||||
}
|
||||
|
||||
- private Optional<CompoundTag> load(String name, String stringUuid, String suffix) { // CraftBukkit
|
||||
+ // Leaf start - Async playerdata saving
|
||||
+ // Leaf start - Async player IO
|
||||
+ private Optional<CompoundTag> load(String name, String stringUuid, String suffix) {
|
||||
+ return load(name, stringUuid, suffix, java.util.UUID.fromString(stringUuid));
|
||||
+ }
|
||||
+ private Optional<CompoundTag> load(String name, String stringUuid, String suffix, java.util.UUID playerUuid) { // CraftBukkit
|
||||
+ lockFor(playerUuid, name);
|
||||
+ // Leaf end - Async playerdata saving
|
||||
+ org.dreeam.leaf.async.storage.AsyncPlayerDataSaving.INSTANCE.blockEntity(playerUuid, name);
|
||||
+ // Leaf end - Async player IO
|
||||
File file = new File(this.playerDir, stringUuid + suffix); // CraftBukkit
|
||||
// Spigot start
|
||||
boolean usingWrongFile = false;
|
||||
@@ -91,7 +161,7 @@ public class PlayerDataStorage {
|
||||
@@ -91,7 +115,7 @@ public class PlayerDataStorage {
|
||||
|
||||
public Optional<CompoundTag> load(Player player) {
|
||||
// CraftBukkit start
|
||||
- return this.load(player.getName().getString(), player.getStringUUID()).map((tag) -> {
|
||||
+ return this.load(player.getName().getString(), player.getStringUUID(), player.getUUID()).map((tag) -> { // Leaf - Async playerdata saving
|
||||
+ return this.load(player.getName().getString(), player.getStringUUID(), player.getUUID()).map((tag) -> { // Leaf - Async player IO
|
||||
if (player instanceof ServerPlayer serverPlayer) {
|
||||
CraftPlayer craftPlayer = serverPlayer.getBukkitEntity();
|
||||
// Only update first played if it is older than the one we have
|
||||
@@ -106,20 +176,25 @@ public class PlayerDataStorage {
|
||||
@@ -106,20 +130,25 @@ public class PlayerDataStorage {
|
||||
});
|
||||
}
|
||||
|
||||
+ // Leaf start - Async playerdata saving
|
||||
+ // Leaf start - Async player IO
|
||||
public Optional<CompoundTag> load(String name, String uuid) {
|
||||
+ return this.load(name, uuid, java.util.UUID.fromString(uuid));
|
||||
+ }
|
||||
@@ -195,7 +346,7 @@ index c44110b123ba5912af18faf0065e9ded780da9b7..fd8b4832c8b4a52bd8f9b3ea59111af8
|
||||
return compoundTag;
|
||||
});
|
||||
}
|
||||
+ // Leaf end - Async playerdata saving
|
||||
+ // Leaf end - Async player IO
|
||||
|
||||
// CraftBukkit start
|
||||
public File getPlayerDir() {
|
||||
|
||||
@@ -6,10 +6,10 @@ Subject: [PATCH] SparklyPaper: Skip dirty stats copy when requesting player
|
||||
|
||||
|
||||
diff --git a/net/minecraft/stats/ServerStatsCounter.java b/net/minecraft/stats/ServerStatsCounter.java
|
||||
index b26dbe807e5cb0a42f6c06b933397902310e5616..ce89060bd01b253af7577fd0e6c03fc95f046b91 100644
|
||||
index 35ad7f249cfb6f5c779136d96f3698ea4de1eb7c..523dc12a8866a199eac1b2f418bf206f068ba80c 100644
|
||||
--- a/net/minecraft/stats/ServerStatsCounter.java
|
||||
+++ b/net/minecraft/stats/ServerStatsCounter.java
|
||||
@@ -81,11 +81,15 @@ public class ServerStatsCounter extends StatsCounter {
|
||||
@@ -118,11 +118,15 @@ public class ServerStatsCounter extends StatsCounter {
|
||||
this.dirty.add(stat);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ index b26dbe807e5cb0a42f6c06b933397902310e5616..ce89060bd01b253af7577fd0e6c03fc9
|
||||
|
||||
public void parseLocal(DataFixer fixerUpper, String json) {
|
||||
try {
|
||||
@@ -194,10 +198,12 @@ public class ServerStatsCounter extends StatsCounter {
|
||||
@@ -231,10 +235,12 @@ public class ServerStatsCounter extends StatsCounter {
|
||||
public void sendStats(ServerPlayer player) {
|
||||
Object2IntMap<Stat<?>> map = new Object2IntOpenHashMap<>();
|
||||
|
||||
|
||||
@@ -458,7 +458,7 @@ index 63ff20f467c7508486a8f274442269b90faea108..15de8904a43c0ee1e6d55d511ebd84df
|
||||
}
|
||||
// CraftBukkit end
|
||||
diff --git a/net/minecraft/server/PlayerAdvancements.java b/net/minecraft/server/PlayerAdvancements.java
|
||||
index d2159a747fe42aa95cfc6bca0e55e3f4485847bb..abe7ffd48766c48fab091947f34db436b3c883d0 100644
|
||||
index d5196b181a0a633cb04ce18b0471cda2dcaa8816..46433f0e6b37d31ec5468b3f4a5b2524d3cb29ed 100644
|
||||
--- a/net/minecraft/server/PlayerAdvancements.java
|
||||
+++ b/net/minecraft/server/PlayerAdvancements.java
|
||||
@@ -53,8 +53,11 @@ public class PlayerAdvancements {
|
||||
@@ -483,7 +483,7 @@ index d2159a747fe42aa95cfc6bca0e55e3f4485847bb..abe7ffd48766c48fab091947f34db436
|
||||
this.isFirstPacket = true;
|
||||
this.lastSelectedTab = null;
|
||||
this.tree = manager.tree();
|
||||
@@ -151,7 +155,7 @@ public class PlayerAdvancements {
|
||||
@@ -158,7 +162,7 @@ public class PlayerAdvancements {
|
||||
if (org.galemc.gale.configuration.GaleGlobalConfiguration.get().logToConsole.ignoredAdvancements) LOGGER.warn("Ignored advancement '{}' in progress file {} - it doesn't exist anymore?", path, this.playerSavePath); // Gale - Purpur - do not log ignored advancements
|
||||
} else {
|
||||
this.startProgress(advancementHolder, progress);
|
||||
@@ -492,7 +492,7 @@ index d2159a747fe42aa95cfc6bca0e55e3f4485847bb..abe7ffd48766c48fab091947f34db436
|
||||
this.markForVisibilityUpdate(advancementHolder);
|
||||
}
|
||||
});
|
||||
@@ -183,10 +187,12 @@ public class PlayerAdvancements {
|
||||
@@ -190,10 +194,12 @@ public class PlayerAdvancements {
|
||||
return false;
|
||||
}
|
||||
// Paper end - Add PlayerAdvancementCriterionGrantEvent
|
||||
@@ -509,7 +509,7 @@ index d2159a747fe42aa95cfc6bca0e55e3f4485847bb..abe7ffd48766c48fab091947f34db436
|
||||
// Paper start - Add Adventure message to PlayerAdvancementDoneEvent
|
||||
final net.kyori.adventure.text.Component message = advancement.value().display().flatMap(info -> {
|
||||
return java.util.Optional.ofNullable(
|
||||
@@ -220,12 +226,14 @@ public class PlayerAdvancements {
|
||||
@@ -227,12 +233,14 @@ public class PlayerAdvancements {
|
||||
AdvancementProgress orStartProgress = this.getOrStartProgress(advancement);
|
||||
boolean isDone = orStartProgress.isDone();
|
||||
if (orStartProgress.revokeProgress(criterionKey)) {
|
||||
@@ -527,7 +527,7 @@ index d2159a747fe42aa95cfc6bca0e55e3f4485847bb..abe7ffd48766c48fab091947f34db436
|
||||
this.markForVisibilityUpdate(advancement);
|
||||
}
|
||||
|
||||
@@ -271,7 +279,10 @@ public class PlayerAdvancements {
|
||||
@@ -278,7 +286,10 @@ public class PlayerAdvancements {
|
||||
}
|
||||
|
||||
public void flushDirty(ServerPlayer serverPlayer) {
|
||||
@@ -539,7 +539,7 @@ index d2159a747fe42aa95cfc6bca0e55e3f4485847bb..abe7ffd48766c48fab091947f34db436
|
||||
Map<ResourceLocation, AdvancementProgress> map = new HashMap<>();
|
||||
Set<AdvancementHolder> set = new java.util.TreeSet<>(java.util.Comparator.comparing(adv -> adv.id().toString())); // Paper - Changed from HashSet to TreeSet ordered alphabetically.
|
||||
Set<ResourceLocation> set1 = new HashSet<>();
|
||||
@@ -279,16 +290,24 @@ public class PlayerAdvancements {
|
||||
@@ -286,16 +297,24 @@ public class PlayerAdvancements {
|
||||
for (AdvancementNode advancementNode : this.rootsToUpdate) {
|
||||
this.updateTreeVisibility(advancementNode, set, set1);
|
||||
}
|
||||
@@ -568,7 +568,7 @@ index d2159a747fe42aa95cfc6bca0e55e3f4485847bb..abe7ffd48766c48fab091947f34db436
|
||||
if (!map.isEmpty() || !set.isEmpty() || !set1.isEmpty()) {
|
||||
serverPlayer.connection.send(new ClientboundUpdateAdvancementsPacket(this.isFirstPacket, set, set1, map));
|
||||
}
|
||||
@@ -331,10 +350,13 @@ public class PlayerAdvancements {
|
||||
@@ -338,10 +357,13 @@ public class PlayerAdvancements {
|
||||
AdvancementHolder advancementHolder = node.holder();
|
||||
if (visible) {
|
||||
if (this.visible.add(advancementHolder)) {
|
||||
@@ -900,7 +900,7 @@ index 75fb49f1596f475278d12c8c7aea9ad4952b6056..de601491b7ecb83f1bb64a95989d6ed4
|
||||
player.isRealPlayer = true; // Paper
|
||||
player.loginTime = System.currentTimeMillis(); // Paper - Replace OfflinePlayer#getLastPlayed
|
||||
GameProfile gameProfile = player.getGameProfile();
|
||||
@@ -891,6 +893,15 @@ public abstract class PlayerList {
|
||||
@@ -933,6 +935,15 @@ public abstract class PlayerList {
|
||||
return this.respawn(player, keepInventory, reason, eventReason, null);
|
||||
}
|
||||
public ServerPlayer respawn(ServerPlayer player, boolean keepInventory, Entity.RemovalReason reason, org.bukkit.event.player.PlayerRespawnEvent.RespawnReason eventReason, org.bukkit.Location location) {
|
||||
@@ -916,7 +916,7 @@ index 75fb49f1596f475278d12c8c7aea9ad4952b6056..de601491b7ecb83f1bb64a95989d6ed4
|
||||
player.stopRiding(); // CraftBukkit
|
||||
this.players.remove(player);
|
||||
this.playersByName.remove(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot
|
||||
@@ -902,6 +913,7 @@ public abstract class PlayerList {
|
||||
@@ -944,6 +955,7 @@ public abstract class PlayerList {
|
||||
ServerPlayer serverPlayer = player;
|
||||
Level fromWorld = player.level();
|
||||
player.wonGame = false;
|
||||
|
||||
@@ -110,10 +110,10 @@ index 2e9eb04c7c4342393c05339906c267bca9ff29b1..53b9daa909c2b89046d5af515e17afe0
|
||||
try {
|
||||
PlayerList playerList = this.server.getPlayerList();
|
||||
diff --git a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
|
||||
index 114b25f933c6a1b011523581a5a02a5a2c1e827e..5907f1c75002be5e2ef1f9875974e665f964db7a 100644
|
||||
index 3da6dad3dd0f4c5750609b382f47a6cd14f18e7a..c1e4ea2f28aba688b5b61e5bea2c295e9f219aba 100644
|
||||
--- a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
|
||||
+++ b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java
|
||||
@@ -494,11 +494,31 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
|
||||
@@ -495,11 +495,31 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener,
|
||||
this.disconnect(ServerCommonPacketListenerImpl.DISCONNECT_UNEXPECTED_QUERY);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ Subject: [PATCH] Async playerdata saving
|
||||
|
||||
|
||||
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java b/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java
|
||||
index f2d87c12dd19210ce7e2147fada5c10191008632..14da4c731391f69fef104b6b3b7f2f977fe5ee95 100644
|
||||
index f2d87c12dd19210ce7e2147fada5c10191008632..ad66046d31c24ba2a7d2b115f6c70adb95b9735b 100644
|
||||
--- a/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java
|
||||
+++ b/src/main/java/org/bukkit/craftbukkit/CraftOfflinePlayer.java
|
||||
@@ -207,7 +207,7 @@ public class CraftOfflinePlayer implements OfflinePlayer, ConfigurationSerializa
|
||||
@@ -13,7 +13,7 @@ index f2d87c12dd19210ce7e2147fada5c10191008632..14da4c731391f69fef104b6b3b7f2f97
|
||||
|
||||
private CompoundTag getData() {
|
||||
- return this.storage.load(this.profile.getName(), this.profile.getId().toString()).orElse(null);
|
||||
+ return this.storage.load(this.profile.getName(), this.profile.getId().toString(), this.profile.getId()).orElse(null); // Leaf - Async playerdata saving
|
||||
+ return this.storage.load(this.profile.getName(), this.profile.getId().toString(), this.profile.getId()).orElse(null); // Leaf - Async player IO
|
||||
}
|
||||
|
||||
private CompoundTag getBukkitData() {
|
||||
@@ -31,7 +31,7 @@ index f2d87c12dd19210ce7e2147fada5c10191008632..14da4c731391f69fef104b6b3b7f2f97
|
||||
- } catch (java.io.IOException e) {
|
||||
- e.printStackTrace();
|
||||
- }
|
||||
+ server.console.playerDataStorage.save(this.getName(), this.getUniqueId(), this.getUniqueId().toString(), compoundTag); // Leaf - Async playerdata saving
|
||||
+ server.console.playerDataStorage.save(this.getName(), this.getUniqueId(), this.getUniqueId().toString(), compoundTag); // Leaf - Async player IO
|
||||
}
|
||||
// Purpur end - OfflinePlayer API
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
package org.dreeam.leaf.async;
|
||||
|
||||
import net.minecraft.Util;
|
||||
import org.dreeam.leaf.config.modules.async.AsyncPlayerDataSave;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class AsyncPlayerDataSaving {
|
||||
|
||||
public static final ExecutorService IO_POOL = new ThreadPoolExecutor(
|
||||
1, 1, 0L, TimeUnit.MILLISECONDS,
|
||||
new LinkedBlockingQueue<>(),
|
||||
new com.google.common.util.concurrent.ThreadFactoryBuilder()
|
||||
.setPriority(Thread.NORM_PRIORITY - 2)
|
||||
.setNameFormat("Leaf IO Thread")
|
||||
.setUncaughtExceptionHandler(Util::onThreadException)
|
||||
.build(),
|
||||
new ThreadPoolExecutor.DiscardPolicy()
|
||||
);
|
||||
|
||||
private AsyncPlayerDataSaving() {
|
||||
}
|
||||
|
||||
public static Optional<Future<?>> submit(Runnable runnable) {
|
||||
if (!AsyncPlayerDataSave.enabled) {
|
||||
runnable.run();
|
||||
return Optional.empty();
|
||||
} else {
|
||||
return Optional.of(IO_POOL.submit(runnable));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.dreeam.leaf.async;
|
||||
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import org.dreeam.leaf.async.storage.AsyncPlayerDataSaving;
|
||||
import org.dreeam.leaf.async.tracker.MultithreadedTracker;
|
||||
|
||||
public class ShutdownExecutors {
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
package org.dreeam.leaf.async.storage;
|
||||
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectMaps;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||
import net.minecraft.Util;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.dreeam.leaf.config.modules.async.AsyncPlayerDataSave;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeFormatterBuilder;
|
||||
import java.time.format.SignStyle;
|
||||
import java.time.temporal.ChronoField;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
public class AsyncPlayerDataSaving {
|
||||
public static final AsyncPlayerDataSaving INSTANCE = new AsyncPlayerDataSaving();
|
||||
private static final Logger LOGGER = LogManager.getLogger("Leaf Async Player IO");
|
||||
public static ExecutorService IO_POOL = null;
|
||||
private static final DateTimeFormatter FORMATTER = new DateTimeFormatterBuilder()
|
||||
.appendValue(ChronoField.YEAR, 4, 10, SignStyle.EXCEEDS_PAD)
|
||||
.appendValue(ChronoField.MONTH_OF_YEAR, 2)
|
||||
.appendValue(ChronoField.DAY_OF_MONTH, 2)
|
||||
.appendValue(ChronoField.HOUR_OF_DAY, 2)
|
||||
.appendValue(ChronoField.MINUTE_OF_HOUR, 2)
|
||||
.appendValue(ChronoField.SECOND_OF_MINUTE, 2)
|
||||
.appendValue(ChronoField.NANO_OF_SECOND, 9)
|
||||
.toFormatter();
|
||||
|
||||
private record SaveTask(Ty ty, Callable<Void> callable, String name, UUID uuid) implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
callable.call();
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Failed to save player {} data for {}", ty, name, e);
|
||||
} finally {
|
||||
switch (ty) {
|
||||
case ENTITY -> INSTANCE.entityFut.remove(uuid);
|
||||
case STATS -> INSTANCE.statsFut.remove(uuid);
|
||||
case ADVANCEMENTS -> INSTANCE.advancementsFut.remove(uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum Ty {
|
||||
ENTITY,
|
||||
STATS,
|
||||
ADVANCEMENTS,
|
||||
}
|
||||
|
||||
// use same lock
|
||||
private final Object2ObjectMap<UUID, Future<?>> entityFut = Object2ObjectMaps.synchronize(new Object2ObjectOpenHashMap<>(), this);
|
||||
private final Object2ObjectMap<UUID, Future<?>> statsFut = Object2ObjectMaps.synchronize(new Object2ObjectOpenHashMap<>(), this);
|
||||
private final Object2ObjectMap<UUID, Future<?>> advancementsFut = Object2ObjectMaps.synchronize(new Object2ObjectOpenHashMap<>(), this);
|
||||
|
||||
private final Object2ObjectMap<Path, Future<?>> levelDatFut = Object2ObjectMaps.synchronize(new Object2ObjectOpenHashMap<>(), this);
|
||||
|
||||
private AsyncPlayerDataSaving() {
|
||||
}
|
||||
|
||||
public static void init() {
|
||||
if (AsyncPlayerDataSaving.IO_POOL != null) {
|
||||
throw new IllegalStateException("Already initialized");
|
||||
}
|
||||
AsyncPlayerDataSaving.IO_POOL = new ThreadPoolExecutor(
|
||||
1, 1, 0L, TimeUnit.MILLISECONDS,
|
||||
new LinkedBlockingQueue<>(),
|
||||
new ThreadFactoryBuilder()
|
||||
.setPriority(Thread.NORM_PRIORITY - 2)
|
||||
.setNameFormat("Leaf Async Player IO Thread")
|
||||
.setUncaughtExceptionHandler(Util::onThreadException)
|
||||
.build(),
|
||||
new ThreadPoolExecutor.DiscardPolicy()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public void saveLevelData(Path path, @Nullable Runnable runnable) {
|
||||
if (!AsyncPlayerDataSave.enabled) {
|
||||
if (runnable != null) {
|
||||
runnable.run();
|
||||
}
|
||||
return;
|
||||
}
|
||||
var fut = levelDatFut.get(path);
|
||||
if (fut != null) {
|
||||
try {
|
||||
while (true) {
|
||||
try {
|
||||
fut.get();
|
||||
break;
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
} catch (ExecutionException e) {
|
||||
LOGGER.error("Failed to save level.dat for {}", path, e);
|
||||
} finally {
|
||||
levelDatFut.remove(path);
|
||||
}
|
||||
}
|
||||
if (runnable != null) {
|
||||
levelDatFut.put(path, IO_POOL.submit(() -> {
|
||||
try {
|
||||
runnable.run();
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(e);
|
||||
} finally {
|
||||
levelDatFut.remove(path);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSaving(UUID uuid) {
|
||||
var entity = entityFut.get(uuid);
|
||||
var advancements = advancementsFut.get(uuid);
|
||||
var stats = statsFut.get(uuid);
|
||||
return entity != null || advancements != null || stats != null;
|
||||
}
|
||||
|
||||
public void submitStats(UUID uuid, String playerName, Callable<Void> callable) {
|
||||
submit(Ty.STATS, uuid, playerName, callable);
|
||||
}
|
||||
|
||||
public void submitEntity(UUID uuid, String playerName, Callable<Void> callable) {
|
||||
submit(Ty.ENTITY, uuid, playerName, callable);
|
||||
}
|
||||
|
||||
public void submitAdvancements(UUID uuid, String playerName, Callable<Void> callable) {
|
||||
submit(Ty.ADVANCEMENTS, uuid, playerName, callable);
|
||||
}
|
||||
|
||||
private void submit(Ty type, UUID uuid, String playerName, Callable<Void> callable) {
|
||||
if (!AsyncPlayerDataSave.enabled) {
|
||||
try {
|
||||
callable.call();
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Failed to save player {} data for {}", type, playerName, e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
block(type, uuid, playerName);
|
||||
var fut = IO_POOL.submit(new SaveTask(type, callable, playerName, uuid));
|
||||
switch (type) {
|
||||
case ENTITY -> entityFut.put(uuid, fut);
|
||||
case ADVANCEMENTS -> advancementsFut.put(uuid, fut);
|
||||
case STATS -> statsFut.put(uuid, fut);
|
||||
}
|
||||
}
|
||||
|
||||
public void blockStats(UUID uuid, String playerName) {
|
||||
block(Ty.STATS, uuid, playerName);
|
||||
}
|
||||
|
||||
public void blockEntity(UUID uuid, String playerName) {
|
||||
block(Ty.ENTITY, uuid, playerName);
|
||||
}
|
||||
|
||||
public void blockAdvancements(UUID uuid, String playerName) {
|
||||
block(Ty.ADVANCEMENTS, uuid, playerName);
|
||||
}
|
||||
|
||||
private void block(Ty type, UUID uuid, String playerName) {
|
||||
if (!AsyncPlayerDataSave.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
Future<?> fut = switch (type) {
|
||||
case ENTITY -> entityFut.get(uuid);
|
||||
case ADVANCEMENTS -> advancementsFut.get(uuid);
|
||||
case STATS -> statsFut.get(uuid);
|
||||
};
|
||||
if (fut == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
while (true) {
|
||||
try {
|
||||
fut.get();
|
||||
break;
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
} catch (ExecutionException exception) {
|
||||
LOGGER.warn("Failed to save player {} data for {}", type, playerName, exception);
|
||||
fut.cancel(true);
|
||||
} finally {
|
||||
switch (type) {
|
||||
case ENTITY -> entityFut.remove(uuid);
|
||||
case ADVANCEMENTS -> advancementsFut.remove(uuid);
|
||||
case STATS -> statsFut.remove(uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final StandardCopyOption[] ATOMIC_MOVE = new StandardCopyOption[]{StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING};
|
||||
private static final StandardCopyOption[] NO_ATOMIC_MOVE = new StandardCopyOption[]{StandardCopyOption.REPLACE_EXISTING};
|
||||
|
||||
public static void safeReplace(Path current, String content) {
|
||||
byte[] bytes = content.getBytes(StandardCharsets.UTF_8);
|
||||
safeReplace(current, bytes, 0, bytes.length);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static void safeReplaceBackup(Path current, Path old, String content) {
|
||||
byte[] bytes = content.getBytes(StandardCharsets.UTF_8);
|
||||
safeReplaceBackup(current, old, bytes, 0, bytes.length);
|
||||
}
|
||||
|
||||
public static void safeReplace(Path current, byte[] bytes, int offset, int length) {
|
||||
File latest = writeTempFile(current, bytes, offset, length);
|
||||
Objects.requireNonNull(latest);
|
||||
for (int i = 1; i <= 10; i++) {
|
||||
try {
|
||||
try {
|
||||
Files.move(latest.toPath(), current, ATOMIC_MOVE);
|
||||
} catch (AtomicMoveNotSupportedException e) {
|
||||
Files.move(latest.toPath(), current, NO_ATOMIC_MOVE);
|
||||
}
|
||||
break;
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Failed move {} to {} retries ({} / 10)", latest, current, i, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void safeReplaceBackup(Path current, Path backup, byte[] bytes, int offset, int length) {
|
||||
File latest = writeTempFile(current, bytes, offset, length);
|
||||
Objects.requireNonNull(latest);
|
||||
for (int i = 1; i <= 10; i++) {
|
||||
try {
|
||||
try {
|
||||
Files.move(current, backup, ATOMIC_MOVE);
|
||||
} catch (AtomicMoveNotSupportedException e) {
|
||||
Files.move(current, backup, NO_ATOMIC_MOVE);
|
||||
}
|
||||
break;
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Failed move {} to {} retries ({} / 10)", current, backup, i, e);
|
||||
}
|
||||
}
|
||||
for (int i = 1; i <= 10; i++) {
|
||||
try {
|
||||
try {
|
||||
Files.move(latest.toPath(), current, ATOMIC_MOVE);
|
||||
} catch (AtomicMoveNotSupportedException e) {
|
||||
Files.move(latest.toPath(), current, NO_ATOMIC_MOVE);
|
||||
}
|
||||
break;
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Failed move {} to {} retries ({} / 10)", latest, current, i, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static File writeTempFile(Path current, byte[] bytes, int offset, int length) {
|
||||
Path dir = current.getParent();
|
||||
for (int i = 1; i <= 10; i++) {
|
||||
File temp = null;
|
||||
try {
|
||||
if (!dir.toFile().isDirectory()) {
|
||||
Files.createDirectories(dir);
|
||||
}
|
||||
temp = tempFileDateTime(current).toFile();
|
||||
if (temp.exists()) {
|
||||
throw new FileAlreadyExistsException(temp.getPath());
|
||||
}
|
||||
// sync content and metadata to device
|
||||
try (RandomAccessFile stream = new RandomAccessFile(temp, "rws")) {
|
||||
stream.write(bytes, offset, length);
|
||||
}
|
||||
return temp;
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Failed write {} retries ({} / 10)", temp == null ? current : temp, i, e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Path tempFileDateTime(Path path) {
|
||||
String now = LocalDateTime.now().format(FORMATTER);
|
||||
String last = path.getFileName().toString();
|
||||
int dot = last.lastIndexOf('.');
|
||||
|
||||
String base = (dot == -1) ? last : last.substring(0, dot);
|
||||
String ext = (dot == -1) ? "" : last.substring(dot);
|
||||
|
||||
String newExt = switch (ext) {
|
||||
case ".json", ".dat" -> ext;
|
||||
default -> ".temp";
|
||||
};
|
||||
return path.resolveSibling(base + "-" + now + newExt);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package org.dreeam.leaf.config.modules.async;
|
||||
|
||||
import org.dreeam.leaf.async.storage.AsyncPlayerDataSaving;
|
||||
import org.dreeam.leaf.config.ConfigModules;
|
||||
import org.dreeam.leaf.config.EnumConfigCategory;
|
||||
import org.dreeam.leaf.config.annotations.Experimental;
|
||||
|
||||
public class AsyncPlayerDataSave extends ConfigModules {
|
||||
|
||||
@@ -9,7 +11,9 @@ public class AsyncPlayerDataSave extends ConfigModules {
|
||||
return EnumConfigCategory.ASYNC.getBaseKeyName() + ".async-playerdata-save";
|
||||
}
|
||||
|
||||
@Experimental
|
||||
public static boolean enabled = false;
|
||||
private static boolean asyncPlayerDataSaveInitialized;
|
||||
|
||||
@Override
|
||||
public void onLoaded() {
|
||||
@@ -18,6 +22,13 @@ public class AsyncPlayerDataSave extends ConfigModules {
|
||||
"""
|
||||
异步保存玩家数据.""");
|
||||
|
||||
if (asyncPlayerDataSaveInitialized) {
|
||||
config.getConfigSection(getBasePath());
|
||||
return;
|
||||
}
|
||||
asyncPlayerDataSaveInitialized = true;
|
||||
|
||||
enabled = config.getBoolean(getBasePath() + ".enabled", enabled);
|
||||
if (enabled) AsyncPlayerDataSaving.init();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ public class BotStatsCounter extends ServerStatsCounter {
|
||||
private static final File UNKOWN_FILE = new File("BOT_STATS_REMOVE_THIS");
|
||||
|
||||
public BotStatsCounter(MinecraftServer server) {
|
||||
super(server, UNKOWN_FILE);
|
||||
super(server, UNKOWN_FILE, "", net.minecraft.Util.NIL_UUID); // Leaf
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user