# CNS Image Format

This documents the CNS variant decoded by `cns110.exe` and implemented in
`tools/decode_cns.py`.

## Container

A `.cns` file is an LZ-style compressed stream. The decompressed payload is:

```
u16  unknown / reserved
u16  width
u16  height
u16  palette_count_minus_1
u32  palette[palette_count]   # BMP order: B, G, R, reserved
u8   packed_pixels[]
```

Pixel bit depth is inferred the same way as `cns110.exe`:

- `palette_count <= 16`: 4bpp indexed pixels
- `palette_count > 16`: 8bpp indexed pixels

Rows are BMP-style, padded to a 4-byte boundary and stored bottom-up.

## Compression

Each command starts with one byte. `0x00` terminates the stream.

| Range | Meaning |
| --- | --- |
| `80..ff` | Copy `(cmd & 0x70) >> 4` literal bytes, then copy `(cmd & 0x0f) + 2` bytes from an 8-bit backward offset. |
| `60..7f` | Copy `((cmd & 0x1f) << 8) + next_u8` literal bytes. |
| `40..5f` | Copy `cmd & 0x1f` literal bytes. |
| `30..3f` | Copy `((cmd & 0x0f) << 8) + next_u8` bytes from a 16-bit backward offset. |
| `20..2f` | Copy `((cmd & 0x0f) << 8) + next_u8` bytes from an 8-bit backward offset. |
| `10..1f` | Copy `(cmd & 0x0f) + 2` bytes from a 16-bit backward offset. |
| `01..0f` | Copy `(cmd & 0x0f) + 2` bytes from an 8-bit backward offset. |

Backward offsets are relative to the current output pointer.

## Current Coverage

Confirmed on the UI CNS files with magic-looking first bytes `60 3f 00 00`.
Those bytes are not a file magic; they are the first LZ command plus the first
literal byte of the decompressed payload.

## Tile Map Layouts

Some `.cns` files are not images after decompression. `tools/classify_cns_payloads.py`
currently classifies 177 image payloads and 200 tile-index maps. Files such as
`map1_01a.cns`, `map1_02b.cns`, and `btl_a1.cns` decode to tile-index maps:

```
u16  width_in_tiles
u16  height_in_tiles
u16  layer0[width * height]
u16  layer1[width * height]
```

`map0_01n.cns` is the one observed compact-header variant:

```
u8   width_in_tiles
u8   height_in_tiles
u16  layer0[width * height]
u16  layer1[width * height]
```

The renderer treats the body as two planar arrays: all `layer0` values first,
then all `layer1` values. `layer0` contains the visual ground tile indexes.
Non-zero tile IDs are treated as 0-based indexes into 16x16 tiles; `0` is left
transparent / empty. The common `640x192` map tilesets contain `40x12` tiles, so
IDs `1..479` point directly into the sheet. A previous probe that subtracted one
from each non-zero tile produced wall/ground substitutions, and a probe that
split the image into four `20x6` pages produced repeated quadrant layouts; both
are rejected for the default renderer.

For field maps, `layer1` is metadata rather than a second visual tile layer:

- low 4 bits: directional collision/passability flags
- `0x10`: conditional foreground flag. The same-index `map_*2` tile is drawn
  according to the actor foot-position ordering, so it can appear behind or in
  front of the actor depending on where the actor stands.
- `0x20`: foreground occlusion flag
- `0x40`: animation redraw flag. EXE redraw preparation marks these cells dirty
  every pass. Current grounded evidence proves the redraw consumer, but does not
  yet prove the visible frame producer; palette-only replacement is not promoted
  as the full explanation for spatial water/fire motion.
- `0x60`: `0x20 | 0x40`, observed on cells that need both foreground occlusion and animation-redraw handling

When a cell has the `0x20` foreground bit, the renderer draws the same cell's
`layer0` tile index from the `map_*2` tileset above the character. When a cell
has the `0x10` conditional bit, the same-index foreground tile participates in
the actor foot-row draw order instead: it draws behind an actor standing below
the tile and in front of an actor standing at or above the tile. Other high bits
are not automatically foreground overlays. This is why drawing all `layer1` high
bits as same-index foreground tiles produces wrong blue/black fragments.

The header width/height are the rendered map dimensions. A probe that treated
the body as a single `(width * 2) * height` or `width * (height * 2)` tile
stream produced repeated 2x-sized layouts, so those expanded interpretations
are rejected. Treating adjacent u16 values as per-cell layer pairs splits one
continuous map into repeated left/right chunks, so that interpretation is also
rejected.

The current visual renderer uses the map filename suffix as the primary
tileset family: for example, `map1_01a.cns` uses `map_a1.cns`/`map_a2.cns`,
and `map1_02b.cns` uses `map_b1.cns`/`map_b2.cns`. Some `Hwanse2.exe`
scene-load table entries bind the same map names to different tilesets, such as
`map1_02b.cns` with `map_f1.cns`/`map_f2.cns`/`map_f3.cns`, but those variants
produce visibly wrong tile semantics for the base preview. The exporter keeps
those observed scene table tilesets as switchable metadata instead of using
them as the default visual mapping.

Field collision is read from `layer1` low bits. Character walk sprites such as
`cara_at*`, `cara_rs*`, and `cara_sm*` use 48px-wide field frames, which are
exactly three 16px map tiles wide. Movement/collision therefore uses the bottom
3x1 tile footprint, not the full 48x64 sprite rectangle. Older
terrain-brightness, four-corner footprint, and `layer0 > 1 && layer1 == 0`
passability probes are no longer the default rule; they are retained only as
historical diagnostics outside the current home surface.

Battle background tilemaps use the same planar payload shape, but their visual
assembly is the `layer0` tilemap with the matching `btl_*` tileset. `layer1` is
kept in the decoded data for review, but it is not drawn as a foreground layer
for the battle background view.

`tools/export_map_js.py` exports one or more of these layouts as a
browser-loadable JS asset for the current Canvas prototype.
`tools/render_map_preview.py` renders the same metadata to PNG previews. Map
tileset transparency is keyed from each tileset image's top-left pixel, because
the transparent palette color differs between tileset families.

Some scene records in `Hwanse2.exe` contain a `63` sentinel and a pointer to a
packed coordinate table. `tools/extract_scene_events.py` extracts these as
`out/scene_events.json`. The observed records use `kind=93` or `kind=94` and
store tile coordinate pairs as `u16 x, u16 y` in one dword. These are treated as
hotspot/event candidates, not confirmed transitions, because identical point
lists appear in multiple scenes.

Map transitions, spawn coordinates, and event/object activation are not part of
the CNS tilemap contract. They still need EXE event/object VM evidence. Manual
edge `transitions` were removed until the original event data or reliable spawn
coordinates are identified.
