@zebbedaja/er-save-parser 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/index.d.mts +145 -0
- package/dist/index.mjs +12271 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daniel Zsebedits
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# ER Save Parser
|
|
2
|
+
|
|
3
|
+
Parse Elden Ring PC save files into structured TypeScript/JavaScript objects.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Full save slot parsing** — all 10 slots with character data, attributes, Flask counts, regions visited, death count, and more
|
|
8
|
+
- **Event flag decoding** — track boss defeats, quest progress, and ending states using bit-level BST map lookups
|
|
9
|
+
- **Settings extraction** — camera, audio, HDR, ray tracing, and other game settings
|
|
10
|
+
- **Profile summaries** — character name, level, play time, starting gift, and archetype per slot
|
|
11
|
+
- **Zero runtime dependencies** — pure ESM, no bundled dependencies
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @zebbedaja/er-save-parser
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { parse, type Save, type Slot, type Character } from '@zebbedaja/er-save-parser'
|
|
23
|
+
import { readFileSync } from 'fs'
|
|
24
|
+
|
|
25
|
+
const buffer = readFileSync('ER0000.sl2').buffer
|
|
26
|
+
const save: Save = parse(buffer)
|
|
27
|
+
|
|
28
|
+
console.log(save.steamId)
|
|
29
|
+
console.log(save.settings?.hud)
|
|
30
|
+
|
|
31
|
+
const slot: Slot | undefined = save.slots?.[0]
|
|
32
|
+
const char: Character | undefined = slot?.character
|
|
33
|
+
|
|
34
|
+
if (char) {
|
|
35
|
+
console.log(char.characterName) // "Tarnished"
|
|
36
|
+
console.log(char.level) // 150
|
|
37
|
+
console.log(char.runes) // 1234567
|
|
38
|
+
console.log(char.strength) // 45
|
|
39
|
+
console.log(char.faith) // 30
|
|
40
|
+
console.log(char.arcane) // 20
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check boss defeats
|
|
44
|
+
slot?.eventFlags?.forEach((flag) => {
|
|
45
|
+
if (flag.state) {
|
|
46
|
+
console.log(`${flag.name} ✓`)
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Reading from a file in the browser
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
async function parseFromFile(file: File): Promise<Save> {
|
|
55
|
+
const buffer = await file.arrayBuffer()
|
|
56
|
+
return parse(buffer)
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Parsed Data
|
|
61
|
+
|
|
62
|
+
### `Save` (root)
|
|
63
|
+
| Field | Type | Description |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `magicBytes` | `string` | Hex string, always `424e4434` (`BND4`) |
|
|
66
|
+
| `checksum` | `string` | Save file checksum |
|
|
67
|
+
| `version` | `number` | Save file version |
|
|
68
|
+
| `steamId` | `string` | Associated Steam64 ID |
|
|
69
|
+
| `settings` | `Settings` | Game settings (camera, audio, HDR, ray tracing…) |
|
|
70
|
+
| `activeProfiles` | `number[]` | Active profile flags per slot |
|
|
71
|
+
| `profileSummaries` | `ProfileSummary[]` | Name, level, play time per slot |
|
|
72
|
+
| `slots` | `Slot[]` | Up to 10 save slots (see below) |
|
|
73
|
+
|
|
74
|
+
### `Slot`
|
|
75
|
+
| Field | Type | Description |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `checksum` | `string` | Slot-level checksum |
|
|
78
|
+
| `version` | `number` | Slot version |
|
|
79
|
+
| `mapId` | `string` | Current location map ID (hex) |
|
|
80
|
+
| `character` | `Character` | Full character data |
|
|
81
|
+
| `regions` | `object` | Regions visited ({ `regionCount`, `regionIds` }) |
|
|
82
|
+
| `totalDeathCount` | `number` | Cumulative deaths across playthroughs |
|
|
83
|
+
| `characterType` | `number` | Save type indicator |
|
|
84
|
+
| `inOnlineSessionFlag` | `number` | Online session flag |
|
|
85
|
+
| `lastRestedGrace` | `number` | Last grace site rested at |
|
|
86
|
+
| `notAloneFlag` | `number` | Co-op phantoms present |
|
|
87
|
+
| `inGameCountdownTimer` | `number` | Online timeout countdown |
|
|
88
|
+
| `eventFlags` | `EventFlag[]` | Boss defeats, quest progress, endings |
|
|
89
|
+
|
|
90
|
+
### `Character`
|
|
91
|
+
| Field | Type | Description |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| `characterName` | `string` | In-game name |
|
|
94
|
+
| `level` | `number` | Character level |
|
|
95
|
+
| `runes` | `number` | Current runes (currency) |
|
|
96
|
+
| `runesMemory` | `number` | Maximum runes that can be held |
|
|
97
|
+
| `hp` / `maxHp` / `baseMaxHp` | `number` | HP current / max / base max |
|
|
98
|
+
| `fp` / `maxFp` / `baseMaxFp` | `number` | FP current / max / base max |
|
|
99
|
+
| `sp` / `maxSp` / `baseMaxSp` | `number` | Stamina current / max / base max |
|
|
100
|
+
| `vigor` / `mind` / `endurance` | `number` | ATTRIBUTE: Vigor / Mind / Endurance |
|
|
101
|
+
| `strength` / `dexterity` / `intelligence` / `faith` / `arcane` | `number` | ATTRIBUTE: STR / DEX / INT / FTH / ARC |
|
|
102
|
+
| `poisonBuildup` | `number` | Current poison status effect |
|
|
103
|
+
| `rotBuildup` | `number` | Current scarlet rot status effect |
|
|
104
|
+
| `bleedBuildup` | `number` | Current hemorrhage status effect |
|
|
105
|
+
| `frostBuildup` | `number` | Current frozen status effect |
|
|
106
|
+
| `madnessBuildup` | `number` | Current madness status effect |
|
|
107
|
+
| `bodyType` | `number` | Body type selection index |
|
|
108
|
+
| `voiceType` | `number` | Voice type selection index |
|
|
109
|
+
| `archetype` | `number` | Starting class selection index |
|
|
110
|
+
| `gift` | `number` | Starting gift selection index |
|
|
111
|
+
| `maxCrimsonTearFlaskCount` | `number` | Flask of Crimson Tears quantity |
|
|
112
|
+
| `maxCeruleanTearFlaskCount` | `number` | Flask of Cerulean Tears quantity |
|
|
113
|
+
| `additionalTalismanSlotCount` | `number` | Extra talisman slots unlocked |
|
|
114
|
+
| `summonSpiritLevel` | `number` | Spirit Ash upgrade level |
|
|
115
|
+
| `aquiredProjectilesCount` | `number` | Acquired projectile count |
|
|
116
|
+
|
|
117
|
+
### `Settings`
|
|
118
|
+
| Field | Type | Description |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| `cameraSpeed` | `number` | Camera rotation speed |
|
|
121
|
+
| `brightness` | `number` | Display brightness |
|
|
122
|
+
| `musicVolume` | `number` | Music volume |
|
|
123
|
+
| `soundEffectsVolume` | `number` | SFX volume |
|
|
124
|
+
| `voiceVolume` | `number` | Dialogue/voice volume |
|
|
125
|
+
| `master_volume` | `number` | Master volume |
|
|
126
|
+
| `hud` | `number` | HUD visibility |
|
|
127
|
+
| `subtitles` | `number` | Subtitle toggle |
|
|
128
|
+
| `displayBlood` | `number` | Gore/display filter |
|
|
129
|
+
| `hdr` | `number` | HDR toggle |
|
|
130
|
+
| `is_raytracing_on` | `number` | Ray tracing toggle |
|
|
131
|
+
| `autotarget` | `number` | Auto-aim toggle |
|
|
132
|
+
| `cameraXAxis` / `cameraYAxis` | `number` | Camera axis configuration |
|
|
133
|
+
| `perform_matchmaking` | `number` | Online matchmaking toggle |
|
|
134
|
+
| ... | | (and more) |
|
|
135
|
+
|
|
136
|
+
### `EventFlag`
|
|
137
|
+
| Field | Type | Description |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| `name` | `string` | Human-readable name (e.g., `"Godrick the Grafted"`) |
|
|
140
|
+
| `id` | `number` | Internal game event ID |
|
|
141
|
+
| `category` | `string` | Category: `boss`, `ending`, `quest`, etc. |
|
|
142
|
+
| `location` | `string` | Location (if applicable) |
|
|
143
|
+
| `state` | `boolean` | `true` if triggered |
|
|
144
|
+
|
|
145
|
+
## Error handling
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
try {
|
|
149
|
+
const save = parse(buffer)
|
|
150
|
+
} catch (error: unknown) {
|
|
151
|
+
if (error instanceof Error) {
|
|
152
|
+
console.error('Failed to parse save file:', error.message)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The parser will throw if:
|
|
158
|
+
- The file magic bytes don't match `BND4`
|
|
159
|
+
- Event flags reference blocks not found in the BST map
|
|
160
|
+
- Calculated byte positions exceed save data bounds
|
|
161
|
+
|
|
162
|
+
## Development
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
npm install # Install dependencies
|
|
166
|
+
npm run test # Run Vitest suite
|
|
167
|
+
npm run typecheck # Run TypeScript type check
|
|
168
|
+
npm run build # Bundle with tsdown
|
|
169
|
+
npm run dev # Watch mode
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## License
|
|
173
|
+
|
|
174
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
interface Save {
|
|
3
|
+
magicBytes?: string;
|
|
4
|
+
checksum?: string;
|
|
5
|
+
version?: number;
|
|
6
|
+
slots?: Slot[];
|
|
7
|
+
steamId?: string;
|
|
8
|
+
settings?: Settings;
|
|
9
|
+
activeProfiles?: number[];
|
|
10
|
+
profileSummaries?: ProfileSummary[];
|
|
11
|
+
}
|
|
12
|
+
interface Settings {
|
|
13
|
+
cameraSpeed?: number;
|
|
14
|
+
controllerVibration?: number;
|
|
15
|
+
brightness?: number;
|
|
16
|
+
unk0x3?: number;
|
|
17
|
+
musicVolume?: number;
|
|
18
|
+
soundEffectsVolume?: number;
|
|
19
|
+
voiceVolume?: number;
|
|
20
|
+
displayBlood?: number;
|
|
21
|
+
subtitles?: number;
|
|
22
|
+
hud?: number;
|
|
23
|
+
cameraXAxis?: number;
|
|
24
|
+
cameraYAxis?: number;
|
|
25
|
+
toggle_auto_lockon?: number;
|
|
26
|
+
camera_auto_wall_recovery?: number;
|
|
27
|
+
unk0xe?: number;
|
|
28
|
+
unk0xf?: number;
|
|
29
|
+
reset_camera_y_axis?: number;
|
|
30
|
+
cinematic_effects?: number;
|
|
31
|
+
unk0x12?: number;
|
|
32
|
+
perform_matchmaking?: number;
|
|
33
|
+
unk0x14?: number;
|
|
34
|
+
unk0x15?: number;
|
|
35
|
+
manual_attack_aim?: number;
|
|
36
|
+
autotarget?: number;
|
|
37
|
+
launchsettings?: number;
|
|
38
|
+
send_summon_sign?: number;
|
|
39
|
+
unk0x1a?: number;
|
|
40
|
+
hdr?: number;
|
|
41
|
+
hdr_adjust_brightness?: number;
|
|
42
|
+
hdr_maximum_brightness?: number;
|
|
43
|
+
hdr_adjust_saturation?: number;
|
|
44
|
+
unk0x1f?: number;
|
|
45
|
+
master_volume?: number;
|
|
46
|
+
is_raytracing_on?: number;
|
|
47
|
+
mark_new_items?: number;
|
|
48
|
+
show_recent_tabs?: number;
|
|
49
|
+
}
|
|
50
|
+
interface ProfileSummary {
|
|
51
|
+
name?: string;
|
|
52
|
+
level?: number;
|
|
53
|
+
secondsPlayed?: number;
|
|
54
|
+
runesMemory?: number;
|
|
55
|
+
mapId?: string;
|
|
56
|
+
unk0x34?: number;
|
|
57
|
+
bodyType?: number;
|
|
58
|
+
archetype?: number;
|
|
59
|
+
startingGift?: number;
|
|
60
|
+
}
|
|
61
|
+
interface Slot {
|
|
62
|
+
checksum?: string;
|
|
63
|
+
version?: number;
|
|
64
|
+
mapId?: string;
|
|
65
|
+
character?: Character;
|
|
66
|
+
regions?: {
|
|
67
|
+
regionCount?: number;
|
|
68
|
+
regionIds?: number[];
|
|
69
|
+
};
|
|
70
|
+
totalDeathCount?: number;
|
|
71
|
+
characterType?: number;
|
|
72
|
+
inOnlineSessionFlag?: number;
|
|
73
|
+
characterTypeOnline?: number;
|
|
74
|
+
lastRestedGrace?: number;
|
|
75
|
+
notAloneFlag?: number;
|
|
76
|
+
inGameCountdownTimer?: number;
|
|
77
|
+
eventFlags?: EventFlag[];
|
|
78
|
+
}
|
|
79
|
+
interface Character {
|
|
80
|
+
unk0x0?: number;
|
|
81
|
+
unk0x4?: number;
|
|
82
|
+
hp?: number;
|
|
83
|
+
maxHp?: number;
|
|
84
|
+
baseMaxHp?: number;
|
|
85
|
+
fp?: number;
|
|
86
|
+
maxFp?: number;
|
|
87
|
+
baseMaxFp?: number;
|
|
88
|
+
unk0x20?: number;
|
|
89
|
+
sp?: number;
|
|
90
|
+
maxSp?: number;
|
|
91
|
+
baseMaxSp?: number;
|
|
92
|
+
unk0x30?: number;
|
|
93
|
+
vigor?: number;
|
|
94
|
+
mind?: number;
|
|
95
|
+
endurance?: number;
|
|
96
|
+
strength?: number;
|
|
97
|
+
dexterity?: number;
|
|
98
|
+
intelligence?: number;
|
|
99
|
+
faith?: number;
|
|
100
|
+
arcane?: number;
|
|
101
|
+
unk0x54?: number;
|
|
102
|
+
unk0x58?: number;
|
|
103
|
+
unk0x5c?: number;
|
|
104
|
+
level?: number;
|
|
105
|
+
runes?: number;
|
|
106
|
+
runesMemory?: number;
|
|
107
|
+
unk0x6c?: number;
|
|
108
|
+
poisonBuildup?: number;
|
|
109
|
+
rotBuildup?: number;
|
|
110
|
+
bleedBuildup?: number;
|
|
111
|
+
deathBuildup?: number;
|
|
112
|
+
frostBuildup?: number;
|
|
113
|
+
sleepBuildup?: number;
|
|
114
|
+
madnessBuildup?: number;
|
|
115
|
+
unk0x8c?: number;
|
|
116
|
+
unk0x90?: number;
|
|
117
|
+
characterName?: string;
|
|
118
|
+
bodyType?: number;
|
|
119
|
+
archetype?: number;
|
|
120
|
+
unk0xb8?: number;
|
|
121
|
+
unk0xb9?: number;
|
|
122
|
+
voiceType?: number;
|
|
123
|
+
gift?: number;
|
|
124
|
+
unk0xbc?: number;
|
|
125
|
+
unk0xbd?: number;
|
|
126
|
+
additionalTalismanSlotCount?: number;
|
|
127
|
+
summonSpiritLevel?: number;
|
|
128
|
+
unk0xc0?: number;
|
|
129
|
+
maxCrimsonTearFlaskCount?: number;
|
|
130
|
+
maxCeruleanTearFlaskCount?: number;
|
|
131
|
+
aquiredProjectilesCount?: number;
|
|
132
|
+
}
|
|
133
|
+
interface EventFlag {
|
|
134
|
+
name: string;
|
|
135
|
+
id: number;
|
|
136
|
+
category?: string;
|
|
137
|
+
location?: string;
|
|
138
|
+
state?: boolean;
|
|
139
|
+
}
|
|
140
|
+
//#endregion
|
|
141
|
+
//#region src/parser.d.ts
|
|
142
|
+
declare function fn(): string;
|
|
143
|
+
declare function parse(buffer: ArrayBuffer): Save;
|
|
144
|
+
//#endregion
|
|
145
|
+
export { Character, EventFlag, ProfileSummary, Save, Settings, Slot, fn, parse };
|