~moody/pse

e4de17d5c93ebf59924d9e9cb90199c04a4267ff — Jacob Moody 8 months ago 7fab316 master
Introduce pse(1)
Refactor how tab marshaling/unmarshaling works to use fmt library
5 files changed, 211 insertions(+), 27 deletions(-)

M README
M box.go
A cmd/pse/main.go
M pokemon.go
M trainer.go
M README => README +31 -0
@@ 7,3 7,34 @@ block regions of the save game.
General control flow is to call NewSafe with a reader of the save file,
modify the struct it retuns and then calling Save.Write() with the output
file writer.

PSE also includes a command line tool pse(1). This tool can be used
to read and write textual representations of data to the save file.

Usage: pse cmd sourcefile <destfile>

Where cmd is one of 'dump' or 'inject' with the later requiring a destfile to be defined.
There also are two flags that are used '-fmt' and '-section' which define the output format
and section to operate on respectivly. The format flag defaults to tab and the section
flag defaults to trainer.

Examples:
# Dump all box information in json
$ pse -section box -fmt json dump ./fire.sav 

# Dump trainer information as tab seperated values
$ pse -section trainer -fmt tab dump ./fire.sav

# Give all Bulbasaur the nickname Chuck
$ pse -section box -fmt tab dump ./fire.sav | sed 's/BULBASAUR/Chuck/g' | ./pse -section box inject ./fire.sav ./newfire.sav

# Make every pokemon in your boxes shiny
$ pse -section box dump fire.sav | awk -F '\t' '{
sid = rshift($3, 16);
tid = and($3, 0xFFFF);
p1 = rshift($2, 16);
p2 = and($2, 0xFFFF);
p1 =  xor(sid, tid, 7, p2);
$3 = lshift(sid, 16)+tid;
$2 = lshift(p1, 16)+p2;
print $0 }' | pse -section box inject fire.sav newfire.sav

M box.go => box.go +34 -7
@@ 3,7 3,8 @@ package pse
import (
	"errors"
	"fmt"
	"strings"
	"io"
	"strconv"
)

type Boxes struct {


@@ 22,14 23,40 @@ type Boxes struct {
	data     [9]*Section
}

func (b *Boxes) MarshalTab() string {
	sb := &strings.Builder{}
	for _, p := range b.Pokemon {
		if p != nil {
			fmt.Fprintf(sb, "%s\n", p.MarshalTab())
func (b *Boxes) Format(f fmt.State, verb rune) {
	switch verb {
	case 'a':
		for i, p := range b.Pokemon {
			if p != nil {
				fmt.Fprintf(f, "%d\t%a\n", i, p)
			}
		}
	}
	return sb.String()
}

func (b *Boxes) Scan(state fmt.ScanState, verb rune) error {
	for {
		istr, err := state.Token(true, nil)
		switch {
		case err == io.EOF:
			return nil
		case err != nil:
			return err
		case len(istr) == 0:
			return nil
		}
		i, err := strconv.Atoi(string(istr))
		if err != nil {
			return err
		}
		if b.Pokemon[i] != nil {
			_, err = fmt.Fscanln(state, b.Pokemon[i])
			if err != nil {
				return err
			}
		}
	}

}

func (b *Boxes) Load(index int, s *Section) error {

A cmd/pse/main.go => cmd/pse/main.go +122 -0
@@ 0,0 1,122 @@
package main

import (
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"os"

	"git.sr.ht/~moody/pse"
)

var fmtFlag string
var sectionFlag string

func init() {
	flag.StringVar(&fmtFlag, "fmt", "tab", "Defines output format for data")
	flag.StringVar(&sectionFlag, "section", "trainer", "What section to operate on")
}

type dumpFunc func(s *pse.Save) (string, error)
type injFunc func(s *pse.Save) error
type spec struct {
	fmt  string
	sect string
}

func jm(v interface{}) (string, error) {
	b, err := json.Marshal(v)
	return string(b), err
}

func jum(r io.Reader, v interface{}) error {
	b, err := ioutil.ReadAll(r)
	if err != nil {
		return err
	}
	return json.Unmarshal(b, v)
}

var dumpTable = map[spec]dumpFunc{
	{"json", "trainer"}: func(s *pse.Save) (string, error) { return jm(s.Info) },
	{"json", "box"}:     func(s *pse.Save) (string, error) { return jm(s.Boxes) },
	{"json", "inv"}:     func(s *pse.Save) (string, error) { return jm(s.Inv) },
	{"tab", "trainer"}:  func(s *pse.Save) (string, error) { return fmt.Sprintf("%a\n", s.Info), nil },
	{"tab", "box"}:      func(s *pse.Save) (string, error) { return fmt.Sprintf("%a", s.Boxes), nil },
}

var injTable = map[spec]injFunc{
	{"json", "trainer"}: func(s *pse.Save) error { return jum(os.Stdin, s.Info) },
	{"json", "box"}:     func(s *pse.Save) error { return jum(os.Stdin, s.Boxes) },
	{"json", "inv"}:     func(s *pse.Save) error { return jum(os.Stdin, s.Inv) },
	{"tab", "trainer"}:  func(s *pse.Save) error { _, err := fmt.Fscan(os.Stdin, s.Info); return err },
	{"tab", "box"}:      func(s *pse.Save) error { _, err := fmt.Fscan(os.Stdin, s.Boxes); return err },
}

func dump(s *pse.Save) (string, error) {
	f, ok := dumpTable[spec{fmtFlag, sectionFlag}]
	if !ok {
		return "", errors.New("improper flag combination")
	}
	return f(s)
}

func inject(s *pse.Save) error {
	f, ok := injTable[spec{fmtFlag, sectionFlag}]
	if !ok {
		return errors.New("improper flag combination")
	}
	return f(s)
}

func usage() {
	fmt.Fprintln(os.Stderr, "Usage: pse cmd sourcefile <destfile>")
	os.Exit(2)
}

func main() {
	var l *log.Logger = log.New(os.Stderr, "", 0)

	flag.Parse()
	args := flag.Args()
	if len(args) < 2 {
		usage()
	}

	f, err := os.Open(args[1])
	if err != nil {
		l.Fatal("Error opening sourcefile:", err)
	}
	s, err := pse.NewSave(f)
	if err != nil {
		l.Fatal("Error parsing savedata:", err)
	}
	f.Close()

	switch args[0] {
	case "dump":
		s, err := dump(s)
		if err != nil {
			l.Fatal("Error dumping:", err)
		}
		fmt.Print(s)
	case "inject":
		out, err := os.Create(args[2])
		if err != nil {
			l.Fatal("Error creating output file:", err)
		}
		err = inject(s)
		if err != nil {
			l.Fatal("Error injecting:", err)
		}
		err = s.Write(out)
		if err != nil {
			l.Fatal("Error writting savedata:", err)
		}
		out.Close()
	}
}

M pokemon.go => pokemon.go +13 -13
@@ 4,8 4,6 @@ package pse
import (
	"errors"
	"fmt"
	"io"
	"strings"
)

type PokeDataG struct {


@@ 237,20 235,22 @@ type Pokemon struct {
	data    []byte //Backing data store
}

func (p *Pokemon) MarshalTab() string {
	b := &strings.Builder{}
	pd := &p.PokeData
	fmt.Fprintf(b, "%v\t%v\t%s\t%v\t%s\t%v\t", p.Personality, p.OTID, p.Name.string, p.Lang, p.OTName.string, p.Marks)
	fmt.Fprintf(b, "%v\t%v\t%v\t%v\t%v\t", pd.Species, pd.Item, pd.EXP, pd.PPBonus, pd.Friendship)
	fmt.Fprintf(b, "%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t", pd.Move1, pd.Move2, pd.Move3, pd.Move4, pd.PP1, pd.PP2, pd.PP3, pd.PP4)
	fmt.Fprintf(b, "%v\t%v\t", pd.Pokerus, pd.IV)
	fmt.Fprintf(b, "%v\t%v\t%v\t%v\t%v\t%v", pd.HP, pd.Atk, pd.Def, pd.Spd, pd.SpAtk, pd.SpDef)
	return b.String()
func (p *Pokemon) Format(f fmt.State, verb rune) {
	switch verb {
	case 'a':
		pd := &p.PokeData
		fmt.Fprintf(f, "%v\t%v\t%q\t%v\t%q\t%v\t", p.Personality, p.OTID, p.Name.string, p.Lang, p.OTName.string, p.Marks)
		fmt.Fprintf(f, "%v\t%v\t%v\t%v\t%v\t", pd.Species, pd.Item, pd.EXP, pd.PPBonus, pd.Friendship)
		fmt.Fprintf(f, "%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t", pd.Move1, pd.Move2, pd.Move3, pd.Move4, pd.PP1, pd.PP2, pd.PP3, pd.PP4)
		fmt.Fprintf(f, "%v\t%v\t", pd.Pokerus, pd.IV)
		fmt.Fprintf(f, "%v\t%v\t%v\t%v\t%v\t%v", pd.HP, pd.Atk, pd.Def, pd.Spd, pd.SpAtk, pd.SpDef)
	}
}

func (p *Pokemon) UnmarshalTab(r io.Reader) error {
func (p *Pokemon) Scan(state fmt.ScanState, verb rune) error {
	pd := &p.PokeData
	_, err := fmt.Scanln(&p.Personality, &p.OTID, &p.Name.string, &p.Lang, &p.OTName.string, &p.Marks,
	_, err := fmt.Fscanf(state, "%v\t%v\t%q\t%v\t%q\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t\n",
		&p.Personality, &p.OTID, &p.Name.string, &p.Lang, &p.OTName.string, &p.Marks,
		&pd.Species, &pd.Item, &pd.EXP, &pd.PPBonus, &pd.Friendship,
		&pd.Move1, &pd.Move2, &pd.Move3, &pd.Move4, &pd.PP1, &pd.PP2, &pd.PP3, &pd.PP4,
		&pd.Pokerus, &pd.IV,

M trainer.go => trainer.go +11 -7
@@ 2,7 2,6 @@ package pse

import (
	"fmt"
	"io"
	"strings"
)



@@ 101,15 100,20 @@ func (ti *TrainerInfo) String() string {
	return b.String()
}

func (ti *TrainerInfo) MarshalTab() string {
	return fmt.Sprintf("%s\t%s\t%v\t%v\t%v\t%v\t%v\t%v", ti.Name.string, ti.GenderString(),
		ti.HoursPlayed, ti.MinPlayed, ti.SecPlayed, ti.FramePlayed,
		ti.ID, ti.SecretID)
func (ti *TrainerInfo) Format(f fmt.State, verb rune) {
	switch verb {
	case 'v':
		fmt.Fprint(f, ti.String())
	case 'a':
		fmt.Fprintf(f, "%s\t%s\t%v\t%v\t%v\t%v\t%v\t%v", ti.Name.string, ti.GenderString(),
			ti.HoursPlayed, ti.MinPlayed, ti.SecPlayed, ti.FramePlayed,
			ti.ID, ti.SecretID)
	}
}

func (ti *TrainerInfo) UnmarshalTab(r io.Reader) error {
func (ti *TrainerInfo) Scan(state fmt.ScanState, verb rune) error {
	var gender string
	_, err := fmt.Fscanln(r, &ti.Name.string, &gender,
	_, err := fmt.Fscanln(state, &ti.Name.string, &gender,
		&ti.HoursPlayed, &ti.MinPlayed, &ti.SecPlayed, &ti.FramePlayed,
		&ti.ID, &ti.SecretID)
	switch gender {