Skip to content

Save/status file (game.cfg)

Crimsonland stores quest progress and several counters in game.cfg (the “status” file). The format is small but obfuscated and checksummed. This page documents the recovered layout implemented in src/crimson/persistence/save_status.py.

File layout

  • Total size: 0x26c bytes.
  • Payload: first 0x268 bytes (game_status_blob).
  • Checksum: final 4 bytes, little‑endian u32.

Struct view (game_status_t)

game_status_blob is typed as game_status_t:

typedef struct game_status_t {
    unsigned short quest_unlock_index;
    unsigned short quest_unlock_index_hardcore;
    unsigned int weapon_usage_counts[53];
    unsigned int quest_play_counts[91];
    unsigned int mode_play_survival;
    unsigned int mode_play_rush;
    unsigned int mode_play_typo;
    unsigned int mode_play_other;
    unsigned int play_time_ms;
    unsigned int reserved_seed_words[4];
} game_status_t;

Obfuscation

Each payload byte is transformed on save using a byte index polynomial and a constant add. Let i be the byte index and i8 be i wrapped to a signed 8‑bit value.

poly(i) = ((i8 * 7 + 0x0f) * i8 + 0x03) * i8
enc[i]  = (dec[i] + poly(i) + 0x6f) & 0xff

The loader reverses it with:

dec[i] = (enc[i] - 0x6f - poly(i)) & 0xff

Checksum

The checksum is computed over the decoded payload. It accumulates in a 32‑bit integer (wrap on overflow).

acc = 0
u   = 0
for i, byte in enumerate(decoded):
    c = signed8(byte)
    acc = (acc + 0x0d + ((c * 7 + i) * c + u)) & 0xffffffff
    u += 0x6f

Known fields

Offsets are relative to the decoded payload (game_status_blob).

Offset Size Name Notes
0x00 u16 quest_unlock_index Max quest unlock index (limited version).
0x02 u16 quest_unlock_index_hardcore Max quest unlock index (full version).
0x04 u32[53] weapon_usage_counts Weapon usage counters indexed by status slot (0..52). Slot 0 is unused; tracked weapon ids map to slots 1..52; weapon id 53 has no slot in this table.
0xD8 u32[91] quest_play_counts Quest play/attempt counters (major * 10 + minor); unsigned int quest_play_counts[91] in game_status_t (third_party/headers/crimsonland_types.h).
0x244 u32 mode_play_survival Incremented when starting Survival (mode 1).
0x248 u32 mode_play_rush Incremented when starting Rush (mode 2).
0x24C u32 mode_play_typo Incremented when starting Typ‑o‑Shooter (mode 4).
0x250 u32 mode_play_other Incremented for other modes (e.g. tutorial).
0x254 u32 play_time_ms Native playTime counter in milliseconds; used by statistics/demo limits and synchronized with the registry value named sequence.
0x258 u32[4] reserved_seed_words Four constructor seeds (rand() % 345354345), named reserved1..reserved4 in the recovered source header.

Port counter updates

Mode, weapon, quest, and playtime increments wrap at 32 bits, matching the native fields. Playtime converts each gameplay frame delta to milliseconds with f32 multiplication followed by truncation. These rules keep long-running statistics serializable without changing the save format.

Unlocking progress from the CLI

With the game closed, run crimson save unlock to unlock every normal and hardcore quest and their progression rewards. Use --base-dir <directory> (or --runtime-dir) to select a different save directory. The default is the same per-user runtime directory as the game, including CRIMSON_RUNTIME_DIR overrides.

The command requires an existing save with a valid size and checksum. It writes an exact copy to a unique game.cfg.<unique-id>.bak beside the save and flushes it before setting both unlock indices to 50 and atomically replacing game.cfg. It prints the backup path; older backups are never overwritten. All other status fields and separate high-score files remain unchanged. If both indices are already 50, the command does nothing and creates no backup. To restore progress, close the game and copy the printed backup over game.cfg.