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 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 set_steam_id(new_bytes, target_slot_index, source_id, target_id);
190
191 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 *get_mut_active(new_bytes, target_slot_index) = 0x01;
197
198 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 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 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}