@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 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
@@ -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 };