diff --git a/divinemc-server/minecraft-patches/features/0004-Async-Pathfinding.patch b/divinemc-server/minecraft-patches/features/0004-Async-Pathfinding.patch index dca0f27..f6233a8 100644 --- a/divinemc-server/minecraft-patches/features/0004-Async-Pathfinding.patch +++ b/divinemc-server/minecraft-patches/features/0004-Async-Pathfinding.patch @@ -9,10 +9,10 @@ You can find the original code on https://github.com/Bloom-host/Petal Makes most pathfinding-related work happen asynchronously diff --git a/net/minecraft/world/entity/ai/behavior/AcquirePoi.java b/net/minecraft/world/entity/ai/behavior/AcquirePoi.java -index 67cbf9f5760fae5db6f31e64095cd1b6be6ade8e..aadc05082a029ab41ab2512f5cd5b3d2a361e097 100644 +index 67cbf9f5760fae5db6f31e64095cd1b6be6ade8e..7ad36864d700f3012a3791bf3814465e5e9deffa 100644 --- a/net/minecraft/world/entity/ai/behavior/AcquirePoi.java +++ b/net/minecraft/world/entity/ai/behavior/AcquirePoi.java -@@ -94,21 +94,54 @@ public class AcquirePoi { +@@ -94,21 +94,18 @@ public class AcquirePoi { } } // Paper end - optimise POI access @@ -27,57 +27,57 @@ index 67cbf9f5760fae5db6f31e64095cd1b6be6ade8e..aadc05082a029ab41ab2512f5cd5b3d2 - DebugPackets.sendPoiTicketCountPacket(level, target); + // DivineMC start - Async path processing + if (org.bxteam.divinemc.DivineConfig.asyncPathfinding) { -+ // await on path async + Path possiblePath = findPathToPois(mob, set); + -+ // wait on the path to be processed + org.bxteam.divinemc.entity.pathfinding.AsyncPathProcessor.awaitProcessing(possiblePath, path -> { -+ // read canReach check -+ if (path == null || !path.canReach()) { -+ for (Pair, BlockPos> pair : set) { -+ map.computeIfAbsent( -+ pair.getSecond().asLong(), -+ m -> new JitteredLinearRetry(mob.level().random, time) -+ ); -+ } -+ return; -+ } -+ BlockPos blockPos = path.getTarget(); -+ poiManager.getType(blockPos).ifPresent(poiType -> { -+ poiManager.take(acquirablePois, -+ (holder, blockPos2) -> blockPos2.equals(blockPos), -+ blockPos, -+ 1 -+ ); -+ memoryAccessor.set(GlobalPos.of(level.dimension(), blockPos)); -+ entityEventId.ifPresent(status -> level.broadcastEntityEvent(mob, status)); -+ map.clear(); -+ DebugPackets.sendPoiTicketCountPacket(level, blockPos); -+ }); ++ processPath(acquirablePois, entityEventId, (Long2ObjectMap) map, memoryAccessor, level, mob, time, poiManager, set, path); }); } else { - for (Pair, BlockPos> pair : set) { - map.computeIfAbsent(pair.getSecond().asLong(), l -> new AcquirePoi.JitteredLinearRetry(level.random, time)); +- } + Path path = findPathToPois(mob, set); -+ if (path != null && path.canReach()) { -+ BlockPos target = path.getTarget(); -+ poiManager.getType(target).ifPresent(holder -> { -+ poiManager.take(acquirablePois, (holder1, blockPos) -> blockPos.equals(target), target, 1); -+ memoryAccessor.set(GlobalPos.of(level.dimension(), target)); -+ entityEventId.ifPresent(id -> level.broadcastEntityEvent(mob, id)); -+ map.clear(); -+ DebugPackets.sendPoiTicketCountPacket(level, target); -+ }); -+ } else { -+ for (Pair, BlockPos> pair : set) { -+ map.computeIfAbsent(pair.getSecond().asLong(), l -> new AcquirePoi.JitteredLinearRetry(level.random, time)); -+ } - } ++ processPath(acquirablePois, entityEventId, (Long2ObjectMap) map, memoryAccessor, level, mob, time, poiManager, set, path); } + // DivineMC end - Async path processing return true; } +@@ -120,6 +117,34 @@ public class AcquirePoi { + : BehaviorBuilder.create(instance -> instance.group(instance.absent(existingAbsentMemory)).apply(instance, memoryAccessor -> oneShot)); + } + ++ // DivineMC start - Async path processing ++ private static void processPath(Predicate> acquirablePois, ++ Optional entityEventId, ++ Long2ObjectMap map, ++ net.minecraft.world.entity.ai.behavior.declarative.MemoryAccessor, GlobalPos> memoryAccessor, ++ ServerLevel level, ++ PathfinderMob mob, ++ long time, ++ PoiManager poiManager, ++ Set, BlockPos>> set, ++ Path path) { ++ if (path != null && path.canReach()) { ++ BlockPos target = path.getTarget(); ++ poiManager.getType(target).ifPresent(holder -> { ++ poiManager.take(acquirablePois, (holder1, blockPos) -> blockPos.equals(target), target, 1); ++ memoryAccessor.set(GlobalPos.of(level.dimension(), target)); ++ entityEventId.ifPresent(id -> level.broadcastEntityEvent(mob, id)); ++ map.clear(); ++ DebugPackets.sendPoiTicketCountPacket(level, target); ++ }); ++ } else { ++ for (Pair, BlockPos> pair : set) { ++ map.computeIfAbsent(pair.getSecond().asLong(), l -> new JitteredLinearRetry(level.random, time)); ++ } ++ } ++ } ++ // DivineMC end - Async path processing ++ + @Nullable + public static Path findPathToPois(Mob mob, Set, BlockPos>> poiPositions) { + if (poiPositions.isEmpty()) { diff --git a/net/minecraft/world/entity/ai/behavior/MoveToTargetSink.java b/net/minecraft/world/entity/ai/behavior/MoveToTargetSink.java index 621ba76784f2b92790eca62be4d0688834335ab6..3f676309af0d5529413fa047a7f3cac99260b9ad 100644 --- a/net/minecraft/world/entity/ai/behavior/MoveToTargetSink.java @@ -222,10 +222,10 @@ index 621ba76784f2b92790eca62be4d0688834335ab6..3f676309af0d5529413fa047a7f3cac9 private boolean tryComputePath(Mob mob, WalkTarget target, long time) { BlockPos blockPos = target.getTarget().currentBlockPosition(); diff --git a/net/minecraft/world/entity/ai/behavior/SetClosestHomeAsWalkTarget.java b/net/minecraft/world/entity/ai/behavior/SetClosestHomeAsWalkTarget.java -index 4f9f3367b1ca3903df03a80fa2b01a3d24e6e77d..5e4e4ab9b50fcddfd022dbab15620c9c0cb53645 100644 +index 4f9f3367b1ca3903df03a80fa2b01a3d24e6e77d..1e9383442d0a0adf4bf621fc35108b35b1375bc7 100644 --- a/net/minecraft/world/entity/ai/behavior/SetClosestHomeAsWalkTarget.java +++ b/net/minecraft/world/entity/ai/behavior/SetClosestHomeAsWalkTarget.java -@@ -60,17 +60,38 @@ public class SetClosestHomeAsWalkTarget { +@@ -60,17 +60,18 @@ public class SetClosestHomeAsWalkTarget { poi -> poi.is(PoiTypes.HOME), predicate, mob.blockPosition(), 48, PoiManager.Occupancy.ANY ) .collect(Collectors.toSet()); @@ -236,43 +236,51 @@ index 4f9f3367b1ca3903df03a80fa2b01a3d24e6e77d..5e4e4ab9b50fcddfd022dbab15620c9c - if (type.isPresent()) { - walkTarget.set(new WalkTarget(target, speedModifier, 1)); - DebugPackets.sendPoiTicketCountPacket(level, target); +- } +- } else if (mutableInt.getValue() < 5) { +- map.long2LongEntrySet().removeIf(entry -> entry.getLongValue() < mutableLong.getValue()); + // DivineMC start - async path processing + if (org.bxteam.divinemc.DivineConfig.asyncPathfinding) { -+ // await on path async + Path possiblePath = AcquirePoi.findPathToPois(mob, set); + -+ // wait on the path to be processed + org.bxteam.divinemc.entity.pathfinding.AsyncPathProcessor.awaitProcessing(possiblePath, path -> { -+ if (path == null || !path.canReach() || mutableInt.getValue() < 5) { // read canReach check -+ map.long2LongEntrySet().removeIf(entry -> entry.getLongValue() < mutableLong.getValue()); -+ return; -+ } -+ BlockPos blockPos = path.getTarget(); -+ Optional> optional2 = poiManager.getType(blockPos); -+ if (optional2.isPresent()) { -+ walkTarget.set(new WalkTarget(blockPos, speedModifier, 1)); -+ DebugPackets.sendPoiTicketCountPacket(level, blockPos); -+ } ++ processPath(speedModifier, map, mutableLong, walkTarget, level, poiManager, mutableInt, path); + }); + } else { + Path path = AcquirePoi.findPathToPois(mob, set); -+ if (path != null && path.canReach()) { -+ BlockPos target = path.getTarget(); -+ Optional> type = poiManager.getType(target); -+ if (type.isPresent()) { -+ walkTarget.set(new WalkTarget(target, speedModifier, 1)); -+ DebugPackets.sendPoiTicketCountPacket(level, target); -+ } -+ } else if (mutableInt.getValue() < 5) { -+ map.long2LongEntrySet().removeIf(entry -> entry.getLongValue() < mutableLong.getValue()); - } -- } else if (mutableInt.getValue() < 5) { -- map.long2LongEntrySet().removeIf(entry -> entry.getLongValue() < mutableLong.getValue()); ++ processPath(speedModifier, map, mutableLong, walkTarget, level, poiManager, mutableInt, path); } + // DivineMC end - async path processing return true; } else { +@@ -81,4 +82,26 @@ public class SetClosestHomeAsWalkTarget { + ) + ); + } ++ ++ // DivineMC start - async path processing ++ private static void processPath(float speedModifier, ++ Long2LongMap map, ++ MutableLong mutableLong, ++ net.minecraft.world.entity.ai.behavior.declarative.MemoryAccessor, WalkTarget> walkTarget, ++ net.minecraft.server.level.ServerLevel level, ++ PoiManager poiManager, ++ MutableInt mutableInt, ++ @org.jetbrains.annotations.Nullable Path path) { ++ if (path != null && path.canReach()) { ++ BlockPos target = path.getTarget(); ++ Optional> type = poiManager.getType(target); ++ if (type.isPresent()) { ++ walkTarget.set(new WalkTarget(target, speedModifier, 1)); ++ DebugPackets.sendPoiTicketCountPacket(level, target); ++ } ++ } else if (mutableInt.getValue() < 5) { ++ map.long2LongEntrySet().removeIf(entry -> entry.getLongValue() < mutableLong.getValue()); ++ } ++ } ++ // DivineMC end - async path processing + } diff --git a/net/minecraft/world/entity/ai/goal/DoorInteractGoal.java b/net/minecraft/world/entity/ai/goal/DoorInteractGoal.java index d8f532c5e68ff4dff933556c4f981e9474c044e6..37f3d3888ea2a862d006cf2b201f9715bcb8ce1e 100644 --- a/net/minecraft/world/entity/ai/goal/DoorInteractGoal.java @@ -504,54 +512,51 @@ index 2979846853898d78a2df19df2287da16dbe4ae71..504911c14aad5c53aeea2c71bda3978f } diff --git a/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java b/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java -index 1f96fd5085bacb4c584576c7cb9f51e7898e9b03..3a8b3c71f6fd0c0f57a3190dc8a0f808c6d1002b 100644 +index 1f96fd5085bacb4c584576c7cb9f51e7898e9b03..9494dd144ff1115c9f0862614ef618035ee2565f 100644 --- a/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java +++ b/net/minecraft/world/entity/ai/sensing/NearestBedSensor.java -@@ -57,17 +57,37 @@ public class NearestBedSensor extends Sensor { +@@ -57,17 +57,32 @@ public class NearestBedSensor extends Sensor { java.util.List, BlockPos>> poiposes = new java.util.ArrayList<>(); // don't ask me why it's unbounded. ask mojang. io.papermc.paper.util.PoiAccess.findAnyPoiPositions(poiManager, type -> type.is(PoiTypes.HOME), predicate, entity.blockPosition(), level.purpurConfig.villagerNearestBedSensorSearchRadius, PoiManager.Occupancy.ANY, false, Integer.MAX_VALUE, poiposes); // Purpur - Configurable villager search radius - Path path = AcquirePoi.findPathToPois(entity, new java.util.HashSet<>(poiposes)); - // Paper end - optimise POI access +- // Paper end - optimise POI access - if (path != null && path.canReach()) { - BlockPos target = path.getTarget(); - Optional> type = poiManager.getType(target); - if (type.isPresent()) { - entity.getBrain().setMemory(MemoryModuleType.NEAREST_BED, target); +- } +- } else if (this.triedCount < 5) { +- this.batchCache.long2LongEntrySet().removeIf(entry -> entry.getLongValue() < this.lastUpdate); + // DivineMC start - async pathfinding + if (org.bxteam.divinemc.DivineConfig.asyncPathfinding) { + Path possiblePath = AcquirePoi.findPathToPois(entity, new java.util.HashSet<>(poiposes)); + org.bxteam.divinemc.entity.pathfinding.AsyncPathProcessor.awaitProcessing(possiblePath, path -> { -+ // read canReach check -+ if ((path == null || !path.canReach()) && this.triedCount < 5) { -+ this.batchCache.long2LongEntrySet().removeIf(entry -> entry.getLongValue() < this.lastUpdate); -+ return; -+ } -+ if (path == null) return; -+ -+ BlockPos blockPos = path.getTarget(); -+ Optional> optional = poiManager.getType(blockPos); -+ if (optional.isPresent()) { -+ entity.getBrain().setMemory(MemoryModuleType.NEAREST_BED, blockPos); -+ } ++ processPath(entity, poiManager, path); + }); + } else { + Path path = AcquirePoi.findPathToPois(entity, new java.util.HashSet<>(poiposes)); -+ if (path != null && path.canReach()) { -+ BlockPos target = path.getTarget(); -+ Optional> type = poiManager.getType(target); -+ if (type.isPresent()) { -+ entity.getBrain().setMemory(MemoryModuleType.NEAREST_BED, target); -+ } -+ } else if (this.triedCount < 5) { -+ this.batchCache.long2LongEntrySet().removeIf(entry -> entry.getLongValue() < this.lastUpdate); - } -- } else if (this.triedCount < 5) { -- this.batchCache.long2LongEntrySet().removeIf(entry -> entry.getLongValue() < this.lastUpdate); - } ++ // Paper end - optimise POI access ++ processPath(entity, poiManager, path); ++ } + // DivineMC end - async pathfinding ++ } ++ } ++ ++ // DivineMC start - async pathfinding ++ private void processPath(Mob entity, PoiManager poiManager, @org.jetbrains.annotations.Nullable Path path) { ++ if (path != null && path.canReach()) { ++ BlockPos target = path.getTarget(); ++ Optional> type = poiManager.getType(target); ++ if (type.isPresent()) { ++ entity.getBrain().setMemory(MemoryModuleType.NEAREST_BED, target); + } ++ } else if (this.triedCount < 5) { ++ this.batchCache.long2LongEntrySet().removeIf(entry -> entry.getLongValue() < this.lastUpdate); } } ++ // DivineMC end - async pathfinding } diff --git a/net/minecraft/world/entity/animal/Bee.java b/net/minecraft/world/entity/animal/Bee.java index d5727999eb67ff30dbf47865d59452483338e170..ddbee0f0f42fae0a26321bb324d22f5e7520ae72 100644 diff --git a/divinemc-server/minecraft-patches/features/0006-Multithreaded-Tracker.patch b/divinemc-server/minecraft-patches/features/0006-Multithreaded-Tracker.patch index d1078a5..4d8fb3a 100644 --- a/divinemc-server/minecraft-patches/features/0006-Multithreaded-Tracker.patch +++ b/divinemc-server/minecraft-patches/features/0006-Multithreaded-Tracker.patch @@ -4,8 +4,25 @@ Date: Tue, 28 Jan 2025 01:18:49 +0300 Subject: [PATCH] Multithreaded Tracker +diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java +index c06784f401dbdc8c215876aa86cd1a7289bd4332..f81cc357618c70f2fcf0bc24b0b25be566ffffcc 100644 +--- a/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/player/RegionizedPlayerChunkLoader.java +@@ -342,7 +342,11 @@ public final class RegionizedPlayerChunkLoader { + private boolean canGenerateChunks = true; + + private final ArrayDeque> delayedTicketOps = new ArrayDeque<>(); +- private final LongOpenHashSet sentChunks = new LongOpenHashSet(); ++ // DivineMC start - Multithreaded tracker ++ private final LongOpenHashSet sentChunks = org.bxteam.divinemc.DivineConfig.multithreadedEnabled && !org.bxteam.divinemc.DivineConfig.multithreadedCompatModeEnabled ++ ? new org.bxteam.divinemc.util.map.ConcurrentLongHashSet() ++ : new LongOpenHashSet(); ++ // DivineMC end - Multithreaded tracker + + private static final byte CHUNK_TICKET_STAGE_NONE = 0; + private static final byte CHUNK_TICKET_STAGE_LOADING = 1; diff --git a/net/minecraft/server/level/ChunkMap.java b/net/minecraft/server/level/ChunkMap.java -index da2921268a9aa4545a12c155a33bed9fccb4f19c..e3427e7aecfc4e1fafb38316824aa1ee50c9901a 100644 +index da2921268a9aa4545a12c155a33bed9fccb4f19c..3bf5f76dd61f6d20819440b28cbf46ed8ba34670 100644 --- a/net/minecraft/server/level/ChunkMap.java +++ b/net/minecraft/server/level/ChunkMap.java @@ -264,9 +264,19 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider @@ -30,27 +47,7 @@ index da2921268a9aa4545a12c155a33bed9fccb4f19c..e3427e7aecfc4e1fafb38316824aa1ee } // Paper start - per player mob count backoff -@@ -950,6 +960,19 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity)entity).moonrise$setTrackedEntity(null); // Paper - optimise entity tracker - } - -+ // DivineMC start - Multithreaded tracker -+ private final java.util.concurrent.ConcurrentLinkedQueue trackerMainThreadTasks = new java.util.concurrent.ConcurrentLinkedQueue<>(); -+ private boolean tracking = false; -+ -+ public void runOnTrackerMainThread(final Runnable runnable) { -+ if (false && this.tracking) { // TODO: check here -+ this.trackerMainThreadTasks.add(runnable); -+ } else { -+ runnable.run(); -+ } -+ } -+ // DivineMC end - Multithreaded tracker -+ - // Paper start - optimise entity tracker - private void newTrackerTick() { - final ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup entityLookup = (ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup)((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel)this.level).moonrise$getEntityLookup();; -@@ -972,6 +995,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -972,6 +982,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider // Paper end - optimise entity tracker protected void tick() { @@ -64,7 +61,7 @@ index da2921268a9aa4545a12c155a33bed9fccb4f19c..e3427e7aecfc4e1fafb38316824aa1ee // Paper start - optimise entity tracker if (true) { this.newTrackerTick(); -@@ -1094,7 +1124,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -1094,7 +1111,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider final Entity entity; private final int range; SectionPos lastSectionPos; @@ -77,7 +74,7 @@ index da2921268a9aa4545a12c155a33bed9fccb4f19c..e3427e7aecfc4e1fafb38316824aa1ee // Paper start - optimise entity tracker private long lastChunkUpdate = -1L; -@@ -1121,21 +1155,55 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -1121,21 +1142,55 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider this.lastTrackedChunk = chunk; final ServerPlayer[] playersRaw = players.getRawDataUnchecked(); @@ -143,29 +140,23 @@ index da2921268a9aa4545a12c155a33bed9fccb4f19c..e3427e7aecfc4e1fafb38316824aa1ee } @Override -@@ -1197,9 +1265,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -1197,7 +1252,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } public void broadcast(Packet packet) { - for (ServerPlayerConnection serverPlayerConnection : this.seenBy) { -+ // DivineMC start - Multithreaded tracker -+ for (ServerPlayerConnection serverPlayerConnection : this.seenBy.toArray(new ServerPlayerConnection[0])) { ++ for (ServerPlayerConnection serverPlayerConnection : this.seenBy.toArray(new ServerPlayerConnection[0])) { // DivineMC - Multithreaded tracker serverPlayerConnection.send(packet); } -+ // DivineMC end - Multithreaded tracker } - - public void broadcastAndSend(Packet packet) { -@@ -1210,21 +1280,22 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -1210,21 +1265,20 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } public void broadcastRemoved() { - for (ServerPlayerConnection serverPlayerConnection : this.seenBy) { -+ // DivineMC start - Multithreaded tracker -+ for (ServerPlayerConnection serverPlayerConnection : this.seenBy.toArray(new ServerPlayerConnection[0])) { ++ for (ServerPlayerConnection serverPlayerConnection : this.seenBy.toArray(new ServerPlayerConnection[0])) { // DivineMC - Multithreaded tracker this.serverEntity.removePairing(serverPlayerConnection.getPlayer()); } -+ // DivineMC end - Multithreaded tracker } public void removePlayer(ServerPlayer player) { @@ -200,10 +191,10 @@ index f106373ef3ac4a8685c2939c9e8361688a285913..3b4dff8867e91884b5720ca8a9cb64af public boolean visible = true; diff --git a/net/minecraft/server/level/ServerEntity.java b/net/minecraft/server/level/ServerEntity.java -index 0fb253aa55a24b56b17f524b3261c5b75c7d7e59..3ee43ca5c49af83a067c7ffe74d3f2bc6e4a6c9e 100644 +index 0fb253aa55a24b56b17f524b3261c5b75c7d7e59..b6053158f5d9b6ad325ea075ab7c60f9966ba496 100644 --- a/net/minecraft/server/level/ServerEntity.java +++ b/net/minecraft/server/level/ServerEntity.java -@@ -110,8 +110,13 @@ public class ServerEntity { +@@ -110,8 +110,16 @@ public class ServerEntity { .forEach( removedPassenger -> { if (removedPassenger instanceof ServerPlayer serverPlayer1) { @@ -211,59 +202,30 @@ index 0fb253aa55a24b56b17f524b3261c5b75c7d7e59..3ee43ca5c49af83a067c7ffe74d3f2bc - .teleport(serverPlayer1.getX(), serverPlayer1.getY(), serverPlayer1.getZ(), serverPlayer1.getYRot(), serverPlayer1.getXRot()); + // DivineMC start - Multithreaded tracker + if (org.bxteam.divinemc.DivineConfig.multithreadedEnabled && Thread.currentThread() instanceof org.bxteam.divinemc.entity.tracking.MultithreadedTracker.MultithreadedTrackerThread) { -+ net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> serverPlayer1.connection.teleport(serverPlayer1.getX(), serverPlayer1.getY(), serverPlayer1.getZ(), serverPlayer1.getYRot(), serverPlayer1.getXRot())); ++ net.minecraft.server.MinecraftServer.getServer().scheduleOnMain(() -> serverPlayer1.connection ++ .teleport(serverPlayer1.getX(), serverPlayer1.getY(), serverPlayer1.getZ(), serverPlayer1.getYRot(), serverPlayer1.getXRot()) ++ ); + } else { -+ serverPlayer1.connection.teleport(serverPlayer1.getX(), serverPlayer1.getY(), serverPlayer1.getZ(), serverPlayer1.getYRot(), serverPlayer1.getXRot()); ++ serverPlayer1.connection ++ .teleport(serverPlayer1.getX(), serverPlayer1.getY(), serverPlayer1.getZ(), serverPlayer1.getYRot(), serverPlayer1.getXRot()); + } + // DivineMC end - Multithreaded tracker } } ); -@@ -304,7 +309,11 @@ public class ServerEntity { - - public void removePairing(ServerPlayer player) { - this.entity.stopSeenByPlayer(player); -- player.connection.send(new ClientboundRemoveEntitiesPacket(this.entity.getId())); -+ // DivineMC start - Multithreaded tracker - send in main thread -+ ((ServerLevel) this.entity.level()).chunkSource.chunkMap.runOnTrackerMainThread(() -> -+ player.connection.send(new ClientboundRemoveEntitiesPacket(this.entity.getId())) -+ ); -+ // DivineMC end - Multithreaded tracker - } - - public void addPairing(ServerPlayer player) { -@@ -404,18 +413,27 @@ public class ServerEntity { - List> list = entityData.packDirty(); - if (list != null) { - this.trackedDataValues = entityData.getNonDefaultValues(); -- this.broadcastAndSend(new ClientboundSetEntityDataPacket(this.entity.getId(), list)); -+ // DivineMC start - Multithreaded tracker - send in main thread -+ ((ServerLevel) this.entity.level()).chunkSource.chunkMap.runOnTrackerMainThread(() -> -+ this.broadcastAndSend(new ClientboundSetEntityDataPacket(this.entity.getId(), list)) -+ ); -+ // DivineMC end - Multithreaded tracker - } - +@@ -410,12 +418,13 @@ public class ServerEntity { if (this.entity instanceof LivingEntity) { Set attributesToSync = ((LivingEntity)this.entity).getAttributes().getAttributesToSync(); if (!attributesToSync.isEmpty()) { -- // CraftBukkit start - Send scaled max health -- if (this.entity instanceof ServerPlayer serverPlayer) { ++ final Set copy = new it.unimi.dsi.fastutil.objects.ObjectOpenHashSet<>(attributesToSync); // DivineMC - Multithreaded tracker + // CraftBukkit start - Send scaled max health + if (this.entity instanceof ServerPlayer serverPlayer) { - serverPlayer.getBukkitEntity().injectScaledMaxHealth(attributesToSync, false); -- } -- // CraftBukkit end ++ serverPlayer.getBukkitEntity().injectScaledMaxHealth(copy, false); // DivineMC - Multithreaded tracker + } + // CraftBukkit end - this.broadcastAndSend(new ClientboundUpdateAttributesPacket(this.entity.getId(), attributesToSync)); -+ // DivineMC start - Multithreaded tracker -+ final Set copy = new it.unimi.dsi.fastutil.objects.ObjectOpenHashSet<>(attributesToSync); -+ ((ServerLevel) this.entity.level()).chunkSource.chunkMap.runOnTrackerMainThread(() -> { -+ // CraftBukkit start - Send scaled max health -+ if (this.entity instanceof ServerPlayer serverPlayer) { -+ serverPlayer.getBukkitEntity().injectScaledMaxHealth(copy, false); -+ } -+ // CraftBukkit end -+ this.broadcastAndSend(new ClientboundUpdateAttributesPacket(this.entity.getId(), copy)); -+ }); -+ // DivineMC end - Multithreaded tracker ++ this.broadcastAndSend(new ClientboundUpdateAttributesPacket(this.entity.getId(), copy)); // DivineMC - Multithreaded tracker } attributesToSync.clear(); diff --git a/divinemc-server/minecraft-patches/features/0026-Skip-distanceToSqr-call-in-ServerEntity-sendChanges-.patch b/divinemc-server/minecraft-patches/features/0026-Skip-distanceToSqr-call-in-ServerEntity-sendChanges-.patch index 4aaea74..c531a69 100644 --- a/divinemc-server/minecraft-patches/features/0026-Skip-distanceToSqr-call-in-ServerEntity-sendChanges-.patch +++ b/divinemc-server/minecraft-patches/features/0026-Skip-distanceToSqr-call-in-ServerEntity-sendChanges-.patch @@ -6,10 +6,10 @@ Subject: [PATCH] Skip distanceToSqr call in ServerEntity#sendChanges if the diff --git a/net/minecraft/server/level/ServerEntity.java b/net/minecraft/server/level/ServerEntity.java -index 3ee43ca5c49af83a067c7ffe74d3f2bc6e4a6c9e..a03dced5231ca47abf5919e3eca358c16a25337b 100644 +index b6053158f5d9b6ad325ea075ab7c60f9966ba496..c7ad6bf5dd8cb8a27de3e3ed82c6eefaa36c5c08 100644 --- a/net/minecraft/server/level/ServerEntity.java +++ b/net/minecraft/server/level/ServerEntity.java -@@ -200,23 +200,27 @@ public class ServerEntity { +@@ -203,23 +203,27 @@ public class ServerEntity { if (this.entity.hasImpulse || this.trackDelta || this.entity instanceof LivingEntity && ((LivingEntity)this.entity).isFallFlying()) { Vec3 deltaMovement = this.entity.getDeltaMovement(); diff --git a/divinemc-server/minecraft-patches/features/0027-Optimize-canSee-checks.patch b/divinemc-server/minecraft-patches/features/0027-Optimize-canSee-checks.patch index 5f4c2bd..6300c0e 100644 --- a/divinemc-server/minecraft-patches/features/0027-Optimize-canSee-checks.patch +++ b/divinemc-server/minecraft-patches/features/0027-Optimize-canSee-checks.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Optimize canSee checks diff --git a/net/minecraft/server/level/ChunkMap.java b/net/minecraft/server/level/ChunkMap.java -index e3427e7aecfc4e1fafb38316824aa1ee50c9901a..9bca28f33aa3b3cb8964c06b2f4d2f0a591e81f5 100644 +index 3bf5f76dd61f6d20819440b28cbf46ed8ba34670..a2742f6c5af8ad128b84121727798ea249e7ef51 100644 --- a/net/minecraft/server/level/ChunkMap.java +++ b/net/minecraft/server/level/ChunkMap.java -@@ -1317,7 +1317,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider +@@ -1300,7 +1300,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider flag = flag && this.entity.broadcastToPlayer(player) && ChunkMap.this.isChunkTracked(player, this.entity.chunkPosition().x, this.entity.chunkPosition().z); // Paper end - Configurable entity tracking range by Y // CraftBukkit start - respect vanish API diff --git a/divinemc-server/src/main/java/org/bxteam/divinemc/DivineConfig.java b/divinemc-server/src/main/java/org/bxteam/divinemc/DivineConfig.java index d965500..552db68 100644 --- a/divinemc-server/src/main/java/org/bxteam/divinemc/DivineConfig.java +++ b/divinemc-server/src/main/java/org/bxteam/divinemc/DivineConfig.java @@ -5,6 +5,7 @@ import org.apache.logging.log4j.Logger; import org.bukkit.Bukkit; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.MemoryConfiguration; +import org.bxteam.divinemc.entity.pathfinding.PathfindTaskRejectPolicy; import org.bxteam.divinemc.server.chunk.ChunkSystemAlgorithms; import org.jetbrains.annotations.Nullable; import org.simpleyaml.configuration.comments.CommentType; @@ -316,33 +317,52 @@ public class DivineConfig { public static boolean asyncPathfinding = true; public static int asyncPathfindingMaxThreads = 2; public static int asyncPathfindingKeepalive = 60; + public static int asyncPathfindingQueueSize = 0; + public static PathfindTaskRejectPolicy asyncPathfindingRejectPolicy = PathfindTaskRejectPolicy.FLUSH_ALL; private static void asyncPathfinding() { asyncPathfinding = getBoolean("settings.async-pathfinding.enable", asyncPathfinding); asyncPathfindingMaxThreads = getInt("settings.async-pathfinding.max-threads", asyncPathfindingMaxThreads); asyncPathfindingKeepalive = getInt("settings.async-pathfinding.keepalive", asyncPathfindingKeepalive); + asyncPathfindingQueueSize = getInt("settings.async-pathfinding.queue-size", asyncPathfindingQueueSize); + final int maxThreads = Runtime.getRuntime().availableProcessors(); if (asyncPathfindingMaxThreads < 0) { - asyncPathfindingMaxThreads = Math.max(Runtime.getRuntime().availableProcessors() + asyncPathfindingMaxThreads, 1); + asyncPathfindingMaxThreads = Math.max(maxThreads + asyncPathfindingMaxThreads, 1); } else if (asyncPathfindingMaxThreads == 0) { - asyncPathfindingMaxThreads = Math.max(Runtime.getRuntime().availableProcessors() / 4, 1); + asyncPathfindingMaxThreads = Math.max(maxThreads / 4, 1); } if (!asyncPathfinding) { asyncPathfindingMaxThreads = 0; } else { - Bukkit.getLogger().log(Level.INFO, "Using " + asyncPathfindingMaxThreads + " threads for Async Pathfinding"); + LOGGER.info("Using " + asyncPathfindingMaxThreads + " threads for Async Pathfinding"); } + + if (asyncPathfindingQueueSize <= 0) asyncPathfindingQueueSize = asyncPathfindingMaxThreads * 256; + + asyncPathfindingRejectPolicy = PathfindTaskRejectPolicy.fromString(getString("settings.async-pathfinding.reject-policy", maxThreads >= 12 && asyncPathfindingQueueSize < 512 ? PathfindTaskRejectPolicy.FLUSH_ALL.toString() : PathfindTaskRejectPolicy.CALLER_RUNS.toString(), + "The policy to use when the queue is full and a new task is submitted.", + "FLUSH_ALL: All pending tasks will be run on server thread.", + "CALLER_RUNS: Newly submitted task will be run on server thread.")); } public static boolean multithreadedEnabled = true; public static boolean multithreadedCompatModeEnabled = false; public static int asyncEntityTrackerMaxThreads = 1; public static int asyncEntityTrackerKeepalive = 60; + public static int asyncEntityTrackerQueueSize = 0; private static void multithreadedTracker() { - multithreadedEnabled = getBoolean("settings.multithreaded-tracker.enable", multithreadedEnabled); - multithreadedCompatModeEnabled = getBoolean("settings.multithreaded-tracker.compat-mode", multithreadedCompatModeEnabled); + multithreadedEnabled = getBoolean("settings.multithreaded-tracker.enable", multithreadedEnabled, + "Make entity tracking saving asynchronously, can improve performance significantly,", + "especially in some massive entities in small area situations."); + multithreadedCompatModeEnabled = getBoolean("settings.multithreaded-tracker.compat-mode", multithreadedCompatModeEnabled, + "Enable compat mode ONLY if Citizens or NPC plugins using real entity has installed.", + "Compat mode fixes visible issues with player type NPCs of Citizens.", + "But we recommend to use packet based / virtual entity NPC plugin, e.g. ZNPC Plus, Adyeshach, Fancy NPC and etc."); + asyncEntityTrackerMaxThreads = getInt("settings.multithreaded-tracker.max-threads", asyncEntityTrackerMaxThreads); asyncEntityTrackerKeepalive = getInt("settings.multithreaded-tracker.keepalive", asyncEntityTrackerKeepalive); + asyncEntityTrackerQueueSize = getInt("settings.multithreaded-tracker.queue-size", asyncEntityTrackerQueueSize); if (asyncEntityTrackerMaxThreads < 0) { asyncEntityTrackerMaxThreads = Math.max(Runtime.getRuntime().availableProcessors() + asyncEntityTrackerMaxThreads, 1); @@ -353,7 +373,9 @@ public class DivineConfig { if (!multithreadedEnabled) { asyncEntityTrackerMaxThreads = 0; } else { - Bukkit.getLogger().log(Level.INFO, "Using " + asyncEntityTrackerMaxThreads + " threads for Async Entity Tracker"); + LOGGER.info("Using " + asyncEntityTrackerMaxThreads + " threads for Async Entity Tracker"); } + + if (asyncEntityTrackerQueueSize <= 0) asyncEntityTrackerQueueSize = asyncEntityTrackerMaxThreads * 384; } } diff --git a/divinemc-server/src/main/java/org/bxteam/divinemc/entity/pathfinding/AsyncPathProcessor.java b/divinemc-server/src/main/java/org/bxteam/divinemc/entity/pathfinding/AsyncPathProcessor.java index ad33ab3..a574e58 100644 --- a/divinemc-server/src/main/java/org/bxteam/divinemc/entity/pathfinding/AsyncPathProcessor.java +++ b/divinemc-server/src/main/java/org/bxteam/divinemc/entity/pathfinding/AsyncPathProcessor.java @@ -3,29 +3,76 @@ package org.bxteam.divinemc.entity.pathfinding; import com.google.common.util.concurrent.ThreadFactoryBuilder; import net.minecraft.server.MinecraftServer; import net.minecraft.world.level.pathfinder.Path; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.bxteam.divinemc.DivineConfig; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.*; import java.util.function.Consumer; /** - * used to handle the scheduling of async path processing + * Used to handle the scheduling of async path processing */ +@SuppressWarnings("DuplicatedCode") public class AsyncPathProcessor { - private static final Executor pathProcessingExecutor = new ThreadPoolExecutor( + private static final String THREAD_PREFIX = "DivineMC Async Pathfinding"; + private static final Logger LOGGER = LogManager.getLogger(THREAD_PREFIX); + + private static long lastWarnMillis = System.currentTimeMillis(); + private static final ThreadPoolExecutor pathProcessingExecutor = new ThreadPoolExecutor( 1, - org.bxteam.divinemc.DivineConfig.asyncPathfindingMaxThreads, - org.bxteam.divinemc.DivineConfig.asyncPathfindingKeepalive, TimeUnit.SECONDS, - new LinkedBlockingQueue<>(), + DivineConfig.asyncPathfindingMaxThreads, + DivineConfig.asyncPathfindingKeepalive, TimeUnit.SECONDS, + getQueueImpl(), new ThreadFactoryBuilder() - .setNameFormat("DivineMC Async Pathfinding Thread - %d") + .setNameFormat(THREAD_PREFIX + " Thread - %d") .setPriority(Thread.NORM_PRIORITY - 2) - .build() + .build(), + new RejectedTaskHandler() ); + private static class RejectedTaskHandler implements RejectedExecutionHandler { + @Override + public void rejectedExecution(Runnable rejectedTask, ThreadPoolExecutor executor) { + BlockingQueue workQueue = executor.getQueue(); + if (!executor.isShutdown()) { + switch (DivineConfig.asyncPathfindingRejectPolicy) { + case FLUSH_ALL -> { + if (!workQueue.isEmpty()) { + List pendingTasks = new ArrayList<>(workQueue.size()); + + workQueue.drainTo(pendingTasks); + + for (Runnable pendingTask : pendingTasks) { + pendingTask.run(); + } + } + rejectedTask.run(); + } + case CALLER_RUNS -> rejectedTask.run(); + } + } + + if (System.currentTimeMillis() - lastWarnMillis > 30000L) { + LOGGER.warn("Async pathfinding processor is busy! Pathfinding tasks will be treated as policy defined in config. Increasing max-threads in DivineMC config may help."); + lastWarnMillis = System.currentTimeMillis(); + } + } + } + protected static CompletableFuture queue(@NotNull AsyncPath path) { - return CompletableFuture.runAsync(path::process, pathProcessingExecutor); + return CompletableFuture.runAsync(path::process, pathProcessingExecutor) + .orTimeout(60L, TimeUnit.SECONDS) + .exceptionally(throwable -> { + if (throwable instanceof TimeoutException e) { + LOGGER.warn("Async Pathfinding process timed out", e); + } else LOGGER.warn("Error occurred while processing async path", throwable); + return null; + }); } /** @@ -45,4 +92,10 @@ public class AsyncPathProcessor { afterProcessing.accept(path); } } + + private static BlockingQueue getQueueImpl() { + final int queueCapacity = DivineConfig.asyncPathfindingQueueSize; + + return new LinkedBlockingQueue<>(queueCapacity); + } } diff --git a/divinemc-server/src/main/java/org/bxteam/divinemc/entity/pathfinding/PathfindTaskRejectPolicy.java b/divinemc-server/src/main/java/org/bxteam/divinemc/entity/pathfinding/PathfindTaskRejectPolicy.java new file mode 100644 index 0000000..ebe6e94 --- /dev/null +++ b/divinemc-server/src/main/java/org/bxteam/divinemc/entity/pathfinding/PathfindTaskRejectPolicy.java @@ -0,0 +1,18 @@ +package org.bxteam.divinemc.entity.pathfinding; + +import org.bxteam.divinemc.DivineConfig; +import java.util.Locale; + +public enum PathfindTaskRejectPolicy { + FLUSH_ALL, + CALLER_RUNS; + + public static PathfindTaskRejectPolicy fromString(String policy) { + try { + return PathfindTaskRejectPolicy.valueOf(policy.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException e) { + DivineConfig.LOGGER.warn("Invalid pathfind task reject policy: {}, falling back to {}.", policy, FLUSH_ALL.toString()); + return FLUSH_ALL; + } + } +} diff --git a/divinemc-server/src/main/java/org/bxteam/divinemc/entity/tracking/MultithreadedTracker.java b/divinemc-server/src/main/java/org/bxteam/divinemc/entity/tracking/MultithreadedTracker.java index 626cbc8..483492d 100644 --- a/divinemc-server/src/main/java/org/bxteam/divinemc/entity/tracking/MultithreadedTracker.java +++ b/divinemc-server/src/main/java/org/bxteam/divinemc/entity/tracking/MultithreadedTracker.java @@ -5,7 +5,6 @@ import ca.spottedleaf.moonrise.common.misc.NearbyPlayers; import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup; import ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity; -import ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity; import com.google.common.util.concurrent.ThreadFactoryBuilder; import net.minecraft.server.level.ChunkMap; import net.minecraft.server.level.FullChunkStatus; @@ -13,41 +12,32 @@ import net.minecraft.server.level.ServerLevel; import net.minecraft.world.entity.Entity; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.bxteam.divinemc.DivineConfig; +import org.jetbrains.annotations.NotNull; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; import java.util.concurrent.Executor; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class MultithreadedTracker { - private static final Logger LOGGER = LogManager.getLogger("MultithreadedTracker"); + private static final String THREAD_PREFIX = "DivineMC Async Tracker"; + private static final Logger LOGGER = LogManager.getLogger(THREAD_PREFIX); - public static class MultithreadedTrackerThread extends Thread { - @Override - public void run() { - super.run(); - } - } - - private static final Executor trackerExecutor = new ThreadPoolExecutor( - 1, - org.bxteam.divinemc.DivineConfig.asyncEntityTrackerMaxThreads, - org.bxteam.divinemc.DivineConfig.asyncEntityTrackerKeepalive, TimeUnit.SECONDS, - new LinkedBlockingQueue<>(), - new ThreadFactoryBuilder() - .setThreadFactory( - r -> new MultithreadedTrackerThread() { - @Override - public void run() { - r.run(); - } - } - ) - .setNameFormat("DivineMC Async Tracker Thread - %d") - .setPriority(Thread.NORM_PRIORITY - 2) - .build()); - - private MultithreadedTracker() { } + private static long lastWarnMillis = System.currentTimeMillis(); + private static final ThreadPoolExecutor trackerExecutor = new ThreadPoolExecutor( + getCorePoolSize(), + getMaxPoolSize(), + getKeepAliveTime(), TimeUnit.SECONDS, + getQueueImpl(), + getThreadFactory(), + getRejectedPolicy() + ); public static Executor getTrackerExecutor() { return trackerExecutor; @@ -55,7 +45,7 @@ public class MultithreadedTracker { public static void tick(ChunkSystemServerLevel level) { try { - if (!org.bxteam.divinemc.DivineConfig.multithreadedCompatModeEnabled) { + if (!DivineConfig.multithreadedCompatModeEnabled) { tickAsync(level); } else { tickAsyncWithCompatMode(level); @@ -81,7 +71,7 @@ public class MultithreadedTracker { if (tracker == null) continue; - ((EntityTrackerTrackedEntity) tracker).moonrise$tick(nearbyPlayers.getChunk(entity.chunkPosition())); + tracker.moonrise$tick(nearbyPlayers.getChunk(entity.chunkPosition())); tracker.serverEntity.sendChanges(); } }); @@ -103,7 +93,7 @@ public class MultithreadedTracker { if (tracker == null) continue; - ((EntityTrackerTrackedEntity) tracker).moonrise$tick(nearbyPlayers.getChunk(entity.chunkPosition())); + tracker.moonrise$tick(nearbyPlayers.getChunk(entity.chunkPosition())); sendChangesTasks[index++] = () -> tracker.serverEntity.sendChanges(); // Collect send changes to task array } @@ -138,4 +128,61 @@ public class MultithreadedTracker { } } } + + private static int getCorePoolSize() { + return 1; + } + + private static int getMaxPoolSize() { + return DivineConfig.asyncEntityTrackerMaxThreads; + } + + private static long getKeepAliveTime() { + return DivineConfig.asyncEntityTrackerKeepalive; + } + + private static BlockingQueue getQueueImpl() { + final int queueCapacity = DivineConfig.asyncEntityTrackerQueueSize; + + return new LinkedBlockingQueue<>(queueCapacity); + } + + private static @NotNull ThreadFactory getThreadFactory() { + return new ThreadFactoryBuilder() + .setThreadFactory(MultithreadedTrackerThread::new) + .setNameFormat(THREAD_PREFIX + " Thread - %d") + .setPriority(Thread.NORM_PRIORITY - 2) + .build(); + } + + private static @NotNull RejectedExecutionHandler getRejectedPolicy() { + return (rejectedTask, executor) -> { + BlockingQueue workQueue = executor.getQueue(); + + if (!executor.isShutdown()) { + if (!workQueue.isEmpty()) { + List pendingTasks = new ArrayList<>(workQueue.size()); + + workQueue.drainTo(pendingTasks); + + for (Runnable pendingTask : pendingTasks) { + pendingTask.run(); + } + } + + rejectedTask.run(); + } + + if (System.currentTimeMillis() - lastWarnMillis > 30000L) { + LOGGER.warn("Async entity tracker is busy! Tracking tasks will be done in the server thread. Increasing max-threads in DivineMC config may help."); + lastWarnMillis = System.currentTimeMillis(); + } + }; + } + + public static class MultithreadedTrackerThread extends Thread { + public MultithreadedTrackerThread(Runnable runnable) { + super(runnable); + } + } } diff --git a/divinemc-server/src/main/java/org/bxteam/divinemc/util/map/ConcurrentLongHashSet.java b/divinemc-server/src/main/java/org/bxteam/divinemc/util/map/ConcurrentLongHashSet.java new file mode 100644 index 0000000..d629fbd --- /dev/null +++ b/divinemc-server/src/main/java/org/bxteam/divinemc/util/map/ConcurrentLongHashSet.java @@ -0,0 +1,213 @@ +package org.bxteam.divinemc.util.map; + +import it.unimi.dsi.fastutil.longs.LongCollection; +import it.unimi.dsi.fastutil.longs.LongIterator; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongSet; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A thread-safe implementation of {@link LongOpenHashSet} using ConcurrentHashMap.KeySetView as backing storage. + * This implementation provides concurrent access and high performance for concurrent operations. + * + * @author HaHaWTH at Leaf + */ +@SuppressWarnings({"unused", "deprecation"}) +public final class ConcurrentLongHashSet extends LongOpenHashSet implements LongSet { + private final ConcurrentHashMap.KeySetView backing; + + /** + * Creates a new empty concurrent long set. + */ + public ConcurrentLongHashSet() { + this.backing = ConcurrentHashMap.newKeySet(); + } + + @Override + public int size() { + return backing.size(); + } + + @Override + public boolean isEmpty() { + return backing.isEmpty(); + } + + @Override + public @NotNull LongIterator iterator() { + return new WrappingLongIterator(backing.iterator()); + } + + @NotNull + @Override + public Object @NotNull [] toArray() { + return backing.toArray(); + } + + @NotNull + @Override + public T @NotNull [] toArray(@NotNull T @NotNull [] array) { + Objects.requireNonNull(array, "Array cannot be null"); + return backing.toArray(array); + } + + @Override + public boolean containsAll(@NotNull Collection collection) { + Objects.requireNonNull(collection, "Collection cannot be null"); + return backing.containsAll(collection); + } + + @Override + public boolean addAll(@NotNull Collection collection) { + Objects.requireNonNull(collection, "Collection cannot be null"); + return backing.addAll(collection); + } + + @Override + public boolean removeAll(@NotNull Collection collection) { + Objects.requireNonNull(collection, "Collection cannot be null"); + return backing.removeAll(collection); + } + + @Override + public boolean retainAll(@NotNull Collection collection) { + Objects.requireNonNull(collection, "Collection cannot be null"); + return backing.retainAll(collection); + } + + @Override + public void clear() { + backing.clear(); + } + + @Override + public boolean add(long key) { + return backing.add(key); + } + + @Override + public boolean contains(long key) { + return backing.contains(key); + } + + @Override + public long[] toLongArray() { + int size = backing.size(); + long[] result = new long[size]; + int i = 0; + for (Long value : backing) { + result[i++] = value; + } + return result; + } + + @Override + public long[] toArray(long[] array) { + Objects.requireNonNull(array, "Array cannot be null"); + long[] result = toLongArray(); + if (array.length < result.length) { + return result; + } + System.arraycopy(result, 0, array, 0, result.length); + if (array.length > result.length) { + array[result.length] = 0; + } + return array; + } + + @Override + public boolean addAll(LongCollection c) { + Objects.requireNonNull(c, "Collection cannot be null"); + boolean modified = false; + LongIterator iterator = c.iterator(); + while (iterator.hasNext()) { + modified |= add(iterator.nextLong()); + } + return modified; + } + + @Override + public boolean containsAll(LongCollection c) { + Objects.requireNonNull(c, "Collection cannot be null"); + LongIterator iterator = c.iterator(); + while (iterator.hasNext()) { + if (!contains(iterator.nextLong())) { + return false; + } + } + return true; + } + + @Override + public boolean removeAll(LongCollection c) { + Objects.requireNonNull(c, "Collection cannot be null"); + boolean modified = false; + LongIterator iterator = c.iterator(); + while (iterator.hasNext()) { + modified |= remove(iterator.nextLong()); + } + return modified; + } + + @Override + public boolean retainAll(LongCollection c) { + Objects.requireNonNull(c, "Collection cannot be null"); + return backing.retainAll(c); + } + + @Override + public boolean remove(long k) { + return backing.remove(k); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof LongSet that)) return false; + if (size() != that.size()) return false; + return containsAll(that); + } + + @Override + public int hashCode() { + return backing.hashCode(); + } + + @Override + public String toString() { + return backing.toString(); + } + + static class WrappingLongIterator implements LongIterator { + private final Iterator backing; + + WrappingLongIterator(Iterator backing) { + this.backing = Objects.requireNonNull(backing); + } + + @Override + public boolean hasNext() { + return backing.hasNext(); + } + + @Override + public long nextLong() { + return backing.next(); + } + + @Override + public Long next() { + return backing.next(); + } + + @Override + public void remove() { + backing.remove(); + } + } +}