---
name: solarus-map-data-format
description: Use whenever creating or hand-editing a Solarus map data file (tests/testing_quest/data/maps/**/*.dat, or any quest's maps/*.dat) — adding/renaming entities, inserting a new sensor/jumper/etc., or writing a brand new map by hand. Also covers where new test maps belong (bugs/ with a real ticket id, features/ with a thematic subdirectory) and registering them. Prevents "Map 'x' differs from the original one after export" failures in the map-data CI test.
---

# Solarus map data (`.dat`) file format

Map `.dat` files are Lua-table-like text, but they are **not** parsed and
re-serialized freely: the `map-data` test (`tests/src/tests/MapData.cpp`)
loads every map and re-exports it, then does a **byte-for-byte comparison**
against the committed file. Any deviation from the exporter's canonical
form — property order, blank lines, trailing newline — fails CI, even
though the file is perfectly valid Lua and the map works fine in-engine.

This test exists so that committed map files always match exactly what the
Solarus Editor (or engine) would save, so nobody ever gets spurious diffs
from opening/resaving a map. That means hand-edits must mimic the exporter
exactly.

## Where to put a new test map

Before creating a brand new test map (not editing an existing one), place
it under the right subdirectory of `tests/testing_quest/data/maps/` —
don't drop it at the top level or guess a location:

- **Bug regression tests** go in `maps/bugs/`, named
  `<ticket_id>_<short_description>` (e.g.
  `bugs/1544_enemy_set_invincible`, `bugs/1987_audio_api_invalid_context`).
  The ticket id is mandatory and must be the *real* tracker id for that
  bug. **If the user hasn't given you one, ask — never invent, guess, or
  omit it.** A multi-map bug repro nests further, e.g.
  `bugs/1015_crash_scrolling_to_invalid_layer/map_1`.

- **Feature tests** go in `maps/features/`, named `<short_description>`
  (e.g. `features/1544_enemy_set_invincible` — note some older feature
  tests are named after the ticket that introduced them, but new ones
  should favor a descriptive name). When a feature area already has, or
  is likely to gain, more than one test map, group them under a thematic
  subdirectory instead of flattening them at the `features/` root — e.g.
  `features/audio/audio_api_tests` alongside a future
  `features/audio/music_channels_test`, rather than
  `features/audio_api_tests`.

Placing the map is only half the job: it also needs a matching `map{...}`
entry (and `file{...}` entries for the `.dat`/`.lua`) in `project_db.dat`
in alphabetical order, and its id added to `LUA_TEST_MAPS` (or
`LUA_TEST_MAPS_NON_FATAL`/`LUA_TEST_MAPS_REQUIRE_WINDOW`) in
`tests/cmake/AddTestMaps.cmake`, or it will never actually run. Prefer
doing the `project_db.dat` part via the Quest Editor (rename/move the
resource there) so it re-exports in canonical form instead of by hand.

## The rules (source of truth: `EntityData::export_to_lua` in
`src/entities/EntityData.cpp`)

For each entity block, fields are written in this exact order — only
include a line if the condition holds:

1. `name = "...",` — only if the entity has a name.
2. `layer = ...,`
3. `x = ...,`
4. `y = ...,`
5. `enabled_at_start = false,` — only if the entity does NOT start enabled.
6. `locked = true,` — only if locked.
7. `group = N,` — only if the group is non-zero.
8. `properties = { ... }` — only if there are user-defined properties.
9. Then the entity type's own specific fields, **in the exact order they
   are declared** in the `entity_type_descriptions` table in
   `EntityData.cpp` for that `EntityType`. For example, `jumper` declares
   `width, height, direction, jump_length` in that order — so a jumper
   block is always
   `name?, layer, x, y, width, height, direction, jump_length`.
   Look up the entity type you're editing in that table before hand-editing
   its fields; don't guess or copy another type's order.

Blank lines:

- Exactly **one** blank line between every two entity blocks.
- The file also ends with a blank line after the **last** entity — i.e.
  the file's last bytes are `}\n\n`, not `}\n`. Every existing map file in
  the repo follows this; a missing final blank line is an easy, silent
  mistake.

## How to avoid breaking this

1. **Prefer not to hand-edit at all.** If you can open the map in the
   Solarus Quest Editor and make the change there (add a jumper, rename an
   entity, etc.), do that — the editor always saves in canonical form.

2. **If you must hand-edit** (e.g. scripting a repetitive change across
   many test maps), never insert a field via a regex/sed anchored on an
   arbitrary line — that's exactly what produced the CI failure this skill
   was written after (a `name = "..."` field got inserted after `y = ...`
   instead of before `layer = ...`). Instead:
   - Copy the *entire* field order from another entity of the **same
     type** already in the file (or from `EntityData.cpp` if none exists),
     never assume order from a different entity type.
   - After editing, check the tail of the file for the trailing blank line.

3. **Always verify with the round-trip test before considering the edit
   done.** Don't trust visual inspection alone:

   ```bash
   # Build (see AGENTS.local.md for the MSYS2 wrapper on Windows), then:
   ./tests/bin/solarus-test-map-data -no-audio -no-video -turbo=yes tests/testing_quest
   ```

   This iterates every map in the quest alphabetically and stops at the
   first mismatch, printing both the original and re-exported text so you
   can diff them.

   **Windows-only caveat:** `.dat` files are checked out with CRLF line
   endings locally (`.gitattributes` marks `*.dat text`), but the exporter
   always writes LF. That makes this test fail on the very first map
   *regardless of content* when run against your normal working tree. To
   get a real signal locally, test against an LF-normalized copy instead:

   ```bash
   rm -rf /tmp/lf_quest && cp -r tests/testing_quest /tmp/lf_quest
   find /tmp/lf_quest -type f \( -name "*.dat" -o -name "*.lua" \) -exec sed -i 's/\r$//' {} \;
   ./tests/bin/solarus-test-map-data -no-audio -no-video -turbo=yes /tmp/lf_quest
   ```

   This local CRLF issue does not affect the actual Linux CI runner, so
   don't "fix" the committed files to have CRLF — keep them LF as git
   stores them.

## Quick self-check when something still fails after a fix

If CI still reports a diff after you believe you've matched the order:
extract the `*** Original map file:` and `*** Exported map file:` sections
from the job log verbatim and `diff` them directly — don't eyeball it. The
log's `std::cerr` dump appends one extra blank line of its own after each
section for formatting, so when comparing tails, expect exactly one extra
blank line in each printed section relative to the real underlying buffer;
what matters is whether *both* sections have the *same* number of extra
blank lines relative to each other.
