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 opens a port on the server that serves world data.


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

Runs the download server.

port

8080

Port the server listens on. Must be reachable by players.

url

"http://localhost:8080"

Base address players are linked to.

expiration-minutes

30

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

max-size-mb

2048

Largest single export.

max-storage-mb

8192

Budget shared by all live downloads.

max-concurrent-downloads

3

Transfers served at once.

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.

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


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. 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 in messages.yml, which takes %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:

  • 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