Skip to content

Commit 2d3955d

Browse files
TheMeinerLPclaude
andauthored
docs: take the coordinates and the API baseline to 1.0.0 (#44)
* docs: take the coordinates and the API baseline to 1.0.0 1.0.0 shipped at 17:59 on 2026-08-03 and carries the chunk, instance and shared instance work. The README still told readers to depend on 0.3.0, which is the release before any of it. apiBaselineVersion moves with it, and that is the part worth reading. Raising it made the build fail on purpose: api-breaks.properties declares accepted API breaks against baseline 0.3.0, but apiBaselineVersion is 1.0.0. Every exception in that file was judged against the older baseline and excludes its type from the check entirely, so each one has to be re-examined and either deleted or re-justified before the version moves. That is what the baseline key exists for. The one exception in the file -- FalcoLightingChunk becoming final under US-3.06 -- was a break against 0.3.0 and is not one against 1.0.0, because 1.0.0 is the release that shipped it. It is deleted rather than carried forward, and japicmp compares the class again: checked by making it package-private, which now fails the build, and reverted. The README's performance section said every figure comes from a JMH benchmark. One no longer does -- the 25 objects and 840 bytes of a fresh chunk are a jol count, with no spread and no statement about speed. It is marked as such where it appears, together with the fact that the instance benchmarks have never been run as a baseline, so no timing about this work is quoted anywhere. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NGpJqdmh7ZNH487GLqPJmK * test(demo): compile the snippets the wiki shows for falco-instance The wiki gained a usage page for the instance module, and a page is the one place where an example can rot without anything noticing. This compiles every code block on it: the builder, the chunk loader form, the light engine combination, both lifecycle routes, the shared world, and the read-only storage accessors. It found one error while being written. The light engine block said `new ChunkLightScheduler(instance)`; the constructor takes a `ChunkLightService` and there is no overload for an instance, so the snippet as first written would not have compiled for anyone who copied it. The class is never run and asserts nothing. Its whole value is failing to compile, which was checked by breaking one line and watching :falco-demo:compileTestJava fail, then reverting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NGpJqdmh7ZNH487GLqPJmK * docs: make the quick start cover all three modules, and compile its snippets The quick start built a server from falco-anvil and falco-light and mentioned falco-instance only in prose. A reader following it never saw the module that gives the world a chunk which allocates what it uses, cleans up when it is unregistered, and can carry Falco's light on the same class -- which is the combination the whole rewrite was for and which did not exist before 1.0.0. Step 1 now declares the BOM and all three modules; the readme is where somebody starts, and one pinned version for three artefacts is the thing to start with. Step 5 is new and shows them together: loader, scheduler supplier, and the builder form that closes the loader after saving, which is the part that is easy to get wrong by hand because a loader closed before the save loses it. It also names the two things that surprise people: getSections() materialises all 24 sections because a caller may write into the result, and a chunk supplier producing anything but a FalcoChunk is refused. And it says that Minestom's own events still fire, so nobody rewrites a working GlobalEventHandler listener. Every snippet of the quick start and of the wiki usage page is now compiled by DocumentationSnippets in falco-demo, renamed from WikiSnippetCheck because it covers both documents. Checked by breaking a line and watching :falco-demo:compileTestJava fail. The wiki called the quick start "four-step" in three places; it has five now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NGpJqdmh7ZNH487GLqPJmK * docs: show what all three modules can do, not just one path through them The quick start walks somebody to a running server, which is what a quick start is for -- and it left most of Falco invisible. A reader finished it without ever seeing that the anvil loader has a builder, that it counts what it could not resolve instead of failing, that the light engine does sky light and incremental updates, that the scheduler can be driven from a plain container, or that a chunk can be read without materialising its sections. A new section after the quick start lists the rest, grouped by module, each with the shortest snippet that shows it: falco-anvil the builder, compression, save parallelism, data version, and diagnostics -- the counters that say a world contained blocks or biomes this loader substituted falco-light calculate, calculateSky, calculateWithNeighbours, blockLightAt; the scheduler builder with executor, area size, cache size and sky-light mode; ChunkLightListener for a plain container; and markChanged/markDirty for changes made outside Falco falco-instance the four parts an instance delegates to, the read-only storage accessors, lifecycle listeners, and the generator that writes into clones so a failure leaves the chunk untouched Every snippet is compiled by DocumentationSnippets, which now covers the quick start, this overview and the wiki usage page, grouped by which document each block belongs to. That mattered here: the overview is thirteen new snippets against an API nobody had written against before, and compiling them is the only reason to believe them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NGpJqdmh7ZNH487GLqPJmK --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 895bb5d commit 2d3955d

5 files changed

Lines changed: 535 additions & 33 deletions

File tree

README.md

Lines changed: 199 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ signatures and behaviour may still change in a minor release.
2626

2727
## Quick start
2828

29-
From nothing to a server that serves a stored world, in four steps. The last one needs no client.
29+
From nothing to a server that serves a stored world, in five steps. Step 4 needs no client, and step
30+
5 replaces the hand-written parts of step 2 with the third module.
3031

3132
### 1. Declare the dependency
3233

@@ -37,8 +38,12 @@ repositories {
3738
}
3839

3940
dependencies {
40-
implementation("net.onelitefeather:falco-anvil:0.3.0")
41-
implementation("net.onelitefeather:falco-light:0.3.0")
41+
// One version for all three, so they cannot drift into a combination nobody tested.
42+
implementation(platform("net.onelitefeather:falco-bom:1.0.0"))
43+
44+
implementation("net.onelitefeather:falco-anvil") // reading and writing Anvil worlds
45+
implementation("net.onelitefeather:falco-light") // block and sky light
46+
implementation("net.onelitefeather:falco-instance") // the instance and the chunk
4247

4348
// Minestom is compileOnly in Falco, so it does not arrive with these
4449
// artefacts. Falco declares no version for it, on purpose. You pick it.
@@ -48,7 +53,9 @@ dependencies {
4853
}
4954
```
5055

51-
The third module, the BOM that pins all three, Maven and snapshots are in
56+
Take only the modules you need — a platform constrains a version for each, it does not pull one in.
57+
Steps 2 to 4 below use the first two; step 5 uses all three. Individual coordinates, Maven and
58+
snapshots are in
5259
[Installation](https://github.com/OneLiteFeatherNET/Falco/wiki/Installation).
5360

5461
### 2. Write the server
@@ -93,17 +100,9 @@ public final class Bootstrap {
93100
}
94101
```
95102

96-
The listener is the explicit route: you decide which chunks are lit and when. There is a shorter one
97-
that needs no listener at all — `instance.setChunkSupplier(scheduler.supplier())`, covered in
98-
[Light Engine](https://github.com/OneLiteFeatherNET/Falco/wiki/Light-Engine).
99-
100-
**That shorter route needs `falco-instance` on the classpath as well**, and the two lines above are
101-
not enough for it. The chunks the supplier produces are `FalcoChunk`s — which is what lets one chunk
102-
carry Falco's light *and* Falco's lifecycle instead of forcing a choice between them — and
103-
`falco-instance` is `compileOnly` in `falco-light`, so it does not arrive with the artefact. Add
104-
`implementation("net.onelitefeather:falco-instance:<version>")` next to the two above before calling
105-
`supplier()`; everything else in `falco-light`, including the `lighting.calculate` route used here,
106-
works without it.
103+
The listener is the explicit route: you decide which chunks are lit and when. Step 5 shows the
104+
shorter one, where the chunks keep their own light and no listener is needed. Everything in
105+
`falco-light` that this step uses works without `falco-instance` on the classpath.
107106

108107
### 3. Put a world where the loader looks
109108

@@ -135,6 +134,181 @@ flag — for a pre-lit world the engine is doing work nobody asked for. It earns
135134
without stored light and after blocks change at runtime. Which case is which is spelled out in
136135
[Light Engine](https://github.com/OneLiteFeatherNET/Falco/wiki/Light-Engine).
137136

137+
### 5. All three modules together
138+
139+
Steps 2 to 4 use an `InstanceContainer` and drive the light yourself. The third module replaces both
140+
of those decisions:
141+
142+
```java
143+
FalcoAnvilLoader loader = new FalcoAnvilLoader(Path.of("worlds", "lobby"), DimensionType.OVERWORLD.key());
144+
145+
// The scheduler takes the light service, not an instance. It reaches the world through
146+
// the chunks its supplier builds.
147+
ChunkLightScheduler scheduler = new ChunkLightScheduler(new ChunkLightService());
148+
149+
FalcoInstance instance = FalcoInstance.builder(DimensionType.OVERWORLD)
150+
.chunkLoader(loader)
151+
.chunkSupplier(scheduler.supplier())
152+
.autoChunkLoad(true)
153+
.ownsLoader(true) // close the loader on shutdown
154+
.saveOnShutdown(true) // and write the chunks first
155+
.registerAndShutdownWith(MinecraftServer.getInstanceManager(),
156+
MinecraftServer.getSchedulerManager());
157+
```
158+
159+
That is the whole server: no light listener, and no shutdown task written by hand. Three things
160+
changed compared with step 2.
161+
162+
**The light keeps itself up to date.** The supplier builds `FalcoLightingChunk`s, which report their
163+
own block changes, loads and ticks to the scheduler. It lights the touched region and sends it, one
164+
tick later, including the ring around it. Before `1.0.0` this combination did not exist: the lighting
165+
chunk and the Falco chunk both extended Minestom's `DynamicChunk`, a class has one superclass, and a
166+
server had to choose one of the two.
167+
168+
**Unregistering the world actually unloads it.** `InstanceManager#unregisterInstance` unloads chunks
169+
only for an `InstanceContainer`; for anything else it leaves every chunk, tick partition and entity
170+
behind. `FalcoInstance` cleans up after itself, and that leak is the reason the module exists at all.
171+
172+
**A chunk allocates what it uses.** Sections are created on the first write into them and every empty
173+
one shares a single instance, which takes a fresh chunk from 192 objects and 6 848 bytes to 25 and
174+
840. It is a count, not a timing — see
175+
[What "high-performance" means here](#what-high-performance-means-here).
176+
177+
Two things to know before building on it. `getSections()` materialises all 24 sections, because a
178+
caller may write into what it gets — use `chunk.storage().views()` to only look. And a chunk supplier
179+
producing anything but a `FalcoChunk` is refused, because such a chunk would be accepted everywhere
180+
except the unload path. Lifecycle listeners, the storage accessors and the rest are in
181+
[Instances and Chunks](https://github.com/OneLiteFeatherNET/Falco/wiki/Instances-And-Chunks).
182+
183+
Minestom's own events are unaffected: `InstanceChunkLoadEvent`, `InstanceChunkUnloadEvent` and
184+
`PlayerBlockBreakEvent` are dispatched here exactly as they are by a container, so listeners on the
185+
`GlobalEventHandler` keep working.
186+
187+
## Everything the three modules offer
188+
189+
The five steps above are one path through Falco. This is the rest of it, so that what exists is
190+
visible without reading three wiki pages first. Every snippet here is compiled by
191+
`DocumentationSnippets` in `falco-demo`.
192+
193+
### falco-anvil — reading and writing Anvil worlds
194+
195+
The two-argument constructor is the whole of it for most servers. The builder is there when the
196+
defaults do not fit:
197+
198+
```java
199+
FalcoAnvilLoader loader = FalcoAnvilLoader.builder()
200+
.openRegionLimit(64) // region files kept open at once
201+
.compressionLevel(2) // 1..9, the trade between write time and file size
202+
.saveParallelism(4) // threads a saveChunks call may use
203+
.dataVersion(4189) // what a written chunk claims to be
204+
.diagnostics(new AnvilDiagnostics())
205+
.exceptionHandler(throwable -> log.warn("chunk load failed", throwable))
206+
.build(Path.of("worlds", "lobby"), DimensionType.OVERWORLD.key());
207+
```
208+
209+
**`diagnostics()` is the one to know about for a live server.** A world written by a different
210+
version, or by a mod, contains blocks and biomes this loader cannot resolve — it substitutes and
211+
counts rather than failing, and the counters are how you find out:
212+
213+
```java
214+
AnvilDiagnostics diagnostics = loader.diagnostics();
215+
diagnostics.reportUnknownBlock("mod:strange_block"); // true the first time, false after
216+
```
217+
218+
`regionDirectory()` says which directory was resolved, `legacyLayout()` whether it fell back to the
219+
pre-26.1 layout, and `openRegionCount()` how many files are open right now. `close()` flushes every
220+
one of them and is what `ownsLoader(true)` calls for you.
221+
222+
### falco-light — block and sky light
223+
224+
Three entry points, in order of how much they do:
225+
226+
```java
227+
ChunkLightService lighting = new ChunkLightService();
228+
229+
lighting.calculate(chunk); // block light, this chunk
230+
lighting.calculateSky(chunk); // sky light, this chunk
231+
lighting.calculateWithNeighbours(instance, 0, 0); // both, and the ring around it
232+
233+
int level = lighting.blockLightAt(chunk, 8, 40, 8); // read one position back
234+
```
235+
236+
`calculateWithNeighbours` is the one to use when a chunk arrives from disk, because light crosses
237+
chunk borders and a chunk lit alone has a dark seam.
238+
239+
For a world that keeps itself lit, the scheduler does the bookkeeping. Its builder carries the knobs
240+
that matter under load:
241+
242+
```java
243+
ChunkLightScheduler scheduler = ChunkLightScheduler.builder(lighting)
244+
.executor(ChunkLightScheduler.defaultExecutor())
245+
.maxAreaSize(4) // chunks per side of one lighting area
246+
.maxCachedChunks(256) // opacity tables kept between passes
247+
.skyLight(ChunkLightScheduler.SkyLight.FROM_DIMENSION)
248+
.onFailure(throwable -> log.error("lighting failed", throwable))
249+
.build();
250+
```
251+
252+
Two ways to drive it. On a `FalcoInstance`, `scheduler.supplier()` as in step 5 and nothing else. On
253+
an `InstanceContainer`, hang a `ChunkLightListener` on the chunks and tick it yourself:
254+
255+
```java
256+
container.setChunkSupplier((instance, x, z) -> {
257+
FalcoChunk chunk = new FalcoChunk(instance, x, z);
258+
chunk.addLifecycleListener(new ChunkLightListener(scheduler));
259+
return chunk;
260+
});
261+
MinecraftServer.getSchedulerManager().buildTask(() -> scheduler.onTick(container, System.currentTimeMillis()))
262+
.repeat(TaskSchedule.tick(1))
263+
.schedule();
264+
```
265+
266+
And when something outside Falco changed the world, tell it:
267+
268+
```java
269+
scheduler.markChanged(instance, 0, 0); // this chunk needs relighting
270+
scheduler.markChanged(instance, 0, 0, 8, 40, 8); // this position did
271+
scheduler.markDirty(instance, 0, 0); // relight without an incremental path
272+
```
273+
274+
### falco-instance — the instance, the chunk, shared views
275+
276+
The builder is in step 5. Beyond it, the instance exposes its four parts, and the chunk exposes its
277+
storage:
278+
279+
```java
280+
instance.registry(); // which chunks are loaded, by position
281+
instance.lifecycle(); // loading, publishing, unloading, and the listeners
282+
instance.blockWriter(); // the write path, including placement and destruction
283+
284+
chunk.storage().views(); // read the sections without materialising them
285+
chunk.storage().materialisedSections(); // how many actually exist
286+
chunk.storage().shared(0); // is section 0 still the shared empty one
287+
```
288+
289+
Lifecycle listeners are the extension point that replaced subclassing. Every method has a default:
290+
291+
```java
292+
instance.lifecycle().addListener(new ChunkLifecycleListener() {
293+
@Override
294+
public void onLoad(ChunkLifecycleEvent event) {
295+
log.info("loaded {} {}", event.chunk().getChunkX(), event.chunk().getChunkZ());
296+
}
297+
});
298+
```
299+
300+
They run **inside** the transition, before anybody else sees the chunk, which is what the light
301+
engine needs — and why a throw from one fails the chunk load. For ordinary application code the
302+
Minestom events named above are the right tool.
303+
304+
Generation is the usual Minestom API, with one difference worth knowing: the generator is handed
305+
copies of the section palettes and they are moved over only when it returns, so a generator that
306+
fails halfway leaves the chunk exactly as it was rather than half built and published.
307+
308+
```java
309+
instance.setGenerator(unit -> unit.modifier().fillHeight(0, 40, Block.STONE));
310+
```
311+
138312
## Shared worlds
139313

140314
Shared worlds are the one case `FalcoInstance` cannot serve, because `SharedInstance` takes an
@@ -177,8 +351,16 @@ chunk from its own. The reasoning is in
177351

178352
## What "high-performance" means here
179353

180-
Measured, not asserted. Every figure comes from a JMH benchmark in this repository and is quoted
181-
with the condition it was measured under.
354+
Measured, not asserted. Every timing below comes from a JMH benchmark in this repository and is
355+
quoted with the condition it was measured under.
356+
357+
One claim is not a timing and is marked as such where it appears: **a chunk allocates its sections
358+
when something writes into them**, which takes a fresh chunk from 192 objects and 6 848 bytes to 25
359+
and 840. That comes from jol rather than from JMH — it is a count of objects on a heap, it has no
360+
spread, and it is unaffected by what else the machine was doing. It also says nothing about speed.
361+
Whether a smaller chunk makes anything faster depends on allocation pressure and on the collector,
362+
and nobody here has measured that. The instance benchmarks exist; they have never been run as a
363+
baseline, and until they have, no timing about the instance or the chunk appears in this file.
182364

183365
**The Anvil loader is 1.9× faster on two threads**1 181 ± 31 against 2 200 ± 445 µs/op, reading
184366
one chunk of 200 distinct block states. On one thread the intervals overlap and nothing is resolved

0 commit comments

Comments
 (0)