feat: serve the lobby world through Falco (Stage 1) - #215
Conversation
Declares net.onelitefeather:falco-anvil and :falco-light in the inline version catalog and adds the public OneLiteFeather release repository they are published to. The :common module consumes both, since the map handling that will use them lives there. The lobby spec names 0.3.0. That version predates Minestom 26.1, so it is not the one that resolves here: 2.1.0 is the current release and the one whose mycelium BOM lines up with the Minestom version the aonyx BOM pins. Pulling it in moves Minestom from 2026.06.05-26.1.2 to 2026.06.20-26.1.2, because falco brings mycelium-bom 1.7.2 where aonyx-bom 0.8.0 brings 1.7.1.
Adds the package-info the OLF Minestom standard asks for (OLF-L3-01) and removes the hand-placed @NotNull annotations it makes redundant, starting with MapEntry, which also gains the type and method javadoc of OLF-L4-01. The remaining classes of the package follow in the commits that change them.
…1.03) Replaces Minestom's AnvilLoader with Falco's FalcoAnvilLoader and computes the block light of every loaded chunk with falco-light's ChunkLightService instead of LightingChunk.relight. Why the loader matters beyond it being ours: Minestom's reports a chunk it cannot read as absent, which makes the server generate a fresh one and overwrite the built world on the next save. Falco's reports the failure (US-1.02). The loader is handed the world root rather than its region directory - it resolves dimensions/<namespace>/<value>/region itself and falls back to a plain region/ for a world in the older layout, which is the layout the lobby worlds are in. It also holds region files open for as long as it lives, so it is created once per world root rather than on every read of the map data, and MapProvider became AutoCloseable so the app shutdown can close it. Lighting keeps LightingChunk as the chunk supplier. Falco writes its result through Light#set, which clears the update flag of the section, so Minestom does not recompute the block light - it stays responsible for sending the light and for the sky pass. The cached packets are dropped and a resend is scheduled after the calculation, because the load event is dispatched after the loading future completes and the chunk may already be on its way to a player by then. Not used here: ChunkLightScheduler with its own chunk supplier, which would be the fuller replacement. In falco-light 2.1.0 its FalcoLightingChunk extends FalcoChunk from falco-instance, a module the artefact neither bundles nor declares, so that route does not link.
Two defects in one method. peekMap took the only entry without ever reading the property when exactly one world was present, so a machine with one world behaved differently from production (US-1.05). With more than one world it threw a bare NoSuchElementException when the named world was absent, which turned a typo into a lobby that does not start and a stack trace that names neither the world that was searched for nor the ones that are there (US-1.04). The property is now read regardless of how many worlds exist. A named world that is not present is reported with both halves that make the typo obvious - the searched name and the found names - and the pool falls back to the default world. Only a pool without any world at all is still fatal. The property is read per instance rather than into a static field, which is what makes any of this testable at all, and the fallback is observable through isRequestedMapSelected() rather than only in the log.
Adds lobby-world-selection.md, which describes what US-1.06 already required and the code already did: one directory per world below worlds/, named by TITAN_LOBBY_MAP, with the region files resolved inside it. Nothing changed for that story - it is verified and written down. The same document carries the parts of stage 1 that did change: the fallback when the named world is absent, and which engine serves the chunks. exploration-lighting.md is brought level with the lighting path that is actually in the code now, and the stage 1 rows of the spec are marked umgesetzt with a note on the three points where the result deviates from what the spec assumed.
Test results 72 files 72 suites 44s ⏱️ Results for commit f80ede1. ♻️ This comment has been updated with latest results. |
falco-light 2.1.0 declares no dependency on falco-instance, and its
ChunkLightScheduler cannot be loaded without one: the class carries the lambda
body of supplier(), which returns a FalcoLightingChunk, and the verifier
resolves that type and its FalcoChunk supertype while linking the scheduler
rather than when the lambda runs. Probed with the published artifacts on a bare
classpath:
FAIL net.onelitefeather.falco.light.ChunkLightScheduler
-> java.lang.NoClassDefFoundError: net/onelitefeather/falco/instance/FalcoChunk
Adding falco-instance of the same release makes the class load. Nothing in
Titan names a type from it, so it is a runtime dependency only.
Three defects, one cause: the light of a chunk was computed once, inline, from whatever neighbourhood happened to be loaded, and written through Light#set, which clears the update flag of the section. Nothing ever looks at such a section again. - A chunk that loaded before its neighbours was lit without them and was never corrected when they arrived, leaving a permanent dark strip along the border - the very thing the class comment claimed to prevent. - FalcoAnvilLoader#supportsParallelLoading() is true, so the load event runs on a virtual thread and adjacent chunks light each other's neighbourhoods concurrently. Falco's own javadoc forbids that: the result is a seam, never an error, and permanent. - Nothing computed sky light at all. A fresh Light reports itself as valid, so LightingChunk skips the sky pass, and a section whose region file carries no SkyLight array stayed at level zero for good. ChunkLightScheduler answers all three. A loaded chunk is only marked, together with the eight around it, so a chunk is lit again when its neighbours turn up; the scheduler groups marks into areas that do not overlap and discards a result whose chunk changed underneath it; and it runs the sky pass itself. LightingChunk stays the chunk type, because sending the light is what it is for - the resend timer is armed from the completion callback. The tests load chunks out of a real region file, which is the only path on which these defects exist: a generated chunk goes through Chunk#onGenerate(), which invalidates its sections, and hides all three.
close() nulled the loader root but left the closed loader wired into the instance and the listeners on its event node. Two things fell out of that: a player who moved while the server was stopping hit the IllegalStateException of a loader that had been closed underneath the instance, and saveMap() failed the same-root check, built a fresh loader and reopened exactly the region files the shutdown had just closed. The provider now carries a closed flag, unwires listeners and loader before closing, hands the instance the no-op loader and refuses to save afterwards. A failure inside the constructor closes the loader it had already installed rather than leaking it. :setup creates a MapProvider and never closed it, which is backwards: it is the module that mutates worlds, and FalcoAnvilLoader#close() is what flushes the region handles. It now registers a shutdown task like :app does.
describeMissingMap announced the default world unconditionally, including in the case where the default world is missing as well and peekMap goes on with the first world it found. The message now names the world that was actually taken and says why, and the second warning that used to follow it is gone with it. clear() also set the entry list to null under an @NotNullByDefault package, which turned every later read of the pool into a NullPointerException instead of an empty list. The list is emptied instead, and it is copied on the way in because one of the two filters hands back an immutable list.
exploration-lighting.md said LightingChunk still owned the sky pass. It did not: a fresh Light reports itself as valid, so the sky pass never ran and sky light came entirely from the region file, with no fallback and no recomputation. The section now describes what actually happens - marks on load, one scheduled pass per tick over non-overlapping areas, block and sky light from Falco, sending from LightingChunk - and why each half of that is load bearing. lobby-world-selection.md claimed the loader is closed on shutdown, which was true for :app only, and described a second warning for the case where the default world is missing too; there is one warning now and it names the world that was really taken. The stage 1 note of the spec recorded the scheduler as unusable. The reason it gave was right in its facts and too wide in its conclusion, and the note now carries what was measured instead.
Every falco artifact carries mycelium-bom 1.7.2 as a platform dependency where aonyx-bom 0.8.0 brings 1.7.1, so highest-wins moved Minestom from 2026.06.05-26.1.2 to 2026.06.20-26.1.2 with nothing announcing it. This particular bump was harmless, but nothing constrained it and the next falco release would repeat it unnoticed - which is what NFR-001 forbids. The three falco dependencies of :common now exclude that BOM. Verified by resolving rather than assuming: :common, :app and :setup resolve net.minestom:minestom:2026.06.05-26.1.2 on compile, runtime and test runtime, and :bridge compiles against the same version, so the divergence where :bridge was built against one Minestom while :app ran another is gone with it. All 47 :common and 31 :app tests pass on that version, and both fat jars still carry the falco classes. The catalog comment claimed 2.1.0 'resolves against the Minestom version the aonyx BOM pins'. It did not resolve against it, it overrode it.
All six review findings fixed — 78 tests greenI was wrong about the scheduler, and the agent proved itI told the implementing agent that the Probed against the published 2.1.0 artifacts on a bare classpath:
This is a genuine The old test was blind by construction
The replacement builds a world on disk (generates chunks in a throwaway instance, writes them through a Per defect
NFR-001 fixed and measured
The catalog comment claiming 2.1.0 "resolves against the Minestom version the aonyx BOM pins" is corrected: it did not resolve against it, it overrode it. Design note
|
Stacked on #214. Implements spec Stage 1 — US-1.01 … US-1.06.
What changed
Engine.
MapProvidernow loads throughFalcoAnvilLoaderinstead of Minestom'sAnvilLoader, and lights chunks throughfalco-light'sChunkLightServiceinstead ofLightingChunk.relight(...). The loader is created once per world root rather than persaveMapcall — it holds region files open — soMapProviderbecameAutoCloseableandTitan.terminate()closes it.The bug this stage exists to fix.
MapPool.peekMap()used to ignoreTITAN_LOBBY_MAPwhenever exactly one world existed, and threw a bareNoSuchElementExceptionotherwise. So a typo was invisible locally and killed startup in production, without naming the world it looked for. Now the property is always honoured; a missing world logs a warning naming both what was searched for and what was found, then falls back toworld. Only an emptyworlds/is still fatal.Tests — 70 green
MapPoolTest(11 cases) covers the regression directly: missing named world with several worlds present. PlusMapProviderIntegrationTest(3 Cyano cases) asserting the loader type, the lighting chunk supplier, and thatclose()really closes the loader.Three things reviewers should know
1. Falco is 2.1.0, not the 0.3.0 the spec named. 0.3.0 predates Minestom 26.1. Side effect: Falco brings
mycelium-bom1.7.2 whereaonyx-bom0.8.0 brings 1.7.1, so Minestom moves2026.06.05-26.1.2→2026.06.20-26.1.2. Same protocol, but it is an unrequested bump — flagging it rather than burying it.2.
falco-light2.1.0 has a packaging defect.FalcoLightingChunkextendsnet.onelitefeather.falco.instance.FalcoChunk, but the artifact neither bundlesfalco-instancenor declares a dependency on it, soChunkLightScheduler.supplier()does not link. This PR therefore takes the explicitChunkLightServiceroute. Worth a fix in Falco — itsmainalready changed the superclass toDynamicChunk, so this looks fixed but unreleased.3. Minestom still owns sky light.
calculateWithNeighbourswrites block light only;LightingChunkstays the chunk type so light is sent and the sky pass still runs. "Falco instead of Minestom's light engine" is therefore only half true today.calculateSky(chunk)exists but is per-chunk with no neighbour exchange and would seam at chunk borders — judged a regression against Minestom's neighbour-aware pass and left alone.Known gaps
InstanceChunkLoadEventis dispatched after the chunk future completes, so a player may already hold the chunk when the listener runs. Pre-existing; mitigated withinvalidate()+invalidateResendDelay(). The scheduler route would remove it once point 2 is fixed.:setupdoes not close the provider — it has no shutdown hook at all. Left alone.MapPool.clear()nullsreferenceList, after whichgetAvailableMaps()NPEs.