From 15b7e5aabb68be95c1b6e0b3dbe60e3ee46634c2 Mon Sep 17 00:00:00 2001 From: Nrleryx Date: Fri, 31 Jul 2026 18:37:54 +0300 Subject: [PATCH 1/9] rebuild patches to canonical form --- .../util/SimpleBitStorage.java.patch | 8 ++--- .../block/state/BlockBehaviour.java.patch | 32 +++++++++---------- .../level/chunk/PalettedContainer.java.patch | 14 ++++---- .../NoiseBasedChunkGenerator.java.patch | 7 ++-- 4 files changed, 31 insertions(+), 30 deletions(-) diff --git a/gale-server/minecraft-patches/sources/net/minecraft/util/SimpleBitStorage.java.patch b/gale-server/minecraft-patches/sources/net/minecraft/util/SimpleBitStorage.java.patch index 7f87c295..5b96c4fe 100644 --- a/gale-server/minecraft-patches/sources/net/minecraft/util/SimpleBitStorage.java.patch +++ b/gale-server/minecraft-patches/sources/net/minecraft/util/SimpleBitStorage.java.patch @@ -36,10 +36,11 @@ public SimpleBitStorage(final int bits, final int size) { this(bits, size, (long[])null); } -@@ -399,6 +_,41 @@ +@@ -397,6 +_,41 @@ + @Override public BitStorage copy() { return new SimpleBitStorage(this.bits, this.size, (long[])this.data.clone()); - } ++ } + + // Gale - Chunk serialization + @Override @@ -74,7 +75,6 @@ + bits >>= this.bits; + } + } -+ } + } // Paper start - block counting - @Override diff --git a/gale-server/minecraft-patches/sources/net/minecraft/world/level/block/state/BlockBehaviour.java.patch b/gale-server/minecraft-patches/sources/net/minecraft/world/level/block/state/BlockBehaviour.java.patch index 4a2662c0..bb1caca6 100644 --- a/gale-server/minecraft-patches/sources/net/minecraft/world/level/block/state/BlockBehaviour.java.patch +++ b/gale-server/minecraft-patches/sources/net/minecraft/world/level/block/state/BlockBehaviour.java.patch @@ -100,22 +100,7 @@ public boolean isSolidRender() { return this.solidRender; -@@ -996,8 +_,10 @@ - return this.is(tag) && predicate.test(this); - } - -- public boolean hasBlockEntity() { -- return this.getBlock() instanceof EntityBlock; -+ public final boolean hasBlockEntity() { -+ // Gale start - Pre-compute - BlockBehaviour.hasBlockEntity() -+ return this.gale$precompute_hasBlockEntity; -+ // Gale end - Pre-compute - BlockBehaviour.hasBlockEntity() - } - - public boolean shouldChangedStateKeepBlockEntity(final BlockState oldState) { -@@ -1037,7 +_,15 @@ - public VoxelShape getCollisionShape(final BlockGetter level, final BlockPos pos) { - return this.cache != null ? this.cache.collisionShape : this.getCollisionShape(level, pos, CollisionContext.empty()); +@@ -820,7 +_,17 @@ } public VoxelShape getCollisionShape(final BlockGetter level, final BlockPos pos, final CollisionContext context) { @@ -132,6 +117,21 @@ + } + return shape; } + + public VoxelShape getEntityInsideCollisionShape(final BlockGetter level, final BlockPos pos, final Entity entity) { +@@ -996,8 +_,10 @@ + return this.is(tag) && predicate.test(this); + } + +- public boolean hasBlockEntity() { +- return this.getBlock() instanceof EntityBlock; ++ public final boolean hasBlockEntity() { ++ // Gale start - Pre-compute - BlockBehaviour.hasBlockEntity() ++ return this.gale$precompute_hasBlockEntity; ++ // Gale end - Pre-compute - BlockBehaviour.hasBlockEntity() + } + + public boolean shouldChangedStateKeepBlockEntity(final BlockState oldState) { @@ -1447,6 +_,18 @@ public interface StateArgumentPredicate { boolean test(BlockState state, BlockGetter level, BlockPos pos, A a); diff --git a/gale-server/minecraft-patches/sources/net/minecraft/world/level/chunk/PalettedContainer.java.patch b/gale-server/minecraft-patches/sources/net/minecraft/world/level/chunk/PalettedContainer.java.patch index 1072cbf5..db540aa8 100644 --- a/gale-server/minecraft-patches/sources/net/minecraft/world/level/chunk/PalettedContainer.java.patch +++ b/gale-server/minecraft-patches/sources/net/minecraft/world/level/chunk/PalettedContainer.java.patch @@ -42,10 +42,12 @@ private static int[] reencodeContents(final BitStorage storage, final Palette oldPalette, final Palette newPalette) { int[] buffer = new int[storage.getSize()]; storage.unpack(buffer); -@@ -390,6 +_,48 @@ - return buffer; - } +@@ -388,6 +_,48 @@ + } + return buffer; ++ } ++ + // Gale - Chunk serialization + private static Optional asOptional(final long[] values) { + return Optional.of(Arrays.stream(values)); @@ -86,8 +88,6 @@ + } finally { + this.release(); + } -+ } -+ + } + @Override - public int getSerializedSize() { - return this.data.getSerializedSize(this.strategy.globalMap()); diff --git a/gale-server/minecraft-patches/sources/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java.patch b/gale-server/minecraft-patches/sources/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java.patch index 19833113..aa2aeada 100644 --- a/gale-server/minecraft-patches/sources/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java.patch +++ b/gale-server/minecraft-patches/sources/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java.patch @@ -1,13 +1,12 @@ --- a/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java +++ b/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java -@@ -58,3 +_,5 @@ +@@ -58,11 +_,18 @@ private static final BlockState AIR = Blocks.AIR.defaultBlockState(); private final Holder settings; private final Supplier globalFluidPicker; + private final org.galemc.gale.util.function.MemoizedBooleanSupplier hasCarvers; // Gale - Cache - NoiseBasedChunkGenerator.applyCarvers() + private int cachedSeaLevel = Integer.MIN_VALUE; // Gale - Cache - NoiseBasedChunkGenerator.getSeaLevel() - -@@ -62,5 +_,10 @@ + public NoiseBasedChunkGenerator(final BiomeSource biomeSource, final Holder settings) { super(biomeSource); this.settings = settings; @@ -18,6 +17,8 @@ + ); + // Gale end - Cache - NoiseBasedChunkGenerator.applyCarvers() } + + private static Aquifer.FluidPicker createFluidPicker(final NoiseGeneratorSettings settings) { @@ -310,6 +_,13 @@ final ChunkAccess chunk ) { From 2597e0191cf412d6825d7e71e2c180e13b938655 Mon Sep 17 00:00:00 2001 From: Nrleryx Date: Fri, 31 Jul 2026 18:42:33 +0300 Subject: [PATCH 2/9] micro optimizations: profiler skip, hopper aabb cache, empty beehive and gossip fast paths --- .../server/level/ServerLevel.java.patch | 35 +++++++++++++- .../entity/npc/villager/Villager.java.patch | 15 ++++++ .../entity/BeehiveBlockEntity.java.patch | 10 ++++ .../block/entity/HopperBlockEntity.java.patch | 47 +++++++++++++++++++ 4 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 gale-server/minecraft-patches/sources/net/minecraft/world/entity/npc/villager/Villager.java.patch create mode 100644 gale-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java.patch create mode 100644 gale-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/HopperBlockEntity.java.patch diff --git a/gale-server/minecraft-patches/sources/net/minecraft/server/level/ServerLevel.java.patch b/gale-server/minecraft-patches/sources/net/minecraft/server/level/ServerLevel.java.patch index 92994feb..120f717e 100644 --- a/gale-server/minecraft-patches/sources/net/minecraft/server/level/ServerLevel.java.patch +++ b/gale-server/minecraft-patches/sources/net/minecraft/server/level/ServerLevel.java.patch @@ -102,16 +102,30 @@ } @Override -@@ -1396,7 +_,7 @@ +@@ -1396,16 +_,21 @@ // Paper end - log detailed entity tick information entity.setOldPosAndRot(); ProfilerFiller profiler = Profiler.get(); - entity.tickCount++; + entity.setTickCount(entity.tickCount + 1); // Gale - Event-driven - Cat.canRemoveWhenFarAway, Ocelot.canRemoveWhenFarAway entity.totalEntityAge++; // Paper - age-like counter for all entities ++ boolean profilerActive = profiler != net.minecraft.util.profiling.InactiveProfiler.INSTANCE; // Gale - Do less work - Avoid profiler supplier allocation and calls when profiling is disabled ++ if (profilerActive) { // Gale - Do less work profiler.push(entity.typeHolder()::getRegisteredName); profiler.incrementCounter("tickNonPassenger"); -@@ -1422,9 +_,9 @@ ++ } // Gale end - Do less work + final boolean isActive = io.papermc.paper.entity.activation.ActivationRange.checkIfActive(entity); // Paper - EAR 2 + if (isActive) { // Paper - EAR 2 + entity.tick(); + entity.postTick(); // CraftBukkit + } else {entity.inactiveTick();} // Paper - EAR 2 ++ if (profilerActive) { // Gale - Do less work + profiler.pop(); ++ } // Gale end - Do less work + + for (Entity passenger : entity.getPassengers()) { + this.tickPassenger(entity, passenger, isActive); // Paper - EAR 2 +@@ -1422,13 +_,16 @@ private void tickPassenger(final Entity vehicle, final Entity entity, final boolean isActive) { // Paper - EAR 2 if (entity.isRemoved() || entity.getVehicle() != vehicle) { entity.stopRiding(); @@ -122,7 +136,24 @@ + entity.setTickCount(entity.tickCount + 1); // Gale - Event-driven - Cat.canRemoveWhenFarAway, Ocelot.canRemoveWhenFarAway entity.totalEntityAge++; // Paper - age-like counter for all entities ProfilerFiller profiler = Profiler.get(); ++ boolean profilerActive = profiler != net.minecraft.util.profiling.InactiveProfiler.INSTANCE; // Gale - Do less work - Avoid profiler supplier allocation and calls when profiling is disabled ++ if (profilerActive) { // Gale - Do less work profiler.push(entity.typeHolder()::getRegisteredName); + profiler.incrementCounter("tickPassenger"); ++ } // Gale end - Do less work + // Paper start - EAR 2 + if (isActive) { + entity.rideTick(); +@@ -1440,7 +_,9 @@ + vehicle.positionRider(entity); + } + // Paper end - EAR 2 ++ if (profilerActive) { // Gale - Do less work + profiler.pop(); ++ } // Gale end - Do less work + + for (Entity passenger : entity.getPassengers()) { + this.tickPassenger(entity, passenger, isActive); // Paper - EAR 2 @@ -1717,6 +_,13 @@ @Override diff --git a/gale-server/minecraft-patches/sources/net/minecraft/world/entity/npc/villager/Villager.java.patch b/gale-server/minecraft-patches/sources/net/minecraft/world/entity/npc/villager/Villager.java.patch new file mode 100644 index 00000000..e0d9c5a6 --- /dev/null +++ b/gale-server/minecraft-patches/sources/net/minecraft/world/entity/npc/villager/Villager.java.patch @@ -0,0 +1,15 @@ +--- a/net/minecraft/world/entity/npc/villager/Villager.java ++++ b/net/minecraft/world/entity/npc/villager/Villager.java +@@ -887,7 +_,11 @@ + if (this.lastGossipDecayTime == 0L) { + this.lastGossipDecayTime = timestamp; + } else if (timestamp >= this.lastGossipDecayTime + 24000L) { +- this.gossips.decay(); ++ // Gale start - Do less work - Skip gossip decay for villagers without any gossip ++ if (!this.gossips.gossips.isEmpty()) { ++ this.gossips.decay(); ++ } ++ // Gale end - Do less work - Skip gossip decay for villagers without any gossip + this.lastGossipDecayTime = timestamp; + } + } diff --git a/gale-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java.patch b/gale-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java.patch new file mode 100644 index 00000000..2f2740e6 --- /dev/null +++ b/gale-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java.patch @@ -0,0 +1,10 @@ +--- a/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java +@@ -335,6 +_,7 @@ + } + + public static void serverTick(final Level level, final BlockPos blockPos, final BlockState state, final BeehiveBlockEntity entity) { ++ if (entity.stored.isEmpty()) return; // Gale - Do less work - Skip bee hive ticking when it holds no bees + tickOccupants(level, blockPos, state, entity.stored, entity.savedFlowerPos); + if (!entity.stored.isEmpty() && level.getRandom().nextDouble() < 0.005) { + double x = blockPos.getX() + 0.5; diff --git a/gale-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/HopperBlockEntity.java.patch b/gale-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/HopperBlockEntity.java.patch new file mode 100644 index 00000000..7109ad67 --- /dev/null +++ b/gale-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/HopperBlockEntity.java.patch @@ -0,0 +1,47 @@ +--- a/net/minecraft/world/level/block/entity/HopperBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/HopperBlockEntity.java +@@ -38,6 +_,7 @@ + public int cooldownTime = -1; + private long tickedGameTime; + private Direction facing; ++ private @Nullable AABB cachedSuckAabb; // Gale - Do less work - Cache hopper suck AABB + + // CraftBukkit start - add fields and methods + public List transaction = new java.util.ArrayList<>(); +@@ -668,7 +_,9 @@ + } + + public static List getItemsAtAndAbove(final Level level, final Hopper hopper) { +- AABB aabb = hopper.getSuckAabb().move(hopper.getLevelX() - 0.5, hopper.getLevelY() - 0.5, hopper.getLevelZ() - 0.5); ++ AABB aabb = hopper instanceof HopperBlockEntity hopperBlockEntity // Gale - Do less work - Cache hopper suck AABB ++ ? hopperBlockEntity.gale$getCachedSuckAabb() ++ : hopper.getSuckAabb().move(hopper.getLevelX() - 0.5, hopper.getLevelY() - 0.5, hopper.getLevelZ() - 0.5); + return level.getEntitiesOfClass(ItemEntity.class, aabb, EntitySelector.ENTITY_STILL_ALIVE); + } + +@@ -742,6 +_,16 @@ + return true; + } + ++ // Gale start - Do less work - Cache hopper suck AABB ++ public AABB gale$getCachedSuckAabb() { ++ AABB aabb = this.cachedSuckAabb; ++ if (aabb == null) { ++ this.cachedSuckAabb = aabb = this.getSuckAabb().move(this.getLevelX() - 0.5, this.getLevelY() - 0.5, this.getLevelZ() - 0.5); ++ } ++ return aabb; ++ } ++ // Gale end - Do less work - Cache hopper suck AABB ++ + public void setCooldown(final int time) { + this.cooldownTime = time; + } +@@ -767,7 +_,7 @@ + public static void entityInside(final Level level, final BlockPos pos, final BlockState blockState, final Entity entity, final HopperBlockEntity hopper) { + if (entity instanceof ItemEntity itemEntity + && !itemEntity.getItem().isEmpty() +- && entity.getBoundingBox().move(-pos.getX(), -pos.getY(), -pos.getZ()).intersects(hopper.getSuckAabb())) { ++ && entity.getBoundingBox().intersects(hopper.gale$getCachedSuckAabb())) { // Gale - Do less work - Cache hopper suck AABB, avoids AABB allocation per item entity + tryMoveItems(level, pos, blockState, hopper, () -> addItem(hopper, itemEntity)); + } + } From 9d4dadcc75a41e480283c92cff77848f44df9a0d Mon Sep 17 00:00:00 2001 From: Nrleryx Date: Fri, 31 Jul 2026 18:57:49 +0300 Subject: [PATCH 3/9] spawner and game event micro optimizations --- .../world/level/BaseSpawner.java.patch | 150 ++++++++++++++++++ ...lideanGameEventListenerRegistry.java.patch | 47 ++++++ .../gameevent/GameEventDispatcher.java.patch | 107 +++++++++++++ 3 files changed, 304 insertions(+) create mode 100644 gale-server/minecraft-patches/sources/net/minecraft/world/level/BaseSpawner.java.patch create mode 100644 gale-server/minecraft-patches/sources/net/minecraft/world/level/gameevent/EuclideanGameEventListenerRegistry.java.patch create mode 100644 gale-server/minecraft-patches/sources/net/minecraft/world/level/gameevent/GameEventDispatcher.java.patch diff --git a/gale-server/minecraft-patches/sources/net/minecraft/world/level/BaseSpawner.java.patch b/gale-server/minecraft-patches/sources/net/minecraft/world/level/BaseSpawner.java.patch new file mode 100644 index 00000000..88b021e1 --- /dev/null +++ b/gale-server/minecraft-patches/sources/net/minecraft/world/level/BaseSpawner.java.patch @@ -0,0 +1,150 @@ +--- a/net/minecraft/world/level/BaseSpawner.java ++++ b/net/minecraft/world/level/BaseSpawner.java +@@ -106,69 +_,81 @@ + boolean delay = false; + RandomSource random = level.getRandom(); + SpawnData nextSpawnData = this.getOrCreateNextSpawnData(level, random, pos); +- ++ // Gale start - Do less work - Parse spawn data once per spawner tick instead of once per spawned entity ++ Optional> entityType; ++ Optional fixedSpawnPos; ++ try (ProblemReporter.ScopedCollector reporter = new ProblemReporter.ScopedCollector(this::toString, LOGGER)) { ++ ValueInput input = TagValueInput.create(reporter, level.registryAccess(), nextSpawnData.getEntityToSpawn()); ++ entityType = EntityType.by(input); ++ if (entityType.isEmpty()) { ++ this.delay(level, pos); ++ return; ++ } ++ ++ fixedSpawnPos = input.read("Pos", Vec3.CODEC); ++ } ++ ++ AABB nearbyEntitiesBox = new AABB(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, pos.getY() + 1, pos.getZ() + 1).inflate(this.spawnRange); ++ // Gale end - Do less work - Parse spawn data once per spawner tick instead of once per spawned entity + for (int c = 0; c < this.spawnCount; c++) { +- try (ProblemReporter.ScopedCollector reporter = new ProblemReporter.ScopedCollector(this::toString, LOGGER)) { +- ValueInput input = TagValueInput.create(reporter, level.registryAccess(), nextSpawnData.getEntityToSpawn()); +- Optional> entityType = EntityType.by(input); +- if (entityType.isEmpty()) { +- this.delay(level, pos); +- return; +- } +- +- Vec3 spawnPos = input.read("Pos", Vec3.CODEC) +- .orElseGet( +- () -> new Vec3( +- pos.getX() + (random.nextDouble() - random.nextDouble()) * this.spawnRange + 0.5, +- pos.getY() + random.nextInt(3) - 1, +- pos.getZ() + (random.nextDouble() - random.nextDouble()) * this.spawnRange + 0.5 +- ) +- ); +- if (level.noCollision(entityType.get().getSpawnAABB(spawnPos.x, spawnPos.y, spawnPos.z))) { +- BlockPos spawnBlockPos = BlockPos.containing(spawnPos); +- if (nextSpawnData.getCustomSpawnRules().isPresent()) { +- if (!entityType.get().getCategory().isFriendly() && level.getDifficulty() == Difficulty.PEACEFUL) { +- continue; +- } +- +- SpawnData.CustomSpawnRules customSpawnRules = nextSpawnData.getCustomSpawnRules().get(); +- if (!customSpawnRules.isValidPosition(spawnBlockPos, level)) { +- continue; +- } +- } else if (!SpawnPlacements.checkSpawnRules(entityType.get(), level, EntitySpawnReason.SPAWNER, spawnBlockPos, level.getRandom())) { +- continue; +- } +- +- // Paper start - PreCreatureSpawnEvent +- com.destroystokyo.paper.event.entity.PreSpawnerSpawnEvent event = new com.destroystokyo.paper.event.entity.PreSpawnerSpawnEvent( +- org.bukkit.craftbukkit.util.CraftLocation.toBukkit(spawnPos, level), +- org.bukkit.craftbukkit.entity.CraftEntityType.minecraftToBukkit(entityType.get()), +- org.bukkit.craftbukkit.util.CraftLocation.toBukkit(pos, level) +- ); +- if (!event.callEvent()) { +- delay = true; +- if (event.shouldAbortSpawn()) { +- break; +- } +- continue; +- } +- // Paper end - PreCreatureSpawnEvent +- +- Entity entity = EntityType.loadEntityRecursive(input, level, EntitySpawnReason.SPAWNER, e -> { ++ Vec3 spawnPos = fixedSpawnPos.orElseGet( ++ () -> new Vec3( ++ pos.getX() + (random.nextDouble() - random.nextDouble()) * this.spawnRange + 0.5, ++ pos.getY() + random.nextInt(3) - 1, ++ pos.getZ() + (random.nextDouble() - random.nextDouble()) * this.spawnRange + 0.5 ++ ) ++ ); ++ if (level.noCollision(entityType.get().getSpawnAABB(spawnPos.x, spawnPos.y, spawnPos.z))) { ++ BlockPos spawnBlockPos = BlockPos.containing(spawnPos); ++ if (nextSpawnData.getCustomSpawnRules().isPresent()) { ++ if (!entityType.get().getCategory().isFriendly() && level.getDifficulty() == Difficulty.PEACEFUL) { ++ continue; ++ } ++ ++ SpawnData.CustomSpawnRules customSpawnRules = nextSpawnData.getCustomSpawnRules().get(); ++ if (!customSpawnRules.isValidPosition(spawnBlockPos, level)) { ++ continue; ++ } ++ } else if (!SpawnPlacements.checkSpawnRules(entityType.get(), level, EntitySpawnReason.SPAWNER, spawnBlockPos, level.getRandom())) { ++ continue; ++ } ++ ++ // Paper start - PreCreatureSpawnEvent ++ com.destroystokyo.paper.event.entity.PreSpawnerSpawnEvent event = new com.destroystokyo.paper.event.entity.PreSpawnerSpawnEvent( ++ org.bukkit.craftbukkit.util.CraftLocation.toBukkit(spawnPos, level), ++ org.bukkit.craftbukkit.entity.CraftEntityType.minecraftToBukkit(entityType.get()), ++ org.bukkit.craftbukkit.util.CraftLocation.toBukkit(pos, level) ++ ); ++ if (!event.callEvent()) { ++ delay = true; ++ if (event.shouldAbortSpawn()) { ++ break; ++ } ++ continue; ++ } ++ // Paper end - PreCreatureSpawnEvent ++ ++ // Gale start - Do less work - Only entity loading needs a fresh input ++ Entity entity; ++ try (ProblemReporter.ScopedCollector reporter = new ProblemReporter.ScopedCollector(this::toString, LOGGER)) { ++ ValueInput input = TagValueInput.create(reporter, level.registryAccess(), nextSpawnData.getEntityToSpawn()); ++ entity = EntityType.loadEntityRecursive(input, level, EntitySpawnReason.SPAWNER, e -> { + e.snapTo(spawnPos.x, spawnPos.y, spawnPos.z, e.getYRot(), e.getXRot()); + return e; + }); +- if (entity == null) { +- this.delay(level, pos); +- return; +- } ++ } ++ // Gale end - Do less work - Only entity loading needs a fresh input ++ if (entity == null) { ++ this.delay(level, pos); ++ return; ++ } + +- int nearBy = level.getEntities( +- EntityTypeTest.forExactClass(entity.getClass()), +- new AABB(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, pos.getY() + 1, pos.getZ() + 1).inflate(this.spawnRange), +- EntitySelector.NO_SPECTATORS +- ) +- .size(); ++ int nearBy = level.getEntities( ++ EntityTypeTest.forExactClass(entity.getClass()), ++ nearbyEntitiesBox, // Gale - Do less work - Hoist nearby entity query box out of the spawn loop ++ EntitySelector.NO_SPECTATORS ++ ) ++ .size(); + if (nearBy >= this.maxNearbyEntities) { + this.delay(level, pos); + return; +@@ -217,7 +_,6 @@ + // delay = true; // Paper - moved up above cancellable event + } + } +- } + + if (delay) { + this.delay(level, pos); diff --git a/gale-server/minecraft-patches/sources/net/minecraft/world/level/gameevent/EuclideanGameEventListenerRegistry.java.patch b/gale-server/minecraft-patches/sources/net/minecraft/world/level/gameevent/EuclideanGameEventListenerRegistry.java.patch new file mode 100644 index 00000000..072ea4fc --- /dev/null +++ b/gale-server/minecraft-patches/sources/net/minecraft/world/level/gameevent/EuclideanGameEventListenerRegistry.java.patch @@ -0,0 +1,47 @@ +--- a/net/minecraft/world/level/gameevent/EuclideanGameEventListenerRegistry.java ++++ b/net/minecraft/world/level/gameevent/EuclideanGameEventListenerRegistry.java +@@ -4,7 +_,6 @@ + import com.google.common.collect.Sets; + import java.util.Iterator; + import java.util.List; +-import java.util.Optional; + import java.util.Set; + import net.minecraft.core.BlockPos; + import net.minecraft.core.Holder; +@@ -88,11 +_,15 @@ + if (this.listenersToRemove.remove(listener)) { + iterator.remove(); + } else { +- Optional optionalPosition = getPostableListenerPosition(this.level, sourcePosition, listener); +- if (optionalPosition.isPresent()) { +- action.visit(listener, optionalPosition.get()); ++ // Gale start - Do less work - Inline the range check to avoid Optional allocations per listener per event ++ Vec3 listenerPosition = listener.getListenerSource().getPosition(this.level).orElse(null); ++ if (listenerPosition != null ++ && BlockPos.containing(listenerPosition).distSqr(BlockPos.containing(sourcePosition)) ++ <= (long) listener.getListenerRadius() * listener.getListenerRadius()) { ++ action.visit(listener, listenerPosition); + applicable = true; + } ++ // Gale end - Do less work - Inline the range check to avoid Optional allocations per listener per event + } + } + } finally { +@@ -110,17 +_,6 @@ + } + + return applicable; +- } +- +- private static Optional getPostableListenerPosition(final ServerLevel level, final Vec3 sourcePosition, final GameEventListener listener) { +- Optional position = listener.getListenerSource().getPosition(level); +- if (position.isEmpty()) { +- return Optional.empty(); +- } +- +- double distanceFromOrigin = BlockPos.containing(position.get()).distSqr(BlockPos.containing(sourcePosition)); +- int radiusSqr = listener.getListenerRadius() * listener.getListenerRadius(); +- return distanceFromOrigin > radiusSqr ? Optional.empty() : position; + } + + @FunctionalInterface diff --git a/gale-server/minecraft-patches/sources/net/minecraft/world/level/gameevent/GameEventDispatcher.java.patch b/gale-server/minecraft-patches/sources/net/minecraft/world/level/gameevent/GameEventDispatcher.java.patch new file mode 100644 index 00000000..9c5e8bef --- /dev/null +++ b/gale-server/minecraft-patches/sources/net/minecraft/world/level/gameevent/GameEventDispatcher.java.patch @@ -0,0 +1,107 @@ +--- a/net/minecraft/world/level/gameevent/GameEventDispatcher.java ++++ b/net/minecraft/world/level/gameevent/GameEventDispatcher.java +@@ -1,5 +_,6 @@ + package net.minecraft.world.level.gameevent; + ++import com.google.common.collect.Lists; + import java.util.ArrayList; + import java.util.Collections; + import java.util.List; +@@ -13,6 +_,13 @@ + import net.minecraft.world.phys.Vec3; + + public class GameEventDispatcher { ++ // Gale start - Do less work - Reuse a single visitor instance and dispatch list across event posts ++ private final List eventsToDispatch = Lists.newArrayList(); ++ private final GameEventListenerRegistry.ListenerVisitor visitor = this::visitListener; ++ private Holder gameEvent; ++ private Vec3 position; ++ private GameEvent.Context context; ++ // Gale end - Do less work - Reuse a single visitor instance and dispatch list across event posts + private final ServerLevel level; + + public GameEventDispatcher(final ServerLevel level) { +@@ -37,30 +_,34 @@ + int sectionMaxX = SectionPos.blockToSectionCoord(center.getX() + radius); + int sectionMaxY = SectionPos.blockToSectionCoord(center.getY() + radius); + int sectionMaxZ = SectionPos.blockToSectionCoord(center.getZ() + radius); +- List toHandleByDistance = new ArrayList<>(); +- GameEventListenerRegistry.ListenerVisitor visitListeners = (listener, pos) -> { +- if (listener.getDeliveryMode() == GameEventListener.DeliveryMode.BY_DISTANCE) { +- toHandleByDistance.add(new GameEvent.ListenerInfo(gameEvent, position, context, listener, pos)); +- } else { +- listener.handleGameEvent(this.level, gameEvent, context, position); +- } +- }; ++ // Gale start - Do less work - Reuse the visitor and dispatch list instead of allocating per post ++ Holder prevGameEvent = this.gameEvent; ++ Vec3 prevPosition = this.position; ++ GameEvent.Context prevContext = this.context; ++ this.gameEvent = gameEvent; ++ this.position = position; ++ this.context = context; + boolean applicable = false; + +- for (int chunkX = sectionMinX; chunkX <= sectionMaxX; chunkX++) { +- for (int chunkZ = sectionMinZ; chunkZ <= sectionMaxZ; chunkZ++) { +- ChunkAccess chunk = this.level.getChunkIfLoadedImmediately(chunkX, chunkZ); // Paper - Use getChunkIfLoadedImmediately +- if (chunk != null) { +- for (int section = sectionMinY; section <= sectionMaxY; section++) { +- applicable |= chunk.getListenerRegistry(section).visitInRangeListeners(gameEvent, position, context, visitListeners); ++ try { ++ for (int chunkX = sectionMinX; chunkX <= sectionMaxX; chunkX++) { ++ for (int chunkZ = sectionMinZ; chunkZ <= sectionMaxZ; chunkZ++) { ++ ChunkAccess chunk = this.level.getChunkIfLoadedImmediately(chunkX, chunkZ); // Paper - Use getChunkIfLoadedImmediately ++ if (chunk != null) { ++ for (int section = sectionMinY; section <= sectionMaxY; section++) { ++ applicable |= chunk.getListenerRegistry(section).visitInRangeListeners(gameEvent, position, context, this.visitor); ++ } + } + } + } +- } + +- if (!toHandleByDistance.isEmpty()) { +- this.handleGameEventMessagesInQueue(toHandleByDistance); ++ this.dispatchPendingEvents(); ++ } finally { ++ this.gameEvent = prevGameEvent; ++ this.position = prevPosition; ++ this.context = prevContext; + } ++ // Gale end - Do less work - Reuse the visitor and dispatch list instead of allocating per post + + if (applicable) { + this.level +@@ -68,6 +_,31 @@ + .broadcastEventToTracking(BlockPos.containing(position), DebugSubscriptions.GAME_EVENTS, new DebugGameEventInfo(gameEvent, position)); + } + } ++ ++ // Gale start - Do less work - Reusable visitor entry point ++ private void visitListener(final GameEventListener listener, final Vec3 pos) { ++ if (listener.getDeliveryMode() == GameEventListener.DeliveryMode.BY_DISTANCE) { ++ this.eventsToDispatch.add(new GameEvent.ListenerInfo(this.gameEvent, this.position, this.context, listener, pos)); ++ } else { ++ listener.handleGameEvent(this.level, this.gameEvent, this.context, this.position); ++ } ++ } ++ ++ private void dispatchPendingEvents() { ++ if (this.eventsToDispatch.isEmpty()) { ++ return; ++ } ++ ++ List toHandle = this.eventsToDispatch; ++ this.eventsToDispatch = new ArrayList<>(); ++ try { ++ this.handleGameEventMessagesInQueue(toHandle); ++ } finally { ++ toHandle.clear(); ++ this.eventsToDispatch = toHandle; ++ } ++ } ++ // Gale end - Do less work - Reusable visitor entry point + + private void handleGameEventMessagesInQueue(final List listenerInfos) { + Collections.sort(listenerInfos); From 0a368b75d19e65e22a8c07164b01147a373af98d Mon Sep 17 00:00:00 2001 From: Nrleryx Date: Fri, 31 Jul 2026 18:59:04 +0300 Subject: [PATCH 4/9] fix non-final eventsToDispatch in game event dispatcher patch --- .../world/level/gameevent/GameEventDispatcher.java.patch | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gale-server/minecraft-patches/sources/net/minecraft/world/level/gameevent/GameEventDispatcher.java.patch b/gale-server/minecraft-patches/sources/net/minecraft/world/level/gameevent/GameEventDispatcher.java.patch index 9c5e8bef..4bbe187c 100644 --- a/gale-server/minecraft-patches/sources/net/minecraft/world/level/gameevent/GameEventDispatcher.java.patch +++ b/gale-server/minecraft-patches/sources/net/minecraft/world/level/gameevent/GameEventDispatcher.java.patch @@ -12,7 +12,7 @@ public class GameEventDispatcher { + // Gale start - Do less work - Reuse a single visitor instance and dispatch list across event posts -+ private final List eventsToDispatch = Lists.newArrayList(); ++ private List eventsToDispatch = Lists.newArrayList(); + private final GameEventListenerRegistry.ListenerVisitor visitor = this::visitListener; + private Holder gameEvent; + private Vec3 position; From b5bbf1bbe27f19ccf0127c5c01293fddb5c4adeb Mon Sep 17 00:00:00 2001 From: Nrleryx Date: Fri, 31 Jul 2026 19:02:44 +0300 Subject: [PATCH 5/9] server level tick micro optimizations --- .../server/level/ServerLevel.java.patch | 53 +++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/gale-server/minecraft-patches/sources/net/minecraft/server/level/ServerLevel.java.patch b/gale-server/minecraft-patches/sources/net/minecraft/server/level/ServerLevel.java.patch index 120f717e..e6575663 100644 --- a/gale-server/minecraft-patches/sources/net/minecraft/server/level/ServerLevel.java.patch +++ b/gale-server/minecraft-patches/sources/net/minecraft/server/level/ServerLevel.java.patch @@ -74,14 +74,46 @@ profiler.push("entities"); if (this.dragonFight != null && runs) { profiler.push("dragonFight"); -@@ -890,6 +_,7 @@ - profiler.push("checkDespawn"); +@@ -887,9 +_,18 @@ + entity -> { + if (!entity.isRemoved()) { + if (!tickRateManager.isEntityFrozen(entity)) { +- profiler.push("checkDespawn"); ++ // Gale start - Do less work - Skip profiler pushes when profiling is inactive ++ boolean isProfilerActive = profiler != net.minecraft.util.profiling.InactiveProfiler.INSTANCE; ++ if (isProfilerActive) { ++ profiler.push("checkDespawn"); ++ } ++ entity.checkDespawn(); - profiler.pop(); +- profiler.pop(); ++ if (isProfilerActive) { ++ profiler.pop(); ++ } ++ // Gale end - Do less work - Skip profiler pushes when profiling is inactive + if (!org.galemc.gale.async.SimulationFlag.REAL) return; // Gale - Speculative execution based on next tick simulation - Simulate next tick if (true) { // Paper - rewrite chunk system Entity vehicle = entity.getVehicle(); if (vehicle != null) { +@@ -900,9 +_,16 @@ + entity.stopRiding(); + } + +- profiler.push("tick"); ++ // Gale start - Do less work - Skip profiler pushes when profiling is inactive ++ if (isProfilerActive) { ++ profiler.push("tick"); ++ } ++ + this.guardEntityTick(this::tickNonPassenger, entity); +- profiler.pop(); ++ if (isProfilerActive) { ++ profiler.pop(); ++ } ++ // Gale end - Do less work - Skip profiler pushes when profiling is inactive + } + } + } @@ -913,9 +_,10 @@ this.tickBlockEntities(); profiler.pop(); @@ -102,6 +134,21 @@ } @Override +@@ -957,7 +_,13 @@ + + private void wakeUpAllPlayers() { + this.sleepStatus.removeAllSleepers(); +- this.players.stream().filter(LivingEntity::isSleeping).collect(Collectors.toList()).forEach(player -> player.stopSleepInBed(false, false)); ++ // Gale start - Do less work - Avoid stream and list allocation ++ for (ServerPlayer player : this.players) { ++ if (player.isSleeping()) { ++ player.stopSleepInBed(false, false); ++ } ++ } ++ // Gale end - Do less work - Avoid stream and list allocation + } + + // Paper start - optimise random ticking @@ -1396,16 +_,21 @@ // Paper end - log detailed entity tick information entity.setOldPosAndRot(); From a0e0d7933d10ed67301def93e19c28a7c64c7bf1 Mon Sep 17 00:00:00 2001 From: Nrleryx Date: Fri, 31 Jul 2026 19:08:02 +0300 Subject: [PATCH 6/9] chunk and block entity tick micro optimizations --- .../server/level/ServerLevel.java.patch | 32 +++++++++++++++++++ .../minecraft/world/level/Level.java.patch | 27 ++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/gale-server/minecraft-patches/sources/net/minecraft/server/level/ServerLevel.java.patch b/gale-server/minecraft-patches/sources/net/minecraft/server/level/ServerLevel.java.patch index e6575663..175666ad 100644 --- a/gale-server/minecraft-patches/sources/net/minecraft/server/level/ServerLevel.java.patch +++ b/gale-server/minecraft-patches/sources/net/minecraft/server/level/ServerLevel.java.patch @@ -149,6 +149,38 @@ } // Paper start - optimise random ticking +@@ -1013,6 +_,11 @@ + // Paper end - optimise random ticking + + public void tickChunk(final LevelChunk chunk, final int tickSpeed) { ++ // Gale start - Do less work - Skip the whole chunk tick when there is nothing to do ++ if (tickSpeed <= 0) { ++ return; ++ } ++ // Gale end - Do less work - Skip the whole chunk tick when there is nothing to do + final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = this.simpleRandom; // Paper - optimise random ticking + ChunkPos chunkPos = chunk.getPos(); + int minX = chunkPos.getMinBlockX(); +@@ -1037,13 +_,17 @@ + } + + public void tickThunder(final LevelChunk chunk) { ++ // Gale start - Do less work - Skip the profiler and random calls when thunder cannot happen ++ if (!this.isRaining() || this.paperConfig().environment.disableThunder || !this.isThundering() || this.spigotConfig.thunderChance <= 0) { ++ return; ++ } ++ // Gale end - Do less work - Skip the profiler and random calls when thunder cannot happen + ChunkPos chunkPos = chunk.getPos(); +- boolean raining = this.isRaining(); + int minX = chunkPos.getMinBlockX(); + int minZ = chunkPos.getMinBlockZ(); + ProfilerFiller profiler = Profiler.get(); + profiler.push("thunder"); +- if (!this.paperConfig().environment.disableThunder && raining && this.isThundering() && this.spigotConfig.thunderChance > 0 && this.random.nextInt(this.spigotConfig.thunderChance) == 0) { // Spigot // Paper - Option to disable thunder ++ if (this.random.nextInt(this.spigotConfig.thunderChance) == 0) { // Spigot // Paper - Option to disable thunder + BlockPos pos = this.findLightningTargetAround(this.getBlockRandomPos(minX, 0, minZ, 15)); + if (this.isRainingAt(pos)) { + DifficultyInstance difficulty = this.getCurrentDifficultyAt(pos); @@ -1396,16 +_,21 @@ // Paper end - log detailed entity tick information entity.setOldPosAndRot(); diff --git a/gale-server/minecraft-patches/sources/net/minecraft/world/level/Level.java.patch b/gale-server/minecraft-patches/sources/net/minecraft/world/level/Level.java.patch index 96f847f7..7a996150 100644 --- a/gale-server/minecraft-patches/sources/net/minecraft/world/level/Level.java.patch +++ b/gale-server/minecraft-patches/sources/net/minecraft/world/level/Level.java.patch @@ -288,6 +288,33 @@ this.tickingBlockEntities = true; if (!this.pendingBlockEntityTickers.isEmpty()) { this.blockEntityTickers.addAll(this.pendingBlockEntityTickers); +@@ -1498,11 +_,13 @@ + // Paper start - Fix MC-117075 use removeAll + final it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<@Nullable TickingBlockEntity> toRemove = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(); + toRemove.add(null); ++ boolean removeAny = false; // Gale - Do less work - Skip the removeAll scan when nothing was removed + for (int tickerIndex = 0; tickerIndex < this.blockEntityTickers.size(); tickerIndex++) { + final TickingBlockEntity ticker = this.blockEntityTickers.get(tickerIndex); + // Paper end - Fix MC-117075 use removeAll + if (ticker.isRemoved()) { + toRemove.add(ticker); // Paper - Fix MC-117075 use removeAll ++ removeAny = true; // Gale - Do less work - Skip the removeAll scan when nothing was removed + } else if (tickBlockEntities && this.shouldTickBlocksAt(ticker.getPos())) { + ticker.tick(); + // Paper start - rewrite chunk system +@@ -1513,7 +_,11 @@ + } + } + +- this.blockEntityTickers.removeAll(toRemove); // Paper - Fix MC-117075 use removeAll ++ // Gale start - Do less work - Skip the removeAll scan when nothing was removed ++ if (removeAny) { ++ this.blockEntityTickers.removeAll(toRemove); // Paper - Fix MC-117075 use removeAll ++ } ++ // Gale end - Do less work - Skip the removeAll scan when nothing was removed + this.tickingBlockEntities = false; + } + @@ -2127,6 +_,13 @@ public BiomeManager getBiomeManager() { return this.biomeManager; From b4d0f05a55cb7dc765ed7707c5858d097797101a Mon Sep 17 00:00:00 2001 From: Nrleryx Date: Fri, 31 Jul 2026 19:26:15 +0300 Subject: [PATCH 7/9] fix hopper suck aabb cache invalidation and game event dispatcher leak --- .../level/block/entity/HopperBlockEntity.java.patch | 9 ++++++--- .../world/level/gameevent/GameEventDispatcher.java.patch | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/gale-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/HopperBlockEntity.java.patch b/gale-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/HopperBlockEntity.java.patch index 7109ad67..83d3b781 100644 --- a/gale-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/HopperBlockEntity.java.patch +++ b/gale-server/minecraft-patches/sources/net/minecraft/world/level/block/entity/HopperBlockEntity.java.patch @@ -1,10 +1,11 @@ --- a/net/minecraft/world/level/block/entity/HopperBlockEntity.java +++ b/net/minecraft/world/level/block/entity/HopperBlockEntity.java -@@ -38,6 +_,7 @@ +@@ -38,6 +_,8 @@ public int cooldownTime = -1; private long tickedGameTime; private Direction facing; + private @Nullable AABB cachedSuckAabb; // Gale - Do less work - Cache hopper suck AABB ++ private @Nullable BlockPos cachedSuckPos; // Gale - Do less work - Cache hopper suck AABB (invalidate on position change) // CraftBukkit start - add fields and methods public List transaction = new java.util.ArrayList<>(); @@ -19,15 +20,17 @@ return level.getEntitiesOfClass(ItemEntity.class, aabb, EntitySelector.ENTITY_STILL_ALIVE); } -@@ -742,6 +_,16 @@ +@@ -742,6 +_,18 @@ return true; } + // Gale start - Do less work - Cache hopper suck AABB + public AABB gale$getCachedSuckAabb() { + AABB aabb = this.cachedSuckAabb; -+ if (aabb == null) { ++ BlockPos pos = this.getBlockPos(); ++ if (aabb == null || !pos.equals(this.cachedSuckPos)) { + this.cachedSuckAabb = aabb = this.getSuckAabb().move(this.getLevelX() - 0.5, this.getLevelY() - 0.5, this.getLevelZ() - 0.5); ++ this.cachedSuckPos = pos; + } + return aabb; + } diff --git a/gale-server/minecraft-patches/sources/net/minecraft/world/level/gameevent/GameEventDispatcher.java.patch b/gale-server/minecraft-patches/sources/net/minecraft/world/level/gameevent/GameEventDispatcher.java.patch index 4bbe187c..06c31ffe 100644 --- a/gale-server/minecraft-patches/sources/net/minecraft/world/level/gameevent/GameEventDispatcher.java.patch +++ b/gale-server/minecraft-patches/sources/net/minecraft/world/level/gameevent/GameEventDispatcher.java.patch @@ -21,7 +21,7 @@ private final ServerLevel level; public GameEventDispatcher(final ServerLevel level) { -@@ -37,30 +_,34 @@ +@@ -37,30 +_,35 @@ int sectionMaxX = SectionPos.blockToSectionCoord(center.getX() + radius); int sectionMaxY = SectionPos.blockToSectionCoord(center.getY() + radius); int sectionMaxZ = SectionPos.blockToSectionCoord(center.getZ() + radius); @@ -65,6 +65,7 @@ - this.handleGameEventMessagesInQueue(toHandleByDistance); + this.dispatchPendingEvents(); + } finally { ++ this.eventsToDispatch.clear(); // Gale - do not leak stale entries if a listener throws mid-visit + this.gameEvent = prevGameEvent; + this.position = prevPosition; + this.context = prevContext; From 3548f399e9153992035145f1599ada9c48fa7516 Mon Sep 17 00:00:00 2001 From: Nrleryx Date: Fri, 31 Jul 2026 19:39:18 +0300 Subject: [PATCH 8/9] revert spawner parse hoist for vanilla-equivalent plugin mutation semantics --- .../world/level/BaseSpawner.java.patch | 152 ++---------------- 1 file changed, 11 insertions(+), 141 deletions(-) diff --git a/gale-server/minecraft-patches/sources/net/minecraft/world/level/BaseSpawner.java.patch b/gale-server/minecraft-patches/sources/net/minecraft/world/level/BaseSpawner.java.patch index 88b021e1..07b17490 100644 --- a/gale-server/minecraft-patches/sources/net/minecraft/world/level/BaseSpawner.java.patch +++ b/gale-server/minecraft-patches/sources/net/minecraft/world/level/BaseSpawner.java.patch @@ -1,150 +1,20 @@ --- a/net/minecraft/world/level/BaseSpawner.java +++ b/net/minecraft/world/level/BaseSpawner.java -@@ -106,69 +_,81 @@ +@@ -106,7 +_,7 @@ boolean delay = false; RandomSource random = level.getRandom(); SpawnData nextSpawnData = this.getOrCreateNextSpawnData(level, random, pos); - -+ // Gale start - Do less work - Parse spawn data once per spawner tick instead of once per spawned entity -+ Optional> entityType; -+ Optional fixedSpawnPos; -+ try (ProblemReporter.ScopedCollector reporter = new ProblemReporter.ScopedCollector(this::toString, LOGGER)) { -+ ValueInput input = TagValueInput.create(reporter, level.registryAccess(), nextSpawnData.getEntityToSpawn()); -+ entityType = EntityType.by(input); -+ if (entityType.isEmpty()) { -+ this.delay(level, pos); -+ return; -+ } -+ -+ fixedSpawnPos = input.read("Pos", Vec3.CODEC); -+ } -+ -+ AABB nearbyEntitiesBox = new AABB(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, pos.getY() + 1, pos.getZ() + 1).inflate(this.spawnRange); -+ // Gale end - Do less work - Parse spawn data once per spawner tick instead of once per spawned entity ++ AABB nearbyEntitiesBox = new AABB(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, pos.getY() + 1, pos.getZ() + 1).inflate(this.spawnRange); // Gale - Do less work - Hoist nearby entity query box out of the spawn loop for (int c = 0; c < this.spawnCount; c++) { -- try (ProblemReporter.ScopedCollector reporter = new ProblemReporter.ScopedCollector(this::toString, LOGGER)) { -- ValueInput input = TagValueInput.create(reporter, level.registryAccess(), nextSpawnData.getEntityToSpawn()); -- Optional> entityType = EntityType.by(input); -- if (entityType.isEmpty()) { -- this.delay(level, pos); -- return; -- } -- -- Vec3 spawnPos = input.read("Pos", Vec3.CODEC) -- .orElseGet( -- () -> new Vec3( -- pos.getX() + (random.nextDouble() - random.nextDouble()) * this.spawnRange + 0.5, -- pos.getY() + random.nextInt(3) - 1, -- pos.getZ() + (random.nextDouble() - random.nextDouble()) * this.spawnRange + 0.5 -- ) -- ); -- if (level.noCollision(entityType.get().getSpawnAABB(spawnPos.x, spawnPos.y, spawnPos.z))) { -- BlockPos spawnBlockPos = BlockPos.containing(spawnPos); -- if (nextSpawnData.getCustomSpawnRules().isPresent()) { -- if (!entityType.get().getCategory().isFriendly() && level.getDifficulty() == Difficulty.PEACEFUL) { -- continue; -- } -- -- SpawnData.CustomSpawnRules customSpawnRules = nextSpawnData.getCustomSpawnRules().get(); -- if (!customSpawnRules.isValidPosition(spawnBlockPos, level)) { -- continue; -- } -- } else if (!SpawnPlacements.checkSpawnRules(entityType.get(), level, EntitySpawnReason.SPAWNER, spawnBlockPos, level.getRandom())) { -- continue; -- } -- -- // Paper start - PreCreatureSpawnEvent -- com.destroystokyo.paper.event.entity.PreSpawnerSpawnEvent event = new com.destroystokyo.paper.event.entity.PreSpawnerSpawnEvent( -- org.bukkit.craftbukkit.util.CraftLocation.toBukkit(spawnPos, level), -- org.bukkit.craftbukkit.entity.CraftEntityType.minecraftToBukkit(entityType.get()), -- org.bukkit.craftbukkit.util.CraftLocation.toBukkit(pos, level) -- ); -- if (!event.callEvent()) { -- delay = true; -- if (event.shouldAbortSpawn()) { -- break; -- } -- continue; -- } -- // Paper end - PreCreatureSpawnEvent -- -- Entity entity = EntityType.loadEntityRecursive(input, level, EntitySpawnReason.SPAWNER, e -> { -+ Vec3 spawnPos = fixedSpawnPos.orElseGet( -+ () -> new Vec3( -+ pos.getX() + (random.nextDouble() - random.nextDouble()) * this.spawnRange + 0.5, -+ pos.getY() + random.nextInt(3) - 1, -+ pos.getZ() + (random.nextDouble() - random.nextDouble()) * this.spawnRange + 0.5 -+ ) -+ ); -+ if (level.noCollision(entityType.get().getSpawnAABB(spawnPos.x, spawnPos.y, spawnPos.z))) { -+ BlockPos spawnBlockPos = BlockPos.containing(spawnPos); -+ if (nextSpawnData.getCustomSpawnRules().isPresent()) { -+ if (!entityType.get().getCategory().isFriendly() && level.getDifficulty() == Difficulty.PEACEFUL) { -+ continue; -+ } -+ -+ SpawnData.CustomSpawnRules customSpawnRules = nextSpawnData.getCustomSpawnRules().get(); -+ if (!customSpawnRules.isValidPosition(spawnBlockPos, level)) { -+ continue; -+ } -+ } else if (!SpawnPlacements.checkSpawnRules(entityType.get(), level, EntitySpawnReason.SPAWNER, spawnBlockPos, level.getRandom())) { -+ continue; -+ } -+ -+ // Paper start - PreCreatureSpawnEvent -+ com.destroystokyo.paper.event.entity.PreSpawnerSpawnEvent event = new com.destroystokyo.paper.event.entity.PreSpawnerSpawnEvent( -+ org.bukkit.craftbukkit.util.CraftLocation.toBukkit(spawnPos, level), -+ org.bukkit.craftbukkit.entity.CraftEntityType.minecraftToBukkit(entityType.get()), -+ org.bukkit.craftbukkit.util.CraftLocation.toBukkit(pos, level) -+ ); -+ if (!event.callEvent()) { -+ delay = true; -+ if (event.shouldAbortSpawn()) { -+ break; -+ } -+ continue; -+ } -+ // Paper end - PreCreatureSpawnEvent -+ -+ // Gale start - Do less work - Only entity loading needs a fresh input -+ Entity entity; -+ try (ProblemReporter.ScopedCollector reporter = new ProblemReporter.ScopedCollector(this::toString, LOGGER)) { -+ ValueInput input = TagValueInput.create(reporter, level.registryAccess(), nextSpawnData.getEntityToSpawn()); -+ entity = EntityType.loadEntityRecursive(input, level, EntitySpawnReason.SPAWNER, e -> { - e.snapTo(spawnPos.x, spawnPos.y, spawnPos.z, e.getYRot(), e.getXRot()); - return e; - }); -- if (entity == null) { -- this.delay(level, pos); -- return; -- } -+ } -+ // Gale end - Do less work - Only entity loading needs a fresh input -+ if (entity == null) { -+ this.delay(level, pos); -+ return; -+ } + try (ProblemReporter.ScopedCollector reporter = new ProblemReporter.ScopedCollector(this::toString, LOGGER)) { + ValueInput input = TagValueInput.create(reporter, level.registryAccess(), nextSpawnData.getEntityToSpawn()); +@@ -165,7 +_,7 @@ -- int nearBy = level.getEntities( -- EntityTypeTest.forExactClass(entity.getClass()), + int nearBy = level.getEntities( + EntityTypeTest.forExactClass(entity.getClass()), - new AABB(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, pos.getY() + 1, pos.getZ() + 1).inflate(this.spawnRange), -- EntitySelector.NO_SPECTATORS -- ) -- .size(); -+ int nearBy = level.getEntities( -+ EntityTypeTest.forExactClass(entity.getClass()), -+ nearbyEntitiesBox, // Gale - Do less work - Hoist nearby entity query box out of the spawn loop -+ EntitySelector.NO_SPECTATORS -+ ) -+ .size(); - if (nearBy >= this.maxNearbyEntities) { - this.delay(level, pos); - return; -@@ -217,7 +_,6 @@ - // delay = true; // Paper - moved up above cancellable event - } - } -- } - - if (delay) { - this.delay(level, pos); ++ nearbyEntitiesBox, // Gale - Do less work - Hoist nearby entity query box out of the spawn loop + EntitySelector.NO_SPECTATORS + ) + .size(); From e87229ed7851953b9271a0bbd09e9f97f2dfb6b6 Mon Sep 17 00:00:00 2001 From: Nrleryx Date: Fri, 31 Jul 2026 23:32:33 +0300 Subject: [PATCH 9/9] address review comments --- .../server/level/ServerLevel.java.patch | 87 ++----------------- .../entity/npc/villager/Villager.java.patch | 3 +- .../world/level/BaseSpawner.java.patch | 5 +- .../block/state/BlockBehaviour.java.patch | 2 +- ...lideanGameEventListenerRegistry.java.patch | 46 +++++----- .../gameevent/GameEventDispatcher.java.patch | 38 ++++---- 6 files changed, 50 insertions(+), 131 deletions(-) diff --git a/gale-server/minecraft-patches/sources/net/minecraft/server/level/ServerLevel.java.patch b/gale-server/minecraft-patches/sources/net/minecraft/server/level/ServerLevel.java.patch index 175666ad..92a22218 100644 --- a/gale-server/minecraft-patches/sources/net/minecraft/server/level/ServerLevel.java.patch +++ b/gale-server/minecraft-patches/sources/net/minecraft/server/level/ServerLevel.java.patch @@ -74,46 +74,14 @@ profiler.push("entities"); if (this.dragonFight != null && runs) { profiler.push("dragonFight"); -@@ -887,9 +_,18 @@ - entity -> { - if (!entity.isRemoved()) { - if (!tickRateManager.isEntityFrozen(entity)) { -- profiler.push("checkDespawn"); -+ // Gale start - Do less work - Skip profiler pushes when profiling is inactive -+ boolean isProfilerActive = profiler != net.minecraft.util.profiling.InactiveProfiler.INSTANCE; -+ if (isProfilerActive) { -+ profiler.push("checkDespawn"); -+ } -+ +@@ -890,6 +_,7 @@ + profiler.push("checkDespawn"); entity.checkDespawn(); -- profiler.pop(); -+ if (isProfilerActive) { -+ profiler.pop(); -+ } -+ // Gale end - Do less work - Skip profiler pushes when profiling is inactive + profiler.pop(); + if (!org.galemc.gale.async.SimulationFlag.REAL) return; // Gale - Speculative execution based on next tick simulation - Simulate next tick if (true) { // Paper - rewrite chunk system Entity vehicle = entity.getVehicle(); if (vehicle != null) { -@@ -900,9 +_,16 @@ - entity.stopRiding(); - } - -- profiler.push("tick"); -+ // Gale start - Do less work - Skip profiler pushes when profiling is inactive -+ if (isProfilerActive) { -+ profiler.push("tick"); -+ } -+ - this.guardEntityTick(this::tickNonPassenger, entity); -- profiler.pop(); -+ if (isProfilerActive) { -+ profiler.pop(); -+ } -+ // Gale end - Do less work - Skip profiler pushes when profiling is inactive - } - } - } @@ -913,9 +_,10 @@ this.tickBlockEntities(); profiler.pop(); @@ -149,24 +117,12 @@ } // Paper start - optimise random ticking -@@ -1013,6 +_,11 @@ - // Paper end - optimise random ticking - - public void tickChunk(final LevelChunk chunk, final int tickSpeed) { -+ // Gale start - Do less work - Skip the whole chunk tick when there is nothing to do -+ if (tickSpeed <= 0) { -+ return; -+ } -+ // Gale end - Do less work - Skip the whole chunk tick when there is nothing to do - final ca.spottedleaf.moonrise.common.util.SimpleThreadUnsafeRandom simpleRandom = this.simpleRandom; // Paper - optimise random ticking - ChunkPos chunkPos = chunk.getPos(); - int minX = chunkPos.getMinBlockX(); @@ -1037,13 +_,17 @@ } public void tickThunder(final LevelChunk chunk) { + // Gale start - Do less work - Skip the profiler and random calls when thunder cannot happen -+ if (!this.isRaining() || this.paperConfig().environment.disableThunder || !this.isThundering() || this.spigotConfig.thunderChance <= 0) { ++ if (!this.isThundering() || this.paperConfig().environment.disableThunder || !this.isRaining() || this.spigotConfig.thunderChance <= 0) { + return; + } + // Gale end - Do less work - Skip the profiler and random calls when thunder cannot happen @@ -181,30 +137,16 @@ BlockPos pos = this.findLightningTargetAround(this.getBlockRandomPos(minX, 0, minZ, 15)); if (this.isRainingAt(pos)) { DifficultyInstance difficulty = this.getCurrentDifficultyAt(pos); -@@ -1396,16 +_,21 @@ +@@ -1396,7 +_,7 @@ // Paper end - log detailed entity tick information entity.setOldPosAndRot(); ProfilerFiller profiler = Profiler.get(); - entity.tickCount++; + entity.setTickCount(entity.tickCount + 1); // Gale - Event-driven - Cat.canRemoveWhenFarAway, Ocelot.canRemoveWhenFarAway entity.totalEntityAge++; // Paper - age-like counter for all entities -+ boolean profilerActive = profiler != net.minecraft.util.profiling.InactiveProfiler.INSTANCE; // Gale - Do less work - Avoid profiler supplier allocation and calls when profiling is disabled -+ if (profilerActive) { // Gale - Do less work profiler.push(entity.typeHolder()::getRegisteredName); profiler.incrementCounter("tickNonPassenger"); -+ } // Gale end - Do less work - final boolean isActive = io.papermc.paper.entity.activation.ActivationRange.checkIfActive(entity); // Paper - EAR 2 - if (isActive) { // Paper - EAR 2 - entity.tick(); - entity.postTick(); // CraftBukkit - } else {entity.inactiveTick();} // Paper - EAR 2 -+ if (profilerActive) { // Gale - Do less work - profiler.pop(); -+ } // Gale end - Do less work - - for (Entity passenger : entity.getPassengers()) { - this.tickPassenger(entity, passenger, isActive); // Paper - EAR 2 -@@ -1422,13 +_,16 @@ +@@ -1422,9 +_,9 @@ private void tickPassenger(final Entity vehicle, final Entity entity, final boolean isActive) { // Paper - EAR 2 if (entity.isRemoved() || entity.getVehicle() != vehicle) { entity.stopRiding(); @@ -215,24 +157,7 @@ + entity.setTickCount(entity.tickCount + 1); // Gale - Event-driven - Cat.canRemoveWhenFarAway, Ocelot.canRemoveWhenFarAway entity.totalEntityAge++; // Paper - age-like counter for all entities ProfilerFiller profiler = Profiler.get(); -+ boolean profilerActive = profiler != net.minecraft.util.profiling.InactiveProfiler.INSTANCE; // Gale - Do less work - Avoid profiler supplier allocation and calls when profiling is disabled -+ if (profilerActive) { // Gale - Do less work profiler.push(entity.typeHolder()::getRegisteredName); - profiler.incrementCounter("tickPassenger"); -+ } // Gale end - Do less work - // Paper start - EAR 2 - if (isActive) { - entity.rideTick(); -@@ -1440,7 +_,9 @@ - vehicle.positionRider(entity); - } - // Paper end - EAR 2 -+ if (profilerActive) { // Gale - Do less work - profiler.pop(); -+ } // Gale end - Do less work - - for (Entity passenger : entity.getPassengers()) { - this.tickPassenger(entity, passenger, isActive); // Paper - EAR 2 @@ -1717,6 +_,13 @@ @Override diff --git a/gale-server/minecraft-patches/sources/net/minecraft/world/entity/npc/villager/Villager.java.patch b/gale-server/minecraft-patches/sources/net/minecraft/world/entity/npc/villager/Villager.java.patch index e0d9c5a6..49904bba 100644 --- a/gale-server/minecraft-patches/sources/net/minecraft/world/entity/npc/villager/Villager.java.patch +++ b/gale-server/minecraft-patches/sources/net/minecraft/world/entity/npc/villager/Villager.java.patch @@ -4,10 +4,9 @@ if (this.lastGossipDecayTime == 0L) { this.lastGossipDecayTime = timestamp; } else if (timestamp >= this.lastGossipDecayTime + 24000L) { -- this.gossips.decay(); + // Gale start - Do less work - Skip gossip decay for villagers without any gossip + if (!this.gossips.gossips.isEmpty()) { -+ this.gossips.decay(); + this.gossips.decay(); + } + // Gale end - Do less work - Skip gossip decay for villagers without any gossip this.lastGossipDecayTime = timestamp; diff --git a/gale-server/minecraft-patches/sources/net/minecraft/world/level/BaseSpawner.java.patch b/gale-server/minecraft-patches/sources/net/minecraft/world/level/BaseSpawner.java.patch index 07b17490..9d304bb9 100644 --- a/gale-server/minecraft-patches/sources/net/minecraft/world/level/BaseSpawner.java.patch +++ b/gale-server/minecraft-patches/sources/net/minecraft/world/level/BaseSpawner.java.patch @@ -1,10 +1,9 @@ --- a/net/minecraft/world/level/BaseSpawner.java +++ b/net/minecraft/world/level/BaseSpawner.java -@@ -106,7 +_,7 @@ - boolean delay = false; +@@ -107,6 +_,7 @@ RandomSource random = level.getRandom(); SpawnData nextSpawnData = this.getOrCreateNextSpawnData(level, random, pos); -- + + AABB nearbyEntitiesBox = new AABB(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, pos.getY() + 1, pos.getZ() + 1).inflate(this.spawnRange); // Gale - Do less work - Hoist nearby entity query box out of the spawn loop for (int c = 0; c < this.spawnCount; c++) { try (ProblemReporter.ScopedCollector reporter = new ProblemReporter.ScopedCollector(this::toString, LOGGER)) { diff --git a/gale-server/minecraft-patches/sources/net/minecraft/world/level/block/state/BlockBehaviour.java.patch b/gale-server/minecraft-patches/sources/net/minecraft/world/level/block/state/BlockBehaviour.java.patch index bb1caca6..5760a8d0 100644 --- a/gale-server/minecraft-patches/sources/net/minecraft/world/level/block/state/BlockBehaviour.java.patch +++ b/gale-server/minecraft-patches/sources/net/minecraft/world/level/block/state/BlockBehaviour.java.patch @@ -125,8 +125,8 @@ - public boolean hasBlockEntity() { - return this.getBlock() instanceof EntityBlock; ++ // Gale start - Pre-compute - BlockBehaviour.hasBlockEntity() + public final boolean hasBlockEntity() { -+ // Gale start - Pre-compute - BlockBehaviour.hasBlockEntity() + return this.gale$precompute_hasBlockEntity; + // Gale end - Pre-compute - BlockBehaviour.hasBlockEntity() } diff --git a/gale-server/minecraft-patches/sources/net/minecraft/world/level/gameevent/EuclideanGameEventListenerRegistry.java.patch b/gale-server/minecraft-patches/sources/net/minecraft/world/level/gameevent/EuclideanGameEventListenerRegistry.java.patch index 072ea4fc..caf2e4fd 100644 --- a/gale-server/minecraft-patches/sources/net/minecraft/world/level/gameevent/EuclideanGameEventListenerRegistry.java.patch +++ b/gale-server/minecraft-patches/sources/net/minecraft/world/level/gameevent/EuclideanGameEventListenerRegistry.java.patch @@ -1,14 +1,6 @@ --- a/net/minecraft/world/level/gameevent/EuclideanGameEventListenerRegistry.java +++ b/net/minecraft/world/level/gameevent/EuclideanGameEventListenerRegistry.java -@@ -4,7 +_,6 @@ - import com.google.common.collect.Sets; - import java.util.Iterator; - import java.util.List; --import java.util.Optional; - import java.util.Set; - import net.minecraft.core.BlockPos; - import net.minecraft.core.Holder; -@@ -88,11 +_,15 @@ +@@ -88,11 +_,13 @@ if (this.listenersToRemove.remove(listener)) { iterator.remove(); } else { @@ -16,10 +8,8 @@ - if (optionalPosition.isPresent()) { - action.visit(listener, optionalPosition.get()); + // Gale start - Do less work - Inline the range check to avoid Optional allocations per listener per event -+ Vec3 listenerPosition = listener.getListenerSource().getPosition(this.level).orElse(null); -+ if (listenerPosition != null -+ && BlockPos.containing(listenerPosition).distSqr(BlockPos.containing(sourcePosition)) -+ <= (long) listener.getListenerRadius() * listener.getListenerRadius()) { ++ Vec3 listenerPosition = gale$getPostableListenerPositionNullable(this.level, sourcePosition, listener); ++ if (listenerPosition != null) { + action.visit(listener, listenerPosition); applicable = true; } @@ -27,21 +17,33 @@ } } } finally { -@@ -110,17 +_,6 @@ - } - +@@ -112,16 +_,22 @@ return applicable; -- } -- + } + - private static Optional getPostableListenerPosition(final ServerLevel level, final Vec3 sourcePosition, final GameEventListener listener) { - Optional position = listener.getListenerSource().getPosition(level); - if (position.isEmpty()) { - return Optional.empty(); -- } -- ++ // Gale start - Do less work - Avoid Optional allocations per listener per event ++ private static @org.jspecify.annotations.Nullable Vec3 gale$getPostableListenerPositionNullable(final ServerLevel level, final Vec3 sourcePosition, final GameEventListener listener) { ++ Vec3 position = listener.getListenerSource().getPosition(level).orElse(null); ++ if (position == null) { ++ return null; + } + - double distanceFromOrigin = BlockPos.containing(position.get()).distSqr(BlockPos.containing(sourcePosition)); -- int radiusSqr = listener.getListenerRadius() * listener.getListenerRadius(); ++ double distanceFromOrigin = BlockPos.containing(position).distSqr(BlockPos.containing(sourcePosition)); + int radiusSqr = listener.getListenerRadius() * listener.getListenerRadius(); - return distanceFromOrigin > radiusSqr ? Optional.empty() : position; - } +- } ++ return distanceFromOrigin > radiusSqr ? null : position; ++ } ++ ++ private static Optional getPostableListenerPosition(final ServerLevel level, final Vec3 sourcePosition, final GameEventListener listener) { ++ return Optional.ofNullable(gale$getPostableListenerPositionNullable(level, sourcePosition, listener)); ++ } ++ // Gale end - Do less work - Avoid Optional allocations per listener per event @FunctionalInterface + public interface OnEmptyAction { diff --git a/gale-server/minecraft-patches/sources/net/minecraft/world/level/gameevent/GameEventDispatcher.java.patch b/gale-server/minecraft-patches/sources/net/minecraft/world/level/gameevent/GameEventDispatcher.java.patch index 06c31ffe..e8059a71 100644 --- a/gale-server/minecraft-patches/sources/net/minecraft/world/level/gameevent/GameEventDispatcher.java.patch +++ b/gale-server/minecraft-patches/sources/net/minecraft/world/level/gameevent/GameEventDispatcher.java.patch @@ -21,7 +21,7 @@ private final ServerLevel level; public GameEventDispatcher(final ServerLevel level) { -@@ -37,30 +_,35 @@ +@@ -37,14 +_,15 @@ int sectionMaxX = SectionPos.blockToSectionCoord(center.getX() + radius); int sectionMaxY = SectionPos.blockToSectionCoord(center.getY() + radius); int sectionMaxZ = SectionPos.blockToSectionCoord(center.getZ() + radius); @@ -34,42 +34,36 @@ - } - }; + // Gale start - Do less work - Reuse the visitor and dispatch list instead of allocating per post ++ this.eventsToDispatch.clear(); // Gale - clear stale entries if a previous post was interrupted by an exception + Holder prevGameEvent = this.gameEvent; + Vec3 prevPosition = this.position; + GameEvent.Context prevContext = this.context; + this.gameEvent = gameEvent; + this.position = position; + this.context = context; ++ // Gale end - Do less work - Reuse the visitor and dispatch list instead of allocating per post boolean applicable = false; -- for (int chunkX = sectionMinX; chunkX <= sectionMaxX; chunkX++) { -- for (int chunkZ = sectionMinZ; chunkZ <= sectionMaxZ; chunkZ++) { -- ChunkAccess chunk = this.level.getChunkIfLoadedImmediately(chunkX, chunkZ); // Paper - Use getChunkIfLoadedImmediately -- if (chunk != null) { -- for (int section = sectionMinY; section <= sectionMaxY; section++) { + for (int chunkX = sectionMinX; chunkX <= sectionMaxX; chunkX++) { +@@ -52,15 +_,18 @@ + ChunkAccess chunk = this.level.getChunkIfLoadedImmediately(chunkX, chunkZ); // Paper - Use getChunkIfLoadedImmediately + if (chunk != null) { + for (int section = sectionMinY; section <= sectionMaxY; section++) { - applicable |= chunk.getListenerRegistry(section).visitInRangeListeners(gameEvent, position, context, visitListeners); -+ try { -+ for (int chunkX = sectionMinX; chunkX <= sectionMaxX; chunkX++) { -+ for (int chunkZ = sectionMinZ; chunkZ <= sectionMaxZ; chunkZ++) { -+ ChunkAccess chunk = this.level.getChunkIfLoadedImmediately(chunkX, chunkZ); // Paper - Use getChunkIfLoadedImmediately -+ if (chunk != null) { -+ for (int section = sectionMinY; section <= sectionMaxY; section++) { -+ applicable |= chunk.getListenerRegistry(section).visitInRangeListeners(gameEvent, position, context, this.visitor); -+ } ++ applicable |= chunk.getListenerRegistry(section).visitInRangeListeners(gameEvent, position, context, this.visitor); } } } -- } + } - if (!toHandleByDistance.isEmpty()) { - this.handleGameEventMessagesInQueue(toHandleByDistance); -+ this.dispatchPendingEvents(); -+ } finally { -+ this.eventsToDispatch.clear(); // Gale - do not leak stale entries if a listener throws mid-visit -+ this.gameEvent = prevGameEvent; -+ this.position = prevPosition; -+ this.context = prevContext; - } +- } ++ // Gale start - Do less work - Reuse the visitor and dispatch list instead of allocating per post ++ this.dispatchPendingEvents(); ++ this.gameEvent = prevGameEvent; ++ this.position = prevPosition; ++ this.context = prevContext; + // Gale end - Do less work - Reuse the visitor and dispatch list instead of allocating per post if (applicable) {