diff --git a/settings.json b/settings.json new file mode 100644 index 0000000..cfcd8ab --- /dev/null +++ b/settings.json @@ -0,0 +1,7 @@ +{ + "Title":"gomeboycolor", + "ScreenSize":2, + "SkipBoot":true, + "SavesDir":"./saves", + "DisplayFPS":false +} diff --git a/src/cartridge/MBC.go b/src/cartridge/MBC.go index b633e32..26e4363 100644 --- a/src/cartridge/MBC.go +++ b/src/cartridge/MBC.go @@ -1,19 +1,12 @@ package cartridge -import ( - "bufio" - "errors" - "fmt" - "io/ioutil" - "os" - "types" -) +import "types" type MemoryBankController interface { Write(addr types.Word, value byte) Read(addr types.Word) byte - SaveRam(filename string) error - LoadRam(filename string) error + SaveRam(savesDir string, game string) error + LoadRam(savesDir string, game string) error switchROMBank(bank int) switchRAMBank(bank int) } @@ -41,61 +34,3 @@ func populateRAMBanks(noOfBanks int) [][]byte { return ramBanks } - -func WriteRAMToDisk(path string, ramBanks [][]byte) error { - file, err := os.Create(path) - if err != nil { - return err - } - defer file.Close() - - writer := bufio.NewWriter(file) - for i := 0; i < len(ramBanks); i++ { - writer.Write(ramBanks[i]) - /* - var b bytes.Buffer - w := zlib.NewWriter(&b) - w.Write(ramBanks[i]) - w.Close() - before := base64.StdEncoding.EncodeToString(b.Bytes()) - fmt.Println("Before = ", len(b.Bytes())) - - after,_ := base64.StdEncoding.DecodeString(before) - b = *bytes.NewBuffer(after) - r, err := zlib.NewReader(&b) - if err != nil { - panic(fmt.Sprintln("Error" , err)) - } - out := bytes.NewBuffer(make([]byte,0)) - io.Copy(out, r) - fmt.Println("After = ", len(out.Bytes())) - fmt.Println(bytes.Compare(ramBanks[i], out.Bytes())) - r.Close() - */ - - } - writer.Flush() - return nil -} - -func ReadRAMFromDisk(path string, chunkSize int, expectedSize int) ([][]byte, error) { - fileBytes, err := ioutil.ReadFile(path) - if err != nil { - return nil, err - } - - if len(fileBytes) != expectedSize { - return nil, errors.New(fmt.Sprintf("RAM file is not %d bytes!", expectedSize)) - } - - var chunk int = 0x0000 - var noOfBanks int = expectedSize / chunkSize - var ramBanks [][]byte = make([][]byte, noOfBanks) - - for i := 0; i < noOfBanks; i++ { - ramBanks[i] = fileBytes[chunk : chunk+chunkSize] - chunk += chunkSize - } - - return ramBanks, nil -} diff --git a/src/cartridge/MBC0.go b/src/cartridge/MBC0.go index dd2a18a..4c01aaa 100644 --- a/src/cartridge/MBC0.go +++ b/src/cartridge/MBC0.go @@ -49,10 +49,10 @@ func (m *MBC0) switchRAMBank(bank int) { // not needed for MBC0 } -func (m *MBC0) SaveRam(filename string) error { +func (m *MBC0) SaveRam(savesDir string, game string) error { return nil } -func (m *MBC0) LoadRam(filename string) error { +func (m *MBC0) LoadRam(savesDir string, game string) error { return nil } diff --git a/src/cartridge/MBC1.go b/src/cartridge/MBC1.go index 26ae627..2312b8b 100644 --- a/src/cartridge/MBC1.go +++ b/src/cartridge/MBC1.go @@ -134,28 +134,25 @@ func (m *MBC1) switchRAMBank(bank int) { m.selectedRAMBank = bank } -func (m *MBC1) SaveRam(filename string) error { +func (m *MBC1) SaveRam(savesDir string, game string) error { if m.hasRAM && m.hasBattery { - log.Println(m.Name+":", "Saving RAM to", filename) - err := WriteRAMToDisk(filename, m.ramBanks) - if err != nil { - return err - } + s := NewSaveFile(savesDir, game) + err := s.Save(m.ramBanks) + s = nil + return err } - return nil } -func (m *MBC1) LoadRam(filename string) error { +func (m *MBC1) LoadRam(savesDir string, game string) error { if m.hasRAM && m.hasBattery { - log.Println(m.Name+":", "Loading RAM from", filename) - ramBanks, err := ReadRAMFromDisk(filename, 0x2000, 0x8000) + s := NewSaveFile(savesDir, game) + banks, err := s.Load(4) if err != nil { return err } - - m.ramBanks = ramBanks + m.ramBanks = banks + s = nil } - return nil } diff --git a/src/cartridge/MBC3.go b/src/cartridge/MBC3.go index 444f804..7ebc3d6 100644 --- a/src/cartridge/MBC3.go +++ b/src/cartridge/MBC3.go @@ -115,28 +115,25 @@ func (m *MBC3) switchRAMBank(bank int) { m.selectedRAMBank = bank } -func (m *MBC3) SaveRam(filename string) error { +func (m *MBC3) SaveRam(savesDir string, game string) error { if m.hasRAM && m.hasBattery { - log.Println(m.Name+":", "Saving RAM to", filename) - err := WriteRAMToDisk(filename, m.ramBanks) - if err != nil { - return err - } + s := NewSaveFile(savesDir, game) + err := s.Save(m.ramBanks) + s = nil + return err } - return nil } -func (m *MBC3) LoadRam(filename string) error { +func (m *MBC3) LoadRam(savesDir string, game string) error { if m.hasRAM && m.hasBattery { - log.Println(m.Name+":", "Loading RAM from", filename) - ramBanks, err := ReadRAMFromDisk(filename, 0x2000, 0x8000) + s := NewSaveFile(savesDir, game) + banks, err := s.Load(4) if err != nil { return err } - - m.ramBanks = ramBanks + m.ramBanks = banks + s = nil } - return nil } diff --git a/src/cartridge/SaveFile.go b/src/cartridge/SaveFile.go new file mode 100644 index 0000000..76a4af4 --- /dev/null +++ b/src/cartridge/SaveFile.go @@ -0,0 +1,160 @@ +package cartridge + +import ( + "bytes" + "compress/zlib" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "hash/crc32" + "io" + "io/ioutil" + "log" + "path/filepath" + "time" +) + +type SaveFile struct { + Game string + Path string + NoOfBanks int + Banks []string + BankHashes []uint32 + LastSaved string +} + +func NewSaveFile(savesDir string, game string) *SaveFile { + var s *SaveFile = new(SaveFile) + s.Game = game + s.Path = filepath.Join(savesDir, game) + ".sav" + return s +} + +func (s *SaveFile) Validate() error { + if s.NoOfBanks != len(s.Banks) { + return errors.New(fmt.Sprintf("No. of banks does (%d) NOT match number of actual banks (%d)", s.NoOfBanks, s.Banks)) + } + + return nil +} + +//Takes a byte array, converts it to a base64 string and compresses it using ZLIB. +func (s *SaveFile) DeflateBank(bank []byte) (string, error) { + var outBuffer bytes.Buffer + zl := zlib.NewWriter(&outBuffer) + _, err := zl.Write(bank) + if err != nil { + return "", err + } + zl.Close() + + compressedBankStr := base64.StdEncoding.EncodeToString(outBuffer.Bytes()) + return compressedBankStr, nil +} + +//Takes a base64 string and decompresses it using ZLIB into a byte array +func (s *SaveFile) InflateBank(bankStr string) ([]byte, error) { + compressedBank, err := base64.StdEncoding.DecodeString(bankStr) + if err != nil { + return nil, err + } + + var inBuffer *bytes.Buffer = bytes.NewBuffer(compressedBank) + zl, err := zlib.NewReader(inBuffer) + if err != nil { + return nil, err + } + + var outBuffer *bytes.Buffer = bytes.NewBuffer(make([]byte, 0)) + io.Copy(outBuffer, zl) + zl.Close() + + return outBuffer.Bytes(), nil +} + +func (s *SaveFile) Load(noOfBanks int) ([][]byte, error) { + log.Println("Loading RAM from", s.Path) + file, err := ioutil.ReadFile(s.Path) + if err != nil { + return nil, err + } + + //Now parse into struct + var saveFile SaveFile + err = json.Unmarshal(file, &saveFile) + if err != nil { + return nil, err + } + + //ensure save file is valid + if err := saveFile.Validate(); err != nil { + return nil, err + } + + s = &saveFile + log.Println("Game was last saved:", s.LastSaved) + + if len(s.Banks) != noOfBanks { + return nil, errors.New(fmt.Sprintln("Error: Expected", noOfBanks, "banks but found", len(s.Banks))) + } + + s.NoOfBanks = noOfBanks + + var result [][]byte = make([][]byte, s.NoOfBanks) + for i, bank := range s.Banks { + log.Println("--> Loading bank", i) + + //decompress into byte array + inflatedBank, err := s.InflateBank(bank) + if err != nil { + return nil, errors.New(fmt.Sprintln("Error attempting to parse and decompress bank %d (%v), save file could be corrupted!", i, err)) + } + + //check to ensure checksum is valid against what we decompressed + hash := crc32.ChecksumIEEE(inflatedBank) + if hash != s.BankHashes[i] { + return nil, errors.New(fmt.Sprintln("Hash error occured, ram save file is corrupted! (inflated bank", i, " does not match hash on disk!)")) + } + + result[i] = inflatedBank + } + + return result, nil +} + +//compresses ram banks and stores as base64 strings. +//hashes are taken each bank +//information is stored on disk in JSON format +func (s *SaveFile) Save(data [][]byte) error { + s.NoOfBanks = len(data) + s.Banks = make([]string, s.NoOfBanks) + s.BankHashes = make([]uint32, s.NoOfBanks) + s.LastSaved = fmt.Sprint(time.Now().Format(time.UnixDate)) + + log.Println("Saving RAM to", s.Path) + for i, bank := range data { + //take crc32 hash of bank + s.BankHashes[i] = crc32.ChecksumIEEE(bank) + + //compress + bankStr, err := s.DeflateBank(bank) + if err != nil { + return errors.New(fmt.Sprintln("Error attempting to compress bank %d (%v)", i, err)) + } + + log.Printf("--> Storing bank %d (Compression ratio: %.1f%%)", i, 100.00-((float32(len(bankStr))/float32(len(bank)))*100)) + s.Banks[i] = bankStr + } + + //serialize to JSON + js, err := json.Marshal(&s) + if err != nil { + return errors.New(fmt.Sprintln("Error attempting to parse into JSON", err)) + } + + log.Println("Save file", s.Path, "size is", len(js), "bytes") + + //write to disk + return ioutil.WriteFile(s.Path, js, 0755) +} diff --git a/src/cartridge/cartridge.go b/src/cartridge/cartridge.go index 066d6be..283fc5b 100644 --- a/src/cartridge/cartridge.go +++ b/src/cartridge/cartridge.go @@ -113,13 +113,11 @@ func (c *Cartridge) Init(rom []byte) error { } func (c *Cartridge) SaveRam(savesDir string) error { - base := filepath.Base(c.Filename) + ".ramsave" - return c.MBC.SaveRam(filepath.Join(savesDir, base)) + return c.MBC.SaveRam(savesDir, filepath.Base(c.Filename)) } func (c *Cartridge) LoadRam(savesDir string) error { - base := filepath.Base(c.Filename) + ".ramsave" - return c.MBC.LoadRam(filepath.Join(savesDir, base)) + return c.MBC.LoadRam(savesDir, filepath.Base(c.Filename)) } func (c *Cartridge) String() string {