1 module gbaid.save;
2 
3 import std.format : format;
4 import std.typecons : tuple, Tuple;
5 import std.file : read, FileException;
6 import std.stdio : File;
7 import std.algorithm.comparison : min;
8 import std.bitmanip : littleEndianToNative, nativeToLittleEndian;
9 import std.digest.crc : CRC32;
10 import std.zlib : compress, uncompress;
11 
12 import gbaid.gba.memory : GamePakData, MainSaveKind, EEPROM_SIZE, SRAM_SIZE, FLASH_512K_SIZE, FLASH_1M_SIZE, RTC_SIZE;
13 
14 import gbaid.util;
15 
16 public alias RawSaveMemory = Tuple!(SaveMemoryKind, void[]);
17 
18 public enum SaveMemoryKind : int {
19     EEPROM = 0,
20     SRAM = 1,
21     FLASH_512K = 2,
22     FLASH_1M = 3,
23     RTC = 4
24 }
25 
26 private enum int SAVE_CURRENT_VERSION = 1;
27 private immutable char[8] SAVE_FORMAT_MAGIC = "GBAiDSav";
28 
29 public enum int[SaveMemoryKind] memoryCapacityForSaveKind = [
30     SaveMemoryKind.EEPROM: EEPROM_SIZE,
31     SaveMemoryKind.SRAM: SRAM_SIZE,
32     SaveMemoryKind.FLASH_512K: FLASH_512K_SIZE,
33     SaveMemoryKind.FLASH_1M: FLASH_1M_SIZE,
34     SaveMemoryKind.RTC: RTC_SIZE,
35 ];
36 
37 private enum string[][MainSaveKind] mainSaveKindIds = [
38     MainSaveKind.SRAM: ["SRAM_V"],
39     MainSaveKind.FLASH_512K: ["FLASH_V", "FLASH512_V"],
40     MainSaveKind.FLASH_1M: ["FLASH1M_V"],
41 ];
42 
43 private enum string eepromSaveId = "EEPROM_V";
44 
45 public enum MainSaveConfig : int {
46     SRAM = MainSaveKind.SRAM,
47     FLASH_512K = MainSaveKind.FLASH_512K,
48     FLASH_1M = MainSaveKind.FLASH_1M,
49     NONE = MainSaveKind.NONE,
50     AUTO = -1
51 }
52 
53 public enum EepromConfig {
54     ON, OFF, AUTO
55 }
56 
57 public enum RtcConfig {
58     ON, OFF, AUTO
59 }
60 
61 public GamePakData gamePakForNewRom(string romFile = null, MainSaveConfig mainSaveConfig = MainSaveConfig.AUTO,
62         EepromConfig eepromConfig = EepromConfig.AUTO,
63         RtcConfig rtcConfig = RtcConfig.AUTO) {
64     return gamePakForRom(romFile, null, mainSaveConfig, eepromConfig, rtcConfig);
65 }
66 
67 public GamePakData gamePakForExistingRom(string romFile, string saveFile,
68         EepromConfig eepromConfig = EepromConfig.AUTO,
69         RtcConfig rtcConfig = RtcConfig.AUTO) {
70     return gamePakForRom(romFile, saveFile, MainSaveConfig.AUTO, eepromConfig, rtcConfig);
71 }
72 
73 private GamePakData gamePakForRom(string romFile, string saveFile, MainSaveConfig mainSaveConfig,
74         EepromConfig eepromConfig, RtcConfig rtcConfig) {
75     GamePakData gamePakData;
76     // Load the ROM if provided
77     if (romFile !is null) {
78         try {
79             gamePakData.rom = romFile.read();
80         } catch (FileException ex) {
81             throw new Exception("Cannot read ROM file", ex);
82         }
83     }
84     // Load the save file if provided, otherwise create the main save from the config
85     if (saveFile !is null) {
86         saveFile.loadSave(gamePakData);
87     } else {
88         final switch (mainSaveConfig) with (MainSaveConfig) {
89             case SRAM:
90             case FLASH_512K:
91             case FLASH_1M:
92             case NONE:
93                 gamePakData.mainSaveKind = cast(MainSaveKind) mainSaveConfig;
94                 break;
95             case AUTO:
96                 gamePakData.mainSaveKind = gamePakData.rom.detectMainSaveKind();
97                 break;
98         }
99     }
100     // Load the EEPROM
101     final switch (eepromConfig) with (EepromConfig) {
102         case ON:
103             gamePakData.eepromEnabled = true;
104             break;
105         case OFF:
106             gamePakData.eepromEnabled = false;
107             break;
108         case AUTO:
109             if (saveFile is null) {
110                 gamePakData.eepromEnabled = gamePakData.rom.detectNeedEeprom();
111             }
112             break;
113     }
114     // Load the RTC
115     final switch (rtcConfig) with (RtcConfig) {
116         case ON:
117             gamePakData.rtcEnabled = true;
118             break;
119         case OFF:
120             gamePakData.rtcEnabled = false;
121             break;
122         case AUTO:
123             if (saveFile is null) {
124                 gamePakData.rtcEnabled = gamePakData.rom.detectNeedRtc();
125             }
126             break;
127     }
128     return gamePakData;
129 }
130 
131 private void loadSave(string saveFile, ref GamePakData gamePakData) {
132     RawSaveMemory[] memories = saveFile.loadSaveFile();
133     bool foundSave = false, foundEeprom = false, foundRtc = false;
134     foreach (memory; memories) {
135         switch (memory[0]) with (SaveMemoryKind) {
136             case SRAM:
137                 checkSaveMissing(foundSave);
138                 gamePakData.mainSave = memory[1];
139                 gamePakData.mainSaveKind = MainSaveKind.SRAM;
140                 break;
141             case FLASH_512K:
142                 checkSaveMissing(foundSave);
143                 gamePakData.mainSave = memory[1];
144                 gamePakData.mainSaveKind = MainSaveKind.FLASH_512K;
145                 break;
146             case FLASH_1M:
147                 checkSaveMissing(foundSave);
148                 gamePakData.mainSave = memory[1];
149                 gamePakData.mainSaveKind = MainSaveKind.FLASH_1M;
150                 break;
151             case EEPROM:
152                 checkSaveMissing(foundEeprom);
153                 gamePakData.eeprom = memory[1];
154                 gamePakData.eepromEnabled = true;
155                 break;
156             case RTC:
157                 checkSaveMissing(foundRtc);
158                 gamePakData.rtc = memory[1];
159                 gamePakData.rtcEnabled = true;
160                 break;
161             default:
162                 throw new Exception(format("Unsupported memory save type: %d", memory[0]));
163         }
164     }
165     // The Classis NES series games only have an EEPROM, so this is can happen
166     if (!foundSave) {
167         gamePakData.mainSaveKind = MainSaveKind.NONE;
168     }
169 }
170 
171 private void checkSaveMissing(ref bool found) {
172     if (found) {
173         throw new Exception("Found more than one possible save memory in the save file");
174     }
175     found = true;
176 }
177 
178 private MainSaveKind detectMainSaveKind(void[] rom) {
179     auto romChars = cast(char[]) rom;
180     // The Classic NES series game lie about having and SRAM and refuse to boot if you have one
181     if (romChars.length >= 0xAC && romChars[0xAC] == 'F') {
182         // If the 4 character game code starts with F, then it is a Classic NES series game
183         return MainSaveKind.NONE;
184     }
185     // Search for the save IDs in the ROM
186     foreach (saveKind, saveIds; mainSaveKindIds) {
187         foreach (saveId; saveIds) {
188             for (size_t i = 0; i < romChars.length; i += 4) {
189                 if (romChars[i .. min(i + saveId.length, $)] == saveId) {
190                     return saveKind;
191                 }
192             }
193         }
194     }
195     // Most games that don't declare a save memory kind use an SRAM
196     return MainSaveKind.SRAM;
197 }
198 
199 private bool detectNeedEeprom(void[] rom) {
200     auto romChars = cast(char[]) rom;
201     for (size_t i = 0; i < romChars.length; i += 4) {
202         if (romChars[i .. min(i + eepromSaveId.length, $)] == eepromSaveId) {
203             return true;
204         }
205     }
206     return false;
207 }
208 
209 private bool detectNeedRtc(void[] rom) {
210     auto romChars = cast(char[]) rom;
211     if (romChars.length < 0xAC) {
212         return false;
213     }
214     // Pokémon Ruby/Sapphire/Emerald and Botkai 1 and 2 use an RTC
215     if (romChars[0xAC] == 'U') {
216         // This is the Botkai code
217         return true;
218     }
219     // For Pokémon we use the game title
220     auto title = romChars[0xA0 .. 0xAC];
221     return title == "POKEMON RUBY" || title == "POKEMON SAPP" || title == "POKEMON EMER";
222 }
223 
224 public void saveGamePak(GamePakData gamePakData, string saveFile) {
225     RawSaveMemory[] memories;
226     final switch (gamePakData.mainSaveKind) with (MainSaveKind) {
227         case SRAM:
228             memories ~= tuple(SaveMemoryKind.SRAM, gamePakData.mainSave);
229             break;
230         case FLASH_512K:
231             memories ~= tuple(SaveMemoryKind.FLASH_512K, gamePakData.mainSave);
232             break;
233         case FLASH_1M:
234             memories ~= tuple(SaveMemoryKind.FLASH_1M, gamePakData.mainSave);
235             break;
236         case NONE:
237             break;
238     }
239     if (gamePakData.eepromEnabled) {
240         memories ~= tuple(SaveMemoryKind.EEPROM, gamePakData.eeprom);
241     }
242     if (gamePakData.rtcEnabled) {
243         memories ~= tuple(SaveMemoryKind.RTC, gamePakData.rtc);
244     }
245     memories.saveSaveFile(saveFile);
246 }
247 
248 /*
249 All ints are 32 bit and stored in little endian.
250 The CRCs are calculated on the little endian values
251 
252 Format:
253     Header:
254         8 bytes: magic number (ASCII string of "GBAiDSav")
255         1 int: version number
256         1 int: option flags
257         1 int: number of memory objects (n)
258         1 int: CRC of the memory header (excludes magic number)
259     Body:
260         n memory blocks:
261             1 int: memory kind ID, as per the SaveMemoryKind enum
262             1 int: compressed memory size in bytes (c)
263             c bytes: memory compressed with zlib
264             1 int: CRC of the memory block
265 */
266 
267 public RawSaveMemory[] loadSaveFile(string filePath) {
268     // Open the file in binary to read
269     auto file = File(filePath, "rb");
270     // Read the first 8 bytes to make sure it is a save file for GBAiD
271     char[8] magicChars;
272     file.rawRead(magicChars);
273     if (magicChars != SAVE_FORMAT_MAGIC) {
274         throw new Exception(format("Not a GBAiD save file (magic number isn't \"%s\" in ASCII)", SAVE_FORMAT_MAGIC));
275     }
276     // Read the version number
277     auto versionNumber = file.readLittleEndianInt();
278     // Use the proper reader for the version
279     switch (versionNumber) {
280         case 1:
281             return file.readRawSaveMemories!1();
282         default:
283             throw new Exception(format("Unknown save file version: %d", versionNumber));
284     }
285 }
286 
287 private RawSaveMemory[] readRawSaveMemories(int versionNumber: 1)(File file) {
288     CRC32 hash;
289     hash.put([0x1, 0x0, 0x0, 0x0]);
290     // Read the options flags, which are unused in this version
291     auto optionFlags = file.readLittleEndianInt(&hash);
292     // Read the number of save memories in the file
293     auto memoryCount = file.readLittleEndianInt(&hash);
294     // Read the header CRC checksum
295     ubyte[4] headerCrc;
296     file.rawRead(headerCrc);
297     // Check the CRC
298     if (hash.finish() != headerCrc) {
299         throw new Exception("The save file has a corrupted header, the CRCs do not match");
300     }
301     // Read all the raw save memories according to the configurations given by the header
302     RawSaveMemory[] saveMemories;
303     foreach (i; 0 .. memoryCount) {
304         saveMemories ~= file.readRawSaveMemory!versionNumber();
305     }
306     return saveMemories;
307 }
308 
309 private RawSaveMemory readRawSaveMemory(int versionNumber: 1)(File file) {
310     CRC32 hash;
311     // Read the memory kind
312     auto kindId = file.readLittleEndianInt(&hash);
313     // Read the memory compressed size
314     auto compressedSize = file.readLittleEndianInt(&hash);
315     // Read the compressed bytes for the memory
316     ubyte[] memoryCompressed;
317     memoryCompressed.length = compressedSize;
318     file.rawRead(memoryCompressed);
319     hash.put(memoryCompressed);
320     // Read the block CRC checksum
321     ubyte[4] blockCrc;
322     file.rawRead(blockCrc);
323     // Check the CRC
324     if (hash.finish() != blockCrc) {
325         throw new Exception("The save file has a corrupted memory block, the CRCs do not match");
326     }
327     // Get the memory length according to the kind
328     auto saveKind = cast(SaveMemoryKind) kindId;
329     auto uncompressedSize = memoryCapacityForSaveKind[saveKind];
330     // Uncompress the memory
331     auto memoryUncompressed = memoryCompressed.uncompress(uncompressedSize);
332     // Check that the uncompressed length matches the one for the kind
333     if (memoryUncompressed.length != uncompressedSize) {
334         throw new Exception(format("The uncompressed save memory has a different length than its kind: %d != %d",
335                 memoryUncompressed.length, uncompressedSize));
336     }
337     return tuple(saveKind, memoryUncompressed);
338 }
339 
340 public void saveSaveFile(RawSaveMemory[] saveMemories, string filePath) {
341     // Open the file in binary to write
342     auto file = File(filePath, "wb");
343     // First we write the magic
344     file.rawWrite(SAVE_FORMAT_MAGIC);
345     // Next we write the current version
346     CRC32 hash;
347     file.writeLittleEndianInt(SAVE_CURRENT_VERSION, &hash);
348     // Next we write the option flags, which are empty since they are unused
349     file.writeLittleEndianInt(0, &hash);
350     // Next we write the number of memory blocks
351     file.writeLittleEndianInt(cast(int) saveMemories.length, &hash);
352     // Next we write the header CRC
353     file.rawWrite(hash.finish());
354     // Finally write the memory blocks
355     foreach (saveMemory; saveMemories) {
356         file.writeRawSaveMemory(saveMemory);
357     }
358 }
359 
360 private void writeRawSaveMemory(File file, RawSaveMemory saveMemory) {
361     CRC32 hash;
362     // Write the memory kind
363     file.writeLittleEndianInt(saveMemory[0], &hash);
364     // Compress the save memory
365     auto memoryCompressed = saveMemory[1].compress();
366     // Write the memory compressed size
367     file.writeLittleEndianInt(cast(int) memoryCompressed.length, &hash);
368     // Write the compressed bytes for the memory
369     hash.put(memoryCompressed);
370     file.rawWrite(memoryCompressed);
371     // Write the block CRC
372     file.rawWrite(hash.finish());
373 }
374 
375 private int readLittleEndianInt(File file, CRC32* hash = null) {
376     ubyte[4] numberBytes;
377     file.rawRead(numberBytes);
378     if (hash !is null) {
379         hash.put(numberBytes);
380     }
381     return numberBytes.littleEndianToNative!int();
382 }
383 
384 private void writeLittleEndianInt(File file, int i, CRC32* hash = null) {
385     auto numberBytes = i.nativeToLittleEndian();
386     if (hash !is null) {
387         hash.put(numberBytes);
388     }
389     file.rawWrite(numberBytes);
390 }