Releases: AtlasStudioMC/AtlasSpigot
Release list
AtlasSpigot 26.2 - build 27 (17 patches)
Rebuilt jar — seventeen optimisation patches.
New: boats
AbstractBoat#tick built a Bukkit Location, cast the boat to a CraftEntity, fired VehicleUpdateEvent, optionally fired VehicleMoveEvent, then allocated a second Location for lastLocation — every boat, every tick.
VehicleUpdateEvent's result is discarded outright, and VehicleMoveEvent only reports movement. With neither listened for, none of it is observable. Boats are everywhere on most servers — transport, ice roads, boat farms — so this adds up in a way the event name doesn't suggest.
lastLocation is nulled rather than left stale in the unlistened branch. The existing code already skips VehicleMoveEvent when it's null, so a plugin registering later seeds cleanly on its first tick instead of receiving one bogus move spanning however long nothing was listening.
What I found already done
EntityMoveEvent in LivingEntity#aiStep and Purpur's RidableMoveEvent are already guarded — via cached hasEntityMoveEvent / hasRidableMoveEvent fields on ServerLevel. That's a better version of this same idea, since it avoids even the handler-list lookup.
My first scan this round flagged them as unguarded because the guard sits on the enclosing if rather than the event line. Widening the search window to 25 lines eliminated that whole class of false positive, along with several other apparent candidates. Worth knowing if you ever run the same search.
Still included
The low-spec profile (AtlasSpigot-26.2-lowspec.zip) for ~3GB RAM, an older CPU near 100% thread usage, ~10GB disk and a heavy plugin count. 1600M heap, view-distance 3, simulation-distance 2, monster cap 15, and hopper.disable-move-event deliberately off so shop and sorting plugins keep working.
Verified
Boots at 1600M on the low-spec profile in 32.17s with zero errors.
Not measured
No performance figures are claimed. 26.2 was benchmarked against Paper and the result was too noisy to publish — see results-26.2.txt.
AtlasSpigot 26.2 - build 26 (16 patches + low-spec profile)
Rebuilt jar with six more patch groups — sixteen total (build 25 had thirteen) — plus a new low-spec profile for small machines.
New patches
All on per-tick or per-spawn paths:
EntityInsideBlockEventacross 24 byte-identical sites — bubble columns, cobwebs, berry bushes, fire, portals, pressure plates. Each built aCraftBlockand an event every tick an entity stays inside one.EntityEffectTickEvent— fires on every effect application tick for every entity carrying an effect, building a CraftEntity handle and converting the potion type each time.ServerTickStartEvent/ServerTickEndEvent— two allocations every tick, forever.PreCreatureSpawnEvent,PreSpawnerSpawnEvent,PhantomPreSpawnEvent— two or three Craft conversions plus an event per spawn attempt.
Worth noting: Leaf already guards the hottest PreCreatureSpawnEvent site itself, in NaturalSpawner, with the comment "Skip PreCreatureSpawnEvent if no listeners." Upstream arriving at the same technique independently is decent evidence the pattern is sound. These are the sites they didn't cover.
New: AtlasSpigot-26.2-lowspec.zip
A complete config set for roughly 3GB RAM, an older CPU already near 100% thread usage, ~10GB disk, and a heavy plugin count.
It isn't the main config nudged — it trades visible world and mob density for tick time throughout: view-distance 3, simulation-distance 2, entity broadcast 25%, monster cap 15, activation ranges 8–12, monsters despawning at 16/32, chunk load and send rates capped, max-joins-per-tick back to 1, saving spread thinner with sync-chunk-writes=false.
Two deliberate differences from the main config:
hopper.disable-move-event is OFF in the low-spec profile. It's the biggest single win available, but it stops InventoryMoveItemEvent firing — and on a 45-plugin server the odds something depends on it are high. A fast server that breaks your shop isn't faster.
The heap is 1600M, not 3G. Metaspace (which grows with plugin count), thread stacks, Netty direct buffers and the JIT code cache all live outside the heap. -Xmx3G on a 3GB box gives you a server the kernel kills, not a bigger one. G1HeapRegionSize drops to 4M so G1 still gets a sensible region count at that size.
density-function-compiler appeared as a new setting and looks made for a CPU-bound machine — it compiles worldgen density functions to JVM bytecode. It stays off: Leaf marks the module @Experimental, which is a hard no here regardless of how good the win looks.
Verified
Main config: boots clean, zero errors. Low-spec profile: boots at 1600M in 29.9s with zero errors, and every changed file returns byte-identical after the server writes it.
Not measured
No performance figures are claimed. 26.2 was benchmarked against Paper and the result was too noisy to publish — see results-26.2.txt. The low-spec profile is built from settings whose behaviour is understood and documented, not from a measured result on your hardware.
AtlasSpigot 26.2 - build 25 (13 optimisation patches)
Rebuilt jar — 13 optimisation patches, up from 10. This round found 27 new guarded sites, the most of any build so far.
EntityInsideBlockEvent — 24 sites
The biggest find. This line appears byte-identical in 24 generated block classes:
if (!new EntityInsideBlockEvent(entity.getBukkitEntity(), CraftBlock.at(level, pos)).callEvent()) { return; }Each allocates a CraftBlock plus an event, and entityInside runs per entity per tick for anything standing in a bubble column, cobweb, berry bush, fire, pressure plate, cactus, campfire, crop or portal. In water-heavy or farm-heavy areas that is continuous.
Because the line is emitted into two dozen generated classes, apply.py gained a bulk mode: a directory-wide replace with a minimum-hit assertion, so it fails loudly rather than silently half-applying if upstream adds or removes a block class.
EntityEffectTickEvent
Fires on every effect application for every entity carrying one — and building it meant a getBukkitLivingEntity() lookup plus a CraftPotionEffectType conversion. Already gated behind shouldApplyEffectTickThisTick(), so it's per application rather than literally every tick.
ServerTickStartEvent / ServerTickEndEvent
Two allocations every tick, for the life of the process.
These got a functional test, not just inspection. The bench plugin in this repo listens to ServerTickEndEvent — and it still collects a full 200-sample set against the patched jar. That proves the guard doesn't suppress the event when something is actually listening, which is exactly the failure mode that would otherwise be invisible until a plugin quietly stopped working.
Not measured
All 13 patches are strictly-fewer-allocations and provably equivalent. No performance figures are claimed — 26.2 was benchmarked and the result was too noisy to publish (results-26.2.txt).
Config: unchanged from build 24
hopper.disable-move-event is on, so shop, sorting and economy plugins will not see hopper transfers (set false in config/paper-world-defaults.yml to restore). Spawner mobs have no AI, monsters despawn at 28/48, mob caps cut ~43%, simulation distance 3.
Verified
Boots in 11.21s reporting AtlasSpigot version 26.2-DEV-ver/26.2@e51867d, 200 entities spawned and ticked, zero exceptions or errors. Still zero net/minecraft classes.
Rebuild: ./gradlew applyAllPatches && python3 source-patches/apply.py . && ./gradlew :leaf-server:createPaperclipJar
AtlasSpigot 26.2 - build 24 (Gale world config)
Config update. Same jar as build 23 (ten optimisation patches); this ships a config surface that was previously running entirely at stock.
New: gale-world-defaults.yml
The tuned bundle carried atlas-global, paper-global and paper-world-defaults but no Gale files at all — so while everything else was tuned hard, every Gale world setting was still on defaults. Two changes:
entities-can-random-stroll-into-non-ticking-chunks → false. Verified against Gale's patch rather than assumed: RandomStrollGoal refuses a destination in a non-ticking chunk. Mobs at the edge of the ticking area stop wandering outward, so they no longer generate activity in chunks nothing is simulating. The cost is that edge mobs pace within the loaded region instead of drifting out.
save-fireworks → false. Firework rockets aren't persisted, so any in flight vanish across a restart. Marginal, but free.
gale-global.yml is left at defaults. Its one candidate, increase-time-statistics, stays rejected — it turns CROUCH_TIME, TIME_SINCE_DEATH and TIME_SINCE_REST into sampled approximations, and TIME_SINCE_REST drives phantom spawning.
The rest of Gale's defaults are already tuned upstream: phantom and climbing-entity chunk loads off, check-stuck-in-wall reduced, projectile chunk loads capped.
Config: otherwise unchanged from build 17
hopper.disable-move-event is on, so shop, sorting and economy plugins will not see hopper transfers (set false in config/paper-world-defaults.yml to restore). Spawner mobs have no AI, monsters despawn at 28/48, mob caps cut ~43%, simulation distance 3.
Verified
Boots in 12.27s with zero errors, and gale-world-defaults.yml comes back byte-identical after the server writes it — neither setting was rejected or silently overridden.
Not measured
No performance figures are claimed. 26.2 was benchmarked against Paper and the result was too noisy to publish — see results-26.2.txt.
AtlasSpigot 26.2 - build 23 (ten optimisation patches)
Rebuilt jar — ten optimisation patches now.
New: the universal entity-removal path
CraftEventFactory#callEntityRemoveEvent sits in Entity#setRemoved, the single path every entity takes on its way out of the world: mob deaths, item and XP orb despawns, projectiles expiring, entities dropped when a chunk unloads.
It returns void, and getBukkitEntity() creates the CraftEntity wrapper when one doesn't exist — so with no listener registered it was allocating wrappers for entities that were already leaving.
This config makes it hotter than a default server, deliberately: monsters despawn at 28/48, items and arrows at 15 seconds. High removal rate is the point of that tuning, which makes the allocation on that path worth removing.
Two I rejected
PlayerNaturallySpawnCreaturesEvent allocates per player per tick during the spawn phase — an obvious-looking win. It isn't: the event is stored on the player, and both consumers in ChunkMap treat a null event as "skip spawning for this player entirely." Skipping construction would silently disable mob spawning. Making it safe means restructuring three files to fall back to the computed radius, which is not worth one small allocation.
callPrepareResultEvent is void, but calls setItem and broadcastChanges afterwards using values read off the event, so those side effects must still run.
Both are recorded in NOTES so they don't get re-examined.
Not measured
All ten patches are strictly-fewer-allocations and provably equivalent. No performance figures are claimed — 26.2 was benchmarked and the result was too noisy to publish (results-26.2.txt).
Config: unchanged from build 17
hopper.disable-move-event is on, so shop, sorting and economy plugins will not see hopper transfers (set false in config/paper-world-defaults.yml to restore). Spawner mobs have no AI, monsters despawn at 28/48, mob caps cut ~43%, simulation distance 3.
Verified
Boots in 13.72s reporting AtlasSpigot version 26.2-DEV-ver/26.2@e51867d, zero exceptions or errors, save-all completes, clean shutdown. Still zero net/minecraft classes.
Rebuild it yourself: ./gradlew applyAllPatches && python3 source-patches/apply.py . && ./gradlew :leaf-server:createPaperclipJar
AtlasSpigot 26.2 - build 22 (chunk patches + reproducible builds)
Rebuilt jar with two more optimisation patches — nine non-branding patches total — plus a fix for how the patches are maintained.
New: the chunk load/unload path
This is one of the highest-frequency operations on any server with players moving around, and four sites on it were doing Bukkit work nothing consumed.
callEntitiesLoadEvent / callEntitiesUnloadEvent streamed every entity in the chunk through Entity::getBukkitEntity, collected them into a new list, then built a CraftChunk and an event. getBukkitEntity() creates the CraftEntity wrapper when one doesn't exist — so this was forcing wrapper allocation for every entity in every chunk that loaded. Both methods return void. With no listener registered, all of it was built and thrown away.
ChunkLoadEvent is notification-only; its CraftChunk is now built lazily, since the populator path is the only other consumer.
ChunkUnloadEvent was constructed purely to read isSaveChunk() back off it.
That last one would mean chunks silently not saving if it were wrong, so it isn't an assumption: the event is constructed with saveChunk = true, and only a listener can call setSaveChunk(). With none registered, isSaveChunk() is definitively true and mustNotSave definitively false. Also checked empirically — this build produces the same world output as build 21 without the patches, including after an explicit save-all.
Also: the patches are now reproducible
The .diff files in source-patches/ are written to be read — they have no valid hunk headers, and git apply rejects all of them. That was fine with five branding patches. At fourteen it meant every rebuild was hand-work, and hand-work drifts.
The generated source trees are gitignored upstream, so there's no tracked baseline to diff against and no real patch file to produce. source-patches/apply.py is the answer: anchored replacement with an assertion on every anchor, so it either applies cleanly or names exactly which anchor upstream moved.
./gradlew applyAllPatches
python3 source-patches/apply.py .
./gradlew :leaf-server:createPaperclipJar
Verified end to end on a clean clone — all thirteen groups applied, result compiled.
Not measured
All nine patches are strictly-fewer-allocations and provably equivalent. No performance figures are claimed. 26.2 was benchmarked against Paper and the result was too noisy to publish — see results-26.2.txt.
Config: unchanged from build 17
hopper.disable-move-event is on, so shop, sorting and economy plugins will not see hopper transfers (set false in config/paper-world-defaults.yml to restore). Spawner mobs have no AI, monsters despawn at 28/48, mob caps cut ~43%, simulation distance 3.
Verified
Boots clean reporting AtlasSpigot version 26.2-DEV-ver/26.2@e51867d, zero exceptions or errors, saves and shuts down cleanly, still zero net/minecraft classes.
AtlasSpigot 26.2 - build 21 (seven optimisation patches)
Rebuilt jar with two more optimisation patches — seven non-branding patches total.
New in this build
Entity world-tracking events · EntityAddToWorldEvent and EntityRemoveFromWorldEvent are notification-only — neither implements Cancellable, and both call sites discard the result. They fire from onTrackingStart/onTrackingEnd, which run for every entity as chunks load and unload. On any server with players moving around, that's continuous churn building event objects and dispatching them through the plugin manager to do nothing at all. Probably the best of the seven.
EntityJumpEvent · Built a CraftEntity handle and an event object on every jump, for an event whose only power is to veto. Mobs jump constantly while pathfinding — over blocks, up slabs, out of water — so it fires far more often than the name suggests.
What did not get patched
Checking upstream first has now prevented three redundant patches. EntityCollideWithEntityEvent, BlockPhysicsEvent (behind ServerLevel.hasPhysicsEvent) and PlayerUntrackEntityEvent are all already guarded by Paper.
The CraftEventFactory spawn helpers were also examined and rejected — they return the event object to their callers, so guarding them saves nothing.
Both are recorded in NOTES so the same ground doesn't get walked twice.
Not measured
All seven are strictly-fewer-allocations changes and provably equivalent, which is the only reason they ship unmeasured. No performance figures are claimed for any of them.
Config: unchanged from build 17
hopper.disable-move-event is on — shop, sorting and economy plugins will not see hopper transfers (set false in config/paper-world-defaults.yml to restore). Spawner mobs have no AI, monsters despawn at 28/48, mob caps cut ~43%, simulation distance 3.
Verified
Thirteen source patches compile together. Boots in 8.85s reporting AtlasSpigot version 26.2-DEV-ver/26.2@e51867d, zero exceptions or errors, saves and shuts down cleanly. Still zero net/minecraft classes.
All patches: source-patches/
AtlasSpigot 26.2 - build 20 (five optimisation patches)
Rebuilt jar with two more optimisation patches — five non-branding patches total.
New in this build
ExperienceOrb#merge · Fired ExperienceOrbMergeEvent unconditionally — two CraftEntity casts and an event object per merge. Orb merging runs continuously at any grinder, and more so now that the tuned spigot.yml sets an XP merge radius, so the config tuning made this path hotter on these servers specifically.
CraftEventFactory#callItemMergeEvent · Built two casts, an ItemMergeEvent and a plugin-manager dispatch on every item merge, to reach an answer that is always true when nothing is listening. Called from ItemEntity#mergeWithNeighbours off the item's tick, so it fires wherever drops pile up — mob farms, mining, mass drops.
The rule these all follow
Every one of the five patches is the same idea: these events can only veto. With no listener registered the outcome is fixed, so the Bukkit object-building is pure waste. Behaviour is identical when a plugin is listening — events fire exactly as before, cancels included.
Two more candidates were checked and left alone because upstream already guards them: BlockPhysicsEvent behind ServerLevel.hasPhysicsEvent, and EntityCollideWithEntityEvent in Entity#push(Entity). Noted in NOTES so they don't get re-examined.
Not measured
All five are strictly-fewer-allocations changes and provably equivalent, which is the only reason they ship unmeasured. No performance figures are claimed, and none is individually large.
Config: unchanged from build 17
hopper.disable-move-event is on — shop, sorting and economy plugins will not see hopper transfers (set it false in config/paper-world-defaults.yml to restore). Spawner mobs have no AI, monsters despawn at 28/48, mob caps cut ~43%, simulation distance 3.
Verified
Eleven source patches compile together. Boots in 8.42s reporting AtlasSpigot version 26.2-DEV-ver/26.2@e51867d, runs with zero exceptions or errors, saves and shuts down cleanly. Still zero net/minecraft classes.
All patches with their reasoning: source-patches/
AtlasSpigot 26.2 - build 19 (three optimisation patches)
Rebuilt jar with two more optimisation patches, bringing the total to three non-branding patches.
What's new
Entity#push — no Vector allocation · A Bukkit Vector was built on every push, before anything checked whether it would be read. Ordinary entity-vs-entity crowding takes the path where that Vector was constructed only to be unpacked straight back into three doubles. EntityPushedByEntityAttackEvent was fired unguarded too. push() runs for every entity shoved by another, every tick — one of the hotter allocation sites on a crowded server.
Entity#setAirSupply — no event object · An EntityAirChangeEvent was allocated before the valid check, so it was built even during worldgen where it is deliberately never fired. Runs per tick for anything drowning or refilling air, so mobs pathing through water hit it continuously.
Both follow the same rule as build 18's equipment patch: resolve listener presence, and do no Bukkit work that nothing will read. Behaviour is unchanged when a plugin is listening — the events fire exactly as before, cancels and rewrites included.
Worth saying: Paper already does this where it matters most — EntityCollideWithEntityEvent is guarded exactly this way. These are the spots it hadn't reached, not a disagreement with upstream.
Not measured
All three patches are strictly-fewer-allocations changes and provably equivalent, which is the only reason they ship unmeasured. No performance figures are claimed for any of them, and none is claimed to be large individually.
Config: unchanged from build 17
hopper.disable-move-event is on, so shop, sorting and economy plugins will not see hopper transfers — set it to false in config/paper-world-defaults.yml to restore. Spawner mobs have no AI, monsters despawn at 28/48, mob caps cut ~43%, simulation distance 3. This is not a vanilla-feeling server.
Verified
All nine source patches compile together. Boots in 12.19s reporting AtlasSpigot version 26.2-DEV-ver/26.2@e51867d, runs with zero exceptions or errors in the log, and shuts down clean. Still zero net/minecraft classes, so the Paperclip legal posture is unchanged.
Every patch is in source-patches/ with its reasoning.
AtlasSpigot 26.2 - build 18 (rebuilt jar + equipment patch)
The jar is rebuilt in this release. Every 26.2 build since 14 shipped the same jar with config changes on top; this one is compiled from source with a new optimisation patch.
New: equipment event allocation patch
LivingEntity#collectEquipmentChanges converted both the old and new ItemStack to Bukkit types on every changed equipment slot. Those are deep copies, and their only consumers are EntityEquipmentChangedEvent and PlayerArmorChangeEvent — both of which were built and fired even with zero listeners.
So on most servers, every armoured mob, armour stand and item-picking-up mob was paying two ItemStack copies, an EnumMap and an event object per changed slot, all thrown away immediately.
Now the listener check happens once per call and the conversions are skipped when nothing will read them. Semantics are identical: if you run a plugin listening to either event, the copies are made and both events fire exactly as before. There is no path where a listener receives a null item.
This is not measured. It's strictly fewer allocations and provably equivalent, which is why it ships without a number attached — it is not claimed to be a large win.
Config: unchanged from build 17
Same maximum tuning tier. If you're coming from an older build, note that stack still applies and is aggressive:
hopper.disable-move-event is on, so InventoryMoveItemEvent does not fire — shop, sorting and some economy plugins will not see hopper transfers. Set hopper.disable-move-event: false in config/paper-world-defaults.yml to restore it.
Also still in effect: spawner mobs have no AI, monsters despawn at 28/48, mob caps cut ~43%, simulation distance 3, entities broadcast at half range. This is not a vanilla-feeling server.
Verified
All seven source patches compile together. The rebuilt jar boots in 8.46s reporting AtlasSpigot version 26.2-DEV-ver/26.2@e51867d, writes atlas.yml and config/atlas-global.yml rather than the Purpur and Leaf filenames, and contains zero net/minecraft classes — the Paperclip download-from-Mojang posture is unchanged.
Full reasoning, including how to rebuild, is in NOTES.md.