4 File Format Specification
Leon Schmidt edited this page 2026-03-31 13:16:13 +02:00

Anno 2205 Savegame Format Specification

Status: Reverse engineered from live save files.
Method: Binary analysis, zlib decompression, cross-save comparison.
Samples analysed: 4 save files (early-game, mid-game, pre-stock-exchange backup, all-easy test save).


1. File Layout

Every .save file has the same top-level structure:

Offset      Size    Description
──────────────────────────────────────────────────────────────
0x000       4       Version / magic  (uint32 LE, always 0x00000224 = 548)
0x004       4       Padding / unknown (always 0x00000000)
0x008       32      Header hash - 32 ASCII hex characters (e.g. "b5d8e5ec…")
0x028       512     Header block - null-padded, unused in known saves
0x228+      var     zlib-compressed game data (magic bytes: 78 DA)

The zlib stream starts at a fixed offset of 0x228 in all observed saves.
The total file size varies by save progress (141 KB - 684 KB compressed).

1.1 Header Hash

The 32-byte hash at 0x008-0x027 is an ASCII hex string (lowercase). Its exact algorithm is unknown - likely MD5 or CRC32 over the compressed payload. The game may recalculate it on load; modifying it manually may or may not be required when patching a save.


2. Decompressed Data Format

The zlib payload decompresses to a tag-value binary stream (1.8 MB - 7.2 MB). The stream encodes a tree of typed game-state objects using a simple field encoding.

2.1 Field Encoding

There are two field encodings:

Full field (marker = 0x80)

[field_id: 1 byte] [0x80] [type_or_len: 1 byte] [value: N bytes]

The third byte determines how to read the value:

Byte value Type Value size Notes
0x01 uint8/bool 1 byte Boolean flags, small enums
0x02 uint16 LE 2 bytes Most difficulty settings
0x04 uint32 LE 4 bytes GUIDs, counters, some difficulty fields
0x08 uint64 LE 8 bytes Timestamps, session IDs
N (other) UTF-16LE N bytes String - N is byte count, not char count; N/2 = number of chars; no null terminator

Compact inline field (second byte ≠ 0x80)

[field_id: 1 byte] [value: 1 byte]

Used for small zero-value fields and padding. The second byte is the raw value (not a type marker). Common pattern: 00 00 or 05 00 as filler between blocks.

2.2 String Encoding Detail

Strings use a length-prefix scheme, not null termination:

03 80 1E  45 00 64 00 65 00 6E 00 ...
│  │  │   └─ UTF-16LE data (0x1E = 30 bytes = 15 chars = "Eden Initiative")
│  │  └── byte length of the string data
│  └───── marker
└──────── field_id = 0x03 (CorporationName)

The length byte encodes the number of bytes, not characters. For pure ASCII content (all Anno names observed so far), byte_len = char_count * 2.

2.3 Structural Notes

  • Field IDs are not globally unique - the same ID means different things in different objects. The IDs in the game-session root object are distinct from those in nested objects.
  • The schema (field_id → name mapping) is stored at the end of the decompressed stream (~last 50 KB). It is read-only reference data and does not need to be modified when patching save values.
  • Absent fields (not written to the stream) are treated as their default value by the game (typically 0). This is normal for early-game saves where optional session state has not yet been initialised.

3. Game-Session Object (Root)

The root object begins at offset 0x000 of the decompressed data. Fields are written in ascending field_id order.

3.1 Header Fields

These appear at fixed offsets because they have fixed types and the string lengths determine all subsequent offsets.

field_id Offset (typical) Type Name Notes
0x01 0x000 uint32 FormatVersion Always 2 in observed saves
0x02 0x007 compact CorporationFileVersion Compact inline, value 0
0x03 0x02B UTF-16LE str CorporationName Human-readable corporation name (e.g. "Eden Initiative")
0x04 dynamic UTF-16LE str CorporationLogo Asset ID string (e.g. "icon_player_logo_dummy_06")
0x05 dynamic uint32 CorporationGUID Unique corporation identifier

Between 0x01 and 0x03 there are additional compact/GUID fields at offsets 0x007-0x02A; their exact semantics are unknown but they appear constant across saves of the same corporation.

Key layout rule: The offset of CorporationLogo, CorporationGUID, and the DifficultySettings block all depend on the byte length of CorporationName. None of these can be assumed to be at a fixed offset. The tool computes them dynamically by parsing the name length from dec[0x02D] (the length byte of field 0x03).

3.2 Post-Difficulty Fields

These appear immediately after the DifficultySettings block:

field_id Type Schema name Notes
0x26 uint32 CorporationTime In-game turn/progression counter. In mid-game saves this correlates with campaign progress (e.g. 25, 11). In fresh saves it holds an uninitialised seed value.
0x27 uint32 (unknown) Observed values: 1, 5, 11 - possible sub-progress counter
0x28 varies (unknown) uint32 in mid-game saves; uint64 (timestamp) in fresh saves
0x2A uint64 (unknown) Unix timestamp in seconds - matches the save file's filename in observed cases

4. DifficultySettings Block

The DifficultySettings block is a contiguous run of uint16 fields starting immediately after the CorporationGUID field (plus one compact-inline skip byte).

Start offset: dynamic - computed as CorporationGUID_offset + 7 + 2
(7 bytes for the uint32 field, 2 bytes for the 05 00 compact-inline that follows).

For the two observed mid-game saves with name "Eden Initiative" (15 chars, 30 bytes), the block starts at 0x08A. For "Eden Initiative_SM" (18 chars, 36 bytes) it starts at 0x090. For "ALL EASY" (8 chars, 16 bytes) it starts at 0x074.

4.1 Encoding

All difficulty fields use the full field encoding with type 0x02 (uint16):

[field_id] 0x80 0x02 [value_low] [value_high]

Value semantics:

Value Meaning
0 Easy
1 Normal
2 Hard

DifficultyTraderPrices (0x25) uses a wider integer range (observed values: 2, 4). Its exact scale is unknown - it may be a multiplier rather than an Easy/Normal/Hard enum.

4.2 Block Termination

The block ends when the parser encounters a field_id in the range 0x06-0x25 with a non-uint16 type (e.g. uint32, uint64, or string). This signals that the session-object fields have resumed and are reusing the same field IDs with different types. Do not read these as difficulty values.

Between some fields there are compact-inline entries (00 00, 05 00, 06 00) that must be skipped. These are padding/flags from the outer object.

4.3 Absent Fields

In early-game or fresh saves, some fields at the tail of the block (0x20-0x25) may be absent from the stream entirely. The game treats absent fields as 0 (Easy). When reading, absent fields should be reported as 0 (implicit). When writing, absent fields should not be inserted - changing an existing u16 value in-place is safe; inserting new bytes into the stream would shift all subsequent data and corrupt the save.

4.4 Field Reference

field_id Field name Type Observed range
0x06 DifficultyConstructionCostRefund uint16 0-2
0x07 DifficultySatisfactionInfluencesTaxes uint16 0-2
0x08 DifficultyTemporarySectorEffects uint16 0-2
0x09 DifficultyConsumption uint16 0-2
0x0A DifficultyDominanceAgriculture uint16 0-2
0x0B DifficultyOptionalQuestTimeout uint16 0-2
0x0C DifficultyNpcLevelSpeed uint16 0-2
0x0D DifficultyRevenue uint16 0-2
0x0E DifficultyWorkforce uint16 0-2
0x0F DifficultyTraderRefillRate uint16 0-2
0x10 DifficultyDistributionCenterOutput uint16 1-2 ¹
0x11 DifficultyMetropolisFactor uint16 0-2
0x12 DifficultyMilitaryProgress uint16 0-2
0x13 DifficultyPermanentSectorEffects uint16 0-2
0x14 DifficultyIncreasingDistributionCenterCosts uint16 0-2
0x15 DifficultyMilitaryEnemyStrength uint16 0-2
0x16 DifficultyRelocateBuildings uint16 0-2
0x17 DifficultyTradeRouteAdminCosts uint16 0-2
0x18 DifficultyOptionalQuestFrequency uint16 0-2
0x19 DifficultyDominanceHiTech uint16 0-2
0x1A DifficultyDominanceHeavy uint16 0-2
0x1B DifficultyDominanceEnergy uint16 0-2
0x1C DifficultyDominanceBiotech uint16 0-2
0x1D DifficultyDominanceShareBonus uint16 0-2
0x1E DifficultyInactiveCosts uint16 0-2
0x1F DifficultyDestructibleShips uint16 0-2
0x20 DifficultyMilitaryProgress2 uint16 0-2 ²
0x21 DifficultyMilitaryInvasions uint16 0-2 ²
0x22 DifficultyMilitaryEnemyStrength2 uint16 0-2 ²
0x23 DifficultyStartCredits uint16 0-2 ²
0x24 DifficultyFacilityAuctions uint16 0-2 ²
0x25 DifficultyTraderPrices uint16 0-4 ³

¹ DifficultyDistributionCenterOutput never observed as 0 even in all-Easy saves. Minimum effective value appears to be 1.

² Fields 0x20-0x24 are absent in early/fresh saves (game default = 0 = Easy). They are present as uint16 in saves with active campaign sessions. Safe to set in-place when present; do not attempt to insert when absent.

³ DifficultyTraderPrices uses a wider value range. Observed values: 2 (SM save), 4 (main save). May be a multiplier rather than a 3-step enum.


5. Safe Patching Procedure

The tool supports in-place patching of difficulty values. The procedure is:

  1. Read the raw file.
  2. Locate the zlib stream at offset 0x228.
  3. Decompress with zlib.decompress().
  4. Parse the DifficultySettings block as described in section 4.
  5. Overwrite the 2-byte value at the known position in the decompressed buffer (struct.pack_into("<H", buf, pos+3, new_value)).
  6. Recompress with zlib.compress(level=6). This produces a 78 9C or 78 DA header, both of which are valid zlib streams.
  7. Write raw[:0x228] + compressed back to disk.
  8. The header hash at 0x008 is not updated. The game appears to either recalculate it on load or not validate it strictly - all patched saves tested successfully. This should be verified before relying on it.

Only overwrite existing fields. Never insert new bytes into the decompressed stream. Field offsets are tightly packed; any insertion would corrupt all subsequent data.


6. Schema Registry

The last ~50 KB of the decompressed stream contains a schema registry that maps field names to their numeric IDs. It is used by the game engine for serialisation and is not needed for reading or writing specific known fields.

Format of each schema entry:

[field_id: 1 byte] [0x80] [name: null-terminated ASCII string]

Example entries relevant to this specification:

01 80  CorporationGUID\0
e5 80  CorporationLevel\0   schema ID 0xe5; not directly observed in data
26 80  CorporationTime\0    field 0x26 in game-session object
30 80  CreatingAccountID\0

Note: The schema uses single-byte field IDs in its own namespace, which may differ from the field IDs used in the actual game-session data stream. Field IDs in the data were determined empirically by value correlation across saves, not by reading the schema.


7. Unknowns and Open Questions

Topic Status
Header hash algorithm Unknown - possibly MD5 or CRC32 of compressed payload
Field IDs >= 0x80 in data Not observed in the first few KB; may use varint encoding
DifficultyTraderPrices scale Unknown - observed range 2-4, not a standard 0/1/2 enum
DifficultyDistributionCenterOutput minimum Appears to be 1, not 0; reason unknown
CorporationLevel (schema field 0xe5) Never observed in data; may only exist in a nested sub-object
corporation_time (field 0x26) semantics Correlates with campaign progress in mid-game saves; uninitialised in fresh saves
Session fields 0x20-0x24 as u32/u64 Appear in fresh saves at the same field IDs as difficulty fields - different object context
Compact-inline fields between difficulty entries Appear to be padding/flags; safe to skip