For the complete documentation index, see llms.txt. This page is also available as Markdown.

Downloading Worlds

Hand players a world as a single-player save over an expiring download link.

/worlds download <world> packs a world into a save a player can drop into their client's saves folder, and sends them a clickable link to fetch it. The feature is off by default — enabling it hands out world data.

Archives are served one of two ways. The built-in server (storage: local) opens a port on the machine and streams the file itself. Pre-signed links (storage: s3) upload the archive to the bucket your backups already use and send the player a signed, expiring URL — no port, no size limit, and the download never touches the game server.


Why an export is needed

Since Paper 26.1 a server world is no longer its own folder. Every world is a dimension of the main level:

worlds/<level-name>/
├── level.dat
└── dimensions/minecraft/
    ├── overworld/
    ├── the_nether/
    ├── the_end/
    └── <your-world>/        <- a BuildSystem world lives here
        ├── region/
        └── data/minecraft/world_gen_settings.dat

Copying that folder out by hand gives you something the client cannot open. It has no level.dat, its chunks sit under the wrong dimension, and its saved data is in the wrong place: Paper keeps a per-world copy of the level-scoped files (world_gen_settings.dat, game_rules.dat, weather.dat, …) inside each dimension folder, while a single-player save keeps one set at the root. A client that finds no generator settings there refuses the world outright.

The export rebuilds all of that. The world becomes the save's overworld, gets a level.dat named after it, and its data is split by scope:

<world>/
├── level.dat                                       from the main level, renamed and scrubbed
├── data/minecraft/                                 level-scoped: world_gen_settings, game_rules, weather, …
└── dimensions/minecraft/overworld/
    ├── region/  entities/  poi/
    └── data/minecraft/                             dimension-scoped: raids, world_border, chunk_tickets

A world still stored in the pre-26.1 flat layout already is a save, so only its level.dat is rewritten - it keeps its own nether and end, and dimensions belonging to other worlds are left behind.


Enabling downloads

Key
Default
Description

enabled

false

Turns downloads on.

storage

local

local for the built-in server, s3 for pre-signed links.

port

8080

Port the built-in server listens on. Must be reachable by players. Ignored on s3.

url

"http://localhost:8080"

Base address players are linked to. Ignored on s3.

behind-proxy

false

Identify clients by X-Forwarded-For. Enable only behind a reverse proxy. Ignored on s3.

expiration-minutes

30

How long a link stays valid. The archive is deleted when it expires.

max-size-mb

2048

Largest single export. local only.

max-storage-mb

8192

Budget shared by all live downloads. local only.

max-concurrent-downloads

3

Transfers served at once. local only.

Set url to whatever players actually reach — a domain, or the address of a reverse proxy. The server speaks plain HTTP, so put it behind a proxy and point url at the HTTPS address if links leave your network. When you do, also set behind-proxy: true, or every request looks like it came from the proxy and link pinning and rate limiting stop telling players apart.

Toggling enabled or storage takes effect on /config reload; no restart needed.

Serving from S3

storage: s3 reads the credentials, bucket and endpoint from the root storage.s3 section — there is nothing to configure twice, and any S3-compatible service works. Backups do not have to be on S3; the two features pick their backend independently. If storage.s3 is incomplete, downloads stay off and the console says which key is missing.

What changes:

  • No port. Nothing has to be reachable from outside, so this works behind NAT or on a host that will not open one.

  • No size limit. Archives upload in parts, so max-size-mb and max-storage-mb do not apply and a world of any size can be downloaded.

  • Your uplink carries the world once, on upload, instead of once per player fetching it.

  • The link is https, signed by S3 and valid for expiration-minutes.

Uploads live under the downloads/ prefix and are deleted when their link expires. Anything left by a crash is cleared at the next startup, so the bucket does not accumulate archives.

You pay S3 for the storage and for egress on every download. A 2 GB world fetched ten times is 20 GB of transfer.


Downloading a world

  • Command: /worlds download <world>

  • Permission: buildsystem.download (OP, supports .self / .other)

The world is saved first - waiting for its chunks to reach disk, so the archive never catches a region file mid-write - then zipped in the background. An animated progress bar runs in the action bar while it packs, showing real progress - bytes packed against the world's total size - with a sweeping highlight and spinner so a pause on one large region file still reads as working. On s3 the bar then switches to the upload and tracks that the same way, so it never sits full while work is still running. When it is ready the player gets a clickable message; the link opens in their browser. Only one export per player runs at a time.

Restyle the bar through worlds_download_progress and worlds_download_uploading in messages.yml, which take %world%, %bar%, %percent% and %spinner%. The finished message is worlds_download_finished; its %button% placeholder marks where the clickable link goes, and only that segment carries the click and hover - its text is worlds_download_button.

Extract the archive into .minecraft/saves/ and the world appears in the single-player list.

A dimension world has no level.dat of its own, so its export borrows the main level's - gamemode and difficulty start out matching the server's main level rather than the exported world. A flat-layout world keeps its own settings.


What is exposed

Treat a link as private. Everything else is closed off.

With storage: s3, the link is signed by S3 and dies with its expiry; the pinning, rate limiting and transfer caps below are properties of the built-in server and do not apply. Everything about what an export contains applies either way.

  • Each export is reachable only through a random 256-bit token that forms the entire URL path. Requests cannot name a file, so nothing outside plugins/BuildSystem/downloads is reachable.

  • Archives and their links are deleted together: on expiry, on reload, and on shutdown. Anything left behind by a crash is wiped at startup, since its link did not survive the restart.

  • An unknown or expired link returns a bare 404.

  • A link is pinned to the first client that uses it. Forwarding it to someone else does not work, while the player who asked for it can still retry or resume.

  • Requests are rate limited to 30 per minute per address, and concurrent transfers are capped, so one host cannot saturate the endpoint. A request over the transfer cap gets 503 with Retry-After.

  • Exports never include player data (players/, playerdata/, stats/, advancements/), so downloading the main world does not hand out the whole server's inventories. The exported level.dat also drops the server's brand, version and server-side datapacks.

Last updated