Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,11 @@
* has to be rewritten, and the decision not to override {@code setBlock} has to be taken again".
*
* <p>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
Expand Down
6 changes: 3 additions & 3 deletions falco-demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,7 @@
// Handler
final BlockHandler handler = block.handler();
final Block lastCachedBlock;
if (handler != null || block.hasNbt() || block.registry().isBlockEntity()) {

Check warning on line 533 in falco-instance/src/main/java/net/onelitefeather/falco/instance/FalcoChunk.java

View workflow job for this annotation

GitHub Actions / build / Build (macos-latest)

[removal] registry() in Block has been deprecated and marked for removal

Check warning on line 533 in falco-instance/src/main/java/net/onelitefeather/falco/instance/FalcoChunk.java

View workflow job for this annotation

GitHub Actions / build / Build (ubuntu-latest)

[removal] registry() in Block has been deprecated and marked for removal
lastCachedBlock = this.entries.put(index, block);
} else {
lastCachedBlock = this.entries.remove(index);
Expand Down Expand Up @@ -940,9 +940,12 @@
}
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());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -16,7 +18,6 @@

import java.util.UUID;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertSame;

/**
Expand All @@ -40,12 +41,25 @@
* direct US-4.01 statement in this file, not a decorative one.
* </p>
* <p>
* {@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.
* </p>
* <h2>Why the unload is an event and not a packet</h2>
* <p>
* 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.
* </p>
*
* @author TheMeinerLP
* @version 1.0.0
Expand Down Expand Up @@ -78,12 +92,17 @@ void testTheFastPathSendsNothing(Env env) {
final Collector<UpdateViewPositionPacket> views = connection.trackIncoming(UpdateViewPositionPacket.class);
final Collector<UnloadChunkPacket> unloads = connection.trackIncoming(UnloadChunkPacket.class);
final Collector<ChunkDataPacket> chunks = connection.trackIncoming(ChunkDataPacket.class);
final Collector<PlayerChunkUnloadEvent> 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();
}

Expand All @@ -99,14 +118,20 @@ void testTheSlowPathSendsTheMarkers(Env env) {
final Collector<UpdateViewPositionPacket> views = connection.trackIncoming(UpdateViewPositionPacket.class);
final Collector<UnloadChunkPacket> unloads = connection.trackIncoming(UnloadChunkPacket.class);
final Collector<ChunkDataPacket> chunks = connection.trackIncoming(ChunkDataPacket.class);
final Collector<PlayerChunkUnloadEvent> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@
* <h2>Where the numbers come from</h2>
* <p>
* 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.
* </p>
Expand Down
2 changes: 1 addition & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading