SL2 Analyzer
Read a FromSoftware .sl2 save and report what is in it
Loading...
Searching...
No Matches
sl2.er Namespace Reference

Functions

 er_roster (menu)
 Read the ER character roster (active flag, name, level per slot).
 
 er_gaitems (buf)
 Walk the ER GaItem array and yield every owned item id.
 
 er_find_stats (buf, level)
 Locate the ER stat block by content, or None if none validates.
 
 er_parse (buf, iddb, name, level)
 Parse one ER slot into the unified dict (full where stats validate).
 
 load_er_db (db_dir)
 Load the ER id tables, category-scoped: {category: {id: name}}.
 
 er_resolve (iid, db)
 Resolve an ER item id to (name, category), type-scoped by its nibble.
 

Variables

int ER_GAITEM_START = 0x30
 Offset of the GaItem array inside an ER slot: past the 16-byte checksum, the version and map-id words, and the 16 bytes after them.
 
int ER_GAITEM_COUNT = 0x1400
 Number of GaItem entries in the array.
 
 ER_MENU_LEN_OFF
 In the menu (header) entry: offset of the variable-length menu-system block's length field, the byte after which its data begins, the number of character slots, the size of one profile summary, and the profile field offsets for name and level.
 
 ER_MENU_DATA_OFF
 In the menu (header) entry: offset of the variable-length menu-system block's length field, the byte after which its data begins, the number of character slots, the size of one profile summary, and the profile field offsets for name and level.
 
 ER_SLOT_COUNT
 
 ER_PROFILE_STRIDE
 
 ER_PROFILE_NAME_LEN
 
 ER_PROFILE_LEVEL_OFF
 
 ER_STAT_D
 ER stat block as signed distances from the Vigor field (the anchor).
 
 ER_HP_D
 ER max HP, stamina, rune level and runes held, same anchor-relative scheme.
 
 ER_STAM_D
 ER max HP, stamina, rune level and runes held, same anchor-relative scheme.
 
 ER_LEVEL_D
 ER max HP, stamina, rune level and runes held, same anchor-relative scheme.
 
 ER_RUNES_D
 ER max HP, stamina, rune level and runes held, same anchor-relative scheme.
 
int ER_LEVEL_BASE = 79
 ER's rune-level identity: level == (sum of the eight attributes) - 79.
 
int ER_LEVEL_MAX = 8 * 99 - ER_LEVEL_BASE
 The highest rune level the identity can produce: eight attributes at 99 sum to 792, minus ER_LEVEL_BASE.
 
 ER_EMPTY_SLOT_IDS
 The ids that mean "this slot is empty", not "the character owns this".
 
dict ER_CAT = {0x0: "weapons", 0x1: "armors", 0x2: "talismans", 0x4: "goods", 0x8: "ashes"}
 ER item category by id top nibble (the ItemGib type code), and the render category each maps to.
 
 ER_DB_FILES = tuple(ER_CAT.values())
 ER db category files (one per type), each {8-hex-id: name}.
 
int ER_WEAPON_BASE_STEP = 10000
 Weapon ids bake affinity+reinforcement into the low digits: the id is base + affinity*100 + level, base spaced by ER_WEAPON_BASE_STEP.
 
int ER_WEAPON_AFFINITY_STEP = 100
 
int ER_MAX_REINFORCE = 25
 Highest reinforcement ER allows (+25 on the standard path; somber stops at +10).
 

Detailed Description

Elden Ring.

Function Documentation

◆ er_roster()

sl2.er.er_roster (   menu)

Read the ER character roster (active flag, name, level per slot).

Walks past the fixed header and the variable-length menu-system block to reach the active-slot bytes and the fixed-stride profile summaries. Names and levels here are reliable; they are the load screen's own data.

Parameters
menuThe header entry blob (from its start, checksum included).
Returns
A list of (active, name, level) tuples, one per slot.

Definition at line 86 of file er.py.

◆ er_gaitems()

sl2.er.er_gaitems (   buf)

Walk the ER GaItem array and yield every owned item id.

Each GaItem is 8 bytes (handle + id) plus a variable tail decided by the id's category nibble: weapons (0x0) carry 13 more bytes, armour (0x1) 8 more, everything else none. Getting that tail right is what keeps the walk aligned across all 0x1400 entries.

Parameters
bufThe ER slot data, checksum and all — ER_GAITEM_START skips it.
Returns
A generator of nonzero item ids.

Definition at line 123 of file er.py.

Referenced by sl2.er.er_parse().

Here is the caller graph for this function:

◆ er_find_stats()

sl2.er.er_find_stats (   buf,
  level 
)

Locate the ER stat block by content, or None if none validates.

The block sits at a different offset in every slot, so it is found rather than read from a fixed spot. The search is anchored on the level field: every place the slot stores level as a little-endian uint32 is a candidate, the block is read back from there, and it is accepted only where each of the eight attributes is 1..99 and their sum minus ER_LEVEL_BASE equals that level. ER's own level formula, so a coincidental match is not credible.

The block is NOT 4-aligned. Variable-length data precedes it — the character name among it — so its offset is whatever that data leaves it at. Scanning on a 4-byte stride finds only the quarter of characters that happen to land on it: across a 182-character corpus a strided scan found 29 blocks where this finds all 182.

The roster level is required, and it is what makes the answer trustworthy. The identity alone false-positives: on that same corpus five slots matched a run of bytes megabytes past the real block and reported levels of 2, 13, 22 and 25 for characters the roster puts at 125, 150 and 160. Checking against the independently stored roster level kills all five. Without a roster level there is nothing to check against, so the slot keeps its inventory tier rather than printing a number that might be junk.

Parameters
bufThe ER slot data.
levelThe character's rune level from the roster.
Returns
The Vigor-field offset (the anchor), or None.

Definition at line 162 of file er.py.

Referenced by sl2.er.er_parse().

Here is the caller graph for this function:

◆ er_parse()

sl2.er.er_parse (   buf,
  iddb,
  name,
  level 
)

Parse one ER slot into the unified dict (full where stats validate).

Owned items come from the GaItem walk resolved against the id table; ids may carry category bits, so a direct hit is tried first, then the masked id. Bosses are inferred from Remembrances held. Attributes are located by content (er_find_stats) — the block's slot offset varies, so it is found by the rune-level identity, not a fixed offset. When it validates the slot is full tier; otherwise stats drop and it stays inventory tier (the roster level still stands). Quantities and the reinforced-weapon base ids are still not read.

Parameters
bufThe ER slot data.
iddbFlat {id: name} table.
nameThe character name from the roster, or None.
levelThe character level from the roster, or None.
Returns
A unified character dict, or None.

Definition at line 195 of file er.py.

References sl2.er.er_find_stats(), sl2.er.er_gaitems(), and sl2.er.er_resolve().

Here is the call graph for this function:

◆ load_er_db()

sl2.er.load_er_db (   db_dir)

Load the ER id tables, category-scoped: {category: {id: name}}.

Parameters
db_dirFolder holding one JSON per category (weapons/armors/…).
Returns
The lookup, or {} if none present.

Definition at line 266 of file er.py.

◆ er_resolve()

sl2.er.er_resolve (   iid,
  db 
)

Resolve an ER item id to (name, category), type-scoped by its nibble.

The category comes from the id's top nibble (ER_CAT); the name is looked up ONLY in that category's table, so an armour id can never resolve to a weapon of the same base number. A weapon miss then strips the reinforcement level to land on the affinity row the table actually names ("Sacred Butchering Knife"), and only failing that falls back to the plain base. The level is appended, so a reinforced weapon reads as itself rather than as its unupgraded twin.

Returns
(name, category); name is None when unresolved, category None when the nibble is not a known type.

Definition at line 287 of file er.py.

Referenced by sl2.er.er_parse().

Here is the caller graph for this function:

Variable Documentation

◆ ER_GAITEM_START

int sl2.er.ER_GAITEM_START = 0x30

Offset of the GaItem array inside an ER slot: past the 16-byte checksum, the version and map-id words, and the 16 bytes after them.

Measured, and it is 0x30 rather than the 0x20 this once used. A 16-byte error here does not read 16 bytes of nonsense and recover — the entries are variable-length (a weapon is 21 bytes, armour 16, everything else 8), so the walk takes its tail length from a misread id and drifts for the rest of the array. The anchor that pins it is the handle column: from 0x30 the handles run strictly sequential for hundreds of entries (0xc080008b, 0x8c, 0x8d, ...), which is not something a misaligned read produces.

The check that settles it is the category nibble. Only 0x0/0x1/0x2/0x4/0x8 are real categories and 0xF is the empty-slot marker, so any other nibble is proof the walk has lost its place. Across 182 characters: 148 of them carried illegal nibbles at 0x20, and none at all at 0x30 — with naming going 89.0% to 98.8% at the same time.

Definition at line 25 of file er.py.

◆ ER_GAITEM_COUNT

int sl2.er.ER_GAITEM_COUNT = 0x1400

Number of GaItem entries in the array.

Definition at line 29 of file er.py.

◆ ER_MENU_LEN_OFF

sl2.er.ER_MENU_LEN_OFF

In the menu (header) entry: offset of the variable-length menu-system block's length field, the byte after which its data begins, the number of character slots, the size of one profile summary, and the profile field offsets for name and level.

Layout per ClayAmore/ER-Save-Editor.

Definition at line 36 of file er.py.

◆ ER_MENU_DATA_OFF

sl2.er.ER_MENU_DATA_OFF

In the menu (header) entry: offset of the variable-length menu-system block's length field, the byte after which its data begins, the number of character slots, the size of one profile summary, and the profile field offsets for name and level.

Layout per ClayAmore/ER-Save-Editor.

Definition at line 36 of file er.py.

◆ ER_SLOT_COUNT

sl2.er.ER_SLOT_COUNT

Definition at line 39 of file er.py.

◆ ER_PROFILE_STRIDE

sl2.er.ER_PROFILE_STRIDE

Definition at line 39 of file er.py.

◆ ER_PROFILE_NAME_LEN

sl2.er.ER_PROFILE_NAME_LEN

Definition at line 42 of file er.py.

◆ ER_PROFILE_LEVEL_OFF

sl2.er.ER_PROFILE_LEVEL_OFF

Definition at line 42 of file er.py.

◆ ER_STAT_D

sl2.er.ER_STAT_D
Initial value:
1= OrderedDict(
2 [
3 ("Vigor", 0),
4 ("Mind", 4),
5 ("Endurance", 8),
6 ("Strength", 12),
7 ("Dexterity", 16),
8 ("Intelligence", 20),
9 ("Faith", 24),
10 ("Arcane", 28),
11 ]
12)

ER stat block as signed distances from the Vigor field (the anchor).

Eight attributes in the game's storage order, read against a real level-266 save (offsets checked on a second character in the same file).

Definition at line 48 of file er.py.

◆ ER_HP_D

sl2.er.ER_HP_D

ER max HP, stamina, rune level and runes held, same anchor-relative scheme.

(The block also carries FP just before stamina; not surfaced.)

Definition at line 64 of file er.py.

◆ ER_STAM_D

sl2.er.ER_STAM_D

ER max HP, stamina, rune level and runes held, same anchor-relative scheme.

(The block also carries FP just before stamina; not surfaced.)

Definition at line 64 of file er.py.

◆ ER_LEVEL_D

sl2.er.ER_LEVEL_D

ER max HP, stamina, rune level and runes held, same anchor-relative scheme.

(The block also carries FP just before stamina; not surfaced.)

Definition at line 64 of file er.py.

◆ ER_RUNES_D

sl2.er.ER_RUNES_D

ER max HP, stamina, rune level and runes held, same anchor-relative scheme.

(The block also carries FP just before stamina; not surfaced.)

Definition at line 64 of file er.py.

◆ ER_LEVEL_BASE

int sl2.er.ER_LEVEL_BASE = 79

ER's rune-level identity: level == (sum of the eight attributes) - 79.

Wretch (all 10, sum 80) is level 1, and it holds at every level — the content check that pins the stat block, whose slot offset varies from character to character (variable-length data precedes it, so a fixed offset will not do).

Definition at line 71 of file er.py.

◆ ER_LEVEL_MAX

int sl2.er.ER_LEVEL_MAX = 8 * 99 - ER_LEVEL_BASE

The highest rune level the identity can produce: eight attributes at 99 sum to 792, minus ER_LEVEL_BASE.

A roster level past it is not a level.

Definition at line 76 of file er.py.

◆ ER_EMPTY_SLOT_IDS

sl2.er.ER_EMPTY_SLOT_IDS
Initial value:
1= frozenset(
2 {0x0001ADB0, 0x10002710, 0x10002774, 0x100027D8, 0x1000283C}
3)

The ids that mean "this slot is empty", not "the character owns this".

Elden Ring fills an unused equipment slot with a real row rather than a zero, and the game's own name tables name them: weapon row 110000 is Unarmed, and armour rows 10000/10100/10200/10300 are the bare Head/Body/Arms/Legs. They are in the GaItem array of every character alive, so listing them as owned gear says "carries: Unarmed, Arms, Legs" about someone wearing a full set. Skipped, and not counted as unrecognised either — they are recognised perfectly well, they just are not items. Stored as save ids, category nibble included.

Definition at line 110 of file er.py.

◆ ER_CAT

dict sl2.er.ER_CAT = {0x0: "weapons", 0x1: "armors", 0x2: "talismans", 0x4: "goods", 0x8: "ashes"}

ER item category by id top nibble (the ItemGib type code), and the render category each maps to.

Weapon (0x0), Protector/armour (0x1), Accessory/talisman (0x2), Goods (0x4), Gem/Ash of War (0x8). The nibble the GaItem walk already trusts for its tail length is the item TYPE, so it also scopes name resolution — the fix for the old flat lookup that collided base ids across types (~20% wrong).

Definition at line 242 of file er.py.

◆ ER_DB_FILES

sl2.er.ER_DB_FILES = tuple(ER_CAT.values())

ER db category files (one per type), each {8-hex-id: name}.

Definition at line 246 of file er.py.

◆ ER_WEAPON_BASE_STEP

int sl2.er.ER_WEAPON_BASE_STEP = 10000

Weapon ids bake affinity+reinforcement into the low digits: the id is base + affinity*100 + level, base spaced by ER_WEAPON_BASE_STEP.

So id % 100 is the reinforcement level, id - id % 100 is the affinity row (which is what the table names), and id - id % 10000 is the plain base.

Definition at line 253 of file er.py.

◆ ER_WEAPON_AFFINITY_STEP

int sl2.er.ER_WEAPON_AFFINITY_STEP = 100

Definition at line 254 of file er.py.

◆ ER_MAX_REINFORCE

int sl2.er.ER_MAX_REINFORCE = 25

Highest reinforcement ER allows (+25 on the standard path; somber stops at +10).

A low-digit remainder above this is not a level, so no level is claimed.

Definition at line 259 of file er.py.