EternalTD Levels & Maps
A "level" in EternalTD is a YAML config in plugins/EternalTD/levels/ paired with a template world folder in plugins/EternalTD/worlds/. When a player joins, EternalTD clones the template world into the server world container and runs the session in that cloned copy.
Level Config Fields
| Field | Type | Default | Notes |
|---|---|---|---|
isEnabled | bool | true | Disabled levels are skipped during load |
levelName | string | null | Display name shown in messages, scoreboards, and NPC menus |
levelDescription | string list | [] | Lines shown in NPC menus. Supports $highscoreWave and $highscorePlayer placeholders |
worldName | string | null | The template folder name under plugins/EternalTD/worlds/ |
startLocation | string list | null | List of serialized locations where mobs spawn |
endLocation | string list | null | List of serialized locations the mobs walk to (the "red" tiles) |
levelLocations | string list | null | Every walkable grid square in the level. Packages provide it; for a custom map, /etd register <level> writes it from a floor selection, or you can write it by hand |
wavesConfigFile | string | required | Filename of the linked wave config (waves/<name>.yml) |
highscoreWave | int | 0 | Best wave reached on this level |
highscorePlayerName | string | "No one" | Display name of the player who set the score |
environment | enum | NORMAL | World environment used when the cloned world is loaded |
The grid size used throughout is 3 blocks per logical square (constant GRID_SIZE in code).
waveCount is unused. Its presence in an older file does not set the number of waves; EternalTD uses the linked wave configuration.
Location String Format
Locations in EternalTD are serialized as comma-separated strings of the form:
worldName,x,y,z,pitch,yaw
Four-part worldName,x,y,z strings are also accepted with zero pitch and yaw, and five-part x,y,z,pitch,yaw strings with no world. For a session's grid, start and end entries, EternalTD replaces the named world with the instance world. Hub and NPC locations instead need their named world loaded.
Downloaded packages normally provide these values. For a custom map, levelLocations holds one integer centre-block coordinate per playable 3×3 floor square, spaced three blocks apart. The loader adds 0.5 to X and Z for the in-world centre. /etd register <level> writes this list from a floor selection (see Map Authoring Workflow); you can also write it by hand.
World Lifecycle
When a player joins a level:
- EternalTD looks up
worldNameamong direct child folders ofplugins/EternalTD/worlds/. Folder names containing any.are excluded. It rejects missing templates, unavailable wave configs and unsafe names. It also rejects levels whoselevelLocations,startLocationorendLocationis missing, empty or contains an entry that does not parse: "This level has missing or invalid floor, entrance, or exit coordinates. Nothing was started." - It rejects a second world-preparation request while the player's first request is pending, and rejects players who are already in a match, including another Nightbreak plugin's match ("You are already in a match!"). The pending request reserves the player on the shared match system, so other plugins' matches refuse them until it finishes. Disconnecting, or 10 minutes without a ready world, cancels it and deletes the partial copy.
- It reserves the next free numeric suffix (
<worldName>_0,<worldName>_1, ...). Reservation is synchronized and remembers names that are reserved but not yet on disk, so two players joining at once cannot collide on the same instance name. - The template is copied into the server world container off the main thread. World loading and session initialization still run on the main thread.
- The cloned world is loaded as a temporary void world through MagmaCore's
TemporaryWorldManager, so Paper 26.1+ migration is quarantined and missing chunks come back as void rather than freshly generated terrain. - EternalTD validates the grid (see Session Start Validation) and
InstanceProtectorapplies EternalTD's protection rules. The match system then admits the player: it saves their recovery record, teleports them to the new world's spawn point on the next tick and hands out the match items. If another plugin refuses that teleport, the player leaves the match and the run ends. Wait for "Created your world! We're ready to go." before starting. The earlier "Joined level!" message alone does not confirm a successful session.
Template Requirements
EternalTD validates the root before creating a clone and checks each descendant while copying. It rolls back a partial clone if a descendant is invalid. A template is rejected if:
- it is not a directory, or it is a symbolic link
- it has no
level.datat its root - it contains a symbolic link anywhere inside it
- it contains a file named
.eternaltd-instance(that name is reserved, see below)
A failed copy triggers rollback and a failure message. Check the console if cleanup itself fails; the message does not prove that every file was removed.
Instance Ownership Markers
Every clone gets a .eternaltd-instance file recording its template and instance names. EternalTD checks this marker before deleting a stale instance and before deleting a copied world that failed validation. A partial copy is removed as soon as copying fails, and the world of a created session is deleted by the match system when the session ends (see below).
Stale instances left over from a crash are cleaned up when levels load, and even then only for folders that carry a valid marker and are not currently loaded.
Session Start Validation
A session only becomes playable if all of the following hold:
levelLocations,startLocation, andendLocationare all present and non-empty — otherwise "This level doesn't have a complete play area, start, and end configuration!"- the play area itself parses into a valid grid — otherwise "This level's play area is invalid!"
- every start tile has both a land path and an air path to some end tile — otherwise "This level does not have complete land and air paths from every start to an end!"
These checks run after the copied world loads and before the player is admitted, so a failure leaves the player's own state untouched. EternalTD then deletes the temporary world and reports "Failed to start the level. Your player and the temporary world were rolled back." If admission itself fails after the recovery record was saved, the record is restored at once.
When the session ends:
- All remaining towers are torn down (a tower that throws while being removed is logged and skipped rather than stranding the rest).
- The player's recovery record is restored and they are moved back to where they were just before entering the level, or to the main world's spawn if that world is no longer loaded.
- The world's protection rules are removed. Anyone else still in the cloned world, such as staff with
eternaltd.match.bypass, is sent to the hub spawn fromconfig.yml, or to the main world's spawn when no hub is configured. - The chunk tickets taken out over the play area are released (see Path Validation).
- Cleanup requests an unload and deletion at both supported layouts. A refused unload skips deletion; while the plugin is enabled, deletion runs asynchronously 5 seconds after the unload. A world loaded again before cleanup is also skipped, and incomplete deletion is logged.
- Ending is idempotent — a second
end()(for example/etd quitimmediately followed by/etd hub) is a no-op.
Instance Protection Rules
While a level is active, the cloned world has these rules applied:
- Explosions disabled
- Liquid flow disabled
- Elytra disabled
- Fly toggling prevented
- Friendly fire prevented
- Vanilla mob spawning prevented
The match system also guards the world: the player in the run cannot teleport into, out of or within it except through EternalTD's own moves, and other players without eternaltd.match.bypass cannot teleport in. See Match Rules.
Map Authoring Workflow
Use the template as your editable source and write the grid into its level YAML:
- Edit the map in a separate world or in single-player, save it, then place the template under
plugins/EternalTD/worlds/<worldName>/. Editing a match clone will not update this template. - Create its level YAML, link a loaded wave config, and populate
levelLocations,startLocationandendLocation. Use floor-centre entries for the grid and start/end coordinates that resolve to those squares. - If using the in-game helpers in your editing world, run
/etd reloadto load the config, then/etd selectfloor. Left-click sets corner 1 and right-click sets corner 2. Keep both corners in the same world and at the same floor Y; inclusive X/Z lengths must be multiples of 3. - Alternatively,
/etd selectfloorcoordinates <x1> <y1> <z1> <x2> <y2> <z2>sets corners in the current world and subtracts 1 from each supplied Y. Supply floor Y plus one. /etd showselection <level>shows oneEND_RODparticle, visible only to you, above each floor square the selection would register. It saves nothing.- Keep the selection active while running
/etd register <level> startorend. These commands append the player's integer block coordinates and save immediately, without subtracting 1 from Y or checking that the point is on the grid. Initialize both lists before using them. You may instead edit both lists directly in YAML. - After those registrations, run
/etd register <level>. It writeslevelLocationsfrom the selection, replacing the existing list, saves the level file and clears the selection. Turning selection mode off also discards the selection. - Save the edited template, reload and join through the NPC menu or
/etd join <level>. Wait for the final world-ready message, then check the map in play.
The selection checker uses this dimension calculation, truncated to an integer:
size = (abs(corner1 - corner2) + 1) / 3
Corner order does not matter. It checks centre blocks at the lower X/Z corner plus one, then every three blocks, at corner 1's Y in the player's current world. A square is skipped if its centre floor block is passable or the block above it is not passable. This checker does not validate every block in each 3×3 square.
Path Validation
EternalTD caches, for each start tile, the cheapest A* path to any end tile — one land path and one air path. With multiple end tiles it compares the finished paths by cost and keeps the shortest, so a map with several exits routes enemies to the nearest reachable one rather than the first one found.
The land cache is rebuilt when the session starts, whenever a tower is placed, and whenever a tower is sold. The air cache is only computed once, on the first rebuild: air paths ignore towers entirely, so nothing a player builds can invalidate them.
Air enemies use that separate air path, which ignores tower blocking entirely and instead follows the airborne offset (4 blocks above the configured path).
Loading the play area also force-loads every chunk that contains a grid square, so a long run cannot stall on an unloaded chunk. Those chunk tickets are released when the session ends, just before the instance world is deleted.
The cache is also the gate on whether a level is playable at all: if any start tile is missing either its land path or its air path, the session is refused at join time rather than starting a run the enemies cannot finish. The same check runs when a tower is placed, so a tower that would wall off a start tile is rejected and the gold is not spent.
Start/end lookup uses X/Z to resolve a grid square; it does not enforce an exact centre coordinate or compare Y. An entry that resolves to no grid square is skipped with a warning. That warning suggests a centre mismatch, but also check that the intended square exists in levelLocations.
Every resolved start and end tile is also marked non-constructable, so players cannot wall off a spawn or an exit by building on it. Those tiles show up light blue under the placement highlight.
NPCs and Level Menus
NPC configs in plugins/EternalTD/npcs/ link villagers to levels. Right-clicking opens a 9-slot menu with entries in slots 0, 2, 4, 6 and 8. Only the first five configured level IDs are considered; missing IDs leave gaps, and later IDs are skipped with a warning. An NPC with no loaded referenced level is skipped entirely. Left-click a green stained-glass pane to request its level.
| Field | Type | Default | Notes |
|---|---|---|---|
isEnabled | bool | true | Disabled NPCs are skipped |
levelIDs | string list | required | Filenames of the levels this NPC offers |
location | string | null | Spawn location in the standard worldName,x,y,z,pitch,yaw format |
name | string | null for a custom config | NPC display name; premade definitions can supply one |
difficulty | string | null for a custom config | Label above the NPC; set it explicitly |
disguise | string | null | LibsDisguises descriptor (e.g. custom:etd_tutorial_npc) |
customDisguiseData | string | null | Extra LibsDisguises command data — usually the long player-skin string |
NPC spawn is scheduled four seconds after its configuration is constructed and requires the location's world to be loaded. The villager is invulnerable, AI-disabled, persistent and tagged with EternalTD's NPC key. With LibsDisguises enabled and both disguise fields set, EternalTD attempts the disguise; an invalid descriptor can leave a plain villager.
A floating armor stand with the difficulty label is spawned 2.3 blocks above the NPC.
Spawn Behavior
DefaultConfig controls how players are managed in the hub world:
setupDone— flag tracking whether first-time setup guidance has been completed (defaultfalse).spawnLocations— defaults toetd_spawn,0,65,0,0,0and is always written to the config. It is only resolved and used when theetd_spawnworld exists.manageSpawn— defaults totrue. When enabled and the destination is loaded, EternalTD requests a spawn teleport 1 tick after login.playerGuide— the in-game guide book text.nightbreak.autoDownloadPluginUpdates— shared MagmaCore setting (defaultfalse). When enabled, plugin and content updates are downloaded automatically on startup.nightbreak.applyPluginUpdatesWithoutRestart(shared MagmaCore setting, defaultnever; alsowhen-emptyorimmediately) controls when a downloaded plugin update is applied without a restart. See Setup.
When initialization finishes, EternalTD also requests that teleport for players already online, unless they are already in the destination world. manageSpawn and a loaded destination are required for this sweep too. These login/sweep paths do not check the teleport result, so another plugin can prevent arrival.