Skip to content

Instances And Chunks

TheMeinerLP edited this page Aug 3, 2026 · 2 revisions

Instances and chunks

falco-instance is the third module. It provides an Instance that unloads its chunks when it is unregistered, a Chunk that allocates its sections when something writes into them, and a SharedInstance that keeps its settings to itself instead of writing them into the container every other view is looking at.

This page is how to use it. Why it exists — the chunk leak that is the whole argument for it, and what it deliberately refuses to do — is Rationale: Instances and Chunks, and it is worth reading before depending on the module.

Contents

When this is worth using

Take it if any of these apply:

  • You unregister worlds while the server runs. InstanceManager#unregisterInstance unloads chunks only for an InstanceContainer; every chunk, tick partition and entity of any other instance is left behind. This is the reason the module exists.
  • You want the light engine to keep a chunk up to date without a listener of your own. ChunkLightScheduler#supplier() produces FalcoChunks, and since 1.0.0 those carry Falco's light and Falco's lifecycle on one class. Before 1.0.0 this combination did not work at all.
  • Empty sections cost you. A chunk that allocates its sections on demand retains 25 objects and 840 bytes where Minestom's retains 192 and 6 848, and on a generated overworld 62.24 % of sections hold nothing. The tables and what they do not say are in Counted.

Keep using InstanceContainer if you need SharedInstance views onto the world you are building — see Shared worlds for what is and is not possible — or if you are relying on any of the four Minestom sites that branch on instanceof InstanceContainer.

No speed claim. Nothing here is faster, no benchmark in this repository says it is, and the memory figures above are counts rather than timings. The distinction is spelled out in No speed gain is claimed.

Usage

The shortest form

FalcoInstance instance = FalcoInstance.builder(DimensionType.OVERWORLD)
        .register(MinecraftServer.getInstanceManager());

register builds the instance and registers it, in that order, and returns it. The builder is immutable — every method returns a new one — so a half-configured builder can be handed around without anybody changing it underneath.

The constructors are public as well if a builder is not what you want: new FalcoInstance(UUID.randomUUID(), DimensionType.OVERWORLD).

With a chunk loader

FalcoAnvilLoader loader = new FalcoAnvilLoader(Path.of("worlds", "lobby"), DimensionType.OVERWORLD.key());

FalcoInstance instance = FalcoInstance.builder(DimensionType.OVERWORLD)
        .chunkLoader(loader)
        .autoChunkLoad(true)
        .ownsLoader(true)
        .saveOnShutdown(true)
        .registerAndShutdownWith(MinecraftServer.getInstanceManager(),
                MinecraftServer.getSchedulerManager());

registerAndShutdownWith is the form worth preferring when the instance owns its loader. ownsLoader(true) means the instance closes the loader on shutdown, saveOnShutdown(true) means it writes its chunks first, and the shutdown task is registered for you — the three together are the part that is easy to get wrong by hand, because a loader closed before the save loses the save.

If you register by hand instead, shutdown(InstanceManager) does the same work and unregister(InstanceManager) unregisters without saving.

With the light engine

This is the combination that did not work before 1.0.0:

// The scheduler takes the light service, not the instance — it binds to an instance later,
// through the chunks its supplier produces.
ChunkLightScheduler scheduler = new ChunkLightScheduler(new ChunkLightService());

FalcoInstance instance = FalcoInstance.builder(DimensionType.OVERWORLD)
        .chunkLoader(loader)
        .chunkSupplier(scheduler.supplier())
        .autoChunkLoad(true)
        .register(MinecraftServer.getInstanceManager());

The supplier produces FalcoLightingChunks, which extend FalcoChunk. Each one reports its block changes, loads and ticks to the scheduler, and the scheduler lights the touched regions and sends them. You write no listener.

falco-light declares falco-instance as compileOnly, so adding the light module does not bring this one with it — declare both, or use the BOM. See Installation.

Watching the lifecycle

Minestom's own events still fire, and for most application code they are the right thing. FalcoInstance dispatches InstanceChunkLoadEvent and InstanceChunkUnloadEvent, and the block writer dispatches PlayerBlockBreakEvent, so a listener registered on the GlobalEventHandler works here exactly as it does on an InstanceContainer. Take those unless you have a reason not to.

ChunkLifecycleListener is the other thing, and the difference is when, not what:

ChunkLifecycleListener InstanceChunkLoadEvent
when during the transition, inside the lock after it, once the chunk is published and the future completed
may call back into the instance mostly no — stated per method yes
a throw fails the chunk load and reaches the caller waiting on it does not
per block written onBlockChange, no allocation no equivalent

Take the listener when you must act before anybody else sees the chunk, or when you need per-block notification — which is what the light engine uses it for. Take the events for anything else.

Two ways into the listener, depending on how much you need.

The builder takes two consumers, which is enough for most things:

FalcoInstance.builder(DimensionType.OVERWORLD)
        .chunkLifecycle(
                chunk -> log.info("loaded {} {}", chunk.getChunkX(), chunk.getChunkZ()),
                chunk -> log.info("unloaded {} {}", chunk.getChunkX(), chunk.getChunkZ()))
        .register(manager);

For the full set, implement ChunkLifecycleListener and register it on the lifecycle. Every method has a default, so implement only what you need:

instance.lifecycle().addListener(new ChunkLifecycleListener() {
    @Override
    public void onPublish(ChunkLifecycleEvent event) { }   // visible to the world, before onLoad

    @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) { }
});

Two things to know before you write one:

  • onBlockChange runs inside the chunk's write lock. Writing another block from it re-enters that lock, and calling into code that takes a different lock is how a deadlock is built. Keep it to recording the position.
  • A listener that throws fails the load rather than being swallowed, and the caller waiting on the chunk sees it. That is deliberate: a chunk whose listener failed is not a chunk anybody should be handed.

Shared worlds

A FalcoInstance cannot back a shared instance, and this has not changed: SharedInstance takes an InstanceContainer and nothing else, so the attempt does not compile. What FalcoSharedInstance does is fix a container-backed view:

InstanceManager manager = MinecraftServer.getInstanceManager();
InstanceContainer world = manager.createInstanceContainer();
world.setChunkSupplier(FalcoChunk::new);

// Not manager.createSharedInstance(world): that factory always builds Minestom's own type.
FalcoSharedInstance view = new FalcoSharedInstance(UUID.randomUUID(), world);
manager.registerSharedInstance(view);

The order matters. createInstanceContainer registers the container, and the view's constructor refuses one that is not registered — registerSharedInstance performs no such check, and an unregistered container is ticked by nobody.

The view keeps its own generator, chunk supplier and auto-load setting, where Minestom's writes all three through to the container and lets one view reconfigure another. saveInstance() writes the view's own tags rather than the container's.

What it does not change is who owns the blocks: setBlock reaches the container, which serialises every write on its own monitor. A world that needs Falco's write path uses FalcoInstance and gives up sharing.

What the chunk does differently

FalcoChunk extends Chunk and holds a BlockStorage. Sections are created on the first write into them; every section that holds nothing shares one instance.

That has one consequence you can trip over. getSections() and getSection(int) materialise every section they hand back, because Minestom's contract lets a caller write into the result. On a lazy chunk that turns a read into 24 allocations. Heightmap#getHeight does the same for the sections it descends through.

If you only want to look, BlockStorage has read-only counterparts:

To read Use Not
one section view(int) section(int)
all sections views() sections()
how many exist materialisedSections()
whether one is still the shared empty shared(int)

Reaching the storage of a chunk is chunk.storage(). The counts per operation — a setBlock at y = 64 materialises ten sections, all of them the heightmap descent rather than the write — are in Counted.

Limits

  • No SharedInstance onto a FalcoInstance. Structural, see above.
  • Foreign chunk types are refused. A ChunkSupplier producing anything but a FalcoChunk is rejected with a message naming the cause, because such a chunk would be accepted everywhere except the unload path and would then report itself as loaded forever.
  • FalcoChunk#tick walks its tickable map without a lock. A concurrent setBlock that rehashes the map while the tick thread walks it can yield garbage or spin. Inherited rather than introduced — DynamicChunk has the identical race — and Minestom's Chunk#tick contract says the method "doesn't necessary have to be thread-safe". Open, and listed as such in Project Status.
  • No throughput measurement exists. Benchmark classes for the instance and the chunk are in falco-benchmarks, but none has been run as a baseline, so there is no timing to quote.

Sources

Falco


Start here

The measured record

Why it is built this way

Working on the build

Clone this wiki locally