A savegame is a file that allows the player to store their progress, enabling them to quit the game and resume later from the same place they left off. Sphinx and the Cursed Mummy has three save slots, letting the player have three different playthroughs at different stages simultaneously.
PC port[]
The format was largely unchanged in the PC port, with the first slot stored in a file called DATA0
and the second and third slots are stored as DATA1
and DATA2
, respectively. This mirrors the contents of a memory card in a sixth-generation console.
Additionally, the PC version holds backups of all the previous versions of a slot, named with a timestamp. This is in order to simplify the task of rolling back to a previous state just by copying and renaming files.
Savegame slot location[]
On Windows the game uses the per-user Saved Games folder. Because macOS and Linux don't have a standardized way of doing the same thing, save slots are instead stored in their respective configuration folders:
Operating system | File path |
---|---|
Windows | %USERPROFILE%\Saved Games\Sphinx\
|
Linux | $XDG_DATA_HOME/Sphinx/
|
macOS | ~/Library/Application Support/Sphinx/
|
Mod savegames[]
Mods use their own independent set of save slots. Instead of just Sphinx
, the folder is suffixed with the name of the mod folder, i.e. Sphinx_ModFolderName
. So Steam Workshop mods will use their unique ID code. For example, the Shadow of Set mod by jmarti856 will use the folder Sphinx_1614156774
.
If two normal mods share the same folder name, they will also share slots.
Internal format[]
The save file is composed of player entry-points, various objective and inventory sections and it is check-summed via a trailing CRC-32 sum of the whole file minus the field for the hash itself. If the game detects that the data is corrupt it will grey out the slot, making it unplayable until overwritten.
A good chunk of the internal structure is described below from reverse-engineered efforts; by comparing saves, loading them and after some trial and error:
Header[]
The header is mostly fixed in place.
Offset | Size | Type | Field name | Value | ||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0h | 2h | uint32_t | version | the constant 8020004h, which is made out of two parts:
0x0802 (2050) for the top 16-bits, and 0x0004 (base v4 file version) for the bottom ones note: autogenerated from the size limits (1700 + 200 + 50 + 100) << 16 i.e. maximum objectives + the (former) inventory items + (former) ability items + saved triggers if the cumulative max size for those fields doesn't match, or the base Sphinx save format is too old, the save won't load | ||||||||||||||||||||||||||||
4h | 2h | uint32_t | version_again | |||||||||||||||||||||||||||||
8h | 4h | uint32_t | game_time_sec | (in 1/60ths of a second, time shown under the slot name) | ||||||||||||||||||||||||||||
Ch | 2h | uint16_t | scarabs | Money count, probably vestigial | ||||||||||||||||||||||||||||
Eh | 2h | uint16_t | health_ankhs | (whole available Ankhs; life upgrades) | ||||||||||||||||||||||||||||
10h | 2h | uint16_t | crowns | (always zero, I think) | ||||||||||||||||||||||||||||
12h | 10h | char[16] | save_name_tag | SPHINX | ||||||||||||||||||||||||||||
22h | 2h | byte | alignment_22h | Uninitialized | ||||||||||||||||||||||||||||
24h | 4h | uint32_t | cur_level_hashcode | e.g. 1000262h | ||||||||||||||||||||||||||||
28h | 4h | uint32_t | cur_level_restart_id | index for the last used checkpoint in the current level, e.g. 2 | ||||||||||||||||||||||||||||
2Ch | 4h | int32_t | cur_level_entrance_id | where to spawn the player from in the current level, e.g. -1 | ||||||||||||||||||||||||||||
30h | 4h | int32_t | cur_level_timer | Time spent in the current map | ||||||||||||||||||||||||||||
34h | 4h | int32_t | game_time_sec_b | (mirrors game_time_sec) | ||||||||||||||||||||||||||||
38h | 8h | byte | alignment_38h | Uninitialized | ||||||||||||||||||||||||||||
3Ch | 58h | notes_t | notes_sphinx
| |||||||||||||||||||||||||||||
94h | 58h | notes_t | notes_mummy |
Objective array[]
Each game can store up to a maximum of 1700 objectives, the space is preallocated.
objectives_t | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Offset | Size | Type | Field name | Value | ||||||||||||
ECh | 4h | uint32_t | objective_count | 0<>1700 | ||||||||||||
F0h | 3520h | objective_entry_t | objective_array[1700] | There is enough space for a maximum of 1700 elements and each element is made out of the following structure.
|
Secondary header fields[]
Offset | Size | Type | Field name | Value |
---|---|---|---|---|
3610h | 4h | uint32_t | health_thirds | remaining life (always <= than the total, each Ankh/piece has three parts) |
3614h | 4h | uint32_t | total_health_thirds | total available life; health_ankhs * 3 |
Book of Sphinx/inventory items and other general fields[]
Most of these are raw dumps of internal game memory, for performance reasons there doesn't seem to have any serialization. Tread carefully.
Offset | Size | Type | Field name | Value | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
3618h | 3D8Ch | bos_inventory_t | bos_items_sphinx | These header fields seem to be used at runtime for the HUD selector, think of when selecting only available monsters or amulets that you present to the Portal God or some seller guy.
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
73A4h | 3D8Ch | bos_inventory_t | bos_items_mummy | Unlike Sphinx the Mummy seems to only use a single inventory cycle/array/section/group for both quest and ability items. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
B12Ch | 4h | uint32_t | unused_ability_items_count | vestigial from the older inventory system; always zero in the final version | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
B130h | 190h | ability_item_t | unused_ability_items[50] | vestigial, holds uninitialized garbage memory in the final version
each ability_item_t was an early version of item_entry_t but with just the first two fields | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
B2C4h | 4h | uint32_t | cur_level_saved_triggers_count | How many of the 100 trigger entries are in use? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
B2C8h | 960h | saved_trigger_t | cur_level_saved_triggers[100] |
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
BC28h | 4h | uint32_t | player_character_idx | Stores which character was being used while saving in the map; Sphinx is index zero and it's numerically followed by the Mummy, Lion and Tut. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
BC2Ch | Ch | uint32_t | mummy_ability_which[3] | The currently active Mummy ability for each of the three possible Mummies.
How long the ability bar is in seconds and how much time remains until it disappears. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
BC38h | Ch | uint32_t | mummy_ability_time_current[3] | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
BC44h | Ch | uint32_t | mummy_ability_time_maximum[3] | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
BC50h | 8h | uint32_t | triple_mummy_copies_are_active[2] | If the second or third Mummies are active they get set to one here. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
BC58h | 20h | vector_xyzw_t | triple_mummy_copies_position[2] |
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
BC78h | 20h | vector_xyzw_t | triple_mummy_copies_rotation[2] | The way each copy is currently facing. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
BC98h | 4h | float | camera_angle | I believe that the camera gets reset after reloading the save, so the previous values aren't actually going to get used. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
BC9Ch | 4h | float | camera_elevation | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
BCA0h | 4h | float | camera_distance | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
BCA4h | 10h | vector_xyzw_t | camera_position | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
BCB4h | 10h | uint32_t | mummy_prog_buttons[4] | Four hashcodes; four configurable HUD buttons that can be mapped to actions or items.
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
BCC4h | 10h | uint32_t | sphinx_prog_buttons[4] | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
BCD4H | 4h | uint32_t | unused_padding |
[]
The cyclic redundancy check uses a standard CRC-32 algorithm computed over all the previous bytes.
Offset | Size | Type | Field name |
---|---|---|---|
BCD8h | 4h | uint32_t | trailing_crc32 |