Skip to content

Latest commit

 

History

History
960 lines (767 loc) · 71.3 KB

File metadata and controls

960 lines (767 loc) · 71.3 KB

LDtk Format Reference (for TileMapPlus interop)

A permanent, field-level engineering reference for the LDtk project JSON format, written to ground TileMapPlus's importer/exporter/data-model work in verified ground truth.

Sources cross-checked throughout: the official JSON Schema (docs/JSON_SCHEMA.json, Draft-7, auto-generated from Haxe source), docs/JSON_DOC.md, the ldtk.io docs site, the LDtk CHANGELOG.md, the ldtkimport C++ rules engine (clearest source for auto-rule match semantics), and real sample files: ldtk-haxe-api sample.ldtk (multi-world, Free, externalLevels:true, jsonVersion 1.5.3), its external level sample/0000-West.ldtkl, and WorldMap_GridVania_layout.ldtk (single-world, GridVania, inline levels). Field descriptions are quoted verbatim from the schema/doc unless marked "(observed)".

Format version context: captured against LDtk 1.5.x. The tileset/entity/enum models have been stable since ~1.0. Version-gated specifics are flagged inline.


Table of Contents

  1. Format conventions (read first)
  2. The two identifier systems: uid vs iid
  3. Project root fields
  4. Worlds, levels, external levels, neighbours, backgrounds
  5. Layer instances and tile data
  6. IntGrid and AutoLayer rules
  7. Tilesets, tile coordinates, and custom data
  8. Entities, field instances, and enums
  9. Shared structs
  10. Versioning and migration semantics
  11. Consolidated round-trip checklist: preserve vs recompute
  12. Gotchas

1. Format conventions (read first)

These conventions apply to every section below.

  • __-prefixed fields are derived/denormalized. The doc preamble: "Important data from definitions is often duplicated in fields prefixed with a double underscore (eg. __identifier or __type)" — "to make the JSON parsing much easier." They are convenience copies, recomputable from defs + the instance. On import: read them, but the authoritative source is the definition + non-__ fields. On export: re-emit them (loose loaders read them) and keep them internally consistent (e.g. intGridCsv.length == __cWid * __cHei, or LDtk rejects the file).
  • "Optional"/nullable fields are typed T | null (oneOf [T, null] in the schema). The key is always present; the value may be null. Distinguish null from [] — they mean different things (see §5, external levels).
  • iid / *Iid are stable UUID-style strings (e.g. "60018d46-66b0-11ec-a36b-a9ab7da03bee"). uid is an integer. See §2.
  • Empty-array sentinels: gridTiles, autoLayerTiles, intGridCsv, optionalRules, entityInstances, fieldInstances, tags, customData, enumTags, savedSelections, nineSliceBorders, realEditorValues are always present arrays (possibly []), never null.
  • Value sentinels: intGridCsv uses 0 = empty cell (real values start at 1); tile f=0 = no flip; tile a defaults to 1; maxCount/maxPerLevel of 0 = unlimited; Linear-layout worldX/worldY = -1; IntGrid value groupUid of 0 = "no group".

2. The two identifier systems: uid vs iid

Two distinct identifier systems coexist; conflating them breaks references.

  • uid — integer Unique IDentifier. On definitions (layer defs, entity defs, tileset defs, enum defs, field defs) and on levels (level.uid). User-meaningful, can be reorganized; new ones are drawn from the project-root nextUid. References within defs use uids: defUid, layerDefUid, tilesetUid / tilesetDefUid, levelId.
  • iid — string Instance IDentifier (UUID-style). Auto-generated, immutable. Present on project, world, level, layer instance, entity instance. Purpose: a permanent anchor that survives definition reorganization. Cross-references between instances always use iids.

Reference plumbing (all iid-based):

  • EntityRef field value / EntityReferenceInfos = a 4-part address: entityIid, layerIid, levelIid, worldIid — together uniquely locate an entity anywhere in the project (worldIid is essential in multi-world projects).
  • toc entries carry the same quadruple per instance.
  • level.__neighbours[] references neighbours by levelIid (was levelUid pre-1.0).
  • layerInstance.levelId points to the owning level uid (integer); the layer's own iid is a separate string id.

Round-trip rule: any iid you read must be written back byte-identical. Generating new iids on round-trip silently breaks every EntityRef, every toc entry, and every neighbour link that points at it. uids may be remapped only if you remap every reference consistently — generally not worth it; preserve them too, and keep nextUid >= max(uid) + 1.


3. Project root fields

Scope = per-project unless noted. Types per the Draft-7 schema.

3.1 Identity / version

Field Type Null? Meaning Round-trip
__header__ object no {fileType:"LDtk Project JSON", app:"LDtk", appVersion, url} — informational Cosmetic; recompute or preserve
iid string no Unique project instance id (UUID-style) PRESERVE verbatim
jsonVersion string no File format version, e.g. "1.5.3" Authoritative; set to your exporter's target, or preserve when passing through
appBuildId number no LDtk build that wrote the file PRESERVE verbatim (editor compat heuristics)
nextUid integer no Next integer UID available for new defs/levels Preserve / advance correctly; never reuse below it

3.2 Default authoring settings (per-project)

Field Type Null? Meaning
defaultGridSize integer no Default grid size for new layers (sample: 8)
defaultPivotX / defaultPivotY number (0..1) no Default entity pivot (sample: 0.5 / 1)
defaultEntityWidth / defaultEntityHeight integer no Default new-entity dims
defaultLevelBgColor string #RRGGBB no Used when a level's bgColor is null
defaultLevelWidth / defaultLevelHeight integer yes DEPRECATED at root → moved into each worlds[]. null in multi-world / 1.5.x sample
bgColor string #RRGGBB no Project canvas/background color (sample: "#18131B")

3.3 World / layout (per-project, partly deprecated)

See §4 for full semantics. Note: worldLayout, worldGridWidth, worldGridHeight are deprecated at root (moved to worlds[]) and go null when MultiWorlds is enabled.

Field Type Null? Meaning
worlds array no Multi-world container. Empty [] unless MultiWorlds — but the 1.5.3 sample populates it (single world). Read both shapes
dummyWorldIid string no IID of the internal single "dummy" world used when NOT in multi-worlds mode. Preserve verbatim
worldLayout enum | null yes Free | GridVania | LinearHorizontal | LinearVertical. Deprecated at root
worldGridWidth / worldGridHeight integer | null yes GridVania cell size in px. Deprecated at root
levels array no Levels (root-level, legacy single-world view). In multi-worlds, levels live under worlds[].levels. Order significant only for Linear layouts

3.4 Definitions & TOC

Field Type Null? Meaning
defs object (Definitions) no Holds layers, entities, tilesets, enums, externalEnums, levelFields. The schema/template; levels hold instances of it
toc array no Pre-computed index of all entity instances whose EntityDef has exportToToc. Derived/regenerable. See §10.6 for the instances vs instancesData version split

3.5 Export / file-layout options (per-project)

Field Type Null? Meaning
externalLevels boolean no If TRUE: project file holds defs + level headers; each level body is a separate .ldtkl file (level.externalRelPath); layerInstances becomes null in the project file
simplifiedExport boolean no Also emit a "Super Simple" export (PNGs + CSV + minimal data)
exportTiled boolean no Also emit a Tiled-compatible .tmx
imageExportMode enum no None | OneImagePerLayer | OneImagePerLevel | LayersAndLevels
exportLevelBg boolean no Include level background in exported PNGs
pngFilePattern string | null yes Filename pattern for exported PNGs

3.6 Naming & backups

Field Type Null? Meaning
identifierStyle enum no Capitalize | Uppercase | Lowercase | Free — identifier normalization
levelNamePattern string no Pattern for generated level identifiers (e.g. Level_%idx)
backupOnSave boolean no Write a backup on each save
backupLimit integer no Backups to retain
backupRelPath string | null yes Relative path for backups
minifyJson boolean no If TRUE, exported JSON is partially minified (formatting only; semantics identical)

3.7 Advanced

Field Type Null? Meaning
customCommands array no {command:String, when:"Manual"|"AfterLoad"|"BeforeSave"|"AfterSave"} — shell commands the editor runs. Preserve verbatim
flags array no Advanced toggles: DiscardPreCsvIntGrid, ExportOldTableOfContentData, ExportPreCsvIntGridFormat, IgnoreBackupSuggest, PrependIndexToLevelFileNames, MultiWorlds, UseMultilinesType. Preserve verbatim — they change serialization shape

Verbatim — project root (v1.5.3 sample):

"__header__": { "fileType":"LDtk Project JSON", "app":"LDtk", "appVersion":"1.5.3", "url":"https://ldtk.io" },
"iid":"a8ea85a0-7820-11ed-b6fd-4507f1bd5fbf",
"jsonVersion":"1.5.3", "appBuildId":473702,
"defaultGridSize":8, "defaultPivotX":0.5, "defaultPivotY":1,
"defaultLevelWidth":null, "defaultLevelHeight":null,
"bgColor":"#18131B", "worldLayout":null, "externalLevels":true

4. Worlds, levels, external levels, neighbours, backgrounds

4.1 The two project shapes: single-world vs multi-worlds

LDtk has a hard split an importer/exporter MUST branch on. There is no multiWorlds boolean — detect mode by whether worlds[] is non-empty.

Single-world (legacy / default) — root carries the world:

{
  "worldLayout": "GridVania", "worldGridWidth": 256, "worldGridHeight": 256,
  "externalLevels": false,
  "levels": [ /* all Level objects here */ ],
  "worlds": [], "dummyWorldIid": "..."
}

(verbatim root values from WorldMap_GridVania_layout.ldtk.)

Multi-worlds — root world fields are null, worlds[] populated:

{
  "worldLayout": null, "worldGridWidth": null, "worldGridHeight": null,
  "externalLevels": true, "levels": [],
  "worlds": [
    { "identifier": "SampleWorld", "iid": "6d9adae0-c640-11ed-b633-01b6d7e2af59",
      "worldGridWidth": 256, "worldGridHeight": 256, "worldLayout": "Free",
      "levels": [ /* Level objects here */ ] }
  ]
}

(verbatim from sample.ldtk.)

Schema semantics (verbatim):

  • Root worlds: "This array will be empty, unless you enable the Multi-Worlds in the project advanced settings."
  • Root dummyWorldIid: "If the project isn't in MultiWorlds mode, this is the IID of the internal "dummy" World." In single-world mode LDtk still internally wraps everything in one dummy World; dummyWorldIid is that World's iid.
  • Root worldGridWidth/worldGridHeight/worldLayout/defaultLevelWidth/defaultLevelHeight are documented as "Moves to worlds array in multi-worlds update".

Importer rule: if worlds[] is non-empty, read layout from each World (root layout fields are null, root levels[] may be empty). Else read from root; dummyWorldIid is the implicit world's id. Exporter rule: write back whichever shape you read — don't silently convert single→multi. Multi-world projects cannot be loaded by pre-1.3 importers.

4.2 worldLayout enum and how level positions derive

Enum: Free, GridVania, LinearHorizontal, LinearVertical, null (null only at root in multi-worlds mode).

Layout Position source worldX/worldY Array order Grid snap
Free per-level worldX,worldY (px) authoritative, arbitrary irrelevant none
GridVania per-level worldX,worldY (px) authoritative, snapped to worldGridWidth/Height irrelevant yes
LinearHorizontal array order in levels[] always -1 authoritative n/a
LinearVertical array order in levels[] always -1 authoritative n/a

Verbatim from schema — Level worldX: "World X coordinate in pixels. Only relevant for world layouts where level spatial positioning is manual (ie. GridVania, Free). For Horizontal and Vertical layouts, the value is always -1 here." (same for worldY.)

Critical importer rule: In Linear layouts you CANNOT use worldX/worldY (they are -1). Lay levels out yourself by iterating levels[] in order and stacking by pxWid (Horizontal) or pxHei (Vertical). Real check: a GridVania sample's first level "Entrance" has worldX:0, worldY:0; a Free sample level "West" has worldX:0, worldY:40 — both live coordinates. In multi-worlds, GridVania snapping uses the owning World's worldGridWidth/Height, not root (which is null).

4.3 The Level object — full field map

A Level is per-world (lives in root levels[] or worlds[].levels[]). __-prefixed fields are derived/recomputable; all others authoritative.

Field Type Null? Authoritative vs derived Meaning
identifier string no authoritative User-defined unique id (per world)
uid integer no authoritative Project-unique Int id. Preserve verbatim (referenced by deprecated levelUid)
iid string (UUID) no authoritative Unique instance id. ROUND-TRIP CRITICAL — neighbours/cross-refs use it
worldX integer no authoritative (Free/GridVania) / sentinel -1 (Linear) World X in px. See §4.2
worldY integer no authoritative / sentinel -1 World Y in px
worldDepth integer no authoritative "Index representing level depth in world. Default 0, greater=above, lower=below." Drives </>/o neighbour codes
pxWid integer no authoritative Level width in px
pxHei integer no authoritative Level height in px
useAutoIdentifier boolean no authoritative If true, identifier auto-follows the naming pattern; false once user renamed. Preserve
__bgColor string #hex no derived Effective bg color = bgColor ?? project.defaultLevelBgColor
bgColor string | null yes authoritative Per-level override; null ⇒ inherit project default
__smartColor string #hex no derived Auto-contrasting UI tint; cosmetic
bgRelPath string | null yes authoritative Relative path to a level background IMAGE (distinct from bgColor)
bgPos enum | null yes authoritative Bg image fit: Unscaled, Contain, Cover, CoverDirty, Repeat, null
bgPivotX / bgPivotY number (0–1) no authoritative Bg image pivot
__bgPos object | null yes derived Computed crop/scale geometry for the bg image (see §4.5). null when no bg image
externalRelPath string | null yes authoritative (path) Relative path to the .ldtkl when externalLevels=true; null when inline OR when reading the .ldtkl itself
fieldInstances array no authoritative Level custom field values (often []). Opaque payload — preserve verbatim
layerInstances array | null YES authoritative All layer instances in display order. null when externalLevels=true (data lives in the .ldtkl); [] means a genuinely layer-less level — don't conflate
__neighbours array no derived Adjacency list (see §4.4). Recomputable; empty for Linear layouts

Verbatim — Level (multi-world Free, externalized — layerInstances:null, externalRelPath set):

{
  "identifier": "West", "iid": "60018d40-66b0-11ec-a36b-3b0761cf900c", "uid": 0,
  "worldX": 0, "worldY": 40, "worldDepth": 0, "pxWid": 272, "pxHei": 280,
  "__bgColor": "#121331", "bgColor": null, "useAutoIdentifier": false,
  "bgRelPath": "N2D - SpaceWallpaper1280x448.png", "bgPos": "Cover",
  "bgPivotX": 1, "bgPivotY": 1, "__smartColor": "#7D7D8E",
  "__bgPos": { "topLeftPx": [0,0], "scale": [1.25,1.25], "cropRect": [422.4,0,217.6,224] },
  "externalRelPath": "sample/0000-West.ldtkl",
  "fieldInstances": [], "layerInstances": null,
  "__neighbours": [
    { "levelIid": "60038910-66b0-11ec-a36b-1ff7a8a0ffd4", "dir": "e" },
    { "levelIid": "60047370-66b0-11ec-a36b-2b89739d30ed", "dir": "e" }
  ]
}

Verbatim — Level (single-world GridVania, inline — layerInstances present, no bg image so __bgPos:null):

{
  "identifier": "Entrance", "iid": "a367c3b0-66b0-11ec-9cd7-91690c910c97", "uid": 0,
  "worldX": 0, "worldY": 0, "worldDepth": 0, "pxWid": 512, "pxHei": 256,
  "__bgColor": "#37494E", "bgColor": null, "bgRelPath": null, "bgPos": null,
  "bgPivotX": 0.5, "bgPivotY": 0.5, "__bgPos": null,
  "externalRelPath": null, "fieldInstances": [ /* ... */ ], "layerInstances": [ /* ... */ ]
}

4.4 __neighbours and the dir codes

__neighbours is an array of NeighbourLevel objects. It is derived (LDtk computes adjacency from world positions): an importer can trust it; an exporter can regenerate it. Empty in Linear layouts.

Verbatim: "An array listing all other levels touching this one on the world map. Since 1.4.0, this includes levels that overlap in the same world layer, or in nearby world layers. Only relevant for world layouts where level spatial positioning is manual (ie. GridVania, Free). For Horizontal and Vertical layouts, this array is always empty."

Field Type Null? Notes
levelIid string no iid of the neighbour. Canonical reference (since 1.2.0)
levelUid integer | null yes DEPRECATED since 1.2.0, replaced by levelIid. May be absent in newer files
dir string no Direction code (below)

dir code domain (verbatim): "A lowercase string tipping on the level location (north, south, west, east). Since 1.4.0, this value can also be < (neighbour depth is lower), > (neighbour depth is greater) or o (levels overlap and share the same world depth). Since 1.5.3, this value can also be nw,ne,sw or se for levels only touching corners."

So: n s e w (edges); nw ne sw se (corners, 1.5.0/1.5.3+); < > (depth-differing, 1.4.0+); o (overlap at same depth, 1.4.0+). A single neighbour can appear multiple times with different dir values. Reference by levelIid, not array index.

4.5 Background fields and __bgPos (LevelBgPosInfos)

Two independent background concepts per level:

  1. Background color — bgColor (authoritative override, may be null) → effective __bgColor (derived: bgColor ?? project.defaultLevelBgColor).
  2. Background image — bgRelPath (path), bgPos (fit mode), bgPivotX/Y (0–1). When an image is set, LDtk computes __bgPos (a LevelBgPosInfos object); when no image, __bgPos is null.

LevelBgPosInfos (all derived geometry — recomputable from image + bgPos + pivots + level size):

Field Type Verbatim meaning
topLeftPx [x,y] ints "the [x,y] pixel coordinates of the top-left corner of the cropped background image, depending on bgPos option."
scale [scaleX,scaleY] floats "the [scaleX,scaleY] values of the cropped background image, depending on bgPos option."
cropRect [cropX,cropY,cropW,cropH] floats "the cropped sub-rectangle of the displayed background image. This cropping happens when original is larger than the level bounds."

bgPos enum: Unscaled (1:1, pivot-placed), Contain (fit inside, no crop), Cover (fill, crops overflow), CoverDirty (legacy/non-recomputed cover variant), Repeat (tile), null. For an importer, prefer rendering from bgRelPath + __bgPos directly (crop+scale+offset already baked) rather than re-deriving from bgPos.

4.6 External levels (externalLevels + .ldtkl)

Project flag externalLevels (boolean, root only): "If TRUE, one file will be saved for the project (incl. all its definitions) and one file in a sub-folder for each level."

Mechanics:

  • When externalLevels=true: the main .ldtk keeps every Level's metadata (identifier, iid, worldX/Y, pxWid/Hei, bg fields, fieldInstances, __neighbours) but sets layerInstances: null and fills externalRelPath with the relative path to the level file.
  • A .ldtkl file is a single complete Level object (same schema as an inline Level) with layerInstances populated and its own externalRelPath: null. Confirmed from 0000-West.ldtkl: identifier:"West", iid, worldX:0/worldY:40, pxWid:272/pxHei:280, externalRelPath:null, and a 4-layer layerInstances array (Entities/Custom_tiles/Collisions/Cavern_background).
  • Path resolution: resolve externalRelPath relative to the main .ldtk file's directory (e.g. "sample/0000-West.ldtkl" → <projectDir>/sample/0000-West.ldtkl). Forward slashes; project-relative.
  • Importer rule: if externalLevels=true and level.layerInstances==null, you MUST open the .ldtkl to get layer data. On metadata mismatch, the .ldtkl is authoritative for layer content; positional/world data is mirrored.
  • Exporter rule: keep iid/uid/identifier/worldX-Y/pxWid-Hei identical between the stub in the main file and the .ldtkl. Filename pattern is NNNN-Identifier.ldtkl (4-digit ordinal prefix; controlled by the PrependIndexToLevelFileNames flag), but match by iid/externalRelPath, not filename.

5. Layer instances and tile data

Layer instances appear under levels[].layerInstances[] (embedded) OR are null when external levels are enabled (then they live in the level's .ldtkl).

"Each level always contains a layer instance for each layer definition found in defs." — exactly one instance per definition per level; you cannot have a level missing a layer or having two of the same.

5.1 Layer instance — header fields

Field Type Null? Meaning (verbatim where quoted)
__identifier string no "Layer definition identifier" (copy of the def's identifier)
__type string no "Layer type (possible values: IntGrid, Entities, Tiles or AutoLayer)". Copy of the layer definition's base type — see §5.3
__cWid int no "Grid-based width" (cells across) = level width / gridSize
__cHei int no "Grid-based height" (cells down)
__gridSize int no "Grid size" in px (cell size for this layer)
__opacity number (0–1) no "Layer opacity as Float [0-1]" (copy of def opacity)
__pxTotalOffsetX int no "Total layer X pixel offset, including both instance and definition offsets" = def.pxOffsetX + inst.pxOffsetX. Use this for rendering, not pxOffsetX
__pxTotalOffsetY int no Total Y offset (def + instance)
__tilesetDefUid int yes "The definition UID of corresponding Tileset, if any". Null on plain Entities layers (observed)
__tilesetRelPath string yes "The relative path to corresponding Tileset, if any"
iid string (UUID) no "Unique layer instance identifier". Round-trip critical
levelId int no "Reference to the UID of the level containing this layer instance"
layerDefUid int no "Reference the Layer definition UID". Join key back to defs.layers[]
pxOffsetX int no "X offset in pixels to render this layer, usually 0". Instance-only; def offset is separate (sum is __pxTotalOffsetX)
pxOffsetY int no "Y offset in pixels..."
visible boolean no "Layer instance visibility"
seed int no "Random seed used for Auto-Layers rendering". Round-trip critical — changing it re-rolls rules so tiles shift
optionalRules int[] no "An Array containing the UIDs of optional rules that were enabled in this specific layer instance." Empty [] common
overrideTilesetUid int yes "This layer can use another tileset by overriding the tileset UID here." Null = use def's tileset
intGridCsv int[] no IntGrid values (see §6.2). Empty [] on non-IntGrid layers
intGrid object[] yes (deprecated) Old [{coordId,v}] form; removed in 1.0.0, replaced by intGridCsv. May appear in ancient files only
autoLayerTiles Tile[] no "Tiles generated by Auto-layer rules, sorted in display order". See §5.2
gridTiles Tile[] no Hand-painted tiles (Tiles layers). See §5.2
entityInstances EntityInstance[] no Entities (see §8)

Authoritative vs derived: all __-prefixed fields are convenience duplicates (derivable from defs + the instance). The authoritative instance-owned values are: iid, levelId, layerDefUid, pxOffsetX/Y, seed, visible, optionalRules, overrideTilesetUid, intGridCsv, and the three array payloads.

5.2 The Tile object (shared by gridTiles & autoLayerTiles)

Schema title "Tile instance"; required = ["a","f","px","src","t","d"] — all six are mandatory on every tile.

Field Type Meaning (verbatim) Notes
px int[2] [x,y] "Pixel coordinates of the tile in the layer ([x,y] format). Don't forget optional layer offsets, if they exist!" Top-left in layer space, before __pxTotalOffsetX/Y. Derivable from coordId (in d) and __gridSize
src int[2] [x,y] "Pixel coordinates of the tile in the tileset ([x,y] format)" Top-left of the source cell in the tileset image. Fully derivable from t + tileset def (§7.3)
f int (0–3) "Flip bits, a 2-bits integer... Bit 0 = X flip, Bit 1 = Y flip" f=0 none, 1 X, 2 Y, 3 both. See §5.4
t int "The Tile ID in the corresponding tileset." Grid index into the tileset (§7.3). The authoritative "which tile"
d int[] "Internal data used by the editor. For auto-layer tiles: [ruleId, coordId]. For tile-layer tiles: [coordId]." Editor-private. Length differs by array (2 vs 1). coordId = cellX + cellY*__cWid. Round-trip critical — preserve verbatim
a number (0–1) "Alpha/opacity of the tile (0-1, defaults to 1)" Added in a later version; defaults 1. Multiplies with __opacity

5.3 The four __types and the AutoLayer/IntGrid subtlety (CRITICAL)

__type ∈ IntGrid, Entities, Tiles, AutoLayer, copied from the layer DEFINITION's type, NOT inferred from whether rules exist.

  • Tiles — hand-painted. Populates gridTiles; autoLayerTiles=[]. Has a tileset.
  • AutoLayer — pure auto-layer (rules paint onto a tileset; reads another layer's grid; no own editable IntGrid values). Populates autoLayerTiles; gridTiles=[].
  • IntGrid — integer grid. Populates intGridCsv. If the IntGrid layer ALSO has auto-layer rules + a tileset, it ALSO populates autoLayerTiles while __type stays "IntGrid". This is the trap: you cannot decide which tile array to read from __type alone.
  • Entities — entity placement. All tile/grid arrays [], __tilesetDefUid usually null.

Empirically verified in 0000-West.ldtkl: layers "Collisions" and "Cavern_background" report "__type":"IntGrid" yet carry non-empty autoLayerTiles and empty gridTiles. (autoTilesetDefUid on the def was merged into tilesetDefUid in v1.2.0; before that an IntGrid def with autoTilesetDefUid is the rule-bearing case.)

Which array each type populates:

__type gridTiles autoLayerTiles intGridCsv
Tiles populated [] []
AutoLayer [] populated []
IntGrid (no rules) [] [] populated
IntGrid (+rules+tileset) [] populated populated
Entities [] [] []

Import rule of thumb: render gridTiles then autoLayerTiles regardless of __type. To detect "has auto rules", check autoLayerTiles.length > 0 (or the def's tilesetDefUid + rule groups), not __type.

5.4 Tile display/ordering and flip bits

Ordering: gridTiles are "all the tiles in display order (1st one is beneath 2nd one, etc.)" — array order IS z-order: earlier elements drawn first (underneath), later on top. autoLayerTiles likewise. The same px cell can hold multiple stacked tiles (base + overlay). Preserve array order exactly on round-trip; reordering changes the result. Across layers, the layer-instance array order (top→bottom in the editor) is the inter-layer z-order.

Flip bits (f): 2-bit mask — Bit 0 (value 1) = X flip (horizontal mirror), Bit 1 (value 2) = Y flip (vertical mirror). f=3 (both) == 180° rotation. LDtk has no arbitrary rotation field — only the two mirror axes; you cannot represent 90°/270° natively (for UE import: map to mirrored UVs, not rotation). Flips are about the tile's own center within its __gridSize box; flip X negates the U axis, flip Y the V axis. The flip does not change px.

Verbatim evidence (0000-West.ldtkl): IntGrid layer that nonetheless fills autoLayerTiles (the §5.3 trap; d is [ruleId=36, coordId]):

{ "__identifier":"Collisions", "__type":"IntGrid", "__cWid":34, "__cHei":35,
  "__gridSize":8, "__tilesetDefUid":1,
  "__tilesetRelPath":"Cavernas_by_Adam_Saltsman-Extended.png",
  "iid":"6001b450-66b0-11ec-a36b-51f8aff2e196", "levelId":0, "layerDefUid":2,
  "seed":4084837, "visible":true, "optionalRules":[], "overrideTilesetUid":null,
  "intGridCsv":[1,1,1,1,1,1,1,1,1,1,1,1, ...],
  "gridTiles":[],
  "autoLayerTiles":[
    {"px":[16,0],"src":[0,0],"f":0,"t":0,"d":[36,2],"a":1},
    {"px":[24,0],"src":[0,0],"f":0,"t":0,"d":[36,3],"a":1}
  ] }

Hand-painted Tiles layer (d is [coordId], length 1; second tile shows f:2 Y-flip):

{ "__identifier":"Custom_tiles", "__type":"Tiles",
  "iid":"60018d4a-66b0-11ec-a36b-6969d93d9836", "layerDefUid":34, "seed":1927915,
  "gridTiles":[
    {"px":[224,24],"src":[88,32],"f":0,"t":59,"d":[130],"a":1},
    {"px":[208,32],"src":[0,160],"f":2,"t":240,"d":[162],"a":1}
  ],
  "autoLayerTiles":[] }

Entities layer (all tile/grid arrays empty, no tileset):

{ "__identifier":"Entities", "__type":"Entities", "__tilesetDefUid":null,
  "__tilesetRelPath":null, "iid":"60018d45-66b0-11ec-a36b-2fc9526ec805",
  "layerDefUid":28, "seed":3895744, "visible":true,
  "gridTiles":[], "autoLayerTiles":[] }

6. IntGrid and AutoLayer rules

6.1 Where things live (level of definition)

Concept Lives in Scope
intGridValues, intGridValuesGroups, autoRuleGroups→rules, tilesetDefUid, autoSourceLayerDefUid, layer type defs.layers[] (LayerDef) per-project / per-layer-DEF (shared by all levels)
intGridCsv, autoLayerTiles, gridTiles, seed, optionalRules, overrideTilesetUid layerInstances[] per-layer-INSTANCE (one per level per layer)
A single tile (px/src/f/t/d/a) inside the tile arrays per-tile
A rule autoRuleGroups[].rules[] per-def (NOT per level)

Mental model: rules are project-level templates; intGridCsv is per-level input; autoLayerTiles is the baked per-level OUTPUT of running the rules over the CSV. Editing a rule changes output for every level using that layer.

6.2 IntGrid: intGridCsv and intGridValues

intGridCsv (layer instance, IntGrid/AutoLayer): "A list of all values in the IntGrid layer, stored in CSV format... Order is from left to right, and top to bottom... 0 means "empty cell" and IntGrid values start at 1. The array size is __cWid x __cHei cells."

  • It is a JSON array of ints (despite the name "CSV" — observed as [1,1,1,...], not a string).
  • Cell (cx,cy) index = cy * __cWid + cx; inversely cx = i % __cWid, cy = floor(i / __cWid).
  • 0 is the empty sentinel; real values are 1..N and map to intGridValues[].value.
  • An empty IntGrid layer still serializes the full array of zeros. A pure AutoLayer that sources another layer's grid carries an empty intGridCsv:[].
  • Legacy intGrid (object array) is the deprecated pre-1.0 form; do not emit.

intGridValues (LayerDef) — IntGridValueDefinition. WARNING (verbatim): "the array order is not related to actual IntGrid values!" — key by .value, never by array index.

Field Type Null? Meaning
value int no The IntGrid integer this row describes (>=1)
identifier string yes User name e.g. "walls"
color string #rrggbb no Display/editor color
groupUid int no Parent intGridValuesGroups uid; 0 = no group
tile TilesetRect yes Optional preview tile
"intGridValues": [
 {"value":1,"identifier":"walls","color":"#2A2B61","tile":null,"groupUid":0},
 {"value":2,"identifier":"ladders","color":"#A36433","tile":null,"groupUid":0},
 {"value":3,"identifier":"lava","color":"#FF0000","tile":null,"groupUid":0}
]

intGridValuesGroups (LayerDef): {uid, identifier|null, color|null}. Purely organizational (folders), referenced by IntGridValueDefinition.groupUid. Editor-organizational; round-trip-preserve to keep authoring UX.

6.3 AutoLayer wiring (LayerDef)

Field Type Meaning
type enum An IntGrid def WITH a tilesetDefUid + rules behaves as an auto-layer (its instance gets autoLayerTiles). A pure AutoLayer def has no own IntGrid data and reads another layer's grid
tilesetDefUid int | null Default tileset the rules paint from. Null on plain IntGrid w/o tiles
autoSourceLayerDefUid int | null The key distinguisher. Null → rules run on THIS layer's own intGridCsv. Set → reads the referenced IntGrid layer's CSV as input (this layer's own intGridCsv is empty)
autoRuleGroups AutoLayerRuleGroup[] All rule groups (§6.4)
autoTilesKilledByOtherLayerUid int | null (advanced) lets one layer mask another's auto-tiles. Preserve verbatim

Two ways to get autoLayerTiles: (a) IntGrid layer + tileset + rules (self-source), or (b) dedicated AutoLayer with autoSourceLayerDefUid pointing at an IntGrid layer.

6.4 Rule groups — AutoLayerRuleGroup (LayerDef)

{"uid":3,"name":"Walls","color":null,"icon":null,"active":true,
 "isOptional":false,"usesWizard":false,
 "requiredBiomeValues":[],"biomeRequirementMode":0,"rules":[ ... ]}
Field Type Null? Meaning Class
uid int no Group id functional
name string no Display name functional
active bool no If false, whole group is skipped functional
isOptional bool no If true, group's rules apply only when its uid is in the instance's optionalRules functional
rules AutoRuleDef[] no Ordered list (order = priority, §6.6) functional
color string | null yes Editor folder color editor-only
icon TilesetRect | null yes Editor folder icon editor-only
usesWizard bool no Created via corner/edge wizard editor-only
requiredBiomeValues string[] no Biome gating (v0.9+); group applies only in matching biomes functional
biomeRequirementMode int no 0/1 = how requiredBiomeValues combine (any vs all) functional
collapsed bool (older) Editor UI state editor-only

6.5 Rule definition — AutoRuleDef (the core)

{"uid":15,"active":true,"size":5,
 "tileRectsIds":[[92],[93]],"alpha":1,"chance":1,"breakOnMatch":false,
 "pattern":[0,0,-1,0,0, 0,0,1,0,0, 0,0,-1,0,0, 0,0,0,0,0, 0,0,0,0,0],
 "flipX":false,"flipY":false,
 "xModulo":1,"yModulo":1,"xOffset":0,"yOffset":0,
 "tileXOffset":0,"tileYOffset":0,
 "tileRandomXMin":0,"tileRandomXMax":0,"tileRandomYMin":0,"tileRandomYMax":0,
 "checker":"None","tileMode":"Single","pivotX":0,"pivotY":0,
 "outOfBoundsValue":null,"invalidated":false,
 "perlinActive":false,"perlinSeed":8199321,"perlinScale":0.2,"perlinOctaves":2}
Field Type Null? Meaning Class
uid int no Rule id. PRNG salt; stored in each baked tile's d[0] essential
active bool no If false, produces no tiles essential
size int no Pattern is size*size; typically 1,3,5,7 (engine may use up to 9 internally) essential
pattern int[] no size*size row-major match grid. Encoding in §6.5.1 essential
tileRectsIds int[][] no Candidate tile selections; each inner array is one "stamp" (1 id for Single, many for Stamp). One inner array picked at random per match essential
chance number 0–1 no Probability rule applies in a matched cell (deterministic PRNG). 1 = always essential
breakOnMatch bool no If true and rule matches, no later rule paints that cell essential
flipX / flipY bool no Also try the mirrored pattern; a mirrored match sets the tile's flip bit essential
checker enum None/Horizontal/Vertical no Brick-style offset filter combined with modulo essential
xModulo / yModulo int (>=1) no Rule only runs where (cell - offset) % modulo == 0 (spacing/gaps) essential
xOffset / yOffset int no Phase offset for the modulo filter essential
tileMode enum Single/Stamp no Single = 1 tile; Stamp = a block of tiles anchored by pivot essential
pivotX / pivotY number 0–1 no Stamp anchor within the matched cell (Stamp only) essential (Stamp)
tileXOffset / tileYOffset int no Pixel nudge applied to placed tile(s) essential
tileRandomXMin/Max, tileRandomYMin/Max int no Seeded random pixel jitter range essential (if used)
alpha number 0–1 no Opacity multiplier baked into each tile's a essential
outOfBoundsValue int | null yes IntGrid value assumed for out-of-level cells the pattern samples. null = treat OOB as wildcard pass (engine uses -1 → skip constraint); a number = pretend that value (e.g. 1 to auto-close walls at edges) essential
perlinActive bool no Gate rule by Perlin noise (organic patches) essential (if true)
perlinScale / perlinOctaves number no Noise frequency/scale, octaves essential (if perlin)
perlinSeed number no Noise seed — always present even when perlin off. Round-trip-preserve round-trip
invalidated bool no Editor flag: rule needs re-evaluation. Editor-private editor-only

6.5.1 Pattern encoding (THE critical semantics)

Confirmed by the ldtkimport engine doc comment AND the sample's real distinct values [-1000001,-2,-1,0,1,2,3]:

  • Positive N → cell at that position must equal IntGrid value N.
  • Negative -N → cell must NOT equal IntGrid value N (any other value, including empty, passes).
  • 0 → "don't care".
  • 1000001 (RULE_PATTERN_ANYTHING) → cell must contain any nonzero IntGrid value ("anything but empty").
  • -1000001 (RULE_PATTERN_NOTHING) → cell must be empty (==0).

Engine source (verbatim, ldtkimport Rule.h): "A value of 1,000,001 (one million one) is special, it means 'Anything'... Likewise with the negative, -1000001 means 'Nothing'... A value of 0 means we don't care about the cell at that position."

Match test (engine matchesCell):

if (patternValue == ANYTHING && intGridValue == 0)  -> fail
if (patternValue == NOTHING && intGridValue != 0)   -> fail
if (patternValue > 0  && patternValue != ANYTHING && intGridValue != patternValue) -> fail
if (patternValue < 0  && patternValue != NOTHING && intGridValue == -patternValue) -> fail
// 0 always passes

Pattern center = index (size*size)/2 (the cell the tile is painted on). E.g. 3×3 [0,1,0, 1,-1,1, 0,1,0]: N/E/S/W must be 1, center must NOT be 1, corners ignored.

6.6 How rules RESOLVE to autoLayerTiles (the algorithm)

Engine-confirmed order:

  1. Iterate rule groups top→bottom, within each group rules top→bottom. breakOnMatch from an earlier rule blocks later rules on that cell.
  2. Skip group if !active (or optional-and-not-enabled). Skip rule if !active or chance<=0.
  3. Modulo/checker filter per cell: e.g. (cellY - yOffset) % yModulo == 0 (and X analog); checker adds a brick offset. Fail → cell skipped for this rule.
  4. Chance (deterministic): getRandomIndex(randomSeed + uid, cellX, cellY, 100) >= chance*100 → skip. So per-instance seed + rule uid + cell coords fully determine randomness (reproducible).
  5. Perlin gate (if perlinActive): noise(scale, octaves, perlinSeed) thresholded → skip.
  6. Pattern match (§6.5.1) using outOfBoundsValue for OOB cells; if flipX/flipY, also try mirrored sampling (a mirrored match sets the tile's flip bits).
  7. On match: pick a random entry from tileRectsIds, place 1 tile (Single) or a block (Stamp, anchored by pivot, with size), applying tileXOffset/tileYOffset + jitter, alpha, flip bits. breakOnMatch marks the cell Final.

Importer decision: consume baked vs re-run

  • Consume autoLayerTiles (recommended): simplest, pixel-exact, no need to implement matching/perlin/PRNG. Each entry already has final px, src, f, t, a. This is what LDtk's own game-side loaders do. Use this unless you need runtime/procedural regeneration.
  • Re-run rules (only for runtime procedural maps / runtime CSV edits): requires faithfully reproducing the PRNG (randomSeed+uid, cell coords, getRandomIndex), modulo/checker, perlin, flips, breakOnMatch, stamps. Higher fidelity risk.

autoLayerTiles is reproducible from defs + CSV + seed — it is computed-then-cached by the editor. Preserve seed verbatim (sample: Collisions seed:4084837) to reproduce the exact bake.


7. Tilesets, tile coordinates, and custom data

All tileset definitions are per-project, in defs.tilesets[]. The docs call this "the most important part among project definitions... If you only had to parse one definition section, that would be the one." Each entry describes ONE imported atlas image (or the internal icon atlas); layers/entities/enums/fields reference it by uid.

7.1 defs.tilesets[] — TilesetDef fields

Field Type Null? Derived? Meaning
uid integer no authoritative "Unique Int identifier". Round-trip critical
identifier string no authoritative "User defined unique identifier"
relPath string | null yes authoritative "Path to the source file, relative to the current project JSON file. It can be null if no image was provided, or when using an embed atlas." Forward-slash, project-relative
embedAtlas enum ["LdtkIcons", null] yes authoritative "If this value is set, then it means that this atlas uses an internal LDtk atlas image instead of a loaded one." Only non-null value is "LdtkIcons"
pxWid integer no authoritative* "Image width in pixels" (sourced from image, stored)
pxHei integer no authoritative* "Image height in pixels"
tileGridSize integer no authoritative Tile size in px (square). Basis for all src math
__cWid integer no DERIVED "Grid-based width" = columns = floor((pxWid - padding*2 + spacing) / (tileGridSize + spacing))
__cHei integer no DERIVED "Grid-based height" = rows, analogous from pxHei
padding integer no authoritative "Distance in pixels from image borders" — margin before first tile
spacing integer no authoritative "Space in pixels between all tiles" — gutter between cells
tags string[] no authoritative "An array of user-defined tags to organize the Tilesets" (NOT per-tile)
tagsSourceEnumUid integer | null yes authoritative "Optional Enum definition UID used for this tileset meta-data". Supplies values for enumTags. Null = none
customData TileCustomMetadata[] no authoritative "An array of custom tile metadata". See §7.4. Empty [] when unused
enumTags EnumTagValue[] no authoritative "Tileset tags using Enum values specified by tagsSourceEnumId". See §7.4. Empty [] when unused
savedSelections object[] no editor-private "Array of group of tiles selections, only meant to be used in the editor". Preserve verbatim. See §7.5
cachedPixelData object | null yes DERIVED/cache "used internally for various optimizations. It's always synced with source image changes." See §7.5

Doc typo: the enumTags description spells the binding field tagsSourceEnumId but the actual JSON key is tagsSourceEnumUid — trust tagsSourceEnumUid.

Verbatim — real tileset def (sample.ldtk):

{
  "__cWid": 12, "__cHei": 32,
  "identifier": "Cavernas_by_Adam_Saltsman_Extended",
  "uid": 1, "relPath": "Cavernas_by_Adam_Saltsman-Extended.png", "embedAtlas": null,
  "pxWid": 96, "pxHei": 256, "tileGridSize": 8, "spacing": 0, "padding": 0,
  "tags": [], "tagsSourceEnumUid": null, "enumTags": [], "customData": [],
  "savedSelections": [ {"ids":[20,21,22],"mode":"Random"}, {"ids":[9,10],"mode":"Random"} ],
  "cachedPixelData": { "opaqueTiles":"111111111111...", "averageColors":"f123f224f224..." }
}

Sanity: pxWid 96 / tileGridSize 8 = 12 = __cWid; pxHei 256 / 8 = 32 = __cHei (exact because padding=spacing=0).

7.2 Embed atlas / Internal_Icons handling

  • LDtk ships a built-in tileset Internal_Icons: embedAtlas:"LdtkIcons", relPath:null. Its image is bundled inside LDtk, not on disk.
  • If embedAtlas == "LdtkIcons", source the image from LDtk's internal icon atlas (or ship a copy); do NOT attempt to resolve relPath.
  • Docs-vs-reality: the schema/quicktype historically typed relPath as non-nullable String, but real files (any project with Internal_Icons) emit relPath:null (corrected to ["string","null"]; see deepnight/ldtk issue #664). Treat relPath as nullable always; treat non-null embedAtlas as the signal that there is no external file.

7.3 Tile coordinate math — t ↔ (gridX,gridY) ↔ pixel src

A tile is identified by a single integer Tile ID (t, also called coordId). The atlas is row-major. __cWid in these formulas is the TILESET def's column count, not the layer instance's same-named field — easy bug.

(a) Tile ID → grid coordinates:

gridTileX = t % __cWid            // tileset's __cWid (columns)
gridTileY = floor(t / __cWid)

(b) Grid coordinates → pixel src (top-left in atlas):

src.x = padding + gridTileX * (tileGridSize + spacing)
src.y = padding + gridTileY * (tileGridSize + spacing)

The tile occupies tileGridSize × tileGridSize px. spacing is added per step (not after the last tile).

(c) Inverse (pixel src → Tile ID), for export:

gridTileX = (src.x - padding) / (tileGridSize + spacing)
gridTileY = (src.y - padding) / (tileGridSize + spacing)
t         = gridTileX + gridTileY * __cWid

src is fully derivable from t + the tileset def, and vice versa. t is the more authoritative/compact identity; src is precomputed pixels. On export, keep them consistent — recompute src from t if you ever mutate t. px (tile position in the LAYER) is independent and lives in the layer instance.

7.4 Per-tile custom data — two parallel systems

Both stored on the tileset def but indexed by Tile ID.

(a) customData — free-form per-tile strings. TileCustomMetadata:

Field Type Null? Meaning
tileId integer no The Tile ID this metadata attaches to
data string no Opaque user string. Editor treats it as plain text; convention is JSON (e.g. collision polygons). LDtk does NOT parse it — parse defensively

Example: { "tileId": 42, "data": "{\"collider\":[[0,0],[8,0],[8,8]]}" }. Only tiles WITH data appear.

(b) enumTags — enum-value tagging of tiles. EnumTagValue:

Field Type Null? Meaning
enumValueId string no The enum value id (matches a value id in the enum referenced by tagsSourceEnumUid)
tileIds integer[] no Every Tile ID tagged with that enum value

One array element per used enum value, each holding all tile IDs carrying that tag. Resolve by joining tagsSourceEnumUid → defs.enums[].values[].id. A non-null tagsSourceEnumUid with empty enumTags is valid (enum bound, nothing tagged yet).

7.5 Editor-private / cache fields

savedSelections — array of { "ids":[int...], "mode":<enum> }. ids = tile IDs in a saved brush group; mode = how it paints (observed "Random"; "Stamp" also exists). Purely an editor convenience; preserve verbatim (treat mode as an open-ish enum string).

cachedPixelData — object { opaqueTiles, averageColors } (whole field nullable):

Field Type Null? Meaning
opaqueTiles string no "An array of 0/1 bytes, encoded in Base64, that tells if a specific TileID is fully opaque (1) or not (0)". One byte per tile, tile-ID order
averageColors string yes "Average color codes for each tileset tile (ARGB format)" — concatenated hex, 4 chars per tile

"Always synced with source image changes" → fully regenerable from the image. Safe to recompute; preserve verbatim if you can't re-derive identically (keep encoding: base64 for opaqueTiles, packed ARGB hex for averageColors; both length-coupled to __cWid * __cHei).


8. Entities, field instances, and enums

Scope: defs.entities[] (EntityDef), EntityDef.fieldDefs[] (FieldDef), entityInstances[] (EntityInstance), fieldInstances[] (FieldInstance), defs.enums[] + defs.externalEnums[] (EnumDef / EnumDefValues).

8.1 EntityDef — defs.entities[] (PER-DEF)

Field Type Null? Meaning
identifier string no Unique entity name
uid integer no Project-unique; instances reference via defUid. Authoritative
width / height integer no Default pixel dims
color string #rrggbb no Base entity color (UI)
pivotX / pivotY number 0..1 no Pivot as fraction of size
renderMode enum no Rectangle, Ellipse, Tile, Cross
tileRenderMode enum no Cover, FitInside, Repeat, Stretch, FullSizeCropped, FullSizeUncropped, NineSlice
nineSliceBorders int[] no 4 ints [up,right,down,left]. Empty [] unused
tilesetId integer yes Tileset UID for optional tile display
tileRect TilesetRect yes Rectangle in tileset to display. Replaces deprecated tileId (removed v1.2.0)
tileId integer yes DEPRECATED (removed 1.2.0) → tileRect
uiTileRect TilesetRect yes Overrides tileRect for UI/panels only
fillOpacity / lineOpacity / tileOpacity number 0..1 no Opacities
hollow boolean no Draw body outline only
keepAspectRatio boolean no Lock aspect when resizable both axes
resizableX / resizableY boolean no Per-axis resizing
minWidth / maxWidth / minHeight / maxHeight integer yes Resize constraints (null = unconstrained)
maxCount integer no Max instances (0 = unlimited)
limitBehavior enum no DiscardOldOnes, PreventAdding, MoveLastOne
limitScope enum no PerLayer, PerLevel, PerWorld
exportToToc boolean no List all instances in project TOC
allowOutOfBounds boolean no Instances may sit outside level bounds
showName boolean no Editor displays the name label
doc string yes Designer documentation
tags string[] no Classifying tags (copied to instance __tags)
fieldDefs FieldDef[] no Custom field defs (§8.2). Empty [] when none

Legacy: pre-1.0 files use maxPerLevel instead of maxCount+limitScope, and editorAlwaysShow on fieldDefs. Tolerate both.

{ "identifier":"Player", "uid":27, "width":8, "height":10, "color":"#00BFFF", "fieldDefs":[] }

8.2 FieldDef — EntityDef.fieldDefs[] (also level field defs) (PER-DEF)

The pairing of type (machine) and __type (human) is central.

Field Type Null? Meaning
identifier string no Field name (= FieldInstance.__identifier)
uid integer no Instances link via defUid. Authoritative
type string no Internal token: F_Int, F_Float, F_String, F_Text, F_Bool, F_Color, F_Enum(<enumUid>), F_Point, F_Path, F_EntityRef, F_Tile
__type string no Human type: Int,Float,Bool,String,Multilines,Color,LocalEnum.<Name>/ExternEnum.<Name>,Point,FilePath,Tile,EntityRef, optionally Array<...>
isArray boolean no If true, __type becomes Array<X> and __value is a JSON array
canBeNull boolean no Whether value (or array elements) may be null
arrayMinLength / arrayMaxLength integer yes Array length bounds (null = unbounded)
min / max number yes Numeric value bounds (Int/Float)
defaultOverride tagged value yes "Default value if selected value is null or invalid." Editor-tagged form (like realEditorValues entries), not the raw scalar
regex string yes Validation pattern
textLanguageMode enum yes Text syntax highlight (LangC, LangJson, LangMarkdown, ...)
editorDisplayMode enum no Hidden,ValueOnly,NameAndValue,EntityTile,LevelTile,Points,PointStar,PointPath,PointPathLoop,RadiusPx,RadiusGrid,ArrayCountWithLabel,ArrayCountNoLabel,RefLinkBetweenPivots,RefLinkBetweenCenters
editorDisplayPos enum no Above,Center,Beneath
editorDisplayColor string yes Override display color
editorTextPrefix / editorTextSuffix string yes Text wrapping around displayed value
useForSmartColor boolean no This field's color feeds the entity __smartColor
tilesetUid integer yes Tileset used for a Tile field
acceptFileTypes string[] yes Allowed extensions for FilePath fields
allowedRefs enum no EntityRef constraint: Any,OnlySame,OnlyTags,OnlySpecificEntity
allowedRefsEntityUid integer yes Target entity UID when OnlySpecificEntity
allowedRefTags string[] no Allowed target tags when OnlyTags
symmetricalRef boolean no EntityRef link is bidirectional
allowOutOfLevelRef boolean no EntityRef may target entities in other levels
exportToToc / searchable boolean no TOC export / searchable in editor
doc string yes Designer help
{ "identifier":"Integer","__type":"Int","uid":16,"type":"F_Int","isArray":false,
  "canBeNull":false,"editorDisplayMode":"NameAndValue","editorDisplayPos":"Center",
  "min":null,"max":null }

8.3 EntityInstance — entityInstances[] (PER-LAYER-INSTANCE)

Field Type Null? Authoritative? Meaning
iid string (UUID) no YES (round-trip) Unique instance id, target of EntityRef links. Never regenerate
defUid integer no YES Links to EntityDef.uid
__identifier string no derived EntityDef.identifier
__grid int[] [x,y] no derived Grid-cell coords (px/cellSize)
__pivot number[] [x,y] no derived Pivot 0..1 from def
__tags string[] no derived Copy of EntityDef.tags
__tile TilesetRect yes derived Tile to display ("can be the field own Tile, or some other Tile guessed from the value, like an Enum")
__smartColor string #rrggbb no derived "smart color, guessed from either Entity definition, or one of its field instances"
__worldX / __worldY integer yes derived World-space coords. Only present (non-null) in GridVania or Free layouts
px int[] [x,y] no YES Pixel coords relative to the level (NOT world). True placement source
width / height integer no YES (if resized) Instance pixel dims (may differ from def if resizable)
fieldInstances FieldInstance[] no YES All custom field values (§8.4). Empty [] if none
{
  "__identifier": "Player", "__grid": [1,3], "__pivot": [0,0], "__tags": [],
  "__tile": null, "__smartColor": "#94D9B3",
  "iid": "a9e6f630-dbe0-11ec-9df7-0dc6c63805d5",
  "width": 16, "height": 16, "defUid": 17, "px": [8,24],
  "fieldInstances": [
    { "__identifier":"NodeType","__value":"KinematicBody2D","__type":"String","__tile":null,"defUid":20,"realEditorValues":[] },
    { "__identifier":"Health","__value":10,"__type":"Int","__tile":null,"defUid":21,"realEditorValues":[] }
  ]
}

px is level-relative; world coords come from __worldX/__worldY (only in GridVania/Free). (The TOC representation uses distinct keys worldX/worldY/widPx/heiPx/fields.)

8.4 FieldInstance — fieldInstances[] (PER-INSTANCE)

Field Type Null? Meaning
__identifier string no = FieldDef.identifier
__type string no Human type; same vocabulary as FieldDef.__type (incl. Array<...>, LocalEnum.X)
__value varies yes Parsed value (shape per __type — table below). Game code reads THIS
__tile TilesetRect yes Tile for display (own Tile field, or guessed from value)
defUid integer no Links to FieldDef.uid
realEditorValues array no Editor-private raw values — round-trip critical. Array of { "id":"V_String"|"V_Int"|"V_Float"|"V_Bool"|..., "params":[...] }. "Only used by editor"; read __value instead, preserve this verbatim. Empty [] when unset/default

__value shape by __type (authoritative — JSON_DOC.md):

__type __value JSON shape Notes
Int number (integer) Defaults to min when unset, to comply with def
Float number
Bool boolean
String string
Multilines string (with \n) __type is Multilines only with the "Use Multilines type" advanced option; otherwise multiline text stays String
FilePath string (rel path) Plain string, e.g. "README.md"
Color string #rrggbb Hex string. NB: differs from EnumDefValues.color (integer)
LocalEnum.X / ExternEnum.X string Selected enum value id (e.g. "A"). null if unset
Point object GridPoint {cx,cy} Grid coords, not pixels
Tile object TilesetRect {tilesetUid,x,y,w,h}
EntityRef object EntityReferenceInfos {entityIid,layerIid,levelIid,worldIid} Resolve via the four IIDs
Array<X> JSON array of the X shape Empty [] allowed

Verbatim field instances (note populated realEditorValues and the Color string-vs-int divergence):

{"__identifier":"Point","__value":{"cx":3,"cy":18},"__type":"Point","defUid":24,"realEditorValues":[{"id":"V_String","params":["3,18"]}]}
{"__identifier":"Enum","__value":"A","__type":"LocalEnum.SomeEnum","defUid":22,"realEditorValues":[{"id":"V_String","params":["A"]}]}
{"__identifier":"Color","__value":"#9B2ABC","__type":"Color","defUid":23,"realEditorValues":[{"id":"V_Int","params":[10169020]}]}
{"__identifier":"Array_Integer","__value":[1,2,3],"__type":"Array<Int>","defUid":25,"realEditorValues":[{"id":"V_Int","params":[1]},{"id":"V_Int","params":[2]},{"id":"V_Int","params":[3]}]}
{"__identifier":"Array_points","__value":[{"cx":11,"cy":21},{"cx":11,"cy":23},{"cx":13,"cy":23}],"__type":"Array<Point>"}

Color.__value is "#9B2ABC" while realEditorValues carries the integer form 10169020 (= 0x9B2ABC).

8.5 EnumDef — defs.enums[] and defs.externalEnums[] (PER-DEF)

Field Type Null? Meaning
uid integer no Referenced inside F_Enum(<uid>). Authoritative
identifier string no Enum name (the X in LocalEnum.X)
values EnumDefValues[] no Ordered possible values (§8.6)
iconTilesetUid integer yes Tileset providing value icons (when values use tileRect)
externalRelPath string yes If set, enum imported from an external file; local enums = null
externalFileChecksum string yes Checksum of external source for change detection
tags string[] no Organizational tags

Local vs external: Local enums are authored in the editor (defs.enums[]). External enums are synced from source files (.hx/.cs/.json/text) and live in defs.externalEnums[]. Abstract Haxe enums are not supported. A round-trip tool must keep each enum in its correct array and preserve externalRelPath/externalFileChecksum.

8.6 EnumDefValues — EnumDef.values[]

Field Type Null? Meaning
id string no The enum value (what __value equals on an Enum field instance)
color integer no Optional color as INTEGER (0xRRGGBB), e.g. 6710903. (Contrast: Color field __value is a hex string)
tileRect TilesetRect yes Icon rectangle in iconTilesetUid. Replaces deprecated tileId/__tileSrcRect (removed 1.4.0)
tileId integer yes DEPRECATED → tileRect
__tileSrcRect int[] [x,y,w,h] yes DEPRECATED → tileRect

Modern (1.5.x): { "id":"Pickaxe", "tileRect":{"tilesetUid":31,"x":0,"y":0,"w":16,"h":16}, "color":6710903 }. Legacy (still loaded): {"id":"A","tileId":60,"__tileSrcRect":[192,48,16,16]}. An importer must read either; map __tileSrcRect [x,y,w,h] + iconTilesetUid → tileRect {x,y,w,h}.


9. Shared structs

  • GridPoint — { "cx": int, "cy": int }. Grid-cell coords (multiply by layer cellSize for pixels). Used as Point __value and Array elements.
  • TilesetRect — { "tilesetUid": int, "x": int, "y": int, "w": int, "h": int }. Pixel rect (top-left x,y + w,h) within a tileset. Can span MULTIPLE tiles (w/h need not equal tileGridSize), unlike a tile instance which is always one grid cell. Used by __tile, EntityDef.tileRect/uiTileRect, EnumDefValues.tileRect, IntGridValueDefinition.tile, and Tile __value. Example: { "tilesetUid":31, "x":0, "y":0, "w":16, "h":16 }.
  • EntityReferenceInfos — { "entityIid", "layerIid", "levelIid", "worldIid" } (all strings). Full path to a referenced entity; EntityRef __value. Resolve by matching the four IIDs (worldIid needed for multi-world projects).

10. Versioning and migration semantics

The JSON Schema is Draft-7, auto-generated from Haxe source (the file notes it must not be hand-edited). Schema, docs, and the 1.5.3 sample agree closely. Key dated changes:

  • 1.0.0: iids introduced across project/world/level/layer/entity; intGridCsv replaced object-array intGrid; neighbour levelIid (replacing levelUid); __smartColor added; embed atlas (LdtkIcons) added.
  • 1.2.0: entity tileId removed → tileRect. Neighbour levelUid deprecated → levelIid. IntGrid def autoTilesetDefUid merged into tilesetDefUid.
  • 1.2.4: TOC instancesData[] introduced (see §10.6).
  • 1.3.0+: Multi-worlds worlds[] (preview); when MultiWorlds is enabled, root worldLayout/worldGridWidth/Height/defaultLevelWidth/Height go null.
  • 1.4.0: enum-value tileId/__tileSrcRect removed → tileRect. Neighbour dir gains <,>,o (depth/overlap). __neighbours includes overlapping levels.
  • 1.5.0 / 1.5.3: corner neighbour dirs nw/ne/sw/se.

Version-aware handling:

  • Check jsonVersion/appBuildId at project root to branch parsing.
  • Read both root-level and worlds[] world params (MultiWorlds nulls the root copies).
  • Follow externalRelPath when externalLevels:true; keep layerInstances:null in the project file.
  • Map on read: enum/entity tileId→tileRect; enum __tileSrcRect→tileRect; neighbour levelUid→levelIid; intGrid→intGridCsv. Emit current forms.
  • Handle corner neighbour dirs only when jsonVersion ≥ 1.5.0.
  • Legacy maxPerLevel/editorAlwaysShow vs modern maxCount+limitScope+editorDisplayMode.

10.6 TOC version split (round-trip trap)

A toc[] entry historically had instances[] (array of EntityReferenceInfos). Newer format uses instancesData[] = { iids:{...the 4 iids...}, worldX, worldY, widPx, heiPx, fields }, where fields holds values of entity fields flagged exportToToc. The legacy instances array is still emitted only if the ExportOldTableOfContentData flag is set. A faithful round-trip should preserve whichever form(s) the source emitted (honor the flag), or regenerate both consistently.


11. Consolidated round-trip checklist: preserve vs recompute

MUST preserve verbatim (authoritative / editor-private / identity)

Identity & version

  • Every iid (project, world, level, layer instance, entity instance)
  • All uid / defUid / layerDefUid / tilesetUid / tagsSourceEnumUid / level.uid; keep nextUid >= max(uid)+1
  • dummyWorldIid (the implicit single world's identity in non-MultiWorlds mode)
  • appBuildId, jsonVersion
  • EntityRef quadruple (entityIid / layerIid / levelIid / worldIid) — all four or the ref dangles

Tiles & layers

  • Tile d array ([ruleId,coordId] auto vs [coordId] tile-layer) — editor-private, not reconstructable; array length is itself a signal of origin
  • Full gridTiles / autoLayerTiles array order (= z-order; stacked tiles depend on it)
  • Tile t, f, px, a (and src consistent with t)
  • Layer seed (re-rolls auto-rule RNG if changed), optionalRules, intGridCsv (authoritative IntGrid; 0=empty, values start at 1), pxOffsetX/Y

Rules

  • Rule pattern sentinels 1000001 (Anything) / -1000001 (Nothing) — undocumented; pass through and match with the special-case logic
  • perlinSeed (always present even when perlin off), invalidated (editor dirty flag), outOfBoundsValue (null vs number changes edge behavior)
  • intGridValues keyed by .value, never array index (order is explicitly unrelated to values)

Entities, fields, enums

  • FieldInstance.realEditorValues ({id,params} raw editor form; not synthesizable from __value — Color stores int here vs hex in __value, Point stores "x,y")
  • FieldDef.defaultOverride (tagged form, not a plain scalar)
  • __value semantics + defUid + __type on field instances
  • Entity px (level-relative, the true placement source), width, height, fieldInstances
  • EnumDefValues.color is an INTEGER; Color field __value is a #rrggbb STRING — do not conflate
  • EnumDef.externalRelPath / externalFileChecksum; place external enums in defs.externalEnums[]

Tilesets

  • relPath (nullable; round-trip null faithfully), embedAtlas (signals Internal_Icons)
  • savedSelections (editor tile-group selections; treat mode as open enum), customData.data (opaque string + its tileId), enumTags (per-tile enum tagging)

Levels & worlds

  • worldDepth, useAutoIdentifier, bgColor (nullable — preserve null-vs-explicit), bgRelPath, bgPos, bgPivotX/Y
  • externalRelPath and the layerInstances null-vs-[]-vs-populated tri-state
  • worldX/worldY -1 sentinel in Linear layouts (don't treat as a real coordinate; don't reorder levels[])
  • Level/world fieldInstances payloads

Project-level behavior

  • flags (e.g. MultiWorlds, ExportOldTableOfContentData, PrependIndexToLevelFileNames, UseMultilinesType), customCommands, identifierStyle, all export options
  • Whichever toc form the source used (honor ExportOldTableOfContentData)

MAY recompute (derived) — but recompute correctly and keep consistent

  • All __-prefixed fields: __identifier, __type, __cWid/__cHei, __gridSize, __opacity, __pxTotalOffsetX/Y, __tilesetDefUid/__tilesetRelPath, __grid, __pivot, __tags, __worldX/Y, __smartColor, __tile, __bgColor, __bgPos, __neighbours
  • Tile src (from t + tileset def — but recompute if you mutate t)
  • Tile px (from coordId in d + __gridSize)
  • toc / instancesData (from entity instances)
  • cachedPixelData (opaqueTiles base64, averageColors packed ARGB — from the source image)
  • __header__ (cosmetic)
  • autoLayerTiles in principle (from defs + intGridCsv + seed + exact ruleset) — but only re-run rules if you must regenerate geometry; otherwise preserve the bake verbatim

If you don't fully implement LDtk's crop/adjacency/PRNG math, prefer to pass derived fields through unchanged rather than emit subtly wrong recomputed values that loose loaders will read.


12. Gotchas

  1. __type:"IntGrid" can still populate autoLayerTiles. You cannot pick the tile array from __type alone — check autoLayerTiles.length > 0. (§5.3)
  2. External levels: layerInstances is null in the .ldtk. You MUST follow externalRelPath to the .ldtkl. null (externalized) ≠ [] (genuinely layer-less). (§4.6, §5.1)
  3. Two different __cWids. Layer-instance __cWid (cells in the level) vs tileset-def __cWid (columns in the atlas). The t→src formula uses the tileset's. (§7.3)
  4. Pattern sentinels 1000001/-1000001 are undocumented in the official docs — only discoverable in real files + engine source. (§6.5.1)
  5. intGridValues array order ≠ IntGrid values. Key by .value. (§6.2)
  6. padding/spacing non-zero breaks naive pxWid/tileGridSize column counting — use the full formula. (§7.1, §7.3)
  7. Color encodings diverge: EnumDefValues.color is an INTEGER; Color field __value and entity color/__smartColor are #rrggbb STRINGS. (§8.4, §8.6)
  8. Linear layouts set worldX/worldY to -1 and rely on levels[] array order; __neighbours is empty there. (§4.2, §4.4)
  9. relPath is nullable (schema once mistyped it non-null; issue #664). embedAtlas:"LdtkIcons" ⇒ image is internal, relPath is null. (§7.2)
  10. Stacked tiles at the same px are legal and intentional — array order is z-order; never dedupe or reorder. (§5.4)
  11. No arbitrary tile rotation — only X/Y mirror via f. 90°/270° cannot be represented; map to mirrored UVs for UE. (§5.4)
  12. Deprecated tile refs coexist with replacements: entity tileId (gone 1.2.0), enum tileId/__tileSrcRect (gone 1.4.0), intGrid (gone 1.0.0), neighbour levelUid (deprecated 1.2.0). Read both; emit current. (§10)
  13. Changing seed silently re-rolls auto-layer output — it is not cosmetic. (§5.1, §6.6)
  14. customData.data is opaque — LDtk never parses it. Parse defensively; never assume JSON. (§7.4)
  15. Multi-worlds vs single-world is detected by worlds[] non-empty, not a boolean. Root layout fields go null in multi-worlds. (§4.1)
  16. __worldX/__worldY are null outside GridVania/Free — fall back to level + px. (§8.3)
  17. Don't define which array a Tiles vs IntGrid+rules layer uses by reading gridTiles/autoLayerTiles emptiness alone if you also need the IntGrid — they can both be non-empty on a rule-bearing IntGrid layer. (§5.3)