Releases: AtlasStudioMC/Atlas
Release list
Astra 26.2 - build 28 (network compression)
Config-only build, like build 14 was. The jar is unchanged from build 27 — same 17 source patches, same code. Only the shipped config differs, so if you already run build 27 you only need the config zip.
network-compression-threshold: 64 → 256
This was the one value in the entire config that had never been justified anywhere. It was set in the very first commit and never revisited, and unlike every other tuned setting it had no entry in NOTES.md.
At 64, almost every packet the server sends gets zlib-compressed, because almost every packet is bigger than 64 bytes. That is not free in either direction — the server burns CPU compressing before the packet can leave, and the client burns CPU decompressing. On small packets the compression ratio is poor, so that cost buys very little. Multiplied by player count, it is real work sitting on the path that carries movement and entity updates.
256 is Paper's own default. The strongest argument for changing it is internal: the low-spec profile — the one tuned specifically for "an old processor" — already ships 512. When this project actually examined CPU cost, it went far the other way. 64 on the main profile was an oversight, not a decision.
Lower it back toward 64 if your bottleneck is bandwidth, not CPU. A host with a tight data cap or a saturated uplink genuinely wants more compression. That is the trade being made here, and it goes the other way for some people.
Also in this build
NOTES.md now documents network.OptimizeNonFlushPacketSending under "still deliberately off". It skips a per-packet thread wakeup on the movement/entity path, so it is a real latency win — but its own config comment warns it is not compatible with ProtocolLib. Held back for the same reason as hopper.disable-move-event: plugin compatibility is not something a default should quietly cancel. Turn it on deliberately if ProtocolLib is not in your plugin list; it needs a restart, not a reload.
The notes also list what is already enabled on the network path (reduce-entity-move-packets, reduce-entity-motion-packets, optimize-player-movement, entity-broadcast-range-percentage=50), because those are easy to miss when hunting for something still left to tune.
On ping
Ping is mostly the distance between the player and the machine, plus their ISP. No server jar changes that. What a server controls is how long a packet waits before it is handled, how many bytes come back, and how long GC pauses freeze everything. This build targets the first of those.
Verified
Boots clean on the build 27 jar with the new config: Done (32.319s)!, JDK 25, no config errors.
No ping or throughput figures are published, because none were measured. The change is argued from mechanism and from this project's own low-spec choice, not from a benchmark.
Astra 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.
Astra 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.
Astra 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
Astra 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.
Astra 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
Astra 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.
Astra 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/
Astra 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/
Astra 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.