From 444c340caf1d5acbf52307a2ebf9147baea64224 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Fri, 11 Sep 2026 23:05:01 +0200 Subject: [PATCH] fix!: move to Minestom 26.2 through a mycelium-bom a consumer can resolve `mycelium-bom` goes from 1.7.2 to 1.8.5, which takes Minestom from `2026.06.20-26.1.2` to `2026.08.28-26.2` and `adventure-text-minimessage` to 5.2.0. The version number is the smaller half of this. Every published module's POM imports `mycelium-bom` in its `dependencyManagement`, and 1.7.2 is on neither `https://repo.onelitefeather.dev/releases` -- which carries 1.8.3 and up -- nor Maven Central. A consumer without OneLiteFeather credentials therefore could not use the artefacts at all: measured against a clean Gradle home with only `mavenCentral()` and the public `releases` endpoint, resolving the 1.7.2 import fails with `Could not find net.onelitefeather:mycelium-bom:1.7.2`, and no Minestom version is constrained. The same probe against 1.8.5 resolves and brings Minestom `2026.08.28-26.2` with it. CONTRIBUTING.md said the opposite in so many words and is corrected here: the jars were always served without authentication, the BOM their POM imported was not. Two source breaks, both in falco-instance: `SnapshotImpl.Chunk` gained a `DynamicRegistry` component, because the record no longer reaches through `MinecraftServer.getBiomeRegistry()` to resolve a biome id. `FalcoChunk#updateSnapshot` passes `instance.registries().biome()`, which is what `DynamicChunk` does on 26.2. `Player#spawnPlayer` no longer sends an `UnloadChunkPacket` for a chunk that is also in the new view: the 26.2 client drops a chunk it receives an unload and a data packet for in the same frame (MC-310041), and Minestom carries the workaround with a `TODO(26.3)` to revert it. `FalcoSharedInstanceResendTest` asserted those packets as its marker for the slow path, and its control transfers to the same position, so all 25 chunks stay in view and not one packet is sent. The marker is now `PlayerChunkUnloadEvent`, which still fires 25 times and measures what the assertion was written to measure; the empty packet collector stays beside it as a statement of the 26.2 suppression, so the revert in 26.3 fails here rather than passing unnoticed. The fast-path test gains the event collector too, since an empty packet collector no longer separates the paths on its own. japicmp against the 1.0.0 baseline reports `No changes.` for falco-anvil, falco-light and falco-instance. The two narrowings a linkage check found against the 26.1.2 jars -- `GenerationUnit.absoluteStart()` and `GeneratorImpl$SectionModifierImpl.start()` returning `BlockVec` where they returned `Point` and `Vec` -- are Minestom's own API, not Falco's, and they are return-type narrowings, so `ChunkGeneration` compiles against them untouched. The baseline stays at 1.0.0 and `gradle/api-breaks.properties` gains no entry. `./gradlew clean build` is green: 1094 tests, 0 failures, 1 skipped. The pinned-version notes that a Minestom upgrade exists to make somebody read again were read again: the four `UNSAFE_setBlock` call sites of `ForeignWritePathTest` are at `InstanceContainer:147`, `:235`, `:262` and `:779` on 26.2 with the method still `private synchronized` at `:161`, and `Section#clone` is at the same lines with the same body. The demo README's client table moves to 26.2 / protocol 776. The README's measured figures are marked unverified on 26.2 rather than re-run: no provenance line records the Minestom version a row was measured against, and no run commit either, so the comparison against the loader Minestom ships cannot be carried across the upgrade by argument. BREAKING CHANGE: the artefacts link against Minestom `2026.08.28-26.2` and no longer run on `26.1.2`. Falco's own signatures are unchanged -- japicmp reports no change -- but a server on 26.1.2 fails at the Minestom members this release compiles against. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NeX3hGmZfGDSFUZxWPECJR --- CONTRIBUTING.md | 17 +++++++-- README.md | 8 ++++ .../architecture/ForeignCouplingTest.java | 3 +- .../architecture/ForeignWritePathTest.java | 8 ++-- falco-demo/README.md | 6 +-- .../falco/instance/FalcoChunk.java | 5 ++- .../instance/FalcoInstanceUnloadTest.java | 2 +- .../FalcoSharedInstanceResendTest.java | 37 ++++++++++++++++--- .../instance/SectionMaterialisationTest.java | 3 +- settings.gradle.kts | 2 +- 10 files changed, 70 insertions(+), 21 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a42a750..8e59b0e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,12 +19,21 @@ That compiles the modules, runs the tests and builds the Javadoc. An incomplete fails the build, so a green `./gradlew build` is also the documentation check. **A build from source needs OneLiteFeather Maven credentials.** Falco compiles against Minestom and -the internal `mycelium-bom`, which are served from an authenticated endpoint, so `./gradlew build` -fails with a 401 without them. This affects only work on Falco itself — the published artefacts are -served without authentication and a build that consumes them needs no credentials. The names the -credentials are read under are in +the internal `mycelium-bom` through `https://repo.onelitefeather.dev/onelitefeather`, which answers +`401` to an anonymous request, so `./gradlew build` fails without them. This affects only work on +Falco itself. The names the credentials are read under are in [Installation](https://github.com/OneLiteFeatherNET/Falco/wiki/Installation#building-from-source). +**Consuming Falco needs no credentials from this release onwards, and did need them before it.** The +jars were always served without authentication, but a POM is resolved as well as a jar, and up to +2.1.0 every module's POM imported `net.onelitefeather:mycelium-bom:1.7.2` in its +`dependencyManagement`. That version is on neither `https://repo.onelitefeather.dev/releases` — which +carries 1.8.3 and up — nor Maven Central, so a consumer without credentials got +`Could not find net.onelitefeather:mycelium-bom:1.7.2` and no Minestom version to constrain against. +The bump to 1.8.5 is what closes that: measured against a clean Gradle home with only `mavenCentral()` +and the public `releases` endpoint declared, the 1.8.5 import resolves and brings Minestom +`2026.08.28-26.2` with it, where the same probe against 1.7.2 fails. + Running a single test class, building the benchmark jar and starting the two demo servers are further commands, listed under [Working on this](https://github.com/OneLiteFeatherNET/Falco/wiki/Contributing#working-on-this). diff --git a/README.md b/README.md index 0374206..888df9f 100644 --- a/README.md +++ b/README.md @@ -363,6 +363,14 @@ chunk from its own. The reasoning is in Measured, not asserted. Every timing below comes from a JMH benchmark in this repository and is quoted with the condition it was measured under. +One condition is missing from all of them and is worth naming here rather than leaving implied: the +Minestom version. The rows that quote a factor compare Falco against the loader Minestom ships, so +the Minestom under test is part of the measurement, and no provenance line below records it — the +run commit is not recorded either, so it cannot be recovered after the fact. Every figure below was +taken before this repository moved to Minestom `2026.08.28-26.2`, and none has been re-run against +it. Read the comparison rows as unverified on 26.2 until a baseline is taken there; the jol count is +unaffected, being a count of objects on a heap rather than a comparison. + One claim is not a timing and is marked as such where it appears: **a chunk allocates its sections when something writes into them**, which takes a fresh chunk from 192 objects and 6 848 bytes to 25 and 840. That comes from jol rather than from JMH — it is a count of objects on a heap, it has no diff --git a/falco-archunit/src/test/java/net/onelitefeather/falco/architecture/ForeignCouplingTest.java b/falco-archunit/src/test/java/net/onelitefeather/falco/architecture/ForeignCouplingTest.java index 027a809..057075a 100644 --- a/falco-archunit/src/test/java/net/onelitefeather/falco/architecture/ForeignCouplingTest.java +++ b/falco-archunit/src/test/java/net/onelitefeather/falco/architecture/ForeignCouplingTest.java @@ -126,7 +126,8 @@ class ForeignCouplingTest { /** * F3a — {@code Section} is a record whose {@code clone()} ({@code instance/Section.java:26-33} - * of Minestom {@code 2026.06.20-26.1.2}) calls {@code Light.sky()}/{@code Light.block()} + * of Minestom {@code 2026.08.28-26.2}, at the same lines and with the same body as in + * {@code 2026.06.20-26.1.2}) calls {@code Light.sky()}/{@code Light.block()} * directly and copies existing light objects only via {@code set(array())}. A foreign * {@code Light} implementation is therefore silently replaced by the built-in one on the next * chunk copy — light looks right locally and disappears later. The prototype that carried a diff --git a/falco-archunit/src/test/java/net/onelitefeather/falco/architecture/ForeignWritePathTest.java b/falco-archunit/src/test/java/net/onelitefeather/falco/architecture/ForeignWritePathTest.java index d815495..9d5cefe 100644 --- a/falco-archunit/src/test/java/net/onelitefeather/falco/architecture/ForeignWritePathTest.java +++ b/falco-archunit/src/test/java/net/onelitefeather/falco/architecture/ForeignWritePathTest.java @@ -44,9 +44,11 @@ * has to be rewritten, and the decision not to override {@code setBlock} has to be taken again". * *

Verified against Minestom {@code 2026.06.20-26.1.2} with {@code javap} before the rules were - * written: {@code UNSAFE_setBlock} is {@code private synchronized} at {@code InstanceContainer:149}, - * and the four call sites are in {@code setBlock} ({@code :135}), {@code placeBlock} ({@code :223}), - * {@code breakBlock} ({@code :250}) and {@code executeNeighboursBlockPlacementRule} ({@code :756}). + * written, and read again against {@code 2026.08.28-26.2} when this repository moved to it: the + * shape is the same and only the line numbers moved. On 26.2 {@code UNSAFE_setBlock} is + * {@code private synchronized} at {@code InstanceContainer:161}, and the four call sites are in + * {@code setBlock} ({@code :147}), {@code placeBlock} ({@code :235}), {@code breakBlock} + * ({@code :262}) and {@code executeNeighboursBlockPlacementRule} ({@code :779}). * The rules assert exactly that, so all four were green on their first run and each had to be proved * to bite on its own: W1 by pointing {@link #UNSAFE_SET_BLOCK} at {@code setBlock}, which is neither * private nor synchronised; W2 by swapping {@code breakBlock} for {@code loadChunk} in diff --git a/falco-demo/README.md b/falco-demo/README.md index aadb56c..fdb714d 100644 --- a/falco-demo/README.md +++ b/falco-demo/README.md @@ -47,11 +47,11 @@ Then connect to **`localhost:25565`**. Stop the server with ctrl-c. | | | | --- | --- | -| **Minecraft client** | **26.1.2** | -| Protocol | 775 | +| **Minecraft client** | **26.2** | +| Protocol | 776 | | Authentication | **offline mode** — no Mojang login, any username is accepted | -The version follows from the Minestom this repository builds against, `2026.06.20-26.1.2`: the part +The version follows from the Minestom this repository builds against, `2026.08.28-26.2`: the part after the dash is the Minecraft version it speaks. A client on any other version is turned away during the handshake, which is by far the most likely reason a connection fails. The server prints both numbers on startup, so after a Minestom upgrade believe the log rather than this table. diff --git a/falco-instance/src/main/java/net/onelitefeather/falco/instance/FalcoChunk.java b/falco-instance/src/main/java/net/onelitefeather/falco/instance/FalcoChunk.java index ac33dca..f7323e1 100644 --- a/falco-instance/src/main/java/net/onelitefeather/falco/instance/FalcoChunk.java +++ b/falco-instance/src/main/java/net/onelitefeather/falco/instance/FalcoChunk.java @@ -940,9 +940,12 @@ public ChunkSnapshot updateSnapshot(SnapshotUpdater updater) { } final var entities = instance.getEntityTracker().chunkEntities(chunkX, chunkZ, EntityTracker.Target.ENTITIES); final int[] entityIds = ArrayUtils.mapToIntArray(entities, Entity::getEntityId); + // The biome registry is passed in rather than read from a static: since 26.2 the snapshot + // record holds the registry it resolves biome ids against, so a snapshot answers with the + // registry of the instance it was taken from instead of the one of the running server. return new SnapshotImpl.Chunk(minSection, chunkX, chunkZ, clonedSections, this.entries.clone(), entityIds, updater.reference(instance), - tagHandler().readableCopy()); + instance.registries().biome(), tagHandler().readableCopy()); } /** diff --git a/falco-instance/src/test/java/net/onelitefeather/falco/instance/FalcoInstanceUnloadTest.java b/falco-instance/src/test/java/net/onelitefeather/falco/instance/FalcoInstanceUnloadTest.java index 28d8a93..87ca3f9 100644 --- a/falco-instance/src/test/java/net/onelitefeather/falco/instance/FalcoInstanceUnloadTest.java +++ b/falco-instance/src/test/java/net/onelitefeather/falco/instance/FalcoInstanceUnloadTest.java @@ -120,7 +120,7 @@ void testTheInstanceManagerAloneStillLeaksTheChunks(Env env) { manager.unregisterInstance(instance); - // Pinned against Minestom 2026.06.20-26.1.2: the manager only unloads chunks for an + // Pinned against Minestom 2026.08.28-26.2: the manager only unloads chunks for an // InstanceContainer, which is why FalcoInstance#unregister exists. If this ever starts to // fail, Minestom has learned to clean up foreign instances and the own path can shrink. assertFalse(instance.isRegistered()); diff --git a/falco-instance/src/test/java/net/onelitefeather/falco/instance/FalcoSharedInstanceResendTest.java b/falco-instance/src/test/java/net/onelitefeather/falco/instance/FalcoSharedInstanceResendTest.java index e64ae42..5545ea3 100644 --- a/falco-instance/src/test/java/net/onelitefeather/falco/instance/FalcoSharedInstanceResendTest.java +++ b/falco-instance/src/test/java/net/onelitefeather/falco/instance/FalcoSharedInstanceResendTest.java @@ -2,6 +2,8 @@ import net.minestom.server.coordinate.Pos; import net.minestom.server.entity.Player; +import net.minestom.server.event.EventFilter; +import net.minestom.server.event.player.PlayerChunkUnloadEvent; import net.minestom.server.instance.InstanceContainer; import net.minestom.server.network.packet.server.play.ChunkDataPacket; import net.minestom.server.network.packet.server.play.UnloadChunkPacket; @@ -16,7 +18,6 @@ import java.util.UUID; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertSame; /** @@ -40,12 +41,25 @@ * direct US-4.01 statement in this file, not a decorative one. *

*

- * {@code UpdateViewPositionPacket} and {@code UnloadChunkPacket} are asserted next to it because - * they leave {@code Player#spawnPlayer} unconditionally under {@code updateChunks == true}, + * {@code UpdateViewPositionPacket} and {@code PlayerChunkUnloadEvent} are asserted next to it + * because they leave {@code Player#spawnPlayer} unconditionally under {@code updateChunks == true}, * independently of how chunk bodies are delivered. They keep the file honest if a future Cyano * stops overriding {@code sendChunk} — at which point the chunk counter would silently drop to zero * on both paths, and the control's {@code assertCount(25)} would say so instead of hiding it. *

+ *

Why the unload is an event and not a packet

+ *

+ * Up to Minestom 26.1.2 the old view left {@code spawnPlayer} as one {@code UnloadChunkPacket} per + * chunk, and this file asserted those packets. Since 26.2 the packet is withheld for a chunk that + * is also in the new view: the client of 26.2 drops a chunk it receives an unload and a data packet + * for within the same frame (MC-310041), and Minestom carries the workaround with a + * {@code TODO(26.3)} to revert it once the client is fixed. The control below transfers to the same + * position, so every one of its 25 chunks is in the new view and not a single packet is sent — the + * per-chunk unload is still there, as the event that fires beside the suppressed packet. Asserting + * the event keeps the marker measuring what it was written to measure across both behaviours, and + * {@code unloads.assertEmpty()} states the 26.2 suppression itself, so the revert in 26.3 fails + * here rather than passing unnoticed. + *

* * @author TheMeinerLP * @version 1.0.0 @@ -78,12 +92,17 @@ void testTheFastPathSendsNothing(Env env) { final Collector views = connection.trackIncoming(UpdateViewPositionPacket.class); final Collector unloads = connection.trackIncoming(UnloadChunkPacket.class); final Collector chunks = connection.trackIncoming(ChunkDataPacket.class); + final Collector unloadEvents = + env.trackEvent(PlayerChunkUnloadEvent.class, EventFilter.PLAYER, player); player.setInstance(shared, SPAWN).join(); assertSame(shared, player.getInstance()); views.assertEmpty(); unloads.assertEmpty(); + // The packet is suppressed by 26.2 whenever the chunk stays in view, so the event is what + // separates the fast path from the slow one: the fast path never enters the unload loop. + unloadEvents.assertEmpty(); chunks.assertEmpty(); } @@ -99,14 +118,20 @@ void testTheSlowPathSendsTheMarkers(Env env) { final Collector views = connection.trackIncoming(UpdateViewPositionPacket.class); final Collector unloads = connection.trackIncoming(UnloadChunkPacket.class); final Collector chunks = connection.trackIncoming(ChunkDataPacket.class); + final Collector unloadEvents = + env.trackEvent(PlayerChunkUnloadEvent.class, EventFilter.PLAYER, player); player.setInstance(unrelated, SPAWN).join(); assertSame(unrelated, player.getInstance()); views.assertCount(1); - assertFalse(unloads.collect().isEmpty(), - "the slow path unloads the old view chunk by chunk; if this is empty the markers are wrong, " - + "not the fast path"); + // The slow path unloads the old view chunk by chunk: 25 events for the 5x5 view, the same + // number as the chunk bodies below. If this ever reads zero the markers are wrong, not the + // fast path. + unloadEvents.assertCount(25); + // The transfer keeps the position, so all 25 chunks of the old view are also in the new one + // and 26.2 sends no unload packet for any of them — see the class comment. + unloads.assertEmpty(); // 25 = the 5x5 view of viewDistance(1). This is the resend the fast path avoids, and it is // the number that makes the empty chunk collector over there mean something. chunks.assertCount(25); diff --git a/falco-instance/src/test/java/net/onelitefeather/falco/instance/SectionMaterialisationTest.java b/falco-instance/src/test/java/net/onelitefeather/falco/instance/SectionMaterialisationTest.java index eab6bbf..3747a57 100644 --- a/falco-instance/src/test/java/net/onelitefeather/falco/instance/SectionMaterialisationTest.java +++ b/falco-instance/src/test/java/net/onelitefeather/falco/instance/SectionMaterialisationTest.java @@ -49,7 +49,8 @@ *

Where the numbers come from

*

* Every expectation is derived from the source of Minestom {@code 2026.06.20-26.1.2} before it is - * run, not corrected afterwards until the bar turns green. The dimension is the overworld: twenty-four + * run, not corrected afterwards until the bar turns green, and every one of them survived the move + * to {@code 2026.08.28-26.2} unchanged. The dimension is the overworld: twenty-four * sections, world Y from {@code -64} to {@code 319}, so {@code Heightmap#minHeight} is {@code -65} and * a column scan that finds nothing walks to the floor. *

diff --git a/settings.gradle.kts b/settings.gradle.kts index 6996fab..6e1421c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -31,7 +31,7 @@ dependencyResolutionManagement { } versionCatalogs { create("libs") { - version("bom", "1.7.2") + version("bom", "1.8.5") version("slf4j", "2.0.18") version("annotations", "26.1.0") version("jmh", "1.37")