From 9f51cc604f7d21f4441b262de06e15b53c16ebd7 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Mon, 3 Aug 2026 20:17:52 +0200 Subject: [PATCH 1/4] 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) Claude-Session: https://claude.ai/code/session_01NGpJqdmh7ZNH487GLqPJmK --- README.md | 18 +++++++++++++----- gradle.properties | 2 +- gradle/api-breaks.properties | 23 ++++++++--------------- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 51f19a8..396be04 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,8 @@ repositories { } dependencies { - implementation("net.onelitefeather:falco-anvil:0.3.0") - implementation("net.onelitefeather:falco-light:0.3.0") + implementation("net.onelitefeather:falco-anvil:1.0.0") + implementation("net.onelitefeather:falco-light:1.0.0") // Minestom is compileOnly in Falco, so it does not arrive with these // artefacts. Falco declares no version for it, on purpose. You pick it. @@ -101,7 +101,7 @@ that needs no listener at all — `instance.setChunkSupplier(scheduler.supplier( not enough for it. The chunks the supplier produces are `FalcoChunk`s — which is what lets one chunk carry Falco's light *and* Falco's lifecycle instead of forcing a choice between them — and `falco-instance` is `compileOnly` in `falco-light`, so it does not arrive with the artefact. Add -`implementation("net.onelitefeather:falco-instance:")` next to the two above before calling +`implementation("net.onelitefeather:falco-instance:1.0.0")` next to the two above before calling `supplier()`; everything else in `falco-light`, including the `lighting.calculate` route used here, works without it. @@ -177,8 +177,16 @@ chunk from its own. The reasoning is in ## What "high-performance" means here -Measured, not asserted. Every figure comes from a JMH benchmark in this repository and is quoted -with the condition it was measured under. +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 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 +spread, and it is unaffected by what else the machine was doing. It also says nothing about speed. +Whether a smaller chunk makes anything faster depends on allocation pressure and on the collector, +and nobody here has measured that. The instance benchmarks exist; they have never been run as a +baseline, and until they have, no timing about the instance or the chunk appears in this file. **The Anvil loader is 1.9× faster on two threads** — 1 181 ± 31 against 2 200 ± 445 µs/op, reading one chunk of 200 distinct block states. On one thread the intervals overlap and nothing is resolved diff --git a/gradle.properties b/gradle.properties index 7c569c9..c5bc50a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,4 +6,4 @@ org.gradle.parallel=true #org.gradle.logging.level=info org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -apiBaselineVersion=0.3.0 +apiBaselineVersion=1.0.0 diff --git a/gradle/api-breaks.properties b/gradle/api-breaks.properties index cd800df..0c59370 100644 --- a/gradle/api-breaks.properties +++ b/gradle/api-breaks.properties @@ -9,20 +9,13 @@ # # Format: .classExcludes = [, ...] -baseline = 0.3.0 +baseline = 1.0.0 -# net.onelitefeather.falco.light.FalcoLightingChunk +# No exceptions are in force. # -# The class became `final` and now extends net.onelitefeather.falco.instance.FalcoChunk instead of -# Minestom's DynamicChunk (US-3.06 of docs/superpowers/specs/2026-08-01-falco-instance-chunk-design.md). -# `final` is a real break: anyone who subclassed it no longer can. It is deliberate -- the light -# engine and the chunk lifecycle had to end up on one instance, which is what the whole storage -# rewrite was for -- and admissible because the package documents every public type in it as -# experimental. -# -# japicmp additionally reports `setBlock(int, int, int, Block, BlockHandler$Placement, -# BlockHandler$Destroy)` and `tick(long)` as removed. Both are wrong. The methods are still public on -# FalcoChunk and callers keep them by inheritance; japicmp cannot see that because FalcoChunk lives -# in another module and `ignoreMissingClasses` is on, so an inherited member looks removed. Verified -# with `javap -p` on falco-instance's FalcoChunk.class -- both are `public` there. -falco-light.classExcludes = net.onelitefeather.falco.light.FalcoLightingChunk +# The one this file was created for -- net.onelitefeather.falco.light.FalcoLightingChunk becoming +# `final` and extending FalcoChunk instead of Minestom's DynamicChunk (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. Raising +# apiBaselineVersion made the build demand this file be looked at again, which is what the baseline +# key is for, and the answer was to delete the entry rather than carry it forward. From here on the +# class is compared like any other, and a further break in it fails the build. From 9c44eb367e84f5b2958174b6e1496deae09600e5 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Mon, 3 Aug 2026 20:25:34 +0200 Subject: [PATCH 2/4] 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) Claude-Session: https://claude.ai/code/session_01NGpJqdmh7ZNH487GLqPJmK --- .../falco/demo/WikiSnippetCheck.java | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 falco-demo/src/test/java/net/onelitefeather/falco/demo/WikiSnippetCheck.java diff --git a/falco-demo/src/test/java/net/onelitefeather/falco/demo/WikiSnippetCheck.java b/falco-demo/src/test/java/net/onelitefeather/falco/demo/WikiSnippetCheck.java new file mode 100644 index 0000000..77810de --- /dev/null +++ b/falco-demo/src/test/java/net/onelitefeather/falco/demo/WikiSnippetCheck.java @@ -0,0 +1,112 @@ +package net.onelitefeather.falco.demo; + +import net.minestom.server.MinecraftServer; +import net.minestom.server.instance.Chunk; +import net.minestom.server.instance.InstanceContainer; +import net.minestom.server.instance.InstanceManager; +import net.minestom.server.instance.block.Block; +import net.minestom.server.world.DimensionType; +import net.onelitefeather.falco.anvil.FalcoAnvilLoader; +import net.onelitefeather.falco.instance.ChunkLifecycleEvent; +import net.onelitefeather.falco.instance.ChunkLifecycleListener; +import net.onelitefeather.falco.instance.FalcoChunk; +import net.onelitefeather.falco.instance.FalcoInstance; +import net.onelitefeather.falco.instance.FalcoSharedInstance; +import net.onelitefeather.falco.light.ChunkLightScheduler; +import net.onelitefeather.falco.light.ChunkLightService; + +import java.nio.file.Path; +import java.util.UUID; + +/** + * Every code block of the wiki page Instances and chunks, compiled. + * + *

This class is never run. It exists so that a snippet in the wiki cannot quietly stop compiling + * against the code it documents.

+ */ +final class WikiSnippetCheck { + + private WikiSnippetCheck() { + } + + static FalcoInstance shortestForm() { + return FalcoInstance.builder(DimensionType.OVERWORLD) + .register(MinecraftServer.getInstanceManager()); + } + + static FalcoInstance withChunkLoader() { + FalcoAnvilLoader loader = new FalcoAnvilLoader(Path.of("worlds", "lobby"), DimensionType.OVERWORLD.key()); + + return FalcoInstance.builder(DimensionType.OVERWORLD) + .chunkLoader(loader) + .autoChunkLoad(true) + .ownsLoader(true) + .saveOnShutdown(true) + .registerAndShutdownWith(MinecraftServer.getInstanceManager(), + MinecraftServer.getSchedulerManager()); + } + + static FalcoInstance withLightEngine() { + FalcoAnvilLoader loader = new FalcoAnvilLoader(Path.of("worlds", "lobby"), DimensionType.OVERWORLD.key()); + ChunkLightScheduler scheduler = new ChunkLightScheduler(new ChunkLightService()); + + return FalcoInstance.builder(DimensionType.OVERWORLD) + .chunkLoader(loader) + .chunkSupplier(scheduler.supplier()) + .autoChunkLoad(true) + .register(MinecraftServer.getInstanceManager()); + } + + static FalcoInstance withLifecycleConsumers(InstanceManager manager) { + return FalcoInstance.builder(DimensionType.OVERWORLD) + .chunkLifecycle( + chunk -> report("loaded", chunk), + chunk -> report("unloaded", chunk)) + .register(manager); + } + + static void withLifecycleListener(FalcoInstance instance) { + instance.lifecycle().addListener(new ChunkLifecycleListener() { + @Override + public void onPublish(ChunkLifecycleEvent event) { + } + + @Override + public void onLoad(ChunkLifecycleEvent event) { + } + + @Override + public void onTick(ChunkLifecycleEvent event) { + } + + @Override + public void onUnload(ChunkLifecycleEvent event) { + } + + @Override + public void onBlockChange(FalcoChunk chunk, int x, int y, int z, Block block) { + } + }); + } + + static FalcoSharedInstance sharedWorld() { + InstanceManager manager = MinecraftServer.getInstanceManager(); + InstanceContainer world = manager.createInstanceContainer(); + world.setChunkSupplier(FalcoChunk::new); + + FalcoSharedInstance view = new FalcoSharedInstance(UUID.randomUUID(), world); + manager.registerSharedInstance(view); + return view; + } + + static int readWithoutMaterialising(FalcoChunk chunk) { + chunk.storage().view(0); + chunk.storage().views(); + chunk.storage().shared(0); + return chunk.storage().materialisedSections(); + } + + private static void report(String what, Chunk chunk) { + System.out.println(what + " " + chunk.getChunkX() + " " + chunk.getChunkZ()); + } +} From 3cfd3045f1056ecfd35e186619969d5a97d96168 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Mon, 3 Aug 2026 20:38:46 +0200 Subject: [PATCH 3/4] 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) Claude-Session: https://claude.ai/code/session_01NGpJqdmh7ZNH487GLqPJmK --- README.md | 79 +++++++++++++++---- ...tCheck.java => DocumentationSnippets.java} | 51 ++++++++++-- 2 files changed, 110 insertions(+), 20 deletions(-) rename falco-demo/src/test/java/net/onelitefeather/falco/demo/{WikiSnippetCheck.java => DocumentationSnippets.java} (64%) diff --git a/README.md b/README.md index 396be04..338eeef 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,8 @@ signatures and behaviour may still change in a minor release. ## Quick start -From nothing to a server that serves a stored world, in four steps. The last one needs no client. +From nothing to a server that serves a stored world, in five steps. Step 4 needs no client, and step +5 replaces the hand-written parts of step 2 with the third module. ### 1. Declare the dependency @@ -37,8 +38,12 @@ repositories { } dependencies { - implementation("net.onelitefeather:falco-anvil:1.0.0") - implementation("net.onelitefeather:falco-light:1.0.0") + // One version for all three, so they cannot drift into a combination nobody tested. + implementation(platform("net.onelitefeather:falco-bom:1.0.0")) + + implementation("net.onelitefeather:falco-anvil") // reading and writing Anvil worlds + implementation("net.onelitefeather:falco-light") // block and sky light + implementation("net.onelitefeather:falco-instance") // the instance and the chunk // Minestom is compileOnly in Falco, so it does not arrive with these // artefacts. Falco declares no version for it, on purpose. You pick it. @@ -48,7 +53,9 @@ dependencies { } ``` -The third module, the BOM that pins all three, Maven and snapshots are in +Take only the modules you need — a platform constrains a version for each, it does not pull one in. +Steps 2 to 4 below use the first two; step 5 uses all three. Individual coordinates, Maven and +snapshots are in [Installation](https://github.com/OneLiteFeatherNET/Falco/wiki/Installation). ### 2. Write the server @@ -93,17 +100,9 @@ public final class Bootstrap { } ``` -The listener is the explicit route: you decide which chunks are lit and when. There is a shorter one -that needs no listener at all — `instance.setChunkSupplier(scheduler.supplier())`, covered in -[Light Engine](https://github.com/OneLiteFeatherNET/Falco/wiki/Light-Engine). - -**That shorter route needs `falco-instance` on the classpath as well**, and the two lines above are -not enough for it. The chunks the supplier produces are `FalcoChunk`s — which is what lets one chunk -carry Falco's light *and* Falco's lifecycle instead of forcing a choice between them — and -`falco-instance` is `compileOnly` in `falco-light`, so it does not arrive with the artefact. Add -`implementation("net.onelitefeather:falco-instance:1.0.0")` next to the two above before calling -`supplier()`; everything else in `falco-light`, including the `lighting.calculate` route used here, -works without it. +The listener is the explicit route: you decide which chunks are lit and when. Step 5 shows the +shorter one, where the chunks keep their own light and no listener is needed. Everything in +`falco-light` that this step uses works without `falco-instance` on the classpath. ### 3. Put a world where the loader looks @@ -135,6 +134,56 @@ flag — for a pre-lit world the engine is doing work nobody asked for. It earns without stored light and after blocks change at runtime. Which case is which is spelled out in [Light Engine](https://github.com/OneLiteFeatherNET/Falco/wiki/Light-Engine). +### 5. All three modules together + +Steps 2 to 4 use an `InstanceContainer` and drive the light yourself. The third module replaces both +of those decisions: + +```java +FalcoAnvilLoader loader = new FalcoAnvilLoader(Path.of("worlds", "lobby"), DimensionType.OVERWORLD.key()); + +// The scheduler takes the light service, not an instance. It reaches the world through +// the chunks its supplier builds. +ChunkLightScheduler scheduler = new ChunkLightScheduler(new ChunkLightService()); + +FalcoInstance instance = FalcoInstance.builder(DimensionType.OVERWORLD) + .chunkLoader(loader) + .chunkSupplier(scheduler.supplier()) + .autoChunkLoad(true) + .ownsLoader(true) // close the loader on shutdown + .saveOnShutdown(true) // and write the chunks first + .registerAndShutdownWith(MinecraftServer.getInstanceManager(), + MinecraftServer.getSchedulerManager()); +``` + +That is the whole server: no light listener, and no shutdown task written by hand. Three things +changed compared with step 2. + +**The light keeps itself up to date.** The supplier builds `FalcoLightingChunk`s, which report their +own block changes, loads and ticks to the scheduler. It lights the touched region and sends it, one +tick later, including the ring around it. Before `1.0.0` this combination did not exist: the lighting +chunk and the Falco chunk both extended Minestom's `DynamicChunk`, a class has one superclass, and a +server had to choose one of the two. + +**Unregistering the world actually unloads it.** `InstanceManager#unregisterInstance` unloads chunks +only for an `InstanceContainer`; for anything else it leaves every chunk, tick partition and entity +behind. `FalcoInstance` cleans up after itself, and that leak is the reason the module exists at all. + +**A chunk allocates what it uses.** Sections are created on the first write into them and every empty +one shares a single instance, which takes a fresh chunk from 192 objects and 6 848 bytes to 25 and +840. It is a count, not a timing — see +[What "high-performance" means here](#what-high-performance-means-here). + +Two things to know before building on it. `getSections()` materialises all 24 sections, because a +caller may write into what it gets — use `chunk.storage().views()` to only look. And a chunk supplier +producing anything but a `FalcoChunk` is refused, because such a chunk would be accepted everywhere +except the unload path. Lifecycle listeners, the storage accessors and the rest are in +[Instances and Chunks](https://github.com/OneLiteFeatherNET/Falco/wiki/Instances-And-Chunks). + +Minestom's own events are unaffected: `InstanceChunkLoadEvent`, `InstanceChunkUnloadEvent` and +`PlayerBlockBreakEvent` are dispatched here exactly as they are by a container, so listeners on the +`GlobalEventHandler` keep working. + ## Shared worlds Shared worlds are the one case `FalcoInstance` cannot serve, because `SharedInstance` takes an diff --git a/falco-demo/src/test/java/net/onelitefeather/falco/demo/WikiSnippetCheck.java b/falco-demo/src/test/java/net/onelitefeather/falco/demo/DocumentationSnippets.java similarity index 64% rename from falco-demo/src/test/java/net/onelitefeather/falco/demo/WikiSnippetCheck.java rename to falco-demo/src/test/java/net/onelitefeather/falco/demo/DocumentationSnippets.java index 77810de..4a5f1da 100644 --- a/falco-demo/src/test/java/net/onelitefeather/falco/demo/WikiSnippetCheck.java +++ b/falco-demo/src/test/java/net/onelitefeather/falco/demo/DocumentationSnippets.java @@ -19,14 +19,55 @@ import java.util.UUID; /** - * Every code block of the wiki page Instances and chunks, compiled. + * Every code block the documentation shows, compiled: the quick start of the readme and the wiki + * page Instances and chunks. * - *

This class is never run. It exists so that a snippet in the wiki cannot quietly stop compiling - * against the code it documents.

+ *

This class is never run and asserts nothing. Its whole value is failing to compile — a snippet + * somebody copies is the one piece of documentation that can be wrong in a way no reader forgives, + * and a page cannot be kept honest by rereading it. Writing the first version already caught one: + * the light engine block said {@code new ChunkLightScheduler(instance)}, and that constructor takes + * a {@code ChunkLightService}.

+ * + *

When a snippet in either document changes, change it here too. When this stops compiling, the + * document is wrong rather than this class.

*/ -final class WikiSnippetCheck { +final class DocumentationSnippets { + + private DocumentationSnippets() { + } + + /** + * Readme, quick start, step 5 — all three modules together. + * + * @return the instance the snippet builds + */ + static FalcoInstance readmeAllThreeModules() { + FalcoAnvilLoader loader = new FalcoAnvilLoader(Path.of("worlds", "lobby"), DimensionType.OVERWORLD.key()); + + ChunkLightScheduler scheduler = new ChunkLightScheduler(new ChunkLightService()); + + return FalcoInstance.builder(DimensionType.OVERWORLD) + .chunkLoader(loader) + .chunkSupplier(scheduler.supplier()) + .autoChunkLoad(true) + .ownsLoader(true) + .saveOnShutdown(true) + .registerAndShutdownWith(MinecraftServer.getInstanceManager(), + MinecraftServer.getSchedulerManager()); + } - private WikiSnippetCheck() { + /** + * Readme, quick start, step 4 — the two operations without a listener around them. + * + * @param instance the instance to read from + * @param lighting the light service + * @return the block light level the snippet prints + */ + static int readmeCheckWithoutClient(FalcoInstance instance, ChunkLightService lighting) { + Chunk chunk = instance.loadChunk(0, 0).join(); + lighting.calculate(chunk); + + return lighting.blockLightAt(chunk, 8, 40, 8); } static FalcoInstance shortestForm() { From c331c21c131588bca290c142b73dd81306d782d7 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Mon, 3 Aug 2026 20:52:36 +0200 Subject: [PATCH 4/4] 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) Claude-Session: https://claude.ai/code/session_01NGpJqdmh7ZNH487GLqPJmK --- README.md | 125 ++++++++++ .../falco/demo/DocumentationSnippets.java | 228 +++++++++++++++--- .../falco/demo/DocumentationSnippets.java.new | 0 3 files changed, 326 insertions(+), 27 deletions(-) create mode 100644 falco-demo/src/test/java/net/onelitefeather/falco/demo/DocumentationSnippets.java.new diff --git a/README.md b/README.md index 338eeef..ed3c65d 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,131 @@ Minestom's own events are unaffected: `InstanceChunkLoadEvent`, `InstanceChunkUn `PlayerBlockBreakEvent` are dispatched here exactly as they are by a container, so listeners on the `GlobalEventHandler` keep working. +## Everything the three modules offer + +The five steps above are one path through Falco. This is the rest of it, so that what exists is +visible without reading three wiki pages first. Every snippet here is compiled by +`DocumentationSnippets` in `falco-demo`. + +### falco-anvil — reading and writing Anvil worlds + +The two-argument constructor is the whole of it for most servers. The builder is there when the +defaults do not fit: + +```java +FalcoAnvilLoader loader = FalcoAnvilLoader.builder() + .openRegionLimit(64) // region files kept open at once + .compressionLevel(2) // 1..9, the trade between write time and file size + .saveParallelism(4) // threads a saveChunks call may use + .dataVersion(4189) // what a written chunk claims to be + .diagnostics(new AnvilDiagnostics()) + .exceptionHandler(throwable -> log.warn("chunk load failed", throwable)) + .build(Path.of("worlds", "lobby"), DimensionType.OVERWORLD.key()); +``` + +**`diagnostics()` is the one to know about for a live server.** A world written by a different +version, or by a mod, contains blocks and biomes this loader cannot resolve — it substitutes and +counts rather than failing, and the counters are how you find out: + +```java +AnvilDiagnostics diagnostics = loader.diagnostics(); +diagnostics.reportUnknownBlock("mod:strange_block"); // true the first time, false after +``` + +`regionDirectory()` says which directory was resolved, `legacyLayout()` whether it fell back to the +pre-26.1 layout, and `openRegionCount()` how many files are open right now. `close()` flushes every +one of them and is what `ownsLoader(true)` calls for you. + +### falco-light — block and sky light + +Three entry points, in order of how much they do: + +```java +ChunkLightService lighting = new ChunkLightService(); + +lighting.calculate(chunk); // block light, this chunk +lighting.calculateSky(chunk); // sky light, this chunk +lighting.calculateWithNeighbours(instance, 0, 0); // both, and the ring around it + +int level = lighting.blockLightAt(chunk, 8, 40, 8); // read one position back +``` + +`calculateWithNeighbours` is the one to use when a chunk arrives from disk, because light crosses +chunk borders and a chunk lit alone has a dark seam. + +For a world that keeps itself lit, the scheduler does the bookkeeping. Its builder carries the knobs +that matter under load: + +```java +ChunkLightScheduler scheduler = ChunkLightScheduler.builder(lighting) + .executor(ChunkLightScheduler.defaultExecutor()) + .maxAreaSize(4) // chunks per side of one lighting area + .maxCachedChunks(256) // opacity tables kept between passes + .skyLight(ChunkLightScheduler.SkyLight.FROM_DIMENSION) + .onFailure(throwable -> log.error("lighting failed", throwable)) + .build(); +``` + +Two ways to drive it. On a `FalcoInstance`, `scheduler.supplier()` as in step 5 and nothing else. On +an `InstanceContainer`, hang a `ChunkLightListener` on the chunks and tick it yourself: + +```java +container.setChunkSupplier((instance, x, z) -> { + FalcoChunk chunk = new FalcoChunk(instance, x, z); + chunk.addLifecycleListener(new ChunkLightListener(scheduler)); + return chunk; +}); +MinecraftServer.getSchedulerManager().buildTask(() -> scheduler.onTick(container, System.currentTimeMillis())) + .repeat(TaskSchedule.tick(1)) + .schedule(); +``` + +And when something outside Falco changed the world, tell it: + +```java +scheduler.markChanged(instance, 0, 0); // this chunk needs relighting +scheduler.markChanged(instance, 0, 0, 8, 40, 8); // this position did +scheduler.markDirty(instance, 0, 0); // relight without an incremental path +``` + +### falco-instance — the instance, the chunk, shared views + +The builder is in step 5. Beyond it, the instance exposes its four parts, and the chunk exposes its +storage: + +```java +instance.registry(); // which chunks are loaded, by position +instance.lifecycle(); // loading, publishing, unloading, and the listeners +instance.blockWriter(); // the write path, including placement and destruction + +chunk.storage().views(); // read the sections without materialising them +chunk.storage().materialisedSections(); // how many actually exist +chunk.storage().shared(0); // is section 0 still the shared empty one +``` + +Lifecycle listeners are the extension point that replaced subclassing. Every method has a default: + +```java +instance.lifecycle().addListener(new ChunkLifecycleListener() { + @Override + public void onLoad(ChunkLifecycleEvent event) { + log.info("loaded {} {}", event.chunk().getChunkX(), event.chunk().getChunkZ()); + } +}); +``` + +They run **inside** the transition, before anybody else sees the chunk, which is what the light +engine needs — and why a throw from one fails the chunk load. For ordinary application code the +Minestom events named above are the right tool. + +Generation is the usual Minestom API, with one difference worth knowing: the generator is handed +copies of the section palettes and they are moved over only when it returns, so a generator that +fails halfway leaves the chunk exactly as it was rather than half built and published. + +```java +instance.setGenerator(unit -> unit.modifier().fillHeight(0, 40, Block.STONE)); +``` + ## Shared worlds Shared worlds are the one case `FalcoInstance` cannot serve, because `SharedInstance` takes an diff --git a/falco-demo/src/test/java/net/onelitefeather/falco/demo/DocumentationSnippets.java b/falco-demo/src/test/java/net/onelitefeather/falco/demo/DocumentationSnippets.java index 4a5f1da..d2cae06 100644 --- a/falco-demo/src/test/java/net/onelitefeather/falco/demo/DocumentationSnippets.java +++ b/falco-demo/src/test/java/net/onelitefeather/falco/demo/DocumentationSnippets.java @@ -2,25 +2,31 @@ import net.minestom.server.MinecraftServer; import net.minestom.server.instance.Chunk; +import net.minestom.server.instance.Instance; import net.minestom.server.instance.InstanceContainer; import net.minestom.server.instance.InstanceManager; import net.minestom.server.instance.block.Block; +import net.minestom.server.timer.TaskSchedule; import net.minestom.server.world.DimensionType; +import net.onelitefeather.falco.anvil.AnvilDiagnostics; import net.onelitefeather.falco.anvil.FalcoAnvilLoader; import net.onelitefeather.falco.instance.ChunkLifecycleEvent; import net.onelitefeather.falco.instance.ChunkLifecycleListener; import net.onelitefeather.falco.instance.FalcoChunk; import net.onelitefeather.falco.instance.FalcoInstance; import net.onelitefeather.falco.instance.FalcoSharedInstance; +import net.onelitefeather.falco.light.ChunkLightListener; import net.onelitefeather.falco.light.ChunkLightScheduler; import net.onelitefeather.falco.light.ChunkLightService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.nio.file.Path; import java.util.UUID; /** - * Every code block the documentation shows, compiled: the quick start of the readme and the wiki - * page Instances and chunks. + * Every code block the documentation shows, compiled: the quick start and the feature overview of + * the readme, and the wiki page Instances and chunks. * *

This class is never run and asserts nothing. Its whole value is failing to compile — a snippet * somebody copies is the one piece of documentation that can be wrong in a way no reader forgives, @@ -33,15 +39,33 @@ */ final class DocumentationSnippets { + private static final Logger log = LoggerFactory.getLogger(DocumentationSnippets.class); + private DocumentationSnippets() { } + // ---- readme, quick start ------------------------------------------------------------------ + + /** + * Step 4 — the two operations without a listener around them. + * + * @param instance the instance to read from + * @param lighting the light service + * @return the block light level the snippet prints + */ + static int quickStartCheckWithoutClient(FalcoInstance instance, ChunkLightService lighting) { + Chunk chunk = instance.loadChunk(0, 0).join(); + lighting.calculate(chunk); + + return lighting.blockLightAt(chunk, 8, 40, 8); + } + /** - * Readme, quick start, step 5 — all three modules together. + * Step 5 — all three modules together. * * @return the instance the snippet builds */ - static FalcoInstance readmeAllThreeModules() { + static FalcoInstance quickStartAllThreeModules() { FalcoAnvilLoader loader = new FalcoAnvilLoader(Path.of("worlds", "lobby"), DimensionType.OVERWORLD.key()); ChunkLightScheduler scheduler = new ChunkLightScheduler(new ChunkLightService()); @@ -57,25 +81,163 @@ static FalcoInstance readmeAllThreeModules() { } /** - * Readme, quick start, step 4 — the two operations without a listener around them. + * Shared worlds — a view that does not write its settings into the container. * - * @param instance the instance to read from - * @param lighting the light service - * @return the block light level the snippet prints + * @return the view the snippet builds */ - static int readmeCheckWithoutClient(FalcoInstance instance, ChunkLightService lighting) { - Chunk chunk = instance.loadChunk(0, 0).join(); + static FalcoSharedInstance quickStartSharedWorld() { + InstanceManager manager = MinecraftServer.getInstanceManager(); + InstanceContainer world = manager.createInstanceContainer(); + world.setChunkSupplier(FalcoChunk::new); + + FalcoSharedInstance view = new FalcoSharedInstance(UUID.randomUUID(), world); + manager.registerSharedInstance(view); + return view; + } + + // ---- readme, everything the three modules offer -------------------------------------------- + + /** + * falco-anvil — the builder, for when the two-argument constructor does not fit. + * + * @return the loader the snippet builds + */ + static FalcoAnvilLoader overviewAnvilBuilder() { + return FalcoAnvilLoader.builder() + .openRegionLimit(64) + .compressionLevel(2) + .saveParallelism(4) + .dataVersion(4189) + .diagnostics(new AnvilDiagnostics()) + .exceptionHandler(throwable -> log.warn("chunk load failed", throwable)) + .build(Path.of("worlds", "lobby"), DimensionType.OVERWORLD.key()); + } + + /** + * falco-anvil — the counters that say what a world contained and this loader could not resolve. + * + * @param loader the loader to ask + * @return what the counter answered + */ + static boolean overviewDiagnostics(FalcoAnvilLoader loader) { + AnvilDiagnostics diagnostics = loader.diagnostics(); + return diagnostics.reportUnknownBlock("mod:strange_block"); + } + + /** + * falco-light — the three entry points, in order of how much they do. + * + * @param instance the instance the chunk belongs to + * @param chunk the chunk to light + * @return the block light level the snippet reads back + */ + static int overviewLightEntryPoints(Instance instance, Chunk chunk) { + ChunkLightService lighting = new ChunkLightService(); + lighting.calculate(chunk); + lighting.calculateSky(chunk); + lighting.calculateWithNeighbours(instance, 0, 0); return lighting.blockLightAt(chunk, 8, 40, 8); } - static FalcoInstance shortestForm() { + /** + * falco-light — the scheduler builder and the knobs that matter under load. + * + * @param lighting the light service the scheduler drives + * @return the scheduler the snippet builds + */ + static ChunkLightScheduler overviewSchedulerBuilder(ChunkLightService lighting) { + return ChunkLightScheduler.builder(lighting) + .executor(ChunkLightScheduler.defaultExecutor()) + .maxAreaSize(4) + .maxCachedChunks(256) + .skyLight(ChunkLightScheduler.SkyLight.FROM_DIMENSION) + .onFailure(throwable -> log.error("lighting failed", throwable)) + .build(); + } + + /** + * falco-light — driving the scheduler from a plain container rather than a FalcoInstance. + * + * @param container the container to hang the listener on + * @param scheduler the scheduler to report to + */ + static void overviewSchedulerOnContainer(InstanceContainer container, ChunkLightScheduler scheduler) { + container.setChunkSupplier((instance, x, z) -> { + FalcoChunk chunk = new FalcoChunk(instance, x, z); + chunk.addLifecycleListener(new ChunkLightListener(scheduler)); + return chunk; + }); + MinecraftServer.getSchedulerManager() + .buildTask(() -> scheduler.onTick(container, System.currentTimeMillis())) + .repeat(TaskSchedule.tick(1)) + .schedule(); + } + + /** + * falco-light — telling the scheduler what changed outside Falco. + * + * @param instance the instance the chunk belongs to + * @param scheduler the scheduler to tell + */ + static void overviewMarkChanged(Instance instance, ChunkLightScheduler scheduler) { + scheduler.markChanged(instance, 0, 0); + scheduler.markChanged(instance, 0, 0, 8, 40, 8); + scheduler.markDirty(instance, 0, 0); + } + + /** + * falco-instance — the four parts of an instance, and reading a chunk without materialising it. + * + * @param instance the instance to take apart + * @param chunk the chunk to read + * @return how many sections actually exist + */ + static int overviewInstanceParts(FalcoInstance instance, FalcoChunk chunk) { + instance.registry(); + instance.lifecycle(); + instance.blockWriter(); + + chunk.storage().views(); + chunk.storage().shared(0); + return chunk.storage().materialisedSections(); + } + + /** + * falco-instance — a lifecycle listener and a generator. + * + * @param instance the instance to configure + */ + static void overviewListenerAndGenerator(FalcoInstance instance) { + instance.lifecycle().addListener(new ChunkLifecycleListener() { + @Override + public void onLoad(ChunkLifecycleEvent event) { + log.info("loaded {} {}", event.chunk().getChunkX(), event.chunk().getChunkZ()); + } + }); + + instance.setGenerator(unit -> unit.modifier().fillHeight(0, 40, Block.STONE)); + } + + // ---- wiki, Instances and chunks ------------------------------------------------------------ + + /** + * The shortest form the wiki page shows. + * + * @return the instance the snippet builds + */ + static FalcoInstance wikiShortestForm() { return FalcoInstance.builder(DimensionType.OVERWORLD) .register(MinecraftServer.getInstanceManager()); } - static FalcoInstance withChunkLoader() { + /** + * The chunk loader form, with the three flags that belong together. + * + * @return the instance the snippet builds + */ + static FalcoInstance wikiWithChunkLoader() { FalcoAnvilLoader loader = new FalcoAnvilLoader(Path.of("worlds", "lobby"), DimensionType.OVERWORLD.key()); return FalcoInstance.builder(DimensionType.OVERWORLD) @@ -87,7 +249,12 @@ static FalcoInstance withChunkLoader() { MinecraftServer.getSchedulerManager()); } - static FalcoInstance withLightEngine() { + /** + * The light engine combination. + * + * @return the instance the snippet builds + */ + static FalcoInstance wikiWithLightEngine() { FalcoAnvilLoader loader = new FalcoAnvilLoader(Path.of("worlds", "lobby"), DimensionType.OVERWORLD.key()); ChunkLightScheduler scheduler = new ChunkLightScheduler(new ChunkLightService()); @@ -98,7 +265,13 @@ static FalcoInstance withLightEngine() { .register(MinecraftServer.getInstanceManager()); } - static FalcoInstance withLifecycleConsumers(InstanceManager manager) { + /** + * The two-consumer lifecycle route of the builder. + * + * @param manager the manager to register with + * @return the instance the snippet builds + */ + static FalcoInstance wikiLifecycleConsumers(InstanceManager manager) { return FalcoInstance.builder(DimensionType.OVERWORLD) .chunkLifecycle( chunk -> report("loaded", chunk), @@ -106,7 +279,12 @@ static FalcoInstance withLifecycleConsumers(InstanceManager manager) { .register(manager); } - static void withLifecycleListener(FalcoInstance instance) { + /** + * The full listener, with every method the interface offers. + * + * @param instance the instance to register on + */ + static void wikiFullLifecycleListener(FalcoInstance instance) { instance.lifecycle().addListener(new ChunkLifecycleListener() { @Override public void onPublish(ChunkLifecycleEvent event) { @@ -130,17 +308,13 @@ public void onBlockChange(FalcoChunk chunk, int x, int y, int z, Block block) { }); } - static FalcoSharedInstance sharedWorld() { - InstanceManager manager = MinecraftServer.getInstanceManager(); - InstanceContainer world = manager.createInstanceContainer(); - world.setChunkSupplier(FalcoChunk::new); - - FalcoSharedInstance view = new FalcoSharedInstance(UUID.randomUUID(), world); - manager.registerSharedInstance(view); - return view; - } - - static int readWithoutMaterialising(FalcoChunk chunk) { + /** + * The read-only storage accessors of the table on that page. + * + * @param chunk the chunk to read + * @return how many sections actually exist + */ + static int wikiReadWithoutMaterialising(FalcoChunk chunk) { chunk.storage().view(0); chunk.storage().views(); chunk.storage().shared(0); @@ -148,6 +322,6 @@ static int readWithoutMaterialising(FalcoChunk chunk) { } private static void report(String what, Chunk chunk) { - System.out.println(what + " " + chunk.getChunkX() + " " + chunk.getChunkZ()); + log.info("{} {} {}", what, chunk.getChunkX(), chunk.getChunkZ()); } } diff --git a/falco-demo/src/test/java/net/onelitefeather/falco/demo/DocumentationSnippets.java.new b/falco-demo/src/test/java/net/onelitefeather/falco/demo/DocumentationSnippets.java.new new file mode 100644 index 0000000..e69de29