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 }