|
SL2 Analyzer
Read a FromSoftware .sl2 save and report what is in it
|
Classes | |
| class | SaveData |
| One parsed save: the game it is, its file-level fields, and its characters. More... | |
Functions | |
| disclaimer_for (cfg) | |
| One-line header note for a generated file: the repo, and how this game is read. | |
| save_format_version (data, entries, cfg, game) | |
| The save-format version this file was written with, or None. | |
| steam_owner (data, entries, game) | |
| The Steam account that owns this save, or None. | |
| ds2_steam_owner (data, entries, game) | |
| DS2's account, read as text rather than as a number. | |
| steam_folder (game, owner) | |
| The folder name the game will look for this save under, or None where the convention has not been verified for that game. | |
| er_game_patch (data, entries) | |
| Elden Ring's game patch, decoded from its regulation version, or None. | |
| footer_for (cfg, n, version=None, patch=None, owner=None, folder=None, meta=None) | |
| The closing "about this file" block: game, tier, slot count and the how-it-works note. | |
| parse_save (data, base_dir) | |
| Read one save file into plain data: which game it is, and every populated character slot already augmented with its progress. | |
| convert (data, filename, base_dir, meta=None, validate=False) | |
| Build the Markdown document for one save file. | |
| render_markdown (save, filename, meta=None, validate=False, data=None) | |
| Render an already-parsed save as the Markdown document. | |
Variables | |
| str | REPO_URL = "https://github.com/darthdemono/sl2-analyzer" |
| Public source repository, printed in every generated file. | |
| dict | GAMES |
| Per-game config: title, tier, db, decrypt/parse, slot range, and a one-line "how it works" for the file header. | |
| dict | SAVE_VERSION_GAMES = {"dsr", "ds3", "er"} |
Games that stamp a save-format version as a uint32 at slot +0. | |
| int | SAVE_VERSION_MAX = 4095 |
| Above this a slot-0 word is not a version counter but data. | |
| dict | STEAM_ID_GAMES |
Where each game stores the Steam account that owns the save: (menu entry, offset of the uint64 inside it). | |
| int | DS2_STEAM_ID_OFF = 53 |
| Dark Souls II keeps the same account, but as ASCII TEXT — entry 0, offset 53, sixteen characters of lowercase hex. | |
| int | DS2_STEAM_ID_LEN = 16 |
| int | STEAM_ID64_HIGH = 0x01100001 |
| High dword of every individual-account SteamID64 (universe 1, type 1, instance 1). | |
| dict | STEAM_FOLDER_HEX = {"ds3", "ds2sotfs", "ds2vanilla"} |
| Games whose save folder is named with the SteamID64 in HEX rather than decimal. | |
| dict | STEAM_FOLDER_DEC = {"sdt", "nr"} |
| Games whose save folder is named with the decimal SteamID64. | |
| int | ER_REG_ENTRY = 11 |
| Elden Ring ships its regulation (the game's own param data) inside the save, in BND4 entry 11 behind a " GER" magic — and that block is versioned. | |
| str | ER_REG_MAGIC = b" GER" |
| int | ER_REG_VER_OFF = 8 |
| dict | META_LABEL |
Display labels for metadata keys whose acronym capitalize() would mangle ("dlc" -> "Dlc", "os" -> "Os"). | |
| tuple | ER_NOTE |
| Elden Ring's item-coverage caveat. | |
| tuple | SDT_NOTE |
| Sekiro's own caveat. | |
The driver: the GAMES control table, the file-level footer fields, and the one pass that turns a .sl2 into Markdown.
| sl2.convert.disclaimer_for | ( | cfg | ) |
One-line header note for a generated file: the repo, and how this game is read.
Replaces the old boilerplate; states the source, not caveats.
Definition at line 219 of file convert.py.
| sl2.convert.save_format_version | ( | data, | |
| entries, | |||
| cfg, | |||
| game | |||
| ) |
The save-format version this file was written with, or None.
Read from the first slot that carries one — an unused slot is all zeros, so the walk continues past it rather than reporting 0.
| data | The full file bytes. |
| entries | BND4 entries. |
| cfg | The GAMES entry. |
| game | The game key. |
Definition at line 248 of file convert.py.
| sl2.convert.steam_owner | ( | data, | |
| entries, | |||
| game | |||
| ) |
The Steam account that owns this save, or None.
Returns (account_id, steam_id64_text) — the account id as a plain int and the full SteamID64 rendered as text, because the number is too large to survive a JavaScript double and the two front ends have to agree byte for byte.
| data | The full file bytes. |
| entries | BND4 entries. |
| game | The game key. |
Definition at line 328 of file convert.py.
References sl2.convert.ds2_steam_owner().
Referenced by sl2.convert.parse_save().
| sl2.convert.ds2_steam_owner | ( | data, | |
| entries, | |||
| game | |||
| ) |
DS2's account, read as text rather than as a number.
Returns the same (account_id, steam_id64_text) pair as steam_owner so everything downstream is unchanged. The string is parsed rather than trusted: it must be sixteen hex digits whose top half is the individual-account constant, which is the same validity gate the numeric games use and is what stops an unrelated run of text being printed as somebody's account.
| data | The full file bytes. |
| entries | BND4 entries. |
| game | The game key. |
Definition at line 355 of file convert.py.
Referenced by sl2.convert.steam_owner().
| sl2.convert.steam_folder | ( | game, | |
| owner | |||
| ) |
The folder name the game will look for this save under, or None where the convention has not been verified for that game.
The account is baked into the save, and the game only reads a save back out of the folder named for that account — so a save moved to another account's folder, or a save whose account id changed underneath it, will not load. That is the whole reason this is worth printing.
| game | The game key. |
| owner | The steam_owner pair. |
Definition at line 382 of file convert.py.
Referenced by sl2.convert.parse_save().
| sl2.convert.er_game_patch | ( | data, | |
| entries | |||
| ) |
Elden Ring's game patch, decoded from its regulation version, or None.
The regulation version is a uint32 laid out as M-mm-p-bbbb: major, minor, patch, build. Both ER saves here read 11601000 → 1.16.0. The layout is not a guess — ER-Save-Lib carries a regulation-version→size table of 24 real ids, and every one of them decodes this way to a version Bandai actually shipped (10210038 → 1.02.1, 10330078 → 1.03.3, 10911000 → 1.09.1, 11611000 → 1.16.1). The build digits are dropped: they identify the regulation revision, not the patch players are told about.
+8 is a size), and DS1/DS2 have no regulation block at all — so nothing to read. | data | The full file bytes. |
| entries | BND4 entries. |
Definition at line 415 of file convert.py.
| sl2.convert.footer_for | ( | cfg, | |
| n, | |||
version = None, |
|||
patch = None, |
|||
owner = None, |
|||
folder = None, |
|||
meta = None |
|||
| ) |
The closing "about this file" block: game, tier, slot count and the how-it-works note.
These are facts about the TOOL, not about the character, and they are identical in every export — so they sit at the end, out of the way of the save's own numbers, and folded into a <details> block so two exports diff cleanly. The save version is the one line here that IS about the file, and it sits here because it is a property of the file rather than of any one character — as is the game patch.
| cfg | The GAMES entry. |
| n | Characters rendered. |
| version | The save-format version, or None where the game has no known field. |
| patch | The game patch (ER only, from its regulation version), or None. |
| owner | The Steam account pair from steam_owner, or None. |
| folder | The folder name from steam_folder, or None. |
| meta | Caller-supplied environment (store, launcher, OS, …), or None. It is printed under its own heading and labelled as SUPPLIED, because none of it is read from the save — the save cannot know which launcher started it. |
Definition at line 463 of file convert.py.
| sl2.convert.parse_save | ( | data, | |
| base_dir | |||
| ) |
Read one save file into plain data: which game it is, and every populated character slot already augmented with its progress.
This is the whole reading pass, with no rendering in it — the Markdown writer and the JSON writer both start here, so neither can drift from the other. The three branches are the three shapes the games come in: Elden Ring (roster-gated, GaItem items), DS3 (id-scan items, content-scan stats, event flags), and everything else (decrypt the slot, hand it to the game's parse hook, then its optional augment hook).
| data | The full file bytes. |
| base_dir | Folder holding the db_* item-table directories. |
Definition at line 541 of file convert.py.
References sl2.convert.steam_folder(), and sl2.convert.steam_owner().
| sl2.convert.convert | ( | data, | |
| filename, | |||
| base_dir, | |||
meta = None, |
|||
validate = False |
|||
| ) |
Build the Markdown document for one save file.
| data | The full file bytes. |
| filename | The source filename, for the header line. |
| base_dir | Folder holding the db_* item-table directories. |
Definition at line 727 of file convert.py.
| sl2.convert.render_markdown | ( | save, | |
| filename, | |||
meta = None, |
|||
validate = False, |
|||
data = None |
|||
| ) |
Render an already-parsed save as the Markdown document.
| save | A SaveData from parse_save. |
| filename | The source filename, for the header line. |
| meta | Caller-supplied environment for the footer, or None. |
| validate | Append each character's validation section (off by default; the validators package is a separate pass and nothing here depends on it). |
| data | The original file bytes, when the caller has them: the file-level rules (entry checksums) need the container, not a character. |
Definition at line 741 of file convert.py.
| str sl2.convert.REPO_URL = "https://github.com/darthdemono/sl2-analyzer" |
Public source repository, printed in every generated file.
Definition at line 52 of file convert.py.
| dict sl2.convert.GAMES |
Per-game config: title, tier, db, decrypt/parse, slot range, and a one-line "how it works" for the file header.
Definition at line 57 of file convert.py.
| dict sl2.convert.SAVE_VERSION_GAMES = {"dsr", "ds3", "er"} |
Games that stamp a save-format version as a uint32 at slot +0.
ClayAmore's ER-Save-Lib names this field ("File version", the word right after the 16-byte checksum) and branches on it, so it is documented rather than inferred. DSR reads 71, DS3 98, ER 220 or 251. It is NOT the game patch — two ER saves here read 220 and 251 with the same regulation version — so it is printed as a bare number beside the patch, not translated into one. DS2 is deliberately absent: it reads a constant 0x6F there on vanilla, Scholar and all 41 mules alike, so that word is structure, not a version. PtDE's first word is its slot size. Neither is guessed at.
Definition at line 235 of file convert.py.
| int sl2.convert.SAVE_VERSION_MAX = 4095 |
Above this a slot-0 word is not a version counter but data.
Definition at line 239 of file convert.py.
| dict sl2.convert.STEAM_ID_GAMES |
Where each game stores the Steam account that owns the save: (menu entry, offset of the uint64 inside it).
Found by scanning every entry of every fixture for a well-formed SteamID64 and checking the hit against the account the save's own folder is named for. DS3 and ER keep it at the very front of the menu block, Sekiro a little further in.
Definition at line 272 of file convert.py.
| int sl2.convert.DS2_STEAM_ID_OFF = 53 |
Dark Souls II keeps the same account, but as ASCII TEXT — entry 0, offset 53, sixteen characters of lowercase hex.
This file used to say DS2 stored no account at all. That was wrong, and the reason it was wrong is worth keeping: the search that "proved" it looked for the SteamID64 as a 64-bit integer and as UTF-16, and DS2 writes the folder name — the printable hex string — so neither form was ever going to hit. mi5hmash/SL2Bonfire names the offset (SteamIdOffsetInUserDataFile 53, UserDataFileNumber 0), and it checks out on every DS2 save here: eight backups in 0110000100001337 all read that string, and the vanilla mule reads 011000015ab603ef, a different account. Two releases, two accounts, and every one matching the folder it was found in.
Definition at line 291 of file convert.py.
| int sl2.convert.DS2_STEAM_ID_LEN = 16 |
Definition at line 292 of file convert.py.
| int sl2.convert.STEAM_ID64_HIGH = 0x01100001 |
High dword of every individual-account SteamID64 (universe 1, type 1, instance 1).
The low dword is the account id proper.
Reading the field as two halves rather than one uint64 is deliberate and has to be matched in the JS port: a SteamID64 is larger than JavaScript's exact integer range, so forming the 64-bit value in a double loses the last digits. Both ports read two uint32 and assemble the printable forms from those. Requiring this exact constant in the high half is also the validity gate: it is what separates the real field from an arbitrary word, so an unrecognised layout omits the field instead of printing a nonsense account.
Definition at line 305 of file convert.py.
| dict sl2.convert.STEAM_FOLDER_HEX = {"ds3", "ds2sotfs", "ds2vanilla"} |
Games whose save folder is named with the SteamID64 in HEX rather than decimal.
Verified against the folders on disk: DS3 sits in 011000013fc93365, Sekiro in the decimal 76561199030416229. DS2 names its folder the same hex way but is NOT listed, because it stores no account to derive one from. ER is not listed either: no ER folder was on hand, so its convention is unchecked and unclaimed.
Definition at line 313 of file convert.py.
| dict sl2.convert.STEAM_FOLDER_DEC = {"sdt", "nr"} |
Games whose save folder is named with the decimal SteamID64.
Nightreign is listed on evidence: the test save reads 76561197960272671 and shipped in a folder of that name.
Definition at line 319 of file convert.py.
| int sl2.convert.ER_REG_ENTRY = 11 |
Elden Ring ships its regulation (the game's own param data) inside the save, in BND4 entry 11 behind a " GER" magic — and that block is versioned.
Definition at line 395 of file convert.py.
| str sl2.convert.ER_REG_MAGIC = b" GER" |
Definition at line 398 of file convert.py.
| int sl2.convert.ER_REG_VER_OFF = 8 |
Definition at line 401 of file convert.py.
| dict sl2.convert.META_LABEL |
Display labels for metadata keys whose acronym capitalize() would mangle ("dlc" -> "Dlc", "os" -> "Os").
Any key not listed falls back to capitalising, so a caller inventing their own key still gets a sane label.
Definition at line 431 of file convert.py.
| tuple sl2.convert.ER_NOTE |
Elden Ring's item-coverage caveat.
ER is the one game whose item list is deliberately partial, so the document says so where the list is.
Definition at line 687 of file convert.py.
| tuple sl2.convert.SDT_NOTE |
Sekiro's own caveat.
The item lists are complete, the two stat maxima are pinned, and the event-flag region is located — so both the idols and the boss kills come out of their own flags. What is left needs id tables the game does not publish.
Definition at line 702 of file convert.py.