Table of Contents
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:
- Read the raw file.
- Locate the zlib stream at offset
0x228. - Decompress with
zlib.decompress(). - Parse the
DifficultySettingsblock as described in section 4. - Overwrite the 2-byte value at the known position in the decompressed buffer
(
struct.pack_into("<H", buf, pos+3, new_value)). - Recompress with
zlib.compress(level=6). This produces a78 9Cor78 DAheader, both of which are valid zlib streams. - Write
raw[:0x228] + compressedback to disk. - The header hash at
0x008is 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 |