Binary export
The binary export is the most efficient format GridCraft produces. Two files come out: a gzipped world.dat the server reads at startup, and a map.json the client reads for render hints. The binary is dense and big-endian. It's laid out for streaming, so a game server in any language can parse it without a heavy parser or a JSON decode pass over a 50-megabyte file.
Both files share the same version field so the server and client agree on which map is loaded.
Files
world.dat (gzipped binary, big-endian) map.json (UTF-8 JSON for the client)
Region size
The map is loaded in fixed-size square regions of 24 × 24 tiles. Your map width and height both have to be multiples of 24, otherwise the export warns and the consumer will refuse the file. The constant is exposed in map.json as divisionSize.
world.dat layout (pre-gzip, big-endian)
width: int16
height: int16
tileSize: int16
version: int64
propertyTilesCount: int16
for each property tile:
tileId: int32
body: PropertyTile (see below)
entityCount: int32
for each entity:
x: int16
y: int16
key: string (int16 length + UTF-8 bytes; -1 = null/empty)
gridCount: int32
for each non-empty cell:
x: int16
y: int16
plateau: int8
idCount: int8 (max 127)
for each id at that cell:
tileId: int32
transform: int8 (flip bits: H=0x04, V=0x02, D=0x01)
objectLayerCount: int16
for each object layer:
name: string (int16 length + UTF-8 bytes)
areaObjectCount: int16
for each area object:
id: int16
x: int16
y: int16
width: int16
height: int16
polygon: int8 count, or -1 for none
if present: count * (x:int16, y:int16)
properties: int8 count, or -1 for none
if present:
type: int8 (0=short, 1=int, 2=double, 3=float, 4=bool, 5=string)
name: string
value: depends on typePropertyTile body
An empty property tile (type NORMAL, no flags) is encoded as a single byte: -1 (0xFF). Anything else is written in full:
type: int8 (-1=EMPTY, 0=NORMAL, 1=ANIMATION, 2=COLLIDING, 3=OBJECT) width: int16 height: int16 collision: int8 (-1 if not set) zIndex: int8 (-1 if not set) ySort: int8 (-1 if not set) objectRadius: int8 (-1 if not set) cursorType: int8 (-1 if none) sand: bool (1 byte) clay: bool invisible: bool nonColliding: bool
Tile property tags
Set these as custom properties on individual tiles in the tileset. The exporter reads them by name and writes them into the property tile body:
c: collision. Marks the tile as colliding and stores the numeric value as the collision byte.o: object. Marks the tile as an object with the numeric value as its radius.horobs: obstructing. When this tile is painted, every tile underneath it in the same cell is dropped.zorv: high tile. Rendered above the player.ncornoncolliding: forces non-colliding, overrides collision flags.invisible: invisible tile.sand,clay: terrain-type tags.cursor: cursor type when hovering. One of: alchemy, axe, bow, cooking, crafting, divinity, fishing, foraging, hand, loot, pickaxe, points, mana, smelting, smithing, spell, sword, talk, bank, kosmetic, startshop, task, billboard, exchange, water, wastelands, housing, adventuring.sfx: sound-effect ID. Plays when the player steps on the tile.- Tile animations: defined in the tileset's animation block. The exporter copies frames into both the property tile and the client's
animationsmap. plateau: not a tile property, a layer name. A layer calledplateau1,plateau2, ... sets a plateau byte on every painted cell. Useful for combat z-indexing.
Required layers
One tile layer named entities (case-insensitive) has to exist. The exporter throws otherwise. Tiles on this layer reference entries from the entities tileset and export as entity spawn records. Tiles whose GID also has a collision property are skipped so entities don't spawn inside walls.
Layers named trees, rocks, foraging, regions (visualization only), or area names are treated as visual-only and are skipped by the exporter.
map.json (client)
{
"version": number, // matches world.dat version
"width": number,
"height": number,
"tileSize": number,
"divisionSize": 24,
"high": [ tileId, ... ],
"animations": { tileId: [ { tileId, duration } ] },
"soundEffects": { tileId: "key" },
"collisions": { tileId: number | polygons[] }
}A polygon-collision entry is an array of polygons, each an array of { x, y } points (16-bit signed). A numeric entry is a simple collision marker (1 for object-style, the collision byte otherwise).
Worked example
A 2×2 map with one collision tile at (0,0). One tileset (firstGid 1), one tile with property c=0, one empty entities layer, and one tile layer with the collision tile painted at the origin. tileSize = 16, version = 1.
(2×2 isn't actually a valid size: dimensions need to be multiples of 24. The example just keeps the byte trace short.)
offset bytes meaning ------ ------------------------------------------ ----------------------------- 0x00 00 02 width = 2 0x02 00 02 height = 2 0x04 00 10 tileSize = 16 0x06 00 00 00 00 00 00 00 01 version = 1 0x0E 00 01 propertyTilesCount = 1 0x10 00 00 00 00 tileId = 0 0x14 02 PropertyTile.type = COLLIDING 0x15 00 10 width = 16 0x17 00 10 height = 16 0x19 00 collision = 0 0x1A FF zIndex = -1 0x1B FF ySort = -1 0x1C FF objectRadius = -1 0x1D FF cursorType = -1 0x1E 00 sand = false 0x1F 00 clay = false 0x20 00 invisible = false 0x21 00 nonColliding = false 0x22 00 00 00 00 entityCount = 0 0x26 00 00 00 01 gridCount = 1 0x2A 00 00 cell.x = 0 0x2C 00 00 cell.y = 0 0x2E 00 plateau = 0 0x2F 01 ids in this cell = 1 0x30 00 00 00 00 tileId = 0 0x34 00 transform = 0 0x35 00 00 objectLayerCount = 0
Total 55 bytes of raw payload. Gzip wraps that. The gzipped wrapper can differ between deflate implementations, but the inflated bytes are deterministic and byte-identical.
The matching map.json is:
{
"version": 1,
"width": 2,
"height": 2,
"tileSize": 16,
"divisionSize": 24,
"high": [],
"animations": {},
"soundEffects": {},
"collisions": { "0": 0 }
}Group layers
The exporter does not recurse into group layers. Only the top-level layer array is walked. If a group layer exists in your map, the exporter prints a console warning and skips it. Flatten the groups before exporting.