feat: download worlds as single-player saves - #520
Merged
Conversation
A textured skull set as a world icon lost its texture: only the material was stored. Read the profile off the held item when it is a head, gated on the profile being ready so a name-only head never blocks the main thread on a Mojang lookup. A blank head clears the texture, so the icon always matches the item that was held.
An offline-mode UUID, a renamed account or a world name that is not a player is a routine miss: the slot keeps its placeholder head. Log those at FINE without a stack trace, and keep the warning for unexpected failures.
Since Paper 26.1 a world is a dimension of the main level and has no level.dat of its own, so its folder is not a save a client can open. /worlds download rebuilds the missing layout - the world becomes the save's overworld and gets a level.dat named after it - and serves the archive over a built-in HTTP server. Disabled by default. An archive is reachable only through a random 256-bit token that forms the whole URL path, so a request can neither name a file nor escape the download folder, and archives are deleted with their token on expiry, reload and shutdown.
Adds the limits the first cut left out: a link is pinned to the client that uses it first, so a forwarded link is useless; requests are rate limited per address and concurrent transfers capped, so one host cannot saturate the endpoint; exports are bounded by a per-archive size and a shared storage budget, so a large world cannot fill the disk. Exports also stop carrying data that is not theirs. The main level holds every player's inventory, position and statistics - none of it ships now - and the exported level.dat is rewritten through the NBT library the plugin already depends on to drop the server's brand, version and server-side datapacks. level.dat_old is excluded for the same reason: it is an untouched copy of what the rewrite removes. TLS stays out of the plugin. A keystore would need managing and reloading on every certificate renewal; a proxy in front of the port does it better, so the server logs a warning while its URL is not https.
Bukkit#getWorldFolder for the main level is not always the level root: on some setups it is <level>/dimensions/minecraft/overworld, and the export then looked for level.dat inside it and failed. Search upwards from the reported folder instead, far enough to cover the dimension shape and no further.
The export walks the world twice - once to size it, once to pack it - so the action bar can show real progress rather than an indeterminate spinner. A highlight sweeps through the filled part of the bar and a spinner turns beside it, both driven by the redraw counter, so an export sitting on one large region file still reads as working. The spinner is ASCII: the client's default font renders it everywhere, which is not true of the braille glyphs usually used for this. The whole line is a message key, so servers can restyle or shorten it.
The whole finished line carried the click and hover, so clicking anywhere on it opened the browser. The message now marks the button with %button% and only that component gets the events; a message customized without the placeholder still gets the button appended rather than losing its link.
Two display bugs from the last two commits. A messages.yml written before %button% existed keeps its old worlds_download_finished line, because loading only fills in missing keys. Appending the button to such a message printed the words a second time; the whole line becomes clickable instead, as it was before. The spinner also shifted the message. The default font is variable-width and the action bar is centered, so the two-pixel | moved everything sideways on every fourth frame. The rotation now uses only six-pixel frames, and the percentage is padded to a fixed width for the same reason.
An exported world would not load: the client reported datapack errors, then invalid save data in Safe Mode. Paper keeps a per-world copy of the level-scoped files inside each dimension folder, because every Bukkit world is a level of its own. A single-player save keeps one set in the save root's data/minecraft and only raids, world_border and chunk_tickets inside the dimension. The export copied the dimension folder wholesale, so world_gen_settings.dat never reached the root and the client could not build the level. Files are now routed by scope. Verified against the layout documented on the wiki for 26.1+, and against a real dimension world from a 26.2 server.
World#save returns while Paper's chunk writer is still running, so both a backup and a download could archive half-written region files - the bug reported as chunks missing from a downloaded world. Paper's save(boolean flush) waits for the writer. It is not in the Spigot API this plugin compiles against, so it is resolved reflectively and falls back to the plain save, which is already synchronous on Spigot. No Paper class is referenced at compile time.
The previous commit removed the import along with the World::save reference, but three other call sites still use the type, so the module did not compile.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds
/worlds download <world>, which packs a world into a single-player save and hands the player an expiring link to fetch it. Since Paper 26.1 a world is a dimension of the main level, so its folder on its own is not something a client can open — the export rebuilds the save layout around it.Two unrelated fixes ride along, both found while working here:
/worlds setItemnow keeps a held skull's texture, and head profiles Mojang does not know (offline-mode UUIDs) no longer log a warning per menu item.The export
A dimension world has no
level.dat, its chunks sit under the wrong dimension, and Paper keeps a per-world copy of the level-scoped data inside the dimension folder while a save keeps one set at the root. The export splits it by scope, following the layout documented for 26.1+:A world still in the pre-26.1 flat layout is already a save: only its
level.datis rewritten, and dimensions belonging to other worlds are dropped.The world is saved with the chunk writer flushed first.
World#save()returns while Paper is still writing region files, so both this and existing backups could archive half-written chunks —BackupProfileImplhad the same bug and is fixed too. Paper'ssave(boolean flush)is resolved reflectively, since this module compiles against spigot-api; Spigot writes chunks synchronously, so its fallback is already flushed.Security
Off by default: enabling it opens a port that serves world data.
plugins/BuildSystem/downloadsis reachable.429), capped concurrent transfers (503+Retry-After), per-archive size limit and a shared storage budget.players/,playerdata/,stats/,advancements/), so downloading the main world does not hand out the server's inventories.level.datalso loses the server brand, version and server-side datapacks.urlpoints at a reverse proxy instead, and startup warns while it is nothttps://.Config
buildsystem.download, default OP, supports.self/.other. New keys are added to an existingconfig.ymlautomatically; no migration needed.Tests
WorldExporterTestcovers both layouts, the scope split, thelevel.datrename, progress reporting and the progress bar;DownloadRegistryTestcovers link pinning, expiry, purging and the rate limiter. Exports were also verified against a real 26.2 server world: layout, no player data, no foreign dimensions,DataPacks.Enabled: ["vanilla"].Not verified
Loading an exported save in a 26.2 client. The layout matches the documented format and a real server world, but no client on this machine could open it. Worth one manual check before merging.
Docs: thomasmny/buildsystem-docs@v4.