# Normal DARKER.SAV format and editor

Open the [offline save viewer/editor](../analysis/saves/index.html) and choose a
save file. No save contents are embedded in the page. It downloads a separate
`*-edited.SAV`, preserves unknown bytes, and recalculates the checksum. The
original installation and supplied save are not modified by the tools.

## File layout

The normal file is exactly **6,600 bytes**. Startup `0364–038A` reads it into
`SS:0FB0`, checks the returned length, computes the checksum and compares it
with the final word. Writer `A110–A154` calculates the checksum in memory and
writes that same complete buffer.

| File range / record offset | Meaning |
|---|---|
| 0–6595 | Four records, each 1649 (`0671h`) bytes |
| Record +0 | Internal stage byte; 0 is empty, 1 skips packed city-state restoration |
| Record +1 onward | Null-terminated displayed name; viewer limits display to the bytes before +1E and leaves it read-only |
| Record +1E | Little-endian weapon mask |
| Record +20 | Return-site packed cell, low byte X and high byte Y |
| Record +22 | Delphi packed city state: 953 bytes |
| Record +3DB | Halon packed city state: 468 bytes |
| Record +5AF through +670 | Not interpreted by this editor; preserved byte-for-byte |
| 6596–6597 | Special-selection trailer; byte 6596 is displayed by the Nightmare menu path (`08BF` reads SS:2974); remaining meaning not fully established |
| 6598–6599 | Little-endian checksum over bytes 0–6597 |

The record layout is supported by the original selection, mission-exit and
city restore routines; see [city retry and selection](city-retry-and-selection.md)
and [persistent cell state](persistent-cell-state.md). The trailer is **not a
fifth complete save slot**. No claim is made here that every internal stage byte
corresponds directly to the level number printed to the player.

## Checksum

Routine `9325` implements the result commonly called **CRC-16/XMODEM**:
polynomial `1021h`, initial value zero, no reflected input/output and no final
XOR. The file stores the resulting word little-endian. In Python it matches:

```
binascii.crc_hqx(payload[:6598], 0)
```

The native algorithm seeds DX from the first two bytes in big-endian order,
shifts the remaining input through the polynomial, then appends sixteen zero
bits. Do not confuse this internal shift-register arrangement with an initial
value of FFFF or with the file's little-endian checksum storage.

The supplied save has a valid checksum. Eleven complete native writer/loader
roundtrips, including random payloads, agree with the standard reference.
Nine single-byte corruptions are rejected, including edits in both ordinary
record data and the trailer/checksum. Four short-file cases reach the alternate
length branch. These tests intercept DOS I/O in memory and never write to the
installation. They check byte format and acceptance, not semantic validity of
random save contents.

## City editing

The editor uses the same eligible-cell order verified against the original
packing/restoration routines: row-major cells whose type descriptor byte +4 is
not FF, excluding empty type 0. Each byte stores four 2-bit codes, most
significant pair first. Delphi has 3,811 eligible cells; Halon has 1,870.

An ordinary code restores state bits `20h`/`40h`. Their meanings differ by city:

| Saved code | Map colour | Delphi | Halon |
|---|---|---|---|
| 0 (`00h`) | Grey | Intact, not marked as a ground target | Damage stage 0 |
| 1 (`20h`) | Orange | Damaged, not marked as a ground target | Damage stage 1 |
| 2 (`40h`) | Blue | Intact, marked as a ground target | Damage stage 2 |
| 3 (`60h`) | Purple | Damaged, still marked as a ground target | Damage stage 3 |

In Delphi, only bit `20h` selects the damage link; `40h` is the ground-target
permission checked by qualifying Caero ground weapons. Thus **1 and 2 are not
two kinds of permission, and 3 is not a universal destroyed value**. Marking a
cell does not make every weapon effective: collision category and a damage link
still matter. Streetlights have a separate eligibility rule and do not need
this permission. See [weapon behaviour](weapon-behaviour.md) and
[streetlight collisions](player-collision-and-damage.md).

Halon uses both bits as a damage-stage number. Model links determine the actual
appearance: types without links cannot acquire a destroyed appearance just by
editing these bits, and successive stages may resolve to the same terminal
model. See [damage model selection](model-colours.md#destruction-confirmed-state-write-and-replacement-model).
Low variant bits are regenerated; the separate `80h` alternate-model bit is not
stored by this ordinary two-bit representation.

Delphi lattice beacons instead use their initialized high-bit template: zero
code restores 00 (off, gold on the map); any nonzero code restores FF (full
power, white). The dropdown switches to beacon-specific labels when one is
selected. Their regional effect on streetlights is described under
[beacon lighting](model-colours.md#beacon-outages-and-streetlights).

### Does Delphi distinguish damaged codes 1 and 3?

Usually the visible result is the same: both select the same damaged model,
collision geometry and destruction-objective completion. Destruction adds
`20h` (`67C7–67CA`) rather than clearing `40h`, so the marking survives without
requiring a separate record of why the object was destroyed.

However, `40h` is not exclusively a targeting permission. In scenario
**04_009 / 4**, two Oppressor scripts explicitly wait for that bit at type-80
chimneys **(52,15)** and **(53,14)**. Instructions at shared-block offsets
11376 and 11385 are `1D 69 0F 40` and `1D 6B 0E 40`; after the wait, each sets
that cell as its target. The condition ignores `20h`: code 3 passes while
code 1 does not. This is a concrete scripted distinction even if the building
already has damaged geometry. The mission's later factory-target marking can
set the bit again, so manually clearing it need not prevent the wait forever.

Warehouse launch-site setup (`8EEE`) also reads `40h` when initialising platform
animation parameters. Aircraft spawning itself (`8E3F`) rejects damaged cells
via `20h`, so neither damaged code permits further launches there. This is not
evidence that code 3 spawns enemies from ruins.

Stage 1 bypasses city-state restoration. Mission setup and scripts can also
change states after load. Editing a saved cell therefore does not guarantee
that an arbitrary mission will display that state. No tunnel stream is exposed:
the normal persistence selector does not save it in these city areas.

Editing record fields does not initialize other state needed for a different
craft or mission. The tool exposes raw established fields, not a campaign
conversion or character creation facility. Names, unknown fields and all
untouched map bits remain unchanged. A bad-CRC file of the normal size can be
inspected, but recalculating its CRC does not repair other corruption.

## Unsupported alternate loading path

A nonstandard file length goes to `0349`, which can recognize an executable
bootstrap-like payload. This is not the normal save format. The viewer rejects
other lengths; the verification harness stops at that branch without executing
its loaded buffer.

## Reproduction

```
PYTHONPATH=/tmp/darker-python python3 tools/verify_save_file.py
python3 tools/build_save_viewer.py
NODE_PATH=/tmp/riotfire-browser/node_modules node tools/verify_save_viewer.js
```

[Native file verification](../analysis/saves/verification.json).
Browser checks include no-change byte roundtrip, single-cell edit isolation,
download, reset, field bounds and preservation of the supplied original.

The browser-exported one-cell edit was additionally fed through the native normal loader and accepted. This validates its checksum/byte-format compatibility; it does not constitute a DOSBox playthrough of the edited slot.
