Skip to content

Commit

Permalink
Merge pull request #4 from myjimnelson/0.4.0-release
Browse files Browse the repository at this point in the history
0.4.0 release with GraphQL
  • Loading branch information
myjimnelson authored Jan 7, 2019
2 parents 16952a0 + d45bf38 commit a7bc6e5
Show file tree
Hide file tree
Showing 10 changed files with 1,100 additions and 253 deletions.
13 changes: 4 additions & 9 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
# data / output files
*.SAV
*.sav
*.gz
*.xxd
*.bak
html/*.svg
html/*.json
html/debug/
*.exe
*.csv
*.json

#python specific
*.pyc
# built executables
civ3sat/civ3sat
*.exe

## generic files to ignore
*~
Expand Down
94 changes: 80 additions & 14 deletions civ3sat/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,46 +8,59 @@ import (
"os"
"text/tabwriter"

"github.com/myjimnelson/c3sat/civ3satgql"
"github.com/myjimnelson/c3sat/parseciv3"
"github.com/urfave/cli"
)

var saveFilePath string

var pathFlag = cli.StringFlag{
Name: "path, p",
// Value: ".",
Usage: "`FILEPATH` of save",
EnvVar: "CIV3SAT_SAV",
Destination: &saveFilePath,
}

func main() {
app := cli.NewApp()
app.Name = "Civ3 Show-And-Tell"
app.Version = "0.3.1"
app.Version = "0.4.0"
app.Usage = "A utility to extract data from Civ3 SAV and BIQ files. Provide a file name of a SAV or BIQ file after the command."

app.Commands = []cli.Command{
{
Name: "seed",
Aliases: []string{"s"},
Usage: "Show the world seed and map settings needed to generate the map, if this map was randomly generated.",
Usage: "Show the world seed and map settings needed to generate the map, if was randomly generated.",
Flags: []cli.Flag{
pathFlag,
},
Action: func(c *cli.Context) error {
var gameData parseciv3.Civ3Data
var err error
path := c.Args().First()
gameData, err = parseciv3.NewCiv3Data(path)
if err != nil {
return err
}
fmt.Println()
w := new(tabwriter.Writer)
defer w.Flush()
w.Init(os.Stdout, 0, 8, 0, '\t', 0)
settings := gameData.WorldSettings()
settings, err := civ3satgql.WorldSettings(saveFilePath)
if err != nil {
return cli.NewExitError(err, 1)
}
for i := range settings {
fmt.Fprintf(w, "%s\t%s\t%s\n", settings[i][0], settings[i][1], settings[i][2])
}
w.Flush()
return nil
},
},
{
Name: "decompress",
Aliases: []string{"d"},
Usage: "decompress a Civ3 data file to out.sav in the current folder",
Flags: []cli.Flag{
pathFlag,
},
Action: func(c *cli.Context) error {
filedata, _, err := parseciv3.ReadFile(c.Args().First())
filedata, _, err := parseciv3.ReadFile(saveFilePath)
if err != nil {
return err
}
Expand All @@ -66,8 +79,11 @@ func main() {
Name: "hexdump",
Aliases: []string{"x"},
Usage: "hex dump a Civ3 data file to stdout",
Flags: []cli.Flag{
pathFlag,
},
Action: func(c *cli.Context) error {
filedata, _, err := parseciv3.ReadFile(c.Args().First())
filedata, _, err := parseciv3.ReadFile(saveFilePath)
if err != nil {
return err
}
Expand All @@ -76,7 +92,57 @@ func main() {
return nil
},
},
{
Name: "graphql",
Aliases: []string{"gql", "g"},
ArgsUsage: "<query>",
Usage: "Execute GraphQL query",
Flags: []cli.Flag{
pathFlag,
},
Action: func(c *cli.Context) error {
// var gameData parseciv3.Civ3Data
var err error
query := c.Args().First()
result, err := civ3satgql.Query(query, saveFilePath)
if err != nil {
return cli.NewExitError(err, 1)
}
fmt.Print(result)
return nil
},
},
{
Name: "api",
Aliases: []string{"www"},
Usage: "Open save, start GraphQL API at http://127.0.0.1:8080/graphql . Control-c to exit.",
Flags: []cli.Flag{
pathFlag,
cli.StringFlag{
Name: "addr",
Value: "127.0.0.1",
Usage: "`ADDRESS` on which to bind",
EnvVar: "CIV3SAT_ADDR",
},
cli.StringFlag{
Name: "port",
Value: "8080",
Usage: "`PORT` on which to listen",
EnvVar: "CIV3SAT_PORT",
},
},
Action: func(c *cli.Context) error {
var err error
fmt.Println("Starting API server for save file at " + saveFilePath)
fmt.Println("GraphQL at http://" + c.String("addr") + ":" + c.String("port") + "/graphql")
fmt.Println("Press control-C to exit")
err = civ3satgql.Server(saveFilePath, c.String("addr"), c.String("port"))
if err != nil {
return cli.NewExitError(err, 1)
}
return nil
},
},
}

app.Run(os.Args)
}
165 changes: 165 additions & 0 deletions civ3satgql/civ3satgql.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package civ3satgql

import (
"encoding/json"
"log"
"net/http"
"strconv"

"github.com/graphql-go/graphql"
"github.com/graphql-go/handler"
"github.com/myjimnelson/c3sat/parseciv3"
)

type sectionType struct {
name string
offset int
length int
}

type saveGameType struct {
data []byte
sections []sectionType
}

var saveGame saveGameType

func findSections() {
var i, count, offset int
for i < len(saveGame.data) {
// for i < 83000 {
if saveGame.data[i] < 0x20 || saveGame.data[i] > 0x5a {
count = 0
} else {
if count == 0 {
offset = i
}
count++
}
i++
if count > 3 {
count = 0
s := new(sectionType)
s.offset = offset
s.name = string(saveGame.data[offset:i])
saveGame.sections = append(saveGame.sections, *s)
// fmt.Println(string(saveGame.data[offset:i]) + " " + strconv.Itoa(offset))
}
}
}

// Handler wrapper to allow adding headers to all responses
// concept yoinked from http://echorand.me/dissecting-golangs-handlerfunc-handle-and-defaultservemux.html
func setHeaders(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Set Origin headers for CORS
// yoinked from http://stackoverflow.com/questions/12830095/setting-http-headers-in-golang Matt Bucci's answer
if origin := r.Header.Get("Origin"); origin != "" {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
w.Header().Set("Access-Control-Allow-Headers",
"Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
}
// Since we're dynamically setting origin, don't let it get cached
w.Header().Set("Vary", "Origin")
handler.ServeHTTP(w, r)
})
}

func Server(path string, bindAddress, bindPort string) error {
var err error
saveGame.data, _, err = parseciv3.ReadFile(path)
if err != nil {
return err
}
findSections()

Schema, err := graphql.NewSchema(graphql.SchemaConfig{
Query: queryType,
// Mutation: MutationType,
})
if err != nil {
return err
}

// create a graphl-go HTTP handler
graphQlHandler := handler.New(&handler.Config{
Schema: &Schema,
Pretty: false,
// GraphiQL provides simple web browser query interface pulled from Internet
GraphiQL: false,
// Playground provides fancier web browser query interface pulled from Internet
Playground: true,
})

http.Handle("/graphql", setHeaders(graphQlHandler))
log.Fatal(http.ListenAndServe(bindAddress+":"+bindPort, nil))
return nil
}

func Query(query, path string) (string, error) {
var err error
saveGame.data, _, err = parseciv3.ReadFile(path)
if err != nil {
return "", err
}
findSections()
// fmt.Println(saveGame.sections[len(saveGame.sections)-1])
// saveGame.sections = []string{"hello", "there"}
Schema, err := graphql.NewSchema(graphql.SchemaConfig{
Query: queryType,
// Mutation: MutationType,
})
if err != nil {
return "", err
}
result := graphql.Do(graphql.Params{
Schema: Schema,
RequestString: query,
})
out, err := json.Marshal(result)
if err != nil {
return "", err
}

// return hex.EncodeToString(saveGame[:4]), nil
return string(out[:]), nil
}

// Adapting this from the parseciv3 module to pull data this module's way
// To keep the existing seed command without having to gql everything
// WorldSettings Returns the information needed to regenerate the map
// presuming the map was originally generated by Civ3 and not later edited
func WorldSettings(path string) ([][3]string, error) {
var worldSize = [...]string{"Tiny", "Small", "Standard", "Large", "Huge", "Random"}
// // "No Barbarians" is actually -1. To make code simpler, add one to value for array index
var barbs = [...]string{"No Barbarians", "Sedentary", "Roaming", "Restless", "Raging", "Random"}
var landMass = [...]string{"Archipelago", "Continents", "Pangea", "Random"}
var oceanCoverage = [...]string{"80% Water", "70% Water", "60% Water", "Random"}
var climate = [...]string{"Arid", "Normal", "Wet", "Random"}
var temperature = [...]string{"Warm", "Temperate", "Cool", "Random"}
var age = [...]string{"3 Billion", "4 Billion", "5 Billion", "Random"}
var settings [][3]string
var err error
saveGame.data, _, err = parseciv3.ReadFile(path)
if err != nil {
return [][3]string{}, err
}
findSections()
wrldSection, err := SectionOffset("WRLD", 1)
if err != nil {
return [][3]string{}, err
}
settings = [][3]string{
{"Setting", "Choice", "Result"},
{"World Seed", strconv.FormatUint(uint64(ReadInt32(wrldSection+170, Signed)), 10), ""},
{"World Size", worldSize[ReadInt32(wrldSection+234, Signed)], ""},
{"Barbarians", barbs[ReadInt32(wrldSection+194, Signed)+1], barbs[ReadInt32(wrldSection+198, Signed)+1]},
{"Land Mass", landMass[ReadInt32(wrldSection+202, Signed)], landMass[ReadInt32(wrldSection+206, Signed)]},
{"Water Coverage", oceanCoverage[ReadInt32(wrldSection+210, Signed)], oceanCoverage[ReadInt32(wrldSection+214, Signed)]},
{"Climate", climate[ReadInt32(wrldSection+186, Signed)], climate[ReadInt32(wrldSection+190, Signed)]},
{"Temperature", temperature[ReadInt32(wrldSection+218, Signed)], temperature[ReadInt32(wrldSection+222, Signed)]},
{"Age", age[ReadInt32(wrldSection+226, Signed)], age[ReadInt32(wrldSection+230, Signed)]},
}
return settings, nil
}
52 changes: 52 additions & 0 deletions civ3satgql/lookups.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package civ3satgql

import (
"errors"
"strconv"
)

// to make calling functions readable
const Signed = true
const Unsigned = false

func ReadInt32(offset int, signed bool) int {
n := int(saveGame.data[offset]) +
int(saveGame.data[offset+1])*0x100 +
int(saveGame.data[offset+2])*0x10000 +
int(saveGame.data[offset+3])*0x1000000
if signed && n > 0x80000000 {
n = n - 0x80000000
}
return n
}

func ReadInt16(offset int, signed bool) int {
n := int(saveGame.data[offset]) +
int(saveGame.data[offset+1])*0x100
if signed && n > 0x8000 {
n = n - 0x8000
}
return n
}

func ReadInt8(offset int, signed bool) int {
n := int(saveGame.data[offset])
if signed && n > 0x80 {
n = n - 0x80
}
return n
}

func SectionOffset(sectionName string, nth int) (int, error) {
var i, n int
for i < len(saveGame.sections) {
if saveGame.sections[i].name == sectionName {
n++
if n >= nth {
return saveGame.sections[i].offset + len(sectionName), nil
}
}
i++
}
return -1, errors.New("Could not find " + strconv.Itoa(nth) + " section named " + sectionName)
}
Loading

0 comments on commit a7bc6e5

Please sign in to comment.