elden_ring_saver/
lib.rs

1pub const SLOT_START_INDEX: usize = 0x310;
2pub const SLOT_LENGTH: usize = 0x280000;
3pub const SAVE_HEAD_S_SECTION_START_INDEX: usize = 0x19003B0;
4pub const SAVE_HEAD_S_SECTION_LENGTH: usize = 0x60000;
5pub const SAVE_HEAD_START_INDEX: usize = 0x1901D0E;
6pub const SAVE_HEAD_LENGTH: usize = 0x24C;
7pub const CHAR_ACTIVE_STATUS_START_INDEX: usize = 0x1901D04;
8pub const CHAR_NAME_LENGTH: usize = 0x22;
9pub const CHAR_LEVEL_LOCATION: usize = 0x22;
10pub const CHAR_PLAYED_START_INDEX: usize = 0x26;
11pub const ID_LOCATION: usize = 0x19003B4;
12pub const MAX_SLOT_SIZE: usize = 10;
13
14pub fn get_slot_range(slot_index: usize) -> std::ops::Range<usize> {
15    let st = slot_start_index(slot_index);
16    st..st + SAVE_HEAD_LENGTH
17}
18
19pub fn get_active(data: &[u8], slot_index: usize) -> u8 {
20    data[CHAR_ACTIVE_STATUS_START_INDEX + slot_index]
21}
22
23pub fn get_mut_active(data: &mut [u8], slot_index: usize) -> &mut u8 {
24    &mut data[CHAR_ACTIVE_STATUS_START_INDEX + slot_index]
25}
26pub fn get_character_name(data: &[u8], slot_index: usize) -> &[u8] {
27    let start = get_slot_start(slot_index);
28    let end = start + CHAR_NAME_LENGTH;
29    &data[start..end]
30}
31
32pub fn get_mut_character_name(data: &mut [u8], slot_index: usize) -> &mut [u8] {
33    let start = get_slot_start(slot_index);
34    let end = start + CHAR_NAME_LENGTH;
35    &mut data[start..end]
36}
37
38pub fn get_slot_start(slot_index: usize) -> usize {
39    SAVE_HEAD_START_INDEX + slot_index * SAVE_HEAD_LENGTH
40}
41pub fn get_character_level(data: &[u8], slot_index: usize) -> &u8 {
42    let start = get_slot_start(slot_index);
43    &data[start + CHAR_LEVEL_LOCATION]
44}
45pub fn get_mut_character_level(data: &mut [u8], slot_index: usize) -> &mut u8 {
46    let start = get_slot_start(slot_index);
47    &mut data[start + CHAR_LEVEL_LOCATION]
48}
49pub fn get_seconds_played(data: &[u8], slot_index: usize) -> &[u8] {
50    let start = get_slot_start(slot_index);
51    &data[start + CHAR_PLAYED_START_INDEX..start + CHAR_PLAYED_START_INDEX + 4]
52}
53pub fn get_mut_seconds_played(data: &mut [u8], slot_index: usize) -> &mut [u8] {
54    let start = get_slot_start(slot_index);
55    &mut data[start + CHAR_PLAYED_START_INDEX..start + CHAR_PLAYED_START_INDEX + 4]
56}
57
58#[derive(Debug, Clone)]
59pub struct Slot {
60    pub active: bool,
61    pub seconds_played: u32,
62    pub character_name: String,
63    pub character_level: u32,
64    pub index: usize,
65}
66fn slot_start_index(index: usize) -> usize {
67    SLOT_START_INDEX + (index * 0x10) + (index * SLOT_LENGTH)
68}
69fn head_start_index(index: usize) -> usize {
70    SAVE_HEAD_START_INDEX + (index * SAVE_HEAD_LENGTH)
71}
72
73pub fn get_save_data_range(slot_index: usize) -> std::ops::Range<usize> {
74    SLOT_START_INDEX + slot_index * 0x10 + slot_index * SLOT_LENGTH
75        ..SLOT_START_INDEX + slot_index * 0x10 + (slot_index + 1) * SLOT_LENGTH
76}
77
78pub fn get_head_data_range(slot_index: usize) -> std::ops::Range<usize> {
79    let st = head_start_index(slot_index);
80    st..st + SAVE_HEAD_LENGTH
81}
82pub fn get_save_data(data: &[u8], slot_index: usize) -> &[u8] {
83    &data[get_save_data_range(slot_index)]
84}
85pub fn get_mut_save_data(data: &mut [u8], slot_index: usize) -> &mut [u8] {
86    &mut data[get_save_data_range(slot_index)]
87}
88pub fn get_head_data(data: &[u8], slot_index: usize) -> &[u8] {
89    &data[get_head_data_range(slot_index)]
90}
91pub fn get_mut_head_data(data: &mut [u8], slot_index: usize) -> &mut [u8] {
92    &mut data[get_head_data_range(slot_index)]
93}
94
95fn find_all_slice(data: &[u8], source_id: &[u8]) -> Vec<usize> {
96    let mut indices = Vec::new();
97    let source_id_len = source_id.len();
98
99    if source_id_len == 0 {
100        return indices;
101    }
102
103    for i in 0..=(data.len() - source_id_len) {
104        if data[i..i + source_id_len] == *source_id {
105            indices.push(i);
106        }
107    }
108
109    indices
110}
111
112pub fn get_steam_id_range() -> std::ops::Range<usize> {
113    ID_LOCATION..ID_LOCATION + 8
114}
115
116pub fn set_steam_id(source: &mut [u8], slot_index: usize, source_id: &[u8], target_id: &[u8]) {
117    let save_data = get_mut_save_data(source, slot_index);
118    for i in find_all_slice(save_data, source_id) {
119        save_data[i..i + 8].copy_from_slice(target_id);
120    }
121}
122
123pub fn get_slot_hash_range(slot_index: usize) -> std::ops::Range<usize> {
124    slot_start_index(slot_index) - 0x10..slot_start_index(slot_index)
125}
126
127pub fn get_head_hash_range() -> std::ops::Range<usize> {
128    SAVE_HEAD_S_SECTION_START_INDEX - 0x10..SAVE_HEAD_S_SECTION_START_INDEX
129}
130
131pub fn get_slot(data: &[u8], slot_index: usize) -> Option<Slot> {
132    if data.len() < SAVE_HEAD_START_INDEX + slot_index * SAVE_HEAD_LENGTH {
133        return None;
134    }
135
136    let active = get_active(data, slot_index) == 1;
137
138    let character_name = String::from_utf16_lossy(
139        &get_character_name(data, slot_index)
140            .chunks(2)
141            .map(|c| u16::from_le_bytes([c[0], c[1]]))
142            .collect::<Vec<u16>>(),
143    )
144    .trim_end_matches('\0')
145    .to_owned();
146
147    let character_level = *get_character_level(data, slot_index) as u32;
148
149    let seconds_played = u32::from_le_bytes(
150        get_seconds_played(data, slot_index)
151            .try_into()
152            .unwrap_or([0; 4]),
153    );
154
155    Some(Slot {
156        active,
157        character_name,
158        index: slot_index,
159        character_level,
160        seconds_played,
161    })
162}
163
164pub fn get_all_slots(data: &[u8]) -> Vec<Slot> {
165    (0..MAX_SLOT_SIZE)
166        .filter_map(|i| get_slot(data, i))
167        .collect()
168}
169
170pub fn replace_slot(
171    target: &[u8],
172    target_slot_index: usize,
173    source: &[u8],
174    source_slot_index: usize,
175) -> Vec<u8> {
176    let mut new_data = target.to_vec().clone();
177    let new_bytes = new_data.as_mut_slice();
178
179    let target_id = &target[ID_LOCATION..ID_LOCATION + 8];
180    let source_id = &source[ID_LOCATION..ID_LOCATION + 8];
181    let target_name = get_character_name(target, target_slot_index);
182    let source_name = get_character_name(source, source_slot_index);
183
184    // save data
185    let source_save_data = get_save_data(source, source_slot_index);
186    new_bytes[get_save_data_range(target_slot_index)].copy_from_slice(source_save_data);
187
188    // steam id
189    set_steam_id(new_bytes, target_slot_index, source_id, target_id);
190
191    // head data
192    let head_data = &source[get_head_data_range(source_slot_index)];
193    new_bytes[get_head_data_range(target_slot_index)].copy_from_slice(head_data);
194
195    // set active
196    *get_mut_active(new_bytes, target_slot_index) = 0x01;
197
198    // name
199    for i in find_all_slice(new_bytes, source_name) {
200        new_bytes[i..i + source_name.len()].copy_from_slice(target_name);
201    }
202
203    // slot hash
204    let slot_hash = md5::compute(get_save_data(new_bytes, target_slot_index)).to_vec();
205    new_bytes[get_slot_hash_range(target_slot_index)].copy_from_slice(&slot_hash);
206
207    // head hash
208    let head_hash = md5::compute(get_head_data(new_bytes, target_slot_index)).to_vec();
209    new_bytes[get_head_hash_range()].copy_from_slice(&head_hash);
210
211    new_data
212}
213
214#[cfg(test)]
215mod test {
216    use crate::{get_all_slots, replace_slot};
217
218    #[test]
219    fn get_name() {
220        let source = std::fs::read("../assets/source.sl2").unwrap();
221        let slot = get_all_slots(&source);
222
223        for i in slot {
224            println!("{:?}", i)
225        }
226        let target = std::fs::read("../assets/empty.sl2").unwrap();
227
228        let slot = get_all_slots(&target);
229
230        for i in slot {
231            println!("{:?}", i)
232        }
233
234        let target_slot_index = 0;
235        let source_slot_index = 5;
236        replace_slot(&target, target_slot_index, &source, source_slot_index);
237    }
238}