Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add F-Zero and Phantasy Star Online patches #4

Merged
merged 1 commit into from
Jan 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions header.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ func (h *header) blocks() int {
return int(h.CardSize) << 4 //nolint:gomnd
}

func (h *header) serialNumbers() (uint32, uint32) {
buf := new(bytes.Buffer)
buf.Grow(binary.Size(h))

_ = binary.Write(buf, binary.BigEndian, h)

serial := make([]uint32, 8) //nolint:gomnd
_ = binary.Read(buf, binary.BigEndian, &serial)

return serial[0] ^ serial[2] ^ serial[4] ^ serial[6], serial[1] ^ serial[3] ^ serial[5] ^ serial[7]
}

func (h *header) generateChecksums() ([]byte, []byte, error) {
b, err := h.MarshalBinary()
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions memcard.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ func (mc *memoryCard) count() int {
return count
}

func (mc *memoryCard) serialNumbers() (uint32, uint32) {
return mc.header.serialNumbers()
}

func (mc *memoryCard) checksum() error {
if err := mc.header.checksum(); err != nil {
return err
Expand Down
119 changes: 119 additions & 0 deletions patches.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package gc

import (
"bytes"
"fmt"
"io"
)

const (
lengthFZero = 0x8000
lengthPSO = 0x6000
)

//nolint:gomnd
func patchFZero(r io.Reader, mc *memoryCard) (io.Reader, error) {
serial1, serial2 := mc.serialNumbers()

b, err := io.ReadAll(r)
if err != nil {
return nil, fmt.Errorf("unable to read save data: %w", err)
}

if len(b) < lengthFZero {
return nil, errInvalidLength
}

b[0x2066] = byte(serial1 >> 24)
b[0x2067] = byte(serial1 >> 16)
b[0x7580] = byte(serial2 >> 24)
b[0x7581] = byte(serial2 >> 16)
b[0x2060] = byte(serial1 >> 8)
b[0x2061] = byte(serial1)
b[0x2200] = byte(serial2 >> 8)
b[0x2201] = byte(serial2)

var chksum uint16 = 0xffff

for i := 2; i < 0x8000; i++ {
chksum ^= uint16(b[i])
for j := 8; j > 0; j-- {
if chksum&1 == 1 {
chksum = (chksum >> 1) ^ 0x8408
} else {
chksum >>= 1
}
}
}

b[0x0000] = byte(^chksum >> 8)
b[0x0001] = byte(^chksum)

return bytes.NewReader(b), nil
}

const (
offsetPSO12 = 0x00
offsetPSO3 = 0x10
)

func patchPSO12(r io.Reader, mc *memoryCard) (io.Reader, error) {
return patchPSO(r, mc, offsetPSO12)
}

func patchPSO3(r io.Reader, mc *memoryCard) (io.Reader, error) {
return patchPSO(r, mc, offsetPSO3)
}

//nolint:gomnd
func patchPSO(r io.Reader, mc *memoryCard, offset int) (io.Reader, error) {
serial1, serial2 := mc.serialNumbers()

b, err := io.ReadAll(r)
if err != nil {
return nil, fmt.Errorf("unable to read save data: %w", err)
}

if len(b) < lengthPSO {
return nil, errInvalidLength
}

b[0x2158] = byte(serial1 >> 24)
b[0x2159] = byte(serial1 >> 16)
b[0x215a] = byte(serial1 >> 8)
b[0x215b] = byte(serial1)
b[0x215c] = byte(serial2 >> 24)
b[0x215d] = byte(serial2 >> 16)
b[0x215e] = byte(serial2 >> 8)
b[0x215f] = byte(serial2)

lut := make([]uint32, 256)

for i := 0; i < 256; i++ {
chksum := uint32(i)
for j := 8; j > 0; j-- {
if chksum&1 == 1 {
chksum = (chksum >> 1) ^ 0xedb88320
} else {
chksum >>= 1
}
}

lut[i] = chksum
}

var chksum uint32 = 0xdebb20e3

for i := 0x204c; i < 0x2164+offset; i++ {
chksum = (chksum>>8)&0xffffff ^ lut[byte(chksum)^b[i]]
}

chksum ^= 0xffffffff

b[0x2048] = byte(chksum >> 24)
b[0x2049] = byte(chksum >> 16)
b[0x204a] = byte(chksum >> 8)
b[0x204b] = byte(chksum)

return bytes.NewReader(b), nil
}
118 changes: 118 additions & 0 deletions patches_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package gc

import (
"bytes"
"crypto/sha256"
"encoding/binary"
"fmt"
"io"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
)

func copyData(f *File, wc *Writer) ([]byte, error) {
fr, err := f.Open()
if err != nil {
return nil, err
}
defer fr.Close()

fw, err := wc.Create()
if err != nil {
return nil, err
}
defer fw.Close()

// Skip header in checksum calculation
if _, err := io.CopyN(fw, fr, int64(binary.Size(entry{}))); err != nil {
return nil, fmt.Errorf("unable to copy save header: %w", err)
}

h := sha256.New()

if _, err := io.Copy(io.MultiWriter(fw, h), fr); err != nil {
return nil, fmt.Errorf("unable to copy save data: %w", err)
}

if err := fw.Close(); err != nil {
return nil, fmt.Errorf("unable to close: %w", err)
}

return h.Sum(nil), nil
}

//nolint:cyclop,funlen
func TestPatches(t *testing.T) {
t.Parallel()

rc, err := OpenReader(filepath.Join("testdata", "patches.raw"))
if err != nil {
t.Fatal(err)
}
defer rc.Close()

buf := new(bytes.Buffer)

// Time needs to be set and non-zero to generate a static non-zero
// serial number
wc, err := NewWriter(buf, FormatTime(1))
if err != nil {
t.Fatal(err)
}

hashes := make(map[string][]byte)

for _, f := range rc.File {
b, err := copyData(f, wc)
if err != nil {
t.Fatal(err)
}

hashes[f.Name] = b
}

if err := wc.Close(); err != nil {
t.Fatal(err)
}

r, err := NewReader(buf)
if err != nil {
t.Fatal(err)
}

for _, f := range r.File {
fr, err := f.Open()
if err != nil {
t.Fatal(err)
}

if _, err := io.CopyN(io.Discard, fr, int64(binary.Size(entry{}))); err != nil {
t.Fatal(err)
}

h := sha256.New()

if _, err := io.Copy(h, fr); err != nil {
t.Fatal(err)
}

b := h.Sum(nil)

match, ok := hashes[f.Name]
if !ok {
t.Fatal("unexpected file: ", f.Name)
} else if bytes.Equal(match, b) {
delete(hashes, f.Name)
}
}

files := make([]string, 0, len(hashes))

for k := range hashes {
files = append(files, k)
}

assert.ElementsMatch(t, []string{"f_zero.dat", "PSO_SYSTEM", "PSO3_SYSTEM"}, files)
}
Binary file added testdata/patches.raw
Binary file not shown.
21 changes: 20 additions & 1 deletion writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ func (w *fileWriter) Write(p []byte) (int, error) {
return w.buf.Write(p) //nolint:wrapcheck
}

var gamePatches = map[string]func(io.Reader, *memoryCard) (io.Reader, error){
"f_zero.dat": patchFZero,
"PSO_SYSTEM": patchPSO12,
"PSO3_SYSTEM": patchPSO3,
}

//nolint:cyclop,funlen
func (w *fileWriter) Close() error {
w.w.mu.Lock()
defer w.w.mu.Unlock()
Expand Down Expand Up @@ -67,13 +74,25 @@ func (w *fileWriter) Close() error {
return errNoFreeSpace
}

var (
r io.Reader = w.buf
err error
)

patchFunc, ok := gamePatches[e.filename()]
if ok {
if r, err = patchFunc(w.buf, mc); err != nil {
return err
}
}

// Set e.FirstBlock to the correct location
e.FirstBlock = mc.blockMap[mc.activeBlockMap()].LastAllocatedBlock + 1

// Write out the blocks
lastBlock := e.FirstBlock + e.FileLength - reservedBlocks
for i := e.FirstBlock - reservedBlocks; i < lastBlock; i++ {
_, _ = w.buf.Read(mc.blocks[i][:])
_, _ = r.Read(mc.blocks[i][:])

if i+1 < lastBlock {
mc.blockMap[mc.activeBlockMap()].Blocks[i] = i + reservedBlocks + 1
Expand Down