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
216 changes: 199 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -37,8 +38,12 @@ repositories {
}

dependencies {
implementation("net.onelitefeather:falco-anvil:0.3.0")
implementation("net.onelitefeather:falco-light:0.3.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.
Expand All @@ -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
Expand Down Expand Up @@ -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:<version>")` 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

Expand Down Expand Up @@ -135,6 +134,181 @@ 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.

## 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
Expand Down Expand Up @@ -177,8 +351,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
Expand Down
Loading
Loading