EternalTD Custom Content Packaging
EternalTD loads custom content from the folders below. ZIP imports use MagmaCore's shared importer; package metadata controls how content appears in /etd setup.
Import Root
Manual imports still go through:
plugins/EternalTD/imports/
After importing content manually, run:
/etd reload
Canonical Content Folders
The current plugin scans these lowercase folders:
plugins/EternalTD/levels/
plugins/EternalTD/waves/
plugins/EternalTD/npcs/
plugins/EternalTD/worlds/
Legacy note:
Levels/andNPCs/are legacy folder names.- EternalTD now migrates them to
levels/andnpcs/during startup and reload.
World Packaging Rules
EternalTD treats the worlds under:
plugins/EternalTD/worlds/
as world templates.
When a player starts a level, EternalTD clones the matching template world into the server world container and runs the session in that cloned copy.
The template root is validated before copying; descendants are checked during the copy. A failure rolls back the partial clone. The copy is refused if any of these hold:
- the template has no
level.datat its root - the template folder is a symbolic link, or contains a symbolic link anywhere inside it
- the template contains a file named
.eternaltd-instance
That last name is reserved: EternalTD writes it into runtime clones as an ownership marker and checks instance identity and ownership before cleanup. Never ship one inside a packaged template.
Practical implication:
- package template worlds under
plugins/EternalTD/worlds/ - do not treat the runtime cloned worlds as the source of truth for packaged content
- templates must be direct children of
worlds/; names containing any.are excluded from discovery - world folder names must also exclude path separators,
:and control characters; match the level'sworldNameexactly
The official hub world is a special case and is tracked in the server world container directly as:
etd_spawn
What Makes a Package Toggleable
Package toggling in /etd setup only works for packages that expose tracked config files.
The current tracked asset fields are:
levelFileswaveFilesnpcFilesworldFoldersworldContainerFoldersmarkerFileName
In practice:
- level, wave, and NPC packages can be enabled or disabled by writing
isEnabledinside those YAML files - tracked file names are matched recursively, so
levels/,waves/, andnpcs/subfolders are fine — only the filename has to match - shared world-container content and marker-only resource packages are not toggleable in the same way
Package Metadata Fields
The current package config field model supports:
isEnablednamedescriptiondownloadLinkversionnightbreakSluglevelFileswaveFilesnpcFilesworldFoldersworldContainerFoldersmarkerFileNamecategorytoggleable
To show custom content in the setup UI, place its metadata YAML directly in plugins/EternalTD/content_packages/, or ship it in the ZIP's content_packages/ folder (see ZIP Layout). Track the actual filenames and world names, and provide at least one tracked asset. An empty asset list never counts as downloaded. The package's isEnabled flag alone does not disable its levels: toggles write the flag into each tracked level, wave and NPC file, save the package flag, then restart EternalTD.
There is one more tracked field, markerFallbackFolders, which lists folders under plugins/ whose non-empty presence also counts as the package being installed. It exists for content that can reach the server without the download flow writing a marker. It is defined in code only — writing it into a package YAML has no effect.
Model-Packaging Notes
The current EternalTD Custom Models package is tracked with a marker file rather than normal EternalTD config files.
That means:
- EternalTD can track whether the shared model pack was downloaded
- the actual model assets live in the FreeMinecraftModels side of the setup when FMM is installed
- cleanup is manual and followed by
/etd reload
ZIP Layout
Place recognized folders directly at the archive root, without a wrapper directory. A minimal layout is:
pack.meta
levels/my_level.yml
waves/my_waves.yml
npcs/my_npc.yml
worlds/my_world/level.dat
worlds/my_world/region/...
The plain text in pack.meta is eternaltd. A ZIP without it uses the importing plugin's platform; an extracted directory is only processed when it contains pack.meta. Put .zip files in the imports folder, then reload.
| Archive root entry | Destination |
|---|---|
levels, waves, npcs, worlds | Matching folders under plugins/EternalTD/ |
content_packages | plugins/EternalTD/content_packages/; package metadata that lists the pack in /etd setup |
content_markers | plugins/EternalTD/content_markers/; install markers named <markerFileName>.marker |
worldcontainer | Server world container; used for shared worlds such as etd_spawn |
models, modelengine | FMM's model folder when FMM is enabled; otherwise ModelEngine's blueprints if enabled; otherwise FMM's model folder for later use |
scripts | plugins/FreeMinecraftModels/scripts/ |
freeminecraftmodels, elitemobs, betterstructures | That plugin's data folder (plugins/FreeMinecraftModels/, plugins/EliteMobs/, plugins/BetterStructures/); lay the contents out like that folder |
FMM's model destination uses an existing legacy Models folder, otherwise models; a freeminecraftmodels/models folder in the archive is merged into the same folder. Any import into FMM's folders triggers FMM's imported-content reload when FMM is enabled. Content placed in BetterStructures' folder triggers a BetterStructures content reload once BetterStructures has finished starting.
The importer runs before EternalTD loads content_packages/, so a package YAML shipped in the ZIP appears in /etd setup after the reload that imports it. A marker shipped in content_markers/ counts as the package's marker asset, so that asset reads as present without going through the setup menu's download flow. The package still needs its other tracked files to count as downloaded.
An unrecognized top-level entry rejects the archive before installation. The importer retains failed ZIPs for retry and attempts to roll back destinations changed by that archive; inspect any rollback-error logs before retrying. Replacing a shared world unloads the old one first: players in it are moved to another loaded world's spawn. If a player cannot be moved or the world refuses to unload, the archive is kept for retry. Successful imports remove the ZIP; this does not establish that the level can be played.