diff --git a/.gitignore b/.gitignore index 10f91d7..f489234 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ pkmn-rom-parser dev/* main -results* \ No newline at end of file +results* + +sav/*_test.go \ No newline at end of file diff --git a/README.md b/README.md index 33aac96..e0b1526 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Application used to parse in-game information in generation IV/V Pokemon games. ## Installation ```sh -go get github.com/dingdongg/pkmn-rom-parser/v6 +go get github.com/dingdongg/pkmn-rom-parser/v7 ``` ## Usage diff --git a/consts/consts.go b/consts/consts.go index 245fddd..9875988 100644 --- a/consts/consts.go +++ b/consts/consts.go @@ -3,6 +3,7 @@ package consts const BLOCK_SIZE_BYTES = 32 const PARTY_POKEMON_SIZE = 236 const PERSONALITY_OFFSET = 0xA0 +const PERSONALITY_OFFSET_HGSS = 0x98 const ( BLOCK_A_ITEM = 0x2 diff --git a/consts/gamever/gamever.go b/consts/gamever/gamever.go new file mode 100644 index 0000000..70d39ed --- /dev/null +++ b/consts/gamever/gamever.go @@ -0,0 +1,9 @@ +package gamever + +type GameVer int + +const ( + DP GameVer = iota + PLAT + HGSS +) \ No newline at end of file diff --git a/crypt/crypt.go b/crypt/crypt.go index a9b1f81..c1f4d00 100644 --- a/crypt/crypt.go +++ b/crypt/crypt.go @@ -4,7 +4,7 @@ import ( "encoding/binary" "log" - "github.com/dingdongg/pkmn-rom-parser/v6/prng" + "github.com/dingdongg/pkmn-rom-parser/v7/prng" ) // Computes a checksum via the CRC16-CCITT algorithm on the given data @@ -12,7 +12,7 @@ func CRC16_CCITT(data []byte) uint16 { sum := uint(0xFFFF) for _, b := range data { - sum = (sum << 8) ^ seeds[b^byte((sum>>8))] + sum = (sum << 8) ^ seeds[b^byte(sum>>8)] } return uint16(sum) diff --git a/go.mod b/go.mod index 262caab..a251149 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/dingdongg/pkmn-rom-parser/v6 +module github.com/dingdongg/pkmn-rom-parser/v7 go 1.22.3 diff --git a/items/items.go b/items/items.go index caab2c6..7fe92f6 100644 --- a/items/items.go +++ b/items/items.go @@ -11,7 +11,7 @@ import ( "strconv" "strings" - "github.com/dingdongg/pkmn-rom-parser/v6/path_resolver" + "github.com/dingdongg/pkmn-rom-parser/v7/path_resolver" ) // fetch item names and cache in RAM to prevent multiple file IO operations diff --git a/parser.go b/parser.go index db4d9b4..5dd7a36 100644 --- a/parser.go +++ b/parser.go @@ -1,30 +1,27 @@ package parser import ( - "github.com/dingdongg/pkmn-rom-parser/v6/consts" - "github.com/dingdongg/pkmn-rom-parser/v6/rom_reader" - "github.com/dingdongg/pkmn-rom-parser/v6/rom_writer" - "github.com/dingdongg/pkmn-rom-parser/v6/rom_writer/req" - "github.com/dingdongg/pkmn-rom-parser/v6/validator" - "github.com/dingdongg/pkmn-rom-parser/v6/validator/locator" + "github.com/dingdongg/pkmn-rom-parser/v7/rom_reader" + "github.com/dingdongg/pkmn-rom-parser/v7/rom_writer" + "github.com/dingdongg/pkmn-rom-parser/v7/rom_writer/req" + "github.com/dingdongg/pkmn-rom-parser/v7/sav" ) func Parse(savefile []byte) ([]rom_reader.Pokemon, error) { - if err := validator.Validate(savefile); err != nil { + game, err := sav.Validate(savefile) + if err != nil { return []rom_reader.Pokemon{}, err } - chunk := locator.GetLatestSaveChunk(savefile) - partyData := chunk.SmallBlock.BlockData[consts.PERSONALITY_OFFSET:] - - return rom_reader.GetPartyPokemon(partyData), nil + partyData := rom_reader.GetPartyPokemon(game) + return partyData, nil } func Write(savefile []byte, newBytes []req.WriteRequest) ([]byte, error) { - if err := validator.Validate(savefile); err != nil { + game, err := sav.Validate(savefile) + if err != nil { return []byte{}, err } - chunk := locator.GetLatestSaveChunk(savefile) - return rom_writer.UpdatePartyPokemon(savefile, *chunk, newBytes) + return rom_writer.UpdatePartyPokemon(game, newBytes) } diff --git a/rom_reader/rom_reader.go b/rom_reader/rom_reader.go index bc3c8b1..33a57f8 100644 --- a/rom_reader/rom_reader.go +++ b/rom_reader/rom_reader.go @@ -2,13 +2,15 @@ package rom_reader import ( "encoding/binary" + "fmt" "log" - "github.com/dingdongg/pkmn-rom-parser/v6/char" - "github.com/dingdongg/pkmn-rom-parser/v6/consts" - "github.com/dingdongg/pkmn-rom-parser/v6/crypt" - "github.com/dingdongg/pkmn-rom-parser/v6/data" - "github.com/dingdongg/pkmn-rom-parser/v6/shuffler" + "github.com/dingdongg/pkmn-rom-parser/v7/char" + "github.com/dingdongg/pkmn-rom-parser/v7/consts" + "github.com/dingdongg/pkmn-rom-parser/v7/crypt" + "github.com/dingdongg/pkmn-rom-parser/v7/data" + "github.com/dingdongg/pkmn-rom-parser/v7/sav" + "github.com/dingdongg/pkmn-rom-parser/v7/shuffler" ) type Stats struct { @@ -36,6 +38,19 @@ type Pokemon struct { IVs Stats } +func (p Pokemon) String() string { + return fmt.Sprintf(` + Pokemon { + %s (#%d) Level: %d + %+v + held item (id): %s + Nature: %s + Ability: %s + EV: %+v + IV: %+v + }`, p.Name, p.PokedexId, p.BattleStat.Level, p.BattleStat.Stats, p.Item, p.Nature, p.Ability, p.EVs, p.IVs) +} + const ( A uint = iota B uint = iota @@ -43,10 +58,13 @@ const ( D uint = iota ) -func GetPartyPokemon(ciphertext []byte) []Pokemon { +// TODO: update function to use ISave methods instead +func GetPartyPokemon(game sav.ISave) []Pokemon { + size := game.PartySize() + ciphertext := game.PartySection() var party []Pokemon - for i := uint(0); i < 6; i++ { + for i := uint(0); i < uint(size); i++ { party = append(party, parsePokemon(ciphertext, i)) } diff --git a/rom_reader/rom_reader_test.go b/rom_reader/rom_reader_test.go index e46ec9a..1e9a557 100644 --- a/rom_reader/rom_reader_test.go +++ b/rom_reader/rom_reader_test.go @@ -5,7 +5,7 @@ import ( "os" "testing" - "github.com/dingdongg/pkmn-rom-parser/v6/consts" + "github.com/dingdongg/pkmn-rom-parser/v7/consts" "github.com/google/go-cmp/cmp" ) diff --git a/rom_writer/req/impl.go b/rom_writer/req/impl.go index fb1a27c..5e8b2d0 100644 --- a/rom_writer/req/impl.go +++ b/rom_writer/req/impl.go @@ -5,8 +5,8 @@ import ( "fmt" "log" - "github.com/dingdongg/pkmn-rom-parser/v6/char" - "github.com/dingdongg/pkmn-rom-parser/v6/data" + "github.com/dingdongg/pkmn-rom-parser/v7/char" + "github.com/dingdongg/pkmn-rom-parser/v7/data" ) func (wr WriteRequest) WriteItem(itemName string) { diff --git a/rom_writer/req/req.go b/rom_writer/req/req.go index 3d7870b..3692e88 100644 --- a/rom_writer/req/req.go +++ b/rom_writer/req/req.go @@ -3,8 +3,8 @@ package req import ( "fmt" - "github.com/dingdongg/pkmn-rom-parser/v6/consts" - "github.com/dingdongg/pkmn-rom-parser/v6/shuffler" + "github.com/dingdongg/pkmn-rom-parser/v7/consts" + "github.com/dingdongg/pkmn-rom-parser/v7/shuffler" ) const ( diff --git a/rom_writer/req/req_test.go b/rom_writer/req/req_test.go index b04444c..6700b62 100644 --- a/rom_writer/req/req_test.go +++ b/rom_writer/req/req_test.go @@ -4,8 +4,8 @@ import ( "encoding/binary" "testing" - "github.com/dingdongg/pkmn-rom-parser/v6/char" - "github.com/dingdongg/pkmn-rom-parser/v6/tutil" + "github.com/dingdongg/pkmn-rom-parser/v7/char" + "github.com/dingdongg/pkmn-rom-parser/v7/tutil" ) var templates = tutil.GetTemplates() diff --git a/rom_writer/req/types.go b/rom_writer/req/types.go index 40f12c5..7702875 100644 --- a/rom_writer/req/types.go +++ b/rom_writer/req/types.go @@ -4,7 +4,7 @@ type Writable interface { Bytes() ([]byte, error) } -// for battle stats +// for battle stats. implements Writable type WriteStats struct { Hp uint Attack uint @@ -14,45 +14,26 @@ type WriteStats struct { Speed uint } -type WriteEffortValue struct { - Hp uint - Attack uint - Defense uint - SpAttack uint - SpDefense uint - Speed uint -} +// implements Writable +type WriteEffortValue WriteStats -type WriteIndivValue struct { - Hp uint - Attack uint - Defense uint - SpAttack uint - SpDefense uint - Speed uint -} +// implements Writable -// for IDs/level +type WriteIndivValue WriteStats + +// for IDs/level. implements Writable type WriteUint struct { Val uint NumBytes uint // number of bytes. Used in Bytes() implementation } -// for nicknames +// for nicknames. implements Writable type WriteString struct { Val string } type NewData map[string]Writable -// types that implement CompressibleStat can have their stat values -// compressed into an unsigned integer. For instance, -// each IV stat uses 5 bits (=30) - so IVs can be packed in a uint32. -// each EV stat uses 8 bits (=48) - so EVs can be packed in a uint64. -type CompressibleStat interface { - Compress(elemBits uint) uint -} - type WriteRequest struct { PartyIndex uint Contents NewData diff --git a/rom_writer/rom_writer.go b/rom_writer/rom_writer.go index 9bbf8c1..8167b36 100644 --- a/rom_writer/rom_writer.go +++ b/rom_writer/rom_writer.go @@ -4,11 +4,11 @@ import ( "encoding/binary" "fmt" - "github.com/dingdongg/pkmn-rom-parser/v6/consts" - "github.com/dingdongg/pkmn-rom-parser/v6/crypt" - "github.com/dingdongg/pkmn-rom-parser/v6/rom_writer/req" - "github.com/dingdongg/pkmn-rom-parser/v6/shuffler" - "github.com/dingdongg/pkmn-rom-parser/v6/validator" + "github.com/dingdongg/pkmn-rom-parser/v7/consts" + "github.com/dingdongg/pkmn-rom-parser/v7/crypt" + "github.com/dingdongg/pkmn-rom-parser/v7/rom_writer/req" + "github.com/dingdongg/pkmn-rom-parser/v7/sav" + "github.com/dingdongg/pkmn-rom-parser/v7/shuffler" ) /* @@ -70,9 +70,12 @@ func (wrb *WriteRequestBuilder) AddRequest(partyIndex uint) (req.WriteRequest, e return request, nil } -func UpdatePartyPokemon(savefile []byte, chunk validator.Chunk, newData []req.WriteRequest) ([]byte, error) { +// TODO: update function to use ISave methods instead +func UpdatePartyPokemon(savefile sav.ISave, newData []req.WriteRequest) ([]byte, error) { updatedPokemonIndexes := make(map[uint]bool, 0) - base := chunk.SmallBlock.Address + consts.PERSONALITY_OFFSET + + latestChunk := savefile.LatestData() + base := latestChunk.SmallBlock.Address + savefile.PartyOffset() changes := make(StagingMap) for _, wr := range newData { @@ -83,7 +86,7 @@ func UpdatePartyPokemon(savefile []byte, chunk validator.Chunk, newData []req.Wr } offset := base + wr.PartyIndex*consts.PARTY_POKEMON_SIZE - personality := binary.LittleEndian.Uint32(savefile[offset : offset+4]) + personality := binary.LittleEndian.Uint32(savefile.Get(offset, 4)) dataOffset, blockIndex, err := req.GetWriteLocation(request) if err != nil { @@ -100,7 +103,8 @@ func UpdatePartyPokemon(savefile []byte, chunk validator.Chunk, newData []req.Wr } if _, ok := changes[wr.PartyIndex]; !ok { - changes[wr.PartyIndex] = crypt.DecryptPokemon(savefile[offset:]) + encryptedPokemon := savefile.Get(offset, consts.PARTY_POKEMON_SIZE) + changes[wr.PartyIndex] = crypt.DecryptPokemon(encryptedPokemon) } size := copy(changes[wr.PartyIndex][blockAddress+uint(dataOffset):], bytes) if size != len(bytes) { @@ -117,17 +121,18 @@ func UpdatePartyPokemon(savefile []byte, chunk validator.Chunk, newData []req.Wr pokemonOffset := base + i*consts.PARTY_POKEMON_SIZE encrypted := crypt.EncryptPokemon(changes[i]) - copy(savefile[AbsAddress(pokemonOffset):], encrypted) + copy(savefile.Get(pokemonOffset, consts.PARTY_POKEMON_SIZE), encrypted) } - updateBlockChecksum(savefile, chunk) - return savefile, nil + updateBlockChecksum(savefile) + return savefile.Data(), nil } -func updateBlockChecksum(savefile []byte, chunk validator.Chunk) { +func updateBlockChecksum(savefile sav.ISave) { + chunk := savefile.LatestData() start := chunk.SmallBlock.Address - end := chunk.SmallBlock.Address + uint(chunk.SmallBlock.Footer.BlockSize) - 0x14 - newChecksum := crypt.CRC16_CCITT(savefile[start:end]) + checksumDataSize := uint(chunk.SmallBlock.Footer.BlockSize) - 0x14 + newChecksum := crypt.CRC16_CCITT(savefile.Get(start, checksumDataSize)) - binary.LittleEndian.PutUint16(savefile[end+18:], newChecksum) + binary.LittleEndian.PutUint16(savefile.Get(start+checksumDataSize+0x12, 2), newChecksum) } diff --git a/sav/consts.go b/sav/consts.go new file mode 100644 index 0000000..da5fbb4 --- /dev/null +++ b/sav/consts.go @@ -0,0 +1,79 @@ +package sav + +import ( + "fmt" +) + +const PLAT_SB_END uint = uint(0xCF2C) // non-inclusive +const PLAT_BB_START uint = PLAT_SB_END + 0x0 +const PLAT_BB_END uint = PLAT_BB_START + 0x121E4 // non-inclusive + +const HGSS_SB_END uint = uint(0xF628) // non-inclusive +const HGSS_BB_START uint = HGSS_SB_END + 0xD8 // padding included in hgss +const HGSS_BB_END uint = HGSS_BB_START + 0x12310 // non-inclusive + +const MAGIC_TIMESTAMP_JP_INTL = 0x20060623 +const MAGIC_TIMESTAMP_KR = 0x20070903 + +func identifyGameVersion(savefile []byte) (ISave, error) { + // gen 4 games start writing to the 0x40000-offset address space, + // check there for the existence of a valid footer + chunkTwoOffset := uint(0x40000) + footerSize := uint(0x14) + + if isPLAT(savefile, chunkTwoOffset, footerSize, PLAT_SB_END, PLAT_BB_END) { + fmt.Println("Game: pokemon PLAT") + return NewSavPLAT(savefile), nil + } else if isHGSS(savefile, chunkTwoOffset, footerSize, HGSS_SB_END, HGSS_BB_END) { + fmt.Println("Game: pokemon HGSS") + return NewSavHGSS(savefile), nil + } + + return nil, fmt.Errorf("unrecognized game file") +} + +func isPLAT(savefile []byte, offset uint, footerSize uint, smallBlockEnd uint, bigBlockEnd uint) bool { + smallFooter := getFooter(savefile[offset+smallBlockEnd-footerSize : offset+smallBlockEnd]) + bigFooter := getFooter(savefile[offset+bigBlockEnd-footerSize : offset+bigBlockEnd]) + + if smallFooter.BlockSize != uint32(0xCF2C) { + return false + } + + if smallFooter.K != MAGIC_TIMESTAMP_JP_INTL && smallFooter.K != MAGIC_TIMESTAMP_KR { + return false + } + + if bigFooter.BlockSize != uint32(0x121E4) { + return false + } + + if bigFooter.K != MAGIC_TIMESTAMP_JP_INTL && bigFooter.K != MAGIC_TIMESTAMP_KR { + return false + } + + return true +} + +func isHGSS(savefile []byte, offset uint, footerSize uint, smallBlockEnd uint, bigBlockEnd uint) bool { + smallFooter := getFooter(savefile[offset+smallBlockEnd-footerSize : offset+smallBlockEnd]) + bigFooter := getFooter(savefile[offset+bigBlockEnd-footerSize : offset+bigBlockEnd]) + + if smallFooter.BlockSize != uint32(0xF628) { + return false + } + + if smallFooter.K != MAGIC_TIMESTAMP_JP_INTL && smallFooter.K != MAGIC_TIMESTAMP_KR { + return false + } + + if bigFooter.BlockSize != uint32(0x12310) { + return false + } + + if bigFooter.K != MAGIC_TIMESTAMP_JP_INTL && bigFooter.K != MAGIC_TIMESTAMP_KR { + return false + } + + return true +} diff --git a/sav/mem_layout.go b/sav/mem_layout.go new file mode 100644 index 0000000..e72fc47 --- /dev/null +++ b/sav/mem_layout.go @@ -0,0 +1,78 @@ +package sav + +import ( + "encoding/binary" + "fmt" + + "github.com/dingdongg/pkmn-rom-parser/v7/crypt" +) + +type Chunk struct { + SmallBlock Block + BigBlock Block +} + +type Block struct { + BlockData []byte + Footer Footer + Address uint +} + +type Footer struct { + Identifier uint32 + SaveNumber uint32 + BlockSize uint32 + K uint32 + T uint16 + Checksum uint16 +} + +func getFooter(buf []byte) Footer { + return Footer{ + binary.LittleEndian.Uint32(buf[:0x4]), + binary.LittleEndian.Uint32(buf[0x4:0x8]), + binary.LittleEndian.Uint32(buf[0x8:0xC]), + binary.LittleEndian.Uint32(buf[0xC:0x10]), + binary.LittleEndian.Uint16(buf[0x10:0x12]), + binary.LittleEndian.Uint16(buf[0x12:0x14]), + } +} + +func NewBlock(data []byte, footer []byte, startAddr uint) Block { + return Block{data, getFooter(footer), startAddr} +} + +func (b Block) String() string { + return fmt.Sprintf(` + Block { + data: % +x..., + %s, + address: 0x%x, + }`, b.BlockData[0:0x10], b.Footer, b.Address) +} + +// footer format specifier +func (f Footer) String() string { + return fmt.Sprintf(` + footer { + identifier = 0x%x, + saveNumber = 0x%x, + blockSize = 0x%x, + K = 0x%x, + T = 0x%x, + checksum = 0x%x, + }`, f.Identifier, f.SaveNumber, f.BlockSize, f.K, f.T, f.Checksum) +} + +func (c Chunk) IsValid() bool { + smallChecksum := crypt.CRC16_CCITT(c.SmallBlock.BlockData) + fmt.Println("checking small block checksum") + if smallChecksum != c.SmallBlock.Footer.Checksum { + return false + } + fmt.Println("checking big block checksum") + + bigChecksum := crypt.CRC16_CCITT(c.BigBlock.BlockData) + fmt.Println("success") + return bigChecksum == c.BigBlock.Footer.Checksum +} \ No newline at end of file diff --git a/sav/sav.go b/sav/sav.go new file mode 100644 index 0000000..2cacd4c --- /dev/null +++ b/sav/sav.go @@ -0,0 +1,41 @@ +package sav + +import ( + "github.com/dingdongg/pkmn-rom-parser/v7/consts/gamever" +) + +type ISave interface { + Chunk(offset uint) Chunk + Validate() error + LatestData() *Chunk + PartySection() []byte + PartySize() uint32 + PartyOffset() uint + Get(start uint, numBytes uint) []byte + Data() []byte +} + +type gen4Savefile struct { + version gamever.GameVer + data []byte + smallBlockSize uint + bigBlockSize uint + partyOffset uint +} + +// tODO: include important offsets as fields +type savPLAT gen4Savefile +type savHGSS gen4Savefile + +func Validate(savefile []byte) (ISave, error) { + game, err := identifyGameVersion(savefile) + if err != nil { + return nil, err + } + + if err = game.Validate(); err != nil { + return nil, err + } + + return game, nil +} diff --git a/sav/sav_hgss.go b/sav/sav_hgss.go new file mode 100644 index 0000000..ffaa972 --- /dev/null +++ b/sav/sav_hgss.go @@ -0,0 +1,96 @@ +package sav + +import ( + "encoding/binary" + "fmt" + + "github.com/dingdongg/pkmn-rom-parser/v7/consts/gamever" +) + +func NewSavHGSS(savefile []byte) *savHGSS { + return &savHGSS{ + version: gamever.HGSS, + data: savefile, + smallBlockSize: 0xF628, + bigBlockSize: 0x12310, + partyOffset: 0x98, + } +} + +func (sav *savHGSS) Chunk(offset uint) Chunk { + sbData := sav.data[0x0+offset : sav.smallBlockSize+offset-0x10] + sbFooter := sav.data[sav.smallBlockSize+offset-0x14 : sav.smallBlockSize+offset] + small := NewBlock(sbData, sbFooter, 0x0+offset) + + padding := uint(0xD8) + bbStart := sav.smallBlockSize + padding + bbData := sav.data[bbStart+offset : bbStart+offset+sav.bigBlockSize-0x10] + bbFooter := sav.data[bbStart+offset+sav.bigBlockSize-0x14 : bbStart+offset+sav.bigBlockSize] + big := NewBlock(bbData, bbFooter, bbStart+offset) + + return Chunk{ + SmallBlock: small, + BigBlock: big, + } +} + +func (sav *savHGSS) Validate() error { + firstChunk := sav.Chunk(0x0) + secondChunk := sav.Chunk(0x40000) + + if !firstChunk.IsValid() { + fmt.Println("First chunk invalid") + return fmt.Errorf("invalid savefile") + } + + if !secondChunk.IsValid() { + fmt.Println("Second chunk invalid") + return fmt.Errorf("invalid savefile") + } + + return nil +} + +func (sav *savHGSS) LatestData() *Chunk { + chunk1 := sav.Chunk(0x0) + chunk2 := sav.Chunk(0x40000) + + var latestSmallBlock Block + if chunk1.SmallBlock.Footer.SaveNumber >= chunk2.SmallBlock.Footer.SaveNumber { + latestSmallBlock = chunk1.SmallBlock + } else { + latestSmallBlock = chunk2.SmallBlock + } + + var latestBigBlock Block + if latestSmallBlock.Footer.Identifier == chunk1.BigBlock.Footer.Identifier { + latestBigBlock = chunk1.BigBlock + } else { + latestBigBlock = chunk2.BigBlock + } + + return &Chunk{ + SmallBlock: latestSmallBlock, + BigBlock: latestBigBlock, + } +} + +func (sav *savHGSS) PartySection() []byte { + return sav.data[sav.partyOffset:] +} + +func (sav *savHGSS) PartySize() uint32 { + return binary.LittleEndian.Uint32(sav.data[sav.partyOffset-4 : sav.partyOffset]) +} + +func (sav *savHGSS) PartyOffset() uint { + return sav.partyOffset +} + +func (sav *savHGSS) Get(start uint, numBytes uint) []byte { + return sav.data[start : start+numBytes] +} + +func (sav *savHGSS) Data() []byte { + return sav.data +} \ No newline at end of file diff --git a/sav/sav_plat.go b/sav/sav_plat.go new file mode 100644 index 0000000..039c99c --- /dev/null +++ b/sav/sav_plat.go @@ -0,0 +1,94 @@ +package sav + +import ( + "encoding/binary" + "fmt" + + "github.com/dingdongg/pkmn-rom-parser/v7/consts/gamever" +) + +func NewSavPLAT(savefile []byte) *savPLAT { + return &savPLAT{ + version: gamever.PLAT, + data: savefile, + smallBlockSize: 0xCF2C, + bigBlockSize: 0x121E4, + partyOffset: 0xA0, + } +} + +func (sav *savPLAT) Chunk(offset uint) Chunk { + sbData := sav.data[0x0+offset : sav.smallBlockSize+offset-0x14] + sbFooter := sav.data[sav.smallBlockSize+offset-0x14 : sav.smallBlockSize+offset] + small := NewBlock(sbData, sbFooter, 0x0+offset) + + bbData := sav.data[sav.smallBlockSize+offset : sav.smallBlockSize+offset+sav.bigBlockSize-0x14] + bbFooter := sav.data[sav.smallBlockSize+offset+sav.bigBlockSize-0x14 : sav.smallBlockSize+offset+sav.bigBlockSize] + big := NewBlock(bbData, bbFooter, sav.smallBlockSize+offset) + + return Chunk{ + SmallBlock: small, + BigBlock: big, + } +} + +func (sav *savPLAT) Validate() error { + firstChunk := sav.Chunk(0x0) + secondChunk := sav.Chunk(0x40000) + + if !firstChunk.IsValid() { + fmt.Println("First chunk invalid") + return fmt.Errorf("invalid savefile") + } + + if !secondChunk.IsValid() { + fmt.Println("Second chunk invalid") + return fmt.Errorf("invalid savefile") + } + + return nil +} + +func (sav *savPLAT) LatestData() *Chunk { + chunk1 := sav.Chunk(0x0) + chunk2 := sav.Chunk(0x40000) + + var latestSmallBlock Block + if chunk1.SmallBlock.Footer.SaveNumber >= chunk2.SmallBlock.Footer.SaveNumber { + latestSmallBlock = chunk1.SmallBlock + } else { + latestSmallBlock = chunk2.SmallBlock + } + + var latestBigBlock Block + if latestSmallBlock.Footer.Identifier == chunk1.BigBlock.Footer.Identifier { + latestBigBlock = chunk1.BigBlock + } else { + latestBigBlock = chunk2.BigBlock + } + + return &Chunk{ + SmallBlock: latestSmallBlock, + BigBlock: latestBigBlock, + } +} + +func (sav *savPLAT) PartySection() []byte { + return sav.data[sav.partyOffset:] +} + +func (sav *savPLAT) PartySize() uint32 { + return binary.LittleEndian.Uint32(sav.data[sav.partyOffset-4 : sav.partyOffset]) +} + +func (sav *savPLAT) PartyOffset() uint { + return sav.partyOffset +} + +func (sav *savPLAT) Get(start uint, numBytes uint) []byte { + return sav.data[start : start+numBytes] +} + +func (sav *savPLAT) Data() []byte { + return sav.data +} \ No newline at end of file diff --git a/shuffler/shuffler.go b/shuffler/shuffler.go index 514e5dc..c0c573b 100644 --- a/shuffler/shuffler.go +++ b/shuffler/shuffler.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/dingdongg/pkmn-rom-parser/v6/consts" + "github.com/dingdongg/pkmn-rom-parser/v7/consts" ) // from https://projectpokemon.org/home/docs/gen-4/pkm-structure-r65/ diff --git a/shuffler/shuffler_test.go b/shuffler/shuffler_test.go index 34cf192..346a3a2 100644 --- a/shuffler/shuffler_test.go +++ b/shuffler/shuffler_test.go @@ -3,7 +3,7 @@ package shuffler import ( "testing" - "github.com/dingdongg/pkmn-rom-parser/v6/consts" + "github.com/dingdongg/pkmn-rom-parser/v7/consts" ) func TestGetUnshuffledPos(t *testing.T) { diff --git a/validator/locator/locator.go b/validator/locator/locator.go deleted file mode 100644 index f9bcaec..0000000 --- a/validator/locator/locator.go +++ /dev/null @@ -1,29 +0,0 @@ -package locator - -import "github.com/dingdongg/pkmn-rom-parser/v6/validator" - -// return address to the start of the latest save chunk -// REQUIREMENT: savefile must be the entire .SAV file -func GetLatestSaveChunk(savefile []byte) *validator.Chunk { - firstChunk := validator.GetChunk(savefile, 0) - secondChunk := validator.GetChunk(savefile, 0x40000) - - var latestSmallBlock validator.Block - if firstChunk.SmallBlock.Footer.SaveNumber >= secondChunk.SmallBlock.Footer.SaveNumber { - latestSmallBlock = firstChunk.SmallBlock - } else { - latestSmallBlock = secondChunk.SmallBlock - } - - var latestBigBlock validator.Block - if latestSmallBlock.Footer.Identifier == firstChunk.BigBlock.Footer.Identifier { - latestBigBlock = firstChunk.BigBlock - } else { - latestBigBlock = secondChunk.BigBlock - } - - return &validator.Chunk{ - SmallBlock: latestSmallBlock, - BigBlock: latestBigBlock, - } -} diff --git a/validator/validator.go b/validator/validator.go deleted file mode 100644 index b600208..0000000 --- a/validator/validator.go +++ /dev/null @@ -1,122 +0,0 @@ -package validator - -import ( - "encoding/binary" - "errors" - "fmt" - - "github.com/dingdongg/pkmn-rom-parser/v6/crypt" -) - -/* -POKEMON PLATINUM -1. savfile size = 2^19 bytes -2. checksums are used for validating (part pokemon data) (per-pokemon basis) - - -2 types of validation -- validating an entire big/small block as a whole -- validating each party pokemon data individually - -*/ - -// a "chunk" denotes a pair of small + big block that are adjacent in memory -type Chunk struct { - SmallBlock Block - BigBlock Block -} - -type Block struct { - BlockData []byte - Footer Footer - Address uint -} - -type Footer struct { - Identifier uint32 - SaveNumber uint32 - BlockSize uint32 - K uint32 - T uint16 - Checksum uint16 -} - -const savefileSize int = 1 << 19 -const footerSize uint = 0x14 -const secondChunkOffset uint = 0x40000 - -func getFooter(buf []byte) Footer { - return Footer{ - binary.LittleEndian.Uint32(buf[:0x4]), - binary.LittleEndian.Uint32(buf[0x4:0x8]), - binary.LittleEndian.Uint32(buf[0x8:0xC]), - binary.LittleEndian.Uint32(buf[0xC:0x10]), - binary.LittleEndian.Uint16(buf[0x10:0x12]), - binary.LittleEndian.Uint16(buf[0x12:0x14]), - } -} - -// footer format specifier -func (f Footer) String() string { - return fmt.Sprintf(` - footer { - identifier = 0x%x, - saveNumber = 0x%x, - blockSize = 0x%x, - K = 0x%x, - T = 0x%x, - checksum = 0x%x, - }`, f.Identifier, f.SaveNumber, f.BlockSize, f.K, f.T, f.Checksum) -} - -func (c Chunk) isValid() bool { - smallChecksum := crypt.CRC16_CCITT(c.SmallBlock.BlockData) - if smallChecksum != c.SmallBlock.Footer.Checksum { - return false - } - - bigChecksum := crypt.CRC16_CCITT(c.BigBlock.BlockData) - return bigChecksum == c.BigBlock.Footer.Checksum -} - -func GetChunk(savefile []byte, offset uint) Chunk { - smallBlockFooterAddr := uint(0x0CF18) + offset - bigBlockFooterAddr := uint(0x1F0FC) + offset - bigBlockStart := uint(0xCF2C) + offset - - smallBlock := Block{ - savefile[offset:smallBlockFooterAddr], - getFooter(savefile[smallBlockFooterAddr : smallBlockFooterAddr+footerSize]), - offset, - } - - bigBlock := Block{ - savefile[bigBlockStart:bigBlockFooterAddr], - getFooter(savefile[bigBlockFooterAddr : bigBlockFooterAddr+footerSize]), - bigBlockStart, - } - - return Chunk{smallBlock, bigBlock} -} - -// validates the given .sav file -func Validate(savefile []byte) error { - if len(savefile) != savefileSize { - return errors.New("invalid savefile") - } - - firstChunk := GetChunk(savefile, 0) - secondChunk := GetChunk(savefile, secondChunkOffset) - - if !firstChunk.isValid() { - fmt.Println("First chunk invalid") - return errors.New("invalid savefile") - } - - if !secondChunk.isValid() { - fmt.Println("Second chunk invalid") - return errors.New("invalid savefile") - } - - return nil -}