mirror of
https://github.com/Winds-Studio/Leaf.git
synced 2025-12-30 20:39:21 +00:00
cleanup
This commit is contained in:
@@ -7,31 +7,32 @@ Original license: GPL v3
|
||||
Original project: https://github.com/Gensokyo-Reimagined/Nitori
|
||||
|
||||
diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java
|
||||
index 5a0d30b8ff5e377224de67c9f464bd1c694a4397..b316c174963cd395186d45f3fb9ca78470fa5c27 100644
|
||||
index 5a0d30b8ff5e377224de67c9f464bd1c694a4397..1cb60107d95296fc9e2c106d70838c057564abeb 100644
|
||||
--- a/net/minecraft/server/MinecraftServer.java
|
||||
+++ b/net/minecraft/server/MinecraftServer.java
|
||||
@@ -1069,6 +1069,12 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
||||
io.papermc.paper.log.CustomLogManager.forceReset(); // Paper - Reset loggers after shutdown
|
||||
this.onServerExit();
|
||||
// Paper end - Improved watchdog support - move final shutdown items here
|
||||
+ // Leaf start
|
||||
+ // Leaf start - Async playerdata saving
|
||||
+ org.dreeam.leaf.async.AsyncPlayerDataSaving.IO_POOL.shutdown();
|
||||
+ try {
|
||||
+ org.dreeam.leaf.async.AsyncPlayerDataSaving.IO_POOL.awaitTermination(30, java.util.concurrent.TimeUnit.SECONDS);
|
||||
+ } catch (java.lang.InterruptedException ignored) {}
|
||||
+ // Leaf end
|
||||
+ // Leaf end - Async playerdata saving
|
||||
}
|
||||
|
||||
public String getLocalIp() {
|
||||
diff --git a/net/minecraft/world/level/storage/LevelStorageSource.java b/net/minecraft/world/level/storage/LevelStorageSource.java
|
||||
index de43e54698125ce9f319d4889dd49f7029fe95e0..4e6737636f754c9e18cb9a2c9e2f3a9ea8b9d02c 100644
|
||||
index de43e54698125ce9f319d4889dd49f7029fe95e0..451cb8987906f0a9e753a870aa629124daba9c73 100644
|
||||
--- a/net/minecraft/world/level/storage/LevelStorageSource.java
|
||||
+++ b/net/minecraft/world/level/storage/LevelStorageSource.java
|
||||
@@ -520,15 +520,25 @@ public class LevelStorageSource {
|
||||
@@ -520,15 +520,26 @@ public class LevelStorageSource {
|
||||
private void saveLevelData(CompoundTag tag) {
|
||||
Path path = this.levelDirectory.path();
|
||||
|
||||
+ // Leaf start - Async IO
|
||||
+ // Leaf start - Async playerdata saving
|
||||
+ // Save level.dat asynchronously
|
||||
+ var nbtBytes = new it.unimi.dsi.fastutil.io.FastByteArrayOutputStream(65536);
|
||||
try {
|
||||
- Path path1 = Files.createTempFile(path, "level", ".dat");
|
||||
@@ -55,27 +56,27 @@ index de43e54698125ce9f319d4889dd49f7029fe95e0..4e6737636f754c9e18cb9a2c9e2f3a9e
|
||||
+ LevelStorageSource.LOGGER.error("Failed to save level {}", path, var6);
|
||||
+ }
|
||||
+ });
|
||||
+ // Leaf end
|
||||
+ // Leaf end - Async playerdata saving
|
||||
}
|
||||
|
||||
public Optional<Path> getIconFile() {
|
||||
diff --git a/net/minecraft/world/level/storage/PlayerDataStorage.java b/net/minecraft/world/level/storage/PlayerDataStorage.java
|
||||
index c44110b123ba5912af18faf0065e9ded780da9b7..0c3ba0b8ea8b1b5b66943c40b0f00bf998cf5f18 100644
|
||||
index c44110b123ba5912af18faf0065e9ded780da9b7..541c440d3211c6625974ac1e0055d62655256846 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();
|
||||
+ public final it.unimi.dsi.fastutil.objects.ObjectOpenHashSet<String> saving = new it.unimi.dsi.fastutil.objects.ObjectOpenHashSet<>(); // Leaf
|
||||
+ public final it.unimi.dsi.fastutil.objects.ObjectOpenHashSet<java.util.UUID> savingQueue = new it.unimi.dsi.fastutil.objects.ObjectOpenHashSet<>(); // Leaf - Async playerdata saving
|
||||
|
||||
public PlayerDataStorage(LevelStorageSource.LevelStorageAccess levelStorageAccess, DataFixer fixerUpper) {
|
||||
this.fixerUpper = fixerUpper;
|
||||
@@ -34,17 +35,42 @@ public class PlayerDataStorage {
|
||||
@@ -34,17 +35,43 @@ public class PlayerDataStorage {
|
||||
|
||||
public void save(Player player) {
|
||||
if (org.spigotmc.SpigotConfig.disablePlayerDataSaving) return; // Spigot
|
||||
+ // Leaf start - Async IO
|
||||
+ // Leaf start - Async playerdata saving
|
||||
+ var nbtBytes = new it.unimi.dsi.fastutil.io.FastByteArrayOutputStream(65536);
|
||||
try {
|
||||
CompoundTag compoundTag = player.saveWithoutId(new CompoundTag());
|
||||
@@ -93,14 +94,15 @@ index c44110b123ba5912af18faf0065e9ded780da9b7..0c3ba0b8ea8b1b5b66943c40b0f00bf9
|
||||
}
|
||||
+ String playerName = player.getScoreboardName();
|
||||
+ String stringUuid = player.getStringUUID();
|
||||
+ java.util.UUID playerUuid = player.getUUID();
|
||||
+ synchronized (PlayerDataStorage.this) {
|
||||
+ while (saving.contains(stringUuid)) {
|
||||
+ while (savingQueue.contains(playerUuid)) {
|
||||
+ try {
|
||||
+ Thread.sleep(1L);
|
||||
+ } catch (InterruptedException ignored) {
|
||||
+ }
|
||||
+ }
|
||||
+ saving.add(stringUuid);
|
||||
+ savingQueue.add(playerUuid);
|
||||
+ }
|
||||
+ org.dreeam.leaf.async.AsyncPlayerDataSaving.save(() -> {
|
||||
+ try {
|
||||
@@ -114,28 +116,70 @@ index c44110b123ba5912af18faf0065e9ded780da9b7..0c3ba0b8ea8b1b5b66943c40b0f00bf9
|
||||
+ LOGGER.warn("Failed to save player data for {}", playerName, var7); // Paper - Print exception
|
||||
+ } finally {
|
||||
+ synchronized (PlayerDataStorage.this) {
|
||||
+ saving.remove(stringUuid);
|
||||
+ savingQueue.remove(playerUuid);
|
||||
+ }
|
||||
+ }
|
||||
+ });
|
||||
+ // Leaf end
|
||||
+ // Leaf end - Async playerdata saving
|
||||
}
|
||||
|
||||
private void backup(String name, String stringUuid, String suffix) { // CraftBukkit
|
||||
@@ -61,6 +87,16 @@ public class PlayerDataStorage {
|
||||
@@ -60,7 +87,20 @@ public class PlayerDataStorage {
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<CompoundTag> load(String name, String stringUuid, String suffix) { // CraftBukkit
|
||||
+ // Leaf start
|
||||
- private Optional<CompoundTag> load(String name, String stringUuid, String suffix) { // CraftBukkit
|
||||
+ // Leaf start - Async playerdata saving
|
||||
+ 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
|
||||
+ synchronized (PlayerDataStorage.this) {
|
||||
+ while (saving.contains(stringUuid)) {
|
||||
+ while (savingQueue.contains(playerUuid)) {
|
||||
+ try {
|
||||
+ Thread.sleep(1L);
|
||||
+ } catch (InterruptedException ignored) {
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ // Leaf end
|
||||
+ // Leaf end - Async playerdata saving
|
||||
File file = new File(this.playerDir, stringUuid + suffix); // CraftBukkit
|
||||
// Spigot start
|
||||
boolean usingWrongFile = false;
|
||||
@@ -91,7 +131,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
|
||||
if (player instanceof ServerPlayer serverPlayer) {
|
||||
CraftPlayer craftPlayer = serverPlayer.getBukkitEntity();
|
||||
// Only update first played if it is older than the one we have
|
||||
@@ -106,20 +146,25 @@ public class PlayerDataStorage {
|
||||
});
|
||||
}
|
||||
|
||||
+ // Leaf start - Async playerdata saving
|
||||
public Optional<CompoundTag> load(String name, String uuid) {
|
||||
+ return this.load(name, uuid, java.util.UUID.fromString(uuid));
|
||||
+ }
|
||||
+ public Optional<CompoundTag> load(String name, String uuid, java.util.UUID playerUuid) {
|
||||
// CraftBukkit end
|
||||
- Optional<CompoundTag> optional = this.load(name, uuid, ".dat"); // CraftBukkit
|
||||
+ Optional<CompoundTag> optional = this.load(name, uuid, ".dat", playerUuid); // CraftBukkit
|
||||
if (optional.isEmpty()) {
|
||||
this.backup(name, uuid, ".dat"); // CraftBukkit
|
||||
}
|
||||
|
||||
- return optional.or(() -> this.load(name, uuid, ".dat_old")).map(compoundTag -> { // CraftBukkit
|
||||
+ return optional.or(() -> this.load(name, uuid, ".dat_old", playerUuid)).map(compoundTag -> { // CraftBukkit
|
||||
int dataVersion = NbtUtils.getDataVersion(compoundTag, -1);
|
||||
compoundTag = ca.spottedleaf.dataconverter.minecraft.MCDataConverter.convertTag(ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.PLAYER, compoundTag, dataVersion, net.minecraft.SharedConstants.getCurrentVersion().getDataVersion().getVersion()); // Paper - rewrite data conversion system
|
||||
// player.load(compoundTag); // CraftBukkit - handled above
|
||||
return compoundTag;
|
||||
});
|
||||
}
|
||||
+ // Leaf end - Async playerdata saving
|
||||
|
||||
// CraftBukkit start
|
||||
public File getPlayerDir() {
|
||||
|
||||
@@ -7,10 +7,10 @@ Original license: AGPL-3.0
|
||||
Original project: https://github.com/snackbag/TT20
|
||||
|
||||
diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java
|
||||
index cc2bf337da1c240b82dc722970d6bbaf57331328..3597f7dd2d71fe136604518985e3d14461a6aad4 100644
|
||||
index 1cb60107d95296fc9e2c106d70838c057564abeb..c50a301a0c2365c2052aefc6a23fcf6fa82e1b9d 100644
|
||||
--- a/net/minecraft/server/MinecraftServer.java
|
||||
+++ b/net/minecraft/server/MinecraftServer.java
|
||||
@@ -1550,6 +1550,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
||||
@@ -1556,6 +1556,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
||||
|
||||
this.server.spark.tickStart(); // Paper - spark
|
||||
new com.destroystokyo.paper.event.server.ServerTickStartEvent(this.tickCount+1).callEvent(); // Paper - Server Tick Events
|
||||
|
||||
@@ -265,7 +265,7 @@ index 5ab2c8333178335515e619b87ae420f948c83bd1..be3b2f023897a8823560ee059cb16ec9
|
||||
}
|
||||
|
||||
diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java
|
||||
index 8744690b69be75f9dbcfe5a52bb90ed387eaffd7..f2d1025a13649c35991a662c267a7653e75d19a2 100644
|
||||
index c50a301a0c2365c2052aefc6a23fcf6fa82e1b9d..ac751d460ae0c8dbb858c4047c459a11b57ae175 100644
|
||||
--- a/net/minecraft/server/MinecraftServer.java
|
||||
+++ b/net/minecraft/server/MinecraftServer.java
|
||||
@@ -291,6 +291,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
||||
@@ -326,7 +326,7 @@ index 8744690b69be75f9dbcfe5a52bb90ed387eaffd7..f2d1025a13649c35991a662c267a7653
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1671,6 +1684,18 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
||||
@@ -1677,6 +1690,18 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,7 +345,7 @@ index 8744690b69be75f9dbcfe5a52bb90ed387eaffd7..f2d1025a13649c35991a662c267a7653
|
||||
protected void tickChildren(BooleanSupplier hasTimeLeft) {
|
||||
this.getPlayerList().getPlayers().forEach(serverPlayer1 -> serverPlayer1.connection.suspendFlushing());
|
||||
this.server.getScheduler().mainThreadHeartbeat(); // CraftBukkit
|
||||
@@ -1737,28 +1762,50 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
||||
@@ -1743,28 +1768,50 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
||||
}
|
||||
|
||||
this.isIteratingOverLevels = true; // Paper - Throw exception on world create while being ticked
|
||||
@@ -414,7 +414,7 @@ index 8744690b69be75f9dbcfe5a52bb90ed387eaffd7..f2d1025a13649c35991a662c267a7653
|
||||
this.isIteratingOverLevels = false; // Paper - Throw exception on world create while being ticked
|
||||
|
||||
this.tickConnection();
|
||||
@@ -1838,6 +1885,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
||||
@@ -1844,6 +1891,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
||||
Map<ResourceKey<Level>, ServerLevel> oldLevels = this.levels;
|
||||
Map<ResourceKey<Level>, ServerLevel> newLevels = Maps.newLinkedHashMap(oldLevels);
|
||||
newLevels.remove(level.dimension());
|
||||
|
||||
@@ -6,10 +6,10 @@ Subject: [PATCH] SparklyPaper: Track each world MSPT
|
||||
Original project: https://github.com/SparklyPower/SparklyPaper
|
||||
|
||||
diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java
|
||||
index f2d1025a13649c35991a662c267a7653e75d19a2..7739b4955dcb489c6bba9c9db65ba87025f7c669 100644
|
||||
index ac751d460ae0c8dbb858c4047c459a11b57ae175..24926aa7ed5c78b235659daf18b224b14beb744c 100644
|
||||
--- a/net/minecraft/server/MinecraftServer.java
|
||||
+++ b/net/minecraft/server/MinecraftServer.java
|
||||
@@ -1687,7 +1687,16 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
||||
@@ -1693,7 +1693,16 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
|
||||
// Leaf start - SparklyPaper - parallel world ticking mod (move level ticking logic out for branch convergence)
|
||||
private void tickLevel(ServerLevel serverLevel, BooleanSupplier hasTimeLeft) {
|
||||
try {
|
||||
|
||||
@@ -3,25 +3,34 @@ From: Taiyou06 <kaandindar21@gmail.com>
|
||||
Date: Sat, 22 Mar 2025 12:51:28 +0100
|
||||
Subject: [PATCH] Remove streams on PlayerDetector
|
||||
|
||||
Dreeam TODO: Merge to single loop
|
||||
|
||||
diff --git a/net/minecraft/world/level/block/entity/trialspawner/PlayerDetector.java b/net/minecraft/world/level/block/entity/trialspawner/PlayerDetector.java
|
||||
index 774d4028f0be7c388abb47f8eb97011341f50f59..1597af2092e993efb1c02f92ed14d8c5a500403a 100644
|
||||
index 774d4028f0be7c388abb47f8eb97011341f50f59..81d2dfdd8bd2a8b205e4b617911c277a366f0369 100644
|
||||
--- a/net/minecraft/world/level/block/entity/trialspawner/PlayerDetector.java
|
||||
+++ b/net/minecraft/world/level/block/entity/trialspawner/PlayerDetector.java
|
||||
@@ -21,27 +21,42 @@ import net.minecraft.world.phys.Vec3;
|
||||
@@ -21,28 +21,45 @@ import net.minecraft.world.phys.Vec3;
|
||||
import net.minecraft.world.phys.shapes.CollisionContext;
|
||||
|
||||
public interface PlayerDetector {
|
||||
- PlayerDetector NO_CREATIVE_PLAYERS = (level, entitySelector, pos, maxDistance, requireLineOfSight) -> entitySelector.getPlayers(
|
||||
+ PlayerDetector NO_CREATIVE_PLAYERS = (level, entitySelector, pos, maxDistance, requireLineOfSight) -> {
|
||||
+ List<? extends Player> players = entitySelector.getPlayers(
|
||||
level, player -> player.blockPosition().closerThan(pos, maxDistance) && !player.isCreative() && !player.isSpectator()
|
||||
- level, player -> player.blockPosition().closerThan(pos, maxDistance) && !player.isCreative() && !player.isSpectator()
|
||||
- )
|
||||
- .stream()
|
||||
- .filter(player -> !requireLineOfSight || inLineOfSight(level, pos.getCenter(), player.getEyePosition()))
|
||||
- .map(Entity::getUUID)
|
||||
- .toList();
|
||||
- PlayerDetector INCLUDING_CREATIVE_PLAYERS = (level, entitySelector, pos, maxDistance, requireLineOfSight) -> entitySelector.getPlayers(
|
||||
- level, player -> player.blockPosition().closerThan(pos, maxDistance) && !player.isSpectator()
|
||||
- )
|
||||
- .stream()
|
||||
- .filter(player -> !requireLineOfSight || inLineOfSight(level, pos.getCenter(), player.getEyePosition()))
|
||||
- .map(Entity::getUUID)
|
||||
- .toList();
|
||||
+ // Leaf start - Remove streams on PlayerDetector
|
||||
+ PlayerDetector NO_CREATIVE_PLAYERS = (level, entitySelector, pos, maxDistance, requireLineOfSight) -> {
|
||||
+ List<? extends Player> players = entitySelector.getPlayers(
|
||||
+ level, player -> player.blockPosition().closerThan(pos, maxDistance) && !player.isCreative() && !player.isSpectator()
|
||||
+ );
|
||||
+ List<UUID> result = new java.util.ArrayList<>();
|
||||
+ for (Player player : players) {
|
||||
@@ -34,12 +43,7 @@ index 774d4028f0be7c388abb47f8eb97011341f50f59..1597af2092e993efb1c02f92ed14d8c5
|
||||
+
|
||||
+ PlayerDetector INCLUDING_CREATIVE_PLAYERS = (level, entitySelector, pos, maxDistance, requireLineOfSight) -> {
|
||||
+ List<? extends Player> players = entitySelector.getPlayers(
|
||||
level, player -> player.blockPosition().closerThan(pos, maxDistance) && !player.isSpectator()
|
||||
- )
|
||||
- .stream()
|
||||
- .filter(player -> !requireLineOfSight || inLineOfSight(level, pos.getCenter(), player.getEyePosition()))
|
||||
- .map(Entity::getUUID)
|
||||
- .toList();
|
||||
+ level, player -> player.blockPosition().closerThan(pos, maxDistance) && !player.isSpectator()
|
||||
+ );
|
||||
+ List<UUID> result = new java.util.ArrayList<>();
|
||||
+ for (Player player : players) {
|
||||
@@ -66,13 +70,16 @@ index 774d4028f0be7c388abb47f8eb97011341f50f59..1597af2092e993efb1c02f92ed14d8c5
|
||||
+ }
|
||||
+ return result;
|
||||
};
|
||||
+ // Leaf end - Remove streams on PlayerDetector
|
||||
|
||||
List<UUID> detect(ServerLevel level, PlayerDetector.EntitySelector entitySelector, BlockPos pos, double maxDistance, boolean flag);
|
||||
@@ -78,14 +93,27 @@ public interface PlayerDetector {
|
||||
|
||||
@@ -78,14 +95,31 @@ public interface PlayerDetector {
|
||||
return new PlayerDetector.EntitySelector() {
|
||||
@Override
|
||||
public List<Player> getPlayers(ServerLevel level, Predicate<? super Player> predicate) {
|
||||
- return players.stream().filter(predicate).toList();
|
||||
+ // Leaf start - Remove streams on PlayerDetector
|
||||
+ List<Player> result = new java.util.ArrayList<>();
|
||||
+ for (Player player : players) {
|
||||
+ if (predicate.test(player)) {
|
||||
@@ -80,6 +87,7 @@ index 774d4028f0be7c388abb47f8eb97011341f50f59..1597af2092e993efb1c02f92ed14d8c5
|
||||
+ }
|
||||
+ }
|
||||
+ return result;
|
||||
+ // Leaf end - Remove streams on PlayerDetector
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -87,6 +95,7 @@ index 774d4028f0be7c388abb47f8eb97011341f50f59..1597af2092e993efb1c02f92ed14d8c5
|
||||
ServerLevel level, EntityTypeTest<Entity, T> typeTest, AABB boundingBox, Predicate<? super T> predicate
|
||||
) {
|
||||
- return players.stream().map(typeTest::tryCast).filter(Objects::nonNull).filter(predicate).toList();
|
||||
+ // Leaf start - Remove streams on PlayerDetector
|
||||
+ List<T> result = new java.util.ArrayList<>();
|
||||
+ for (Player player : players) {
|
||||
+ T entity = typeTest.tryCast(player);
|
||||
@@ -95,6 +104,7 @@ index 774d4028f0be7c388abb47f8eb97011341f50f59..1597af2092e993efb1c02f92ed14d8c5
|
||||
+ }
|
||||
+ }
|
||||
+ return result;
|
||||
+ // Leaf end - Remove streams on PlayerDetector
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,50 +5,36 @@ Subject: [PATCH] Async Block Finding
|
||||
|
||||
|
||||
diff --git a/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java b/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java
|
||||
index 3f080b15543bf8c5fa0774b62d7f12e13b82511a..51a949dbb88d7fff076379962000b70acecacce3 100644
|
||||
index 3f080b15543bf8c5fa0774b62d7f12e13b82511a..2bc0f19b86067491f33f647d2e387acd83492844 100644
|
||||
--- a/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java
|
||||
+++ b/net/minecraft/world/entity/ai/goal/MoveToBlockGoal.java
|
||||
@@ -1,9 +1,16 @@
|
||||
package net.minecraft.world.entity.ai.goal;
|
||||
|
||||
import java.util.EnumSet;
|
||||
+import java.util.ArrayList;
|
||||
+import java.util.List;
|
||||
+import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
+import java.util.concurrent.ExecutorService;
|
||||
+import java.util.concurrent.Executors;
|
||||
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.entity.PathfinderMob;
|
||||
import net.minecraft.world.level.LevelReader;
|
||||
+import org.dreeam.leaf.config.modules.async.AsyncBlockFinding;
|
||||
|
||||
public abstract class MoveToBlockGoal extends Goal {
|
||||
private static final int GIVE_UP_TICKS = 1200;
|
||||
@@ -20,6 +27,17 @@ public abstract class MoveToBlockGoal extends Goal {
|
||||
@@ -20,6 +20,19 @@ public abstract class MoveToBlockGoal extends Goal {
|
||||
private final int verticalSearchRange;
|
||||
protected int verticalSearchStart;
|
||||
|
||||
+ private static final ExecutorService BLOCK_FINDER_EXECUTOR =
|
||||
+ Executors.newSingleThreadExecutor(
|
||||
+ new ThreadFactoryBuilder()
|
||||
+ .setNameFormat("block-finder-%d")
|
||||
+ // Leaf start - Async Block Finding
|
||||
+ private static final java.util.concurrent.ExecutorService BLOCK_FINDER_EXECUTOR =
|
||||
+ java.util.concurrent.Executors.newSingleThreadExecutor(
|
||||
+ new com.google.common.util.concurrent.ThreadFactoryBuilder()
|
||||
+ .setNameFormat("Leaf Block Finding Thread - %d")
|
||||
+ .setDaemon(true)
|
||||
+ .build());
|
||||
+
|
||||
+ private final ConcurrentLinkedQueue<BlockPos> candidateBlocks = new ConcurrentLinkedQueue<>();
|
||||
+ private final java.util.concurrent.ConcurrentLinkedQueue<BlockPos> candidateBlocks = new java.util.concurrent.ConcurrentLinkedQueue<>();
|
||||
+ private boolean asyncSearchInProgress = false;
|
||||
+ private boolean searchComplete = false;
|
||||
+ // Leaf end - Async Block Finding
|
||||
+
|
||||
public MoveToBlockGoal(PathfinderMob mob, double speedModifier, int searchRange) {
|
||||
this(mob, speedModifier, searchRange, 1);
|
||||
}
|
||||
@@ -109,6 +127,92 @@ public abstract class MoveToBlockGoal extends Goal {
|
||||
@@ -109,6 +122,95 @@ public abstract class MoveToBlockGoal extends Goal {
|
||||
}
|
||||
|
||||
protected boolean findNearestBlock() {
|
||||
+ // Leaf start - Async Block Finding
|
||||
+ // Check if async is enabled, if not use the original implementation
|
||||
+ if (!AsyncBlockFinding.enabled) {
|
||||
+ if (!org.dreeam.leaf.config.modules.async.AsyncBlockFinding.enabled) {
|
||||
+ return findNearestBlockSync();
|
||||
+ }
|
||||
+
|
||||
@@ -59,6 +45,7 @@ index 3f080b15543bf8c5fa0774b62d7f12e13b82511a..51a949dbb88d7fff076379962000b70a
|
||||
+ candidateBlocks.clear();
|
||||
+ return false;
|
||||
+ }
|
||||
+
|
||||
+ while (!candidateBlocks.isEmpty()) {
|
||||
+ BlockPos pos = candidateBlocks.poll();
|
||||
+
|
||||
@@ -105,7 +92,7 @@ index 3f080b15543bf8c5fa0774b62d7f12e13b82511a..51a949dbb88d7fff076379962000b70a
|
||||
+ // Generate candidate blocks in a spiral pattern
|
||||
+ private void generateCandidateBlocks(BlockPos center, int searchRange, int verticalRange, int verticalStart) {
|
||||
+ // Pre-calculate a prioritized list of positions
|
||||
+ List<BlockPos> positions = new ArrayList<>();
|
||||
+ java.util.List<BlockPos> positions = new java.util.ArrayList<>();
|
||||
+
|
||||
+ for (int i2 = verticalStart; i2 <= verticalRange; i2 = i2 > 0 ? -i2 : 1 - i2) {
|
||||
+ for (int i3 = 0; i3 < searchRange; i3++) {
|
||||
@@ -133,6 +120,7 @@ index 3f080b15543bf8c5fa0774b62d7f12e13b82511a..51a949dbb88d7fff076379962000b70a
|
||||
+
|
||||
+ // The original method renamed
|
||||
+ protected boolean findNearestBlockSync() {
|
||||
+ // Leaf end - Async Block Finding
|
||||
int i = this.searchRange;
|
||||
int i1 = this.verticalSearchRange;
|
||||
BlockPos blockPos = this.mob.blockPosition();
|
||||
|
||||
@@ -5,25 +5,20 @@ Subject: [PATCH] Use direct iteration on Sensing.tick
|
||||
|
||||
|
||||
diff --git a/net/minecraft/world/entity/ai/sensing/Sensing.java b/net/minecraft/world/entity/ai/sensing/Sensing.java
|
||||
index cc25f5838aec5ed9fca2fb8b0322fafad9397a46..2ea5095a4290f5236caad7ea30a59d9b3eb32aaa 100644
|
||||
index cc25f5838aec5ed9fca2fb8b0322fafad9397a46..002d3c0d8b1107a275020d5c582c37e9a5c536ee 100644
|
||||
--- a/net/minecraft/world/entity/ai/sensing/Sensing.java
|
||||
+++ b/net/minecraft/world/entity/ai/sensing/Sensing.java
|
||||
@@ -34,12 +34,16 @@ public class Sensing {
|
||||
|
||||
public void tick() {
|
||||
if (this.expiring == null) { // Gale - Petal - reduce line of sight updates
|
||||
- this.seen.clear();
|
||||
+ this.seen.clear();
|
||||
// Gale start - Petal - reduce line of sight updates
|
||||
@@ -39,7 +39,12 @@ public class Sensing {
|
||||
} else {
|
||||
var expiringNow = this.expiring[this.nextToExpireIndex];
|
||||
|
||||
- expiringNow.forEach(this.seen::remove);
|
||||
+ // Leaf - use direct iteration
|
||||
+ // Leaf start - Use direct iteration on Sensing.tick
|
||||
+ var iterator = expiringNow.iterator();
|
||||
+ while (iterator.hasNext()) {
|
||||
+ this.seen.remove(iterator.nextInt());
|
||||
+ }
|
||||
+ // Leaf end - Use direct iteration on Sensing.tick
|
||||
expiringNow.clear();
|
||||
|
||||
this.currentCacheAddIndex++;
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Taiyou06 <kaandindar21@gmail.com>
|
||||
Date: Wed, 26 Mar 2025 17:33:30 +0100
|
||||
Subject: [PATCH] Optimize NonFlush PacketSending
|
||||
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
|
||||
Date: Tue, 22 Sep 2020 01:49:19 -0700
|
||||
Subject: [PATCH] Optimise non-flush packet sending
|
||||
|
||||
Original license: GPLv3
|
||||
Original project: https://github.com/PaperMC/Paper
|
||||
Paper pull request: https://github.com/PaperMC/Paper/pull/10172
|
||||
|
||||
Places like entity tracking make heavy use of packet sending,
|
||||
and internally netty will use some very expensive thread wakeup
|
||||
@@ -22,14 +26,14 @@ Locally this patch drops the entity tracker tick by a full 1.5x.
|
||||
Co-authored-by: Quang Tran <3d7777456@gmail.com>
|
||||
|
||||
diff --git a/net/minecraft/network/Connection.java b/net/minecraft/network/Connection.java
|
||||
index 5b46036868b6c9d082e35591e58735e16adaae62..208674673169b7258d9411d818870a640df08b24 100644
|
||||
index 5b46036868b6c9d082e35591e58735e16adaae62..9563165c945757996da11f55e2221e620dd93327 100644
|
||||
--- a/net/minecraft/network/Connection.java
|
||||
+++ b/net/minecraft/network/Connection.java
|
||||
@@ -147,6 +147,7 @@ public class Connection extends SimpleChannelInboundHandler<Packet<?>> {
|
||||
@Nullable public SocketAddress haProxyAddress; // Paper - Add API to get player's proxy address
|
||||
// Paper start - Optimize network
|
||||
public boolean isPending = true;
|
||||
+ private io.netty.channel.SingleThreadEventLoop eventLoop; // Leaf - optimise packets that are not flushed
|
||||
+ private io.netty.channel.SingleThreadEventLoop eventLoop; // Paper - optimise packets that are not flushed
|
||||
public boolean queueImmunity;
|
||||
// Paper end - Optimize network
|
||||
|
||||
@@ -37,7 +41,7 @@ index 5b46036868b6c9d082e35591e58735e16adaae62..208674673169b7258d9411d818870a64
|
||||
public void channelActive(ChannelHandlerContext context) throws Exception {
|
||||
super.channelActive(context);
|
||||
this.channel = context.channel();
|
||||
+ this.eventLoop = (io.netty.channel.SingleThreadEventLoop) this.channel.eventLoop(); // Leaf - optimise packets that are not flushed
|
||||
+ this.eventLoop = (io.netty.channel.SingleThreadEventLoop) this.channel.eventLoop(); // Paper - optimise packets that are not flushed
|
||||
this.address = this.channel.remoteAddress();
|
||||
this.preparing = false; // Spigot
|
||||
if (this.delayedDisconnect != null) {
|
||||
@@ -45,11 +49,11 @@ index 5b46036868b6c9d082e35591e58735e16adaae62..208674673169b7258d9411d818870a64
|
||||
if (this.channel.eventLoop().inEventLoop()) {
|
||||
this.doSendPacket(packet, sendListener, flush);
|
||||
} else {
|
||||
+ // Leaf start - optimise packets that are not flushed
|
||||
+ // Paper start - optimise packets that are not flushed
|
||||
+ if (!flush && org.dreeam.leaf.config.modules.network.OptimizeNonFlushPacketSending.enabled) {
|
||||
+ this.eventLoop.lazyExecute(() -> this.doSendPacket(packet, sendListener, flush));
|
||||
+ } else
|
||||
+ // Leaf end - optimise packets that are not flushed
|
||||
+ // Paper end - optimise packets that are not flushed
|
||||
this.channel.eventLoop().execute(() -> this.doSendPacket(packet, sendListener, flush));
|
||||
}
|
||||
}
|
||||
@@ -612,7 +612,7 @@ index 55572e799b5c8a74a546ac8febc14f80d5731c52..08a06c23c831a4de45b3e537228b8379
|
||||
// Paper end
|
||||
}
|
||||
diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
|
||||
index c2552c3706831f7012b5b449fa43c7d5990056a4..2f9b7627f1801239ee6a89f334a266629b98c989 100644
|
||||
index c2552c3706831f7012b5b449fa43c7d5990056a4..4e8a1d01a6c0afef92ae56cc4909af06d63e5268 100644
|
||||
--- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
|
||||
+++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java
|
||||
@@ -961,6 +961,28 @@ public class CraftEventFactory {
|
||||
@@ -652,7 +652,7 @@ index c2552c3706831f7012b5b449fa43c7d5990056a4..2f9b7627f1801239ee6a89f334a26662
|
||||
+ // Leaf start - SparklyPaper parallel world ticking mod (collapse original behavior)
|
||||
+ final BlockPos sourceBlockOverrideRTSnap = getSourceBlockOverrideRT();
|
||||
+ BlockSpreadEvent event = new BlockSpreadEvent(state.getBlock(), CraftBlock.at(world, sourceBlockOverrideRTSnap != null ? sourceBlockOverrideRTSnap : source), state); // SparklyPaper - parallel world ticking
|
||||
+ // Leaf end
|
||||
+ // Leaf end - SparklyPaper parallel world ticking mod (collapse original behavior)
|
||||
Bukkit.getPluginManager().callEvent(event);
|
||||
|
||||
if (!event.isCancelled()) {
|
||||
@@ -666,23 +666,26 @@ index c2552c3706831f7012b5b449fa43c7d5990056a4..2f9b7627f1801239ee6a89f334a26662
|
||||
return itemStack;
|
||||
}
|
||||
diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncTask.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncTask.java
|
||||
index e4e2e42d0ca25df7fe9f2dd4275610e45fcb2c84..c4de7346703cbe457dbffed7716a5816314996a1 100644
|
||||
index e4e2e42d0ca25df7fe9f2dd4275610e45fcb2c84..e7c6b2ab5f2c68f3319ccd52785c8d3488a2eef7 100644
|
||||
--- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncTask.java
|
||||
+++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftAsyncTask.java
|
||||
@@ -19,11 +19,35 @@ class CraftAsyncTask extends CraftTask {
|
||||
@@ -19,11 +19,39 @@ class CraftAsyncTask extends CraftTask {
|
||||
|
||||
@Override
|
||||
public boolean isSync() {
|
||||
+ // Leaf start - Parallel world ticking
|
||||
+ // Return true if we should run this task synchronously when parallel world ticking is enabled and runAsyncTasksSync is true
|
||||
+ if (org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.enabled &&
|
||||
+ org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.runAsyncTasksSync) {
|
||||
+ return true;
|
||||
+ }
|
||||
+ // Leaf end - Parallel world ticking
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
+ // Leaf start - Parallel world ticking
|
||||
+ // If parallel world ticking is enabled and we're configured to run async tasks sync,
|
||||
+ // execute the task as if it were a sync task (directly on the main thread)
|
||||
+ if (org.dreeam.leaf.config.modules.async.SparklyPaperParallelWorldTicking.enabled &&
|
||||
@@ -700,85 +703,9 @@ index e4e2e42d0ca25df7fe9f2dd4275610e45fcb2c84..c4de7346703cbe457dbffed7716a5816
|
||||
+ }
|
||||
+ return;
|
||||
+ }
|
||||
+ // Leaf end - Parallel world ticking
|
||||
+
|
||||
+ // Original async implementation
|
||||
final Thread thread = Thread.currentThread();
|
||||
// Paper start - name threads according to running plugin
|
||||
final String nameBefore = thread.getName();
|
||||
@@ -52,49 +76,49 @@ class CraftAsyncTask extends CraftTask {
|
||||
}
|
||||
});
|
||||
}
|
||||
- Throwable thrown = null;
|
||||
- try {
|
||||
- super.run();
|
||||
- } catch (final Throwable t) {
|
||||
- thrown = t;
|
||||
- this.getOwner().getLogger().log(
|
||||
+ Throwable thrown = null;
|
||||
+ try {
|
||||
+ super.run();
|
||||
+ } catch (final Throwable t) {
|
||||
+ thrown = t;
|
||||
+ this.getOwner().getLogger().log(
|
||||
Level.WARNING,
|
||||
String.format(
|
||||
"Plugin %s generated an exception while executing task %s",
|
||||
this.getOwner().getDescription().getFullName(),
|
||||
this.getTaskId()),
|
||||
thrown);
|
||||
- } finally {
|
||||
- // Cleanup is important for any async task, otherwise ghost tasks are everywhere
|
||||
- synchronized (this.workers) {
|
||||
- try {
|
||||
- final Iterator<BukkitWorker> workers = this.workers.iterator();
|
||||
- boolean removed = false;
|
||||
- while (workers.hasNext()) {
|
||||
- if (workers.next().getThread() == thread) {
|
||||
- workers.remove();
|
||||
- removed = true; // Don't throw exception
|
||||
- break;
|
||||
+ } finally {
|
||||
+ // Cleanup is important for any async task, otherwise ghost tasks are everywhere
|
||||
+ synchronized (this.workers) {
|
||||
+ try {
|
||||
+ final Iterator<BukkitWorker> workers = this.workers.iterator();
|
||||
+ boolean removed = false;
|
||||
+ while (workers.hasNext()) {
|
||||
+ if (workers.next().getThread() == thread) {
|
||||
+ workers.remove();
|
||||
+ removed = true; // Don't throw exception
|
||||
+ break;
|
||||
+ }
|
||||
}
|
||||
- }
|
||||
- if (!removed) {
|
||||
- throw new IllegalStateException(
|
||||
+ if (!removed) {
|
||||
+ throw new IllegalStateException(
|
||||
String.format(
|
||||
"Unable to remove worker %s on task %s for %s",
|
||||
thread.getName(),
|
||||
this.getTaskId(),
|
||||
this.getOwner().getDescription().getFullName()),
|
||||
thrown); // We don't want to lose the original exception, if any
|
||||
- }
|
||||
- } finally {
|
||||
- if (this.getPeriod() < 0 && this.workers.isEmpty()) {
|
||||
- // At this spot, we know we are the final async task being executed!
|
||||
- // Because we have the lock, nothing else is running or will run because delay < 0
|
||||
- this.runners.remove(this.getTaskId());
|
||||
+ }
|
||||
+ } finally {
|
||||
+ if (this.getPeriod() < 0 && this.workers.isEmpty()) {
|
||||
+ // At this spot, we know we are the final async task being executed!
|
||||
+ // Because we have the lock, nothing else is running or will run because delay < 0
|
||||
+ this.runners.remove(this.getTaskId());
|
||||
+ }
|
||||
}
|
||||
}
|
||||
}
|
||||
- }
|
||||
} finally { thread.setName(nameBefore); } // Paper - name threads according to running plugin
|
||||
}
|
||||
|
||||
|
||||
@@ -5,24 +5,33 @@ 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..f64844fb39cd02d8645491a8b482b21643334500 100644
|
||||
index f2d87c12dd19210ce7e2147fada5c10191008632..0cc6b6c597ca8c955b246764de637be4c25bfb40 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
private CompoundTag getBukkitData() {
|
||||
@@ -813,6 +813,17 @@ public class CraftOfflinePlayer implements OfflinePlayer, ConfigurationSerializa
|
||||
* @param compoundTag
|
||||
*/
|
||||
private void save(CompoundTag compoundTag) {
|
||||
+ // Leaf start
|
||||
+ // Leaf start - Async playerdata saving
|
||||
+ synchronized (server.console.playerDataStorage) {
|
||||
+ while (server.console.playerDataStorage.saving.contains(getUniqueId().toString())) {
|
||||
+ while (server.console.playerDataStorage.savingQueue.contains(getUniqueId())) {
|
||||
+ try {
|
||||
+ Thread.sleep(1L);
|
||||
+ } catch (InterruptedException ignored) {
|
||||
+ }
|
||||
+ }
|
||||
+ server.console.playerDataStorage.saving.add(getUniqueId().toString());
|
||||
+ server.console.playerDataStorage.savingQueue.add(getUniqueId());
|
||||
+ }
|
||||
+ // Leaf end
|
||||
+ // Leaf end - Async playerdata saving
|
||||
File playerDir = server.console.playerDataStorage.getPlayerDir();
|
||||
try {
|
||||
File tempFile = File.createTempFile(this.getUniqueId()+"-", ".dat", playerDir);
|
||||
@@ -30,12 +39,12 @@ index f2d87c12dd19210ce7e2147fada5c10191008632..f64844fb39cd02d8645491a8b482b216
|
||||
net.minecraft.Util.safeReplaceFile(playerDataFile.toPath(), tempFile.toPath(), playerDataFileOld.toPath());
|
||||
} catch (java.io.IOException e) {
|
||||
e.printStackTrace();
|
||||
+ // Leaf start
|
||||
+ // Leaf start - Async playerdata saving
|
||||
+ } finally {
|
||||
+ synchronized (server.console.playerDataStorage) {
|
||||
+ server.console.playerDataStorage.saving.remove(getUniqueId().toString());
|
||||
+ server.console.playerDataStorage.savingQueue.remove(getUniqueId());
|
||||
+ }
|
||||
+ // Leaf end
|
||||
+ // Leaf end - Async playerdata saving
|
||||
}
|
||||
}
|
||||
// Purpur end - OfflinePlayer API
|
||||
|
||||
@@ -8,6 +8,7 @@ import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class AsyncPlayerDataSaving {
|
||||
|
||||
public static final ExecutorService IO_POOL = new ThreadPoolExecutor(
|
||||
1, 1, 0, TimeUnit.MILLISECONDS,
|
||||
new LinkedBlockingQueue<>(),
|
||||
|
||||
@@ -12,7 +12,6 @@ public class AsyncBlockFinding extends ConfigModules {
|
||||
|
||||
@Experimental
|
||||
public static boolean enabled = false;
|
||||
|
||||
public static boolean asyncBlockFindingInitialized;
|
||||
|
||||
@Override
|
||||
|
||||
@@ -14,22 +14,22 @@ public class OptimizeNonFlushPacketSending extends ConfigModules {
|
||||
@Override
|
||||
public void onLoaded() {
|
||||
enabled = config.getBoolean(getBasePath() + ".OptimizeNonFlushPacketSending", enabled, config.pickStringRegionBased("""
|
||||
WARNING: This option is NOT compatible with ProtocolLib and may cause
|
||||
WARNING: This option is NOT compatible with ProtocolLib and may cause
|
||||
issues with other plugins that modify packet handling.
|
||||
|
||||
Optimizes non-flush packet sending by using Netty's lazyExecute method to avoid
|
||||
Optimizes non-flush packet sending by using Netty's lazyExecute method to avoid
|
||||
expensive thread wakeup calls when scheduling packet operations.
|
||||
|
||||
Requires server restart to take effect.
|
||||
""",
|
||||
"""
|
||||
警告:此选项与 ProtocolLib 不兼容,并可能导致与其他修改数据包
|
||||
处理的插件出现问题。
|
||||
警告: 此选项与 ProtocolLib 不兼容, 并可能导致与其他修改数据包
|
||||
处理的插件出现问题.
|
||||
|
||||
通过使用 Netty 的 lazyExecute 方法来优化非刷新数据包的发送,
|
||||
避免在调度数据包操作时进行昂贵的线程唤醒调用。
|
||||
通过使用 Netty 的 lazyExecute 方法来优化非刷新数据包的发送,
|
||||
避免在调度数据包操作时进行昂贵的线程唤醒调用.
|
||||
|
||||
需要重启服务器才能生效。
|
||||
需要重启服务器才能生效.
|
||||
"""));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user