Initial commit

Partially copied from justinian/ark, updated to be a single process that
watches save files and updates a single sqlite3 database.
This commit is contained in:
Justin C. Miller
2021-08-22 02:42:22 -07:00
commit d5b1e3264b
24 changed files with 1261 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
menagerie
*.ark
*.db
/*.json

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "obelisk"]
path = obelisk
url = https://github.com/arkutils/Obelisk.git

20
Dockerfile Normal file
View File

@@ -0,0 +1,20 @@
FROM golang:1.16 as build
WORKDIR /build
ADD . /build
RUN go build -o menagerie
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /app
EXPOSE 8090
VOLUME /app/run
ADD static /app/static
ADD obelisk/data/wiki/species.json /app/
ADD obelisk/data/wiki/items.json /app/
COPY --from=0 /build/menagerie /app/
CMD ["./menagerie", "-s", "species.json", "-s", "items.json", "-o", "/app/run/ark.db", "/app/saves"]

148
api_handler.go Normal file
View File

@@ -0,0 +1,148 @@
package main
import (
"encoding/json"
"log"
"net/http"
"sync"
"github.com/jmoiron/sqlx"
)
type apiHandler struct {
lock sync.Mutex
loader *Loader
stmt *sqlx.Stmt
}
const getAllDinos = `
SELECT
d.name,
w.name as world,
c1.name as class_name,
d.dino_id1|d.dino_id2 as dino_id,
level_wild,
level_tamed,
level_total,
is_cryo,
c2.name as parent_class,
parent_name,
x, y, z,
color0, color1, color2, color3, color4, color5,
health_current, stamina_current, torpor_current, oxygen_current, food_current, weight_current, melee_current, speed_current,
health_wild, stamina_wild, torpor_wild, oxygen_wild, food_wild, weight_wild, melee_wild, speed_wild,
health_tamed, stamina_tamed, torpor_tamed, oxygen_tamed, food_tamed, weight_tamed, melee_tamed, speed_tamed,
health_total, stamina_total, torpor_total, oxygen_total, food_total, weight_total, melee_total, speed_total
FROM
dinos d
LEFT JOIN worlds w ON d.world == w.id
LEFT JOIN classes c1 ON d.class == c1.id
LEFT JOIN classes c2 ON d.parent_class == c2.id
`
type dinoResult struct {
Name string `json:"name" db:"name"`
World string `json:"world" db:"world"`
Class string `json:"class_name" db:"class_name"`
DinoId int `json:"dino_id" db:"dino_id"`
LevelsWild int `json:"levels_wild" db:"level_wild"`
LevelsTamed int `json:"levels_tamed" db:"level_tamed"`
LevelsTotal int `json:"levels_total" db:"level_total"`
IsCryopod bool `json:"is_cryo" db:"is_cryo"`
ParentClass *string `json:"parent_class" db:"parent_class"`
ParentName *string `json:"parent_name" db:"parent_name"`
X float64 `json:"x" db:"x"`
Y float64 `json:"y" db:"y"`
Z float64 `json:"z" db:"z"`
Color0 int `json:"color0" db:"color0"`
Color1 int `json:"color1" db:"color1"`
Color2 int `json:"color2" db:"color2"`
Color3 int `json:"color3" db:"color3"`
Color4 int `json:"color4" db:"color4"`
Color5 int `json:"color5" db:"color5"`
HealthCurrent float64 `json:"health_current" db:"health_current"`
StaminaCurrent float64 `json:"stamina_current" db:"stamina_current"`
TorporCurrent float64 `json:"torpor_current" db:"torpor_current"`
OxygenCurrent float64 `json:"oxygen_current" db:"oxygen_current"`
FoodCurrent float64 `json:"food_current" db:"food_current"`
WeightCurrent float64 `json:"weight_current" db:"weight_current"`
MeleeCurrent float64 `json:"melee_current" db:"melee_current"`
SpeedCurrent float64 `json:"speed_current" db:"speed_current"`
HealthWild int64 `json:"health_wild" db:"health_wild"`
StaminaWild int64 `json:"stamina_wild" db:"stamina_wild"`
TorporWild int64 `json:"torpor_wild" db:"torpor_wild"`
OxygenWild int64 `json:"oxygen_wild" db:"oxygen_wild"`
FoodWild int64 `json:"food_wild" db:"food_wild"`
WeightWild int64 `json:"weight_wild" db:"weight_wild"`
MeleeWild int64 `json:"melee_wild" db:"melee_wild"`
SpeedWild int64 `json:"speed_wild" db:"speed_wild"`
HealthTamed int64 `json:"health_tamed" db:"health_tamed"`
StaminaTamed int64 `json:"stamina_tamed" db:"stamina_tamed"`
TorporTamed int64 `json:"torpor_tamed" db:"torpor_tamed"`
OxygenTamed int64 `json:"oxygen_tamed" db:"oxygen_tamed"`
FoodTamed int64 `json:"food_tamed" db:"food_tamed"`
WeightTamed int64 `json:"weight_tamed" db:"weight_tamed"`
MeleeTamed int64 `json:"melee_tamed" db:"melee_tamed"`
SpeedTamed int64 `json:"speed_tamed" db:"speed_tamed"`
HealthTotal int64 `json:"health_total" db:"health_total"`
StaminaTotal int64 `json:"stamina_total" db:"stamina_total"`
TorporTotal int64 `json:"torpor_total" db:"torpor_total"`
OxygenTotal int64 `json:"oxygen_total" db:"oxygen_total"`
FoodTotal int64 `json:"food_total" db:"food_total"`
WeightTotal int64 `json:"weight_total" db:"weight_total"`
MeleeTotal int64 `json:"melee_total" db:"melee_total"`
SpeedTotal int64 `json:"speed_total" db:"speed_total"`
}
func (ah *apiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ah.loader.lock.Lock()
defer ah.loader.lock.Unlock()
db := ah.loader.db
if ah.stmt == nil {
stmt, err := db.Preparex(getAllDinos)
if err != nil {
log.Printf("Error preparing SQL: %s", err)
http.Error(w, "Database Error", http.StatusInternalServerError)
return
}
ah.stmt = stmt
}
var result []dinoResult
err := ah.stmt.Select(&result)
if err != nil {
log.Printf("Error querying database: %s", err)
http.Error(w, "Database Error", http.StatusInternalServerError)
return
}
data, err := json.Marshal(result)
if err != nil {
log.Printf("Error marshalling result: %s", err)
http.Error(w, "JSON Error", http.StatusInternalServerError)
return
}
w.Write(data)
}
func runServer(loader *Loader, addr string) {
apiHandler := &apiHandler{loader: loader}
sm := http.NewServeMux()
sm.Handle("/api/dinos", apiHandler)
sm.Handle("/", http.FileServer(http.Dir("static")))
log.Printf("Listening on: %s", addr)
log.Fatal(http.ListenAndServe(addr, loggingWrapper(sm)))
}

91
classes.go Normal file
View File

@@ -0,0 +1,91 @@
package main
import (
"encoding/json"
"fmt"
"io"
"os"
"regexp"
"strings"
)
var keynames = []string{"items", "species"}
var whitespace = regexp.MustCompile(`\s+`)
var cSuffix = regexp.MustCompile(`_C$`)
type classSpec struct {
Name string `json:"name"`
Blueprint string `json:"bp"`
}
type Class struct {
Name string
Id int
}
type ClassMap struct {
Map map[string]Class
nextId int
}
func (cm *ClassMap) Get(bpName string) *Class {
if class, ok := cm.Map[bpName]; ok {
return &class
}
return nil
}
func (cm *ClassMap) Add(bpName string) *Class {
class := Class{
Name: cSuffix.ReplaceAllString(bpName, ""),
Id: cm.nextId,
}
cm.Map[bpName] = class
cm.nextId++
return &class
}
func readSpecFiles(paths ...string) (*ClassMap, error) {
classCount := 1 // leave 0 for "none"
classNames := make(map[string]Class)
for _, path := range paths {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("Opening spec file %f:\n%w", path, err)
}
jsonData, err := io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("Reading spec file %f:\n%w", path, err)
}
var values map[string]json.RawMessage
if err := json.Unmarshal(jsonData, &values); err != nil {
return nil, fmt.Errorf("Loading spec file %f:\n%w", path, err)
}
for _, key := range keynames {
if raw, ok := values[key]; ok {
var specs []classSpec
if err := json.Unmarshal(raw, &specs); err != nil {
return nil, fmt.Errorf("Loading specs from file %f:\n%w", path, err)
}
for _, spec := range specs {
parts := strings.Split(spec.Blueprint, ".")
className := parts[len(parts)-1]
classNames[className] = Class{
Name: whitespace.ReplaceAllString(spec.Name, " "),
Id: classCount,
}
classCount++
}
}
}
}
return &ClassMap{Map: classNames, nextId: classCount}, nil
}

80
database_schema.go Normal file
View File

@@ -0,0 +1,80 @@
package main
var databaseSchema = []string{`
CREATE TABLE worlds (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE,
iter INTEGER DEFAULT 0
);`,
`CREATE TABLE classes (
id INTEGER PRIMARY KEY,
class TEXT,
name TEXT
);`,
`CREATE TABLE dinos (
id INTEGER,
list INTEGER,
world INTEGER,
class INTEGER,
name TEXT,
level_wild INTEGER,
level_tamed INTEGER,
dino_id1 INTEGER,
dino_id2 INTEGER,
is_cryo BOOLEAN,
parent_class INTEGER,
parent_name TEXT,
x FLOAT,
y FLOAT,
z FLOAT,
color0 INTEGER,
color1 INTEGER,
color2 INTEGER,
color3 INTEGER,
color4 INTEGER,
color5 INTEGER,
health_current FLOAT,
stamina_current FLOAT,
torpor_current FLOAT,
oxygen_current FLOAT,
food_current FLOAT,
weight_current FLOAT,
melee_current FLOAT,
speed_current FLOAT,
health_wild INTEGER,
stamina_wild INTEGER,
torpor_wild INTEGER,
oxygen_wild INTEGER,
food_wild INTEGER,
weight_wild INTEGER,
melee_wild INTEGER,
speed_wild INTEGER,
health_tamed INTEGER,
stamina_tamed INTEGER,
torpor_tamed INTEGER,
oxygen_tamed INTEGER,
food_tamed INTEGER,
weight_tamed INTEGER,
melee_tamed INTEGER,
speed_tamed INTEGER,
level_total INTEGER AS (level_wild+level_tamed),
health_total INTEGER AS (health_wild+health_tamed),
stamina_total INTEGER AS (stamina_wild+stamina_tamed),
torpor_total INTEGER AS (torpor_wild+torpor_tamed),
oxygen_total INTEGER AS (oxygen_wild+oxygen_tamed),
food_total INTEGER AS (food_wild+food_tamed),
weight_total INTEGER AS (weight_wild+weight_tamed),
melee_total INTEGER AS (melee_wild+melee_tamed),
speed_total INTEGER AS (speed_wild+speed_tamed),
PRIMARY KEY (id, list, world)
);`,
}

11
go.mod Normal file
View File

@@ -0,0 +1,11 @@
module github.com/justinian/menagerie
go 1.16
require (
github.com/fsnotify/fsnotify v1.5.0
github.com/jmoiron/sqlx v1.3.4
github.com/justinian/ark v0.0.0-20210822045058-dd888d05317b
github.com/mattn/go-sqlite3 v1.14.8
github.com/spf13/pflag v1.0.5
)

19
go.sum Normal file
View File

@@ -0,0 +1,19 @@
github.com/fsnotify/fsnotify v1.5.0 h1:NO5hkcB+srp1x6QmwvNZLeaOgbM8cmBTN32THzjvu2k=
github.com/fsnotify/fsnotify v1.5.0/go.mod h1:BX0DCEr5pT4jm2CnQdVP1lFV521fcCNcyEeNp4DQQDk=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jmoiron/sqlx v1.3.4 h1:wv+0IJZfL5z0uZoUjlpKgHkgaFSYD+r9CfrXjEXsO7w=
github.com/jmoiron/sqlx v1.3.4/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
github.com/justinian/ark v0.0.0-20210822045058-dd888d05317b h1:U/6HBeRnUoS1i+fymZxc/h9AeyThMyOEcA35wMd89Gw=
github.com/justinian/ark v0.0.0-20210822045058-dd888d05317b/go.mod h1:rWERZwRn9NUgudnTqcamOqfGHG8OW+6bTYxidxmSCJI=
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU=
github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

303
loader.go Normal file
View File

@@ -0,0 +1,303 @@
package main
import (
"database/sql"
"fmt"
"log"
"os"
"strings"
"sync"
"github.com/fsnotify/fsnotify"
"github.com/jmoiron/sqlx"
"github.com/justinian/ark"
_ "github.com/mattn/go-sqlite3"
)
type Loader struct {
lock sync.Mutex
db *sqlx.DB
classMap *ClassMap
saveFiles []string
}
func createLoader(dbname string, specfiles, savefiles []string) (*Loader, error) {
// Always start with a fresh-loaded db, because options could have
// changed.
err := os.Remove(dbname)
if err != nil {
if !os.IsNotExist(err) {
return nil, fmt.Errorf("Could not move old db file:\n%w", err)
}
}
log.Printf("Creating sqlite3 database: %s", dbname)
db, err := sqlx.Connect("sqlite3", dbname)
if err != nil {
return nil, fmt.Errorf("Could not open db file:\n%w", err)
}
for _, table := range databaseSchema {
_, err = db.Exec(table)
if err != nil {
return nil, fmt.Errorf("Could not create SQL schema:\n%w", err)
}
}
classMap, err := readSpecFiles(specfiles...)
if err != nil {
return nil, fmt.Errorf("Reading spec files:\n%w", err)
}
tx, err := db.Begin()
if err != nil {
return nil, fmt.Errorf("Could not begin SQL transaction:\n%w", err)
}
stmt, err := tx.Prepare("INSERT INTO classes VALUES (?,?,?)")
if err != nil {
return nil, fmt.Errorf("Could not prepare SQL class insert:\n%w", err)
}
for bpName, class := range classMap.Map {
_, err = stmt.Exec(class.Id, bpName, class.Name)
if err != nil {
return nil, fmt.Errorf("Inserting class: (%d, %s):\n%w", class.Id, class.Name, err)
}
}
log.Printf("Inserted %d class names from %d spec files.", len(classMap.Map), len(specfiles))
err = tx.Commit()
if err != nil {
return nil, fmt.Errorf("Could not commit class names:\n%w", err)
}
return &Loader{db: db, classMap: classMap, saveFiles: savefiles}, nil
}
func (l *Loader) run() error {
for _, savefile := range l.saveFiles {
err := l.processSavefile(savefile)
if err != nil {
return fmt.Errorf("Processing %s:\n%w", savefile, err)
}
}
go l.watcher()
return nil
}
func (l *Loader) processSavefile(filename string) error {
l.lock.Lock()
defer l.lock.Unlock()
log.Printf("Processing save file: %s", filename)
archive, err := ark.OpenArchive(filename)
if err != nil {
return fmt.Errorf("Could not open save file:\n%w", err)
}
save, err := ark.ReadSaveGame(archive)
if err != nil {
return fmt.Errorf("Could not read save game:\n%w", err)
}
worldName := save.DataFiles[0]
if strings.HasSuffix(worldName, "_P") {
worldName = worldName[:len(worldName)-2]
}
tx, err := l.db.Begin()
if err != nil {
return fmt.Errorf("Could not begin SQL transaction:\n%w", err)
}
res, err := tx.Exec(`
INSERT INTO worlds (name) VALUES (?)
ON CONFLICT (name) DO UPDATE SET iter=iter+1`, worldName)
if err != nil {
return fmt.Errorf("Could not insert world name:\n%w", err)
}
worldId, err := res.LastInsertId()
if err != nil {
return fmt.Errorf("Could not get world id:\n%w", err)
}
_, err = tx.Exec("DELETE FROM dinos WHERE world = ?", worldId)
if err != nil {
return fmt.Errorf("Could not clear previous world iteration:\n%w", err)
}
err = l.insertDinos(save.Objects, int(worldId), tx)
if err != nil {
return fmt.Errorf("Inserting dino:\n%w", err)
}
err = tx.Commit()
if err != nil {
return fmt.Errorf("Could not commit SQL transaction:\n%w", err)
}
return nil
}
func (l *Loader) insertDinos(objlists [][]*ark.GameObject, world int, tx *sql.Tx) error {
stmt, err := tx.Prepare(`INSERT INTO dinos VALUES (
?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,
?,?,?,?,?,?,
?,?,?,?,?,?,?,?,
?,?,?,?,?,?,?,?,
?,?,?,?,?,?,?,?)`)
if err != nil {
return fmt.Errorf("Could not prepare SQL insert:\n%w", err)
}
for listNum, objlist := range objlists {
for i, obj := range objlist {
// TamedOnServerName is a good canary for tamed dinos
server := obj.Properties.Get("TamedOnServerName", 0)
if server == nil {
continue
}
name := obj.Properties.GetString("TamedName", 0)
statsCurrent := make([]float64, 12)
pointsWild := make([]int64, 12)
pointsTamed := make([]int64, 12)
var levelWild int64
var levelTamed int64
loc := obj.Location
var err error
parentClass := 0
parentName := ""
if obj.Parent != nil {
loc = obj.Parent.Location
parentClass, err = l.getOrAddClass(tx, obj.Parent.ClassName.Name)
if err != nil {
return err
}
parentName = obj.Parent.Properties.GetString("BoxName", 0)
if parentName == "" {
parentName = obj.Parent.Properties.GetString("PlayerName", 0)
}
}
cscProp := obj.Properties.GetTyped("MyCharacterStatusComponent", 0, ark.ObjectPropertyType)
if cscProp != nil {
cscId := cscProp.(*ark.ObjectProperty).Id
csc := objlist[cscId]
for index := 0; index < 12; index++ {
statsCurrent[index] = csc.Properties.GetFloat("CurrentStatusValues", index)
pointsWild[index] = csc.Properties.GetInt("NumberOfLevelUpPointsApplied", index)
pointsTamed[index] = csc.Properties.GetInt("NumberOfLevelUpPointsAppliedTamed", index)
}
levelWild = csc.Properties.GetInt("BaseCharacterLevel", 0)
levelTamed = csc.Properties.GetInt("ExtraCharacterLevel", 0)
}
dinoId1 := obj.Properties.GetInt("DinoID1", 0)
dinoId2 := obj.Properties.GetInt("DinoID2", 0)
colors := make([]int64, 6)
for i := range colors {
colors[i] = obj.Properties.GetInt("ColorSetIndices", i)
}
classId, err := l.getOrAddClass(tx, obj.ClassName.Name)
if err != nil {
return err
}
_, err = stmt.Exec(
i,
listNum,
world,
classId,
name,
levelWild,
levelTamed,
dinoId1,
dinoId2,
obj.IsCryopod,
parentClass,
parentName,
loc.X, loc.Y, loc.Z,
colors[0], colors[1], colors[2],
colors[3], colors[4], colors[5],
statsCurrent[0], statsCurrent[1], statsCurrent[2], statsCurrent[3],
statsCurrent[4], statsCurrent[7], statsCurrent[8], statsCurrent[9],
pointsWild[0], pointsWild[1], pointsWild[2], pointsWild[3],
pointsWild[4], pointsWild[7], pointsWild[8], pointsWild[9],
pointsTamed[0], pointsTamed[1], pointsTamed[2], pointsTamed[3],
pointsTamed[4], pointsTamed[7], pointsTamed[8], pointsTamed[9],
)
if err != nil {
return fmt.Errorf("Could not insert object %d:\n%w", i, err)
}
}
}
return nil
}
func (l *Loader) getOrAddClass(tx *sql.Tx, bpName string) (int, error) {
class := l.classMap.Get(bpName)
if class == nil {
class = l.classMap.Add(bpName)
_, err := tx.Exec("INSERT INTO classes (id, class, name) VALUES (?,?,?)",
class.Id, bpName, class.Name)
if err != nil {
return 0, fmt.Errorf("Adding %s to the class table:\n%w", bpName, err)
}
}
return class.Id, nil
}
func (l *Loader) watcher() {
for {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatalf("Error creating file watcher: %s", err)
}
for _, path := range l.saveFiles {
err = watcher.Add(path)
if err != nil {
log.Fatalf("Error watching %s: %s", path, err)
}
}
select {
case event := <-watcher.Events:
err = watcher.Close()
if err != nil {
log.Fatalf("Error closing watcher: %s", err)
}
err = l.processSavefile(event.Name)
if err != nil {
log.Fatalf("Error reloading save %s: %s", err)
}
case err := <-watcher.Errors:
log.Fatalf("Error watching save file: %s", err)
}
}
}

24
logging_wrapper.go Normal file
View File

@@ -0,0 +1,24 @@
package main
import (
"log"
"net/http"
)
type statusSaver struct {
s int
w http.ResponseWriter
}
func (s *statusSaver) Status() int { return s.s }
func (s *statusSaver) Header() http.Header { return s.w.Header() }
func (s *statusSaver) Write(b []byte) (int, error) { return s.w.Write(b) }
func (s *statusSaver) WriteHeader(c int) { s.s = c; s.w.WriteHeader(c) }
func loggingWrapper(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
s := statusSaver{s: 200, w: w}
h.ServeHTTP(&s, r)
log.Printf("%21s %3d%7s %s", r.RemoteAddr, s.Status(), r.Method, r.URL)
})
}

74
main.go Normal file
View File

@@ -0,0 +1,74 @@
package main
import (
"fmt"
"log"
"os"
"path"
"strings"
"github.com/spf13/pflag"
)
func main() {
var output string
var address string
var specfiles []string
pflag.StringVarP(&output, "out", "o", "ark.db", "Filename of the database to create")
pflag.StringVarP(&address, "addr", "a", "[::]:8090", "Address to listen on")
pflag.StringArrayVarP(&specfiles, "spec", "s", nil, "JSON species/item files to load")
pflag.Parse()
args := pflag.Args()
if len(args) < 1 {
fmt.Fprintf(os.Stderr, "Usage: %s [options] <savefile> ...\n", os.Args[0])
fmt.Fprintf(os.Stderr, " %s -h for help\n", os.Args[0])
os.Exit(1)
}
saves := make([]string, 0, len(args))
for _, savepath := range args {
info, err := os.Stat(savepath)
if err != nil {
log.Fatal("%s: %s", savepath, err)
}
if !info.IsDir() {
saves = append(saves, savepath)
continue
}
entries, err := os.ReadDir(savepath)
if err != nil {
log.Fatal("Directory %s: %s", savepath, err)
}
for _, ent := range entries {
if ent.IsDir() {
continue
}
if strings.HasSuffix(ent.Name(), ".ark") {
saves = append(saves, path.Join(savepath, ent.Name()))
}
}
}
log.Print("Menagerie starting.")
for _, save := range saves {
log.Printf("Using save file: %s", save)
}
loader, err := createLoader(output, specfiles, saves)
if err != nil {
log.Fatal(err)
}
defer loader.db.Close()
err = loader.run()
if err != nil {
log.Fatal(err)
}
runServer(loader, address)
}

1
obelisk Submodule

Submodule obelisk added at 7e2bc55b7d

132
static/colors.js Normal file
View File

@@ -0,0 +1,132 @@
var colors = [
null,
{"id": 1, "name": "Red", "color": "#ff0000"},
{"id": 2, "name": "Blue", "color": "#0000ff"},
{"id": 3, "name": "Green", "color": "#00ff00"},
{"id": 4, "name": "Yellow", "color": "#ffff00"},
{"id": 5, "name": "Cyan", "color": "#00ffff"},
{"id": 6, "name": "Magenta", "color": "#ff00ff"},
{"id": 7, "name": "Light Green", "color": "#c0ffba"},
{"id": 8, "name": "Light Grey", "color": "#c8caca"},
{"id": 9, "name": "Light Brown", "color": "#786759"},
{"id": 10, "name": "Light Orange", "color": "#ffb46c"},
{"id": 11, "name": "Light Yellow", "color": "#fffa8a"},
{"id": 12, "name": "Light Red", "color": "#ff756c"},
{"id": 13, "name": "Dark Grey", "color": "#7b7b7b"},
{"id": 14, "name": "Black", "color": "#3b3b3b"},
{"id": 15, "name": "Brown", "color": "#593a2a"},
{"id": 16, "name": "Dark Green", "color": "#224900"},
{"id": 17, "name": "Dark Red", "color": "#812118"},
{"id": 18, "name": "White", "color": "#ffffff"},
{"id": 19, "name": "Dino Light Red", "color": "#ffa8a8"},
{"id": 20, "name": "Dino Dark Red", "color": "#592b2b"},
{"id": 21, "name": "Dino Light Orange", "color": "#ffb694"},
{"id": 22, "name": "Dino Dark Orange", "color": "#88532f"},
{"id": 23, "name": "Dino Light Yellow", "color": "#cacaa0"},
{"id": 24, "name": "Dino Dark Yellow", "color": "#94946c"},
{"id": 25, "name": "Dino Light Green", "color": "#e0ffe0"},
{"id": 26, "name": "Dino Medium Green", "color": "#799479"},
{"id": 27, "name": "Dino Dark Green", "color": "#224122"},
{"id": 28, "name": "Dino Light Blue", "color": "#d9e0ff"},
{"id": 29, "name": "Dino Dark Blue", "color": "#394263"},
{"id": 30, "name": "Dino Light Purple", "color": "#e4d9ff"},
{"id": 31, "name": "Dino Dark Purple", "color": "#403459"},
{"id": 32, "name": "Dino Light Brown", "color": "#ffe0ba"},
{"id": 33, "name": "Dino Medium Brown", "color": "#948575"},
{"id": 34, "name": "Dino Dark Brown", "color": "#594e41"},
{"id": 35, "name": "Dino Darker Grey", "color": "#595959"},
{"id": 36, "name": "Dino Albino", "color": "#ffffff"},
{"id": 37, "name": "BigFoot0", "color": "#b79683"},
{"id": 38, "name": "BigFoot4", "color": "#eadad5"},
{"id": 39, "name": "BigFoot5", "color": "#d0a794"},
{"id": 40, "name": "WolfFur", "color": "#c3b39f"},
{"id": 41, "name": "DarkWolfFur", "color": "#887666"},
{"id": 42, "name": "DragonBase0", "color": "#a0664b"},
{"id": 43, "name": "DragonBase1", "color": "#cb7956"},
{"id": 44, "name": "DragonFire", "color": "#bc4f00"},
{"id": 45, "name": "DragonGreen0", "color": "#79846c"},
{"id": 46, "name": "DragonGreen1", "color": "#909c79"},
{"id": 47, "name": "DragonGreen2", "color": "#a5a48b"},
{"id": 48, "name": "DragonGreen3", "color": "#74939c"},
{"id": 49, "name": "WyvernPurple0", "color": "#787496"},
{"id": 50, "name": "WyvernPurple1", "color": "#b0a2c0"},
{"id": 51, "name": "WyvernBlue0", "color": "#6281a7"},
{"id": 52, "name": "WyvernBlue1", "color": "#485c75"},
{"id": 53, "name": "Dino Medium Blue", "color": "#5fa4ea"},
{"id": 54, "name": "Dino Deep Blue", "color": "#4568d4"},
{"id": 55, "name": "NearWhite", "color": "#ededed"},
{"id": 56, "name": "NearBlack", "color": "#515151"},
{"id": 57, "name": "DarkTurquoise", "color": "#184546"},
{"id": 58, "name": "MediumTurquoise", "color": "#007060"},
{"id": 59, "name": "Turquoise", "color": "#00c5ab"},
{"id": 60, "name": "GreenSlate", "color": "#40594c"},
{"id": 61, "name": "Sage", "color": "#3e4f40"},
{"id": 62, "name": "DarkWarmGray", "color": "#3b3938"},
{"id": 63, "name": "MediumWarmGray", "color": "#585554"},
{"id": 64, "name": "LightWarmGray", "color": "#9b9290"},
{"id": 65, "name": "DarkCement", "color": "#525b56"},
{"id": 66, "name": "LightCement", "color": "#8aa196"},
{"id": 67, "name": "LightPink", "color": "#e8b0ff"},
{"id": 68, "name": "DeepPink", "color": "#ff119a"},
{"id": 69, "name": "DarkViolet", "color": "#730046"},
{"id": 70, "name": "DarkMagenta", "color": "#b70042"},
{"id": 71, "name": "BurntSienna", "color": "#7e331e"},
{"id": 72, "name": "MediumAutumn", "color": "#a93000"},
{"id": 73, "name": "Vermillion", "color": "#ef3100"},
{"id": 74, "name": "Coral", "color": "#ff5834"},
{"id": 75, "name": "Orange", "color": "#ff7f00"},
{"id": 76, "name": "Peach", "color": "#ffa73a"},
{"id": 77, "name": "LightAutumn", "color": "#ae7000"},
{"id": 78, "name": "Mustard", "color": "#949427"},
{"id": 79, "name": "ActualBlack", "color": "#171717"},
{"id": 80, "name": "MidnightBlue", "color": "#191d36"},
{"id": 81, "name": "DarkBlue", "color": "#152b3a"},
{"id": 82, "name": "BlackSands", "color": "#302531"},
{"id": 83, "name": "LemonLime", "color": "#a8ff44"},
{"id": 84, "name": "Mint", "color": "#38e985"},
{"id": 85, "name": "Jade", "color": "#008840"},
{"id": 86, "name": "PineGreen", "color": "#0f552e"},
{"id": 87, "name": "SpruceGreen", "color": "#005b45"},
{"id": 88, "name": "LeafGreen", "color": "#5b9725"},
{"id": 89, "name": "DarkLavender", "color": "#5e275f"},
{"id": 90, "name": "MediumLavender", "color": "#853587"},
{"id": 91, "name": "Lavender", "color": "#bd77be"},
{"id": 92, "name": "DarkTeal", "color": "#0e404a"},
{"id": 93, "name": "MediumTeal", "color": "#105563"},
{"id": 94, "name": "Teal", "color": "#14849c"},
{"id": 95, "name": "PowderBlue", "color": "#82a7ff"},
{"id": 96, "name": "Glacial", "color": "#aceaff"},
{"id": 97, "name": "Cammo", "color": "#505118"},
{"id": 98, "name": "DryMoss", "color": "#766e3f"},
{"id": 99, "name": "Custard", "color": "#c0bd5e"},
{"id": 100, "name": "Cream", "color": "#f4ffc0"},
]
var dyes = [
{"id": 201, "name": "Black Dye", "color": "#1f1f1f"},
{"id": 202, "name": "Blue Dye", "color": "#0000ff"},
{"id": 203, "name": "Brown Dye", "color": "#756147"},
{"id": 204, "name": "Cyan Dye", "color": "#00ffff"},
{"id": 205, "name": "Forest Dye", "color": "#006c00"},
{"id": 206, "name": "Green Dye", "color": "#00ff00"},
{"id": 207, "name": "Unused Purple Dye", "color": "#6c00ba"},
{"id": 208, "name": "Orange Dye", "color": "#ff8800"},
{"id": 209, "name": "Parchment Dye", "color": "#ffffba"},
{"id": 210, "name": "Pink Dye", "color": "#ff7be1"},
{"id": 211, "name": "Purple Dye", "color": "#7b00e0"},
{"id": 212, "name": "Red Dye", "color": "#ff0000"},
{"id": 213, "name": "Royalty Dye", "color": "#7b00a8"},
{"id": 214, "name": "Silver Dye", "color": "#e0e0e0"},
{"id": 215, "name": "Sky Dye", "color": "#bad4ff"},
{"id": 216, "name": "Tan Dye", "color": "#ffed82"},
{"id": 217, "name": "Tangerine Dye", "color": "#ad652c"},
{"id": 218, "name": "White Dye", "color": "#fefefe"},
{"id": 219, "name": "Yellow Dye", "color": "#ffff00"},
{"id": 220, "name": "Magenta Dye", "color": "#e71fd9"},
{"id": 221, "name": "Brick Dye", "color": "#94341f"},
{"id": 222, "name": "Cantaloupe Dye", "color": "#ff9a00"},
{"id": 223, "name": "Mud Dye", "color": "#473b2b"},
{"id": 224, "name": "Navy Dye", "color": "#34346c"},
{"id": 225, "name": "Olive Dye", "color": "#baba59"},
{"id": 226, "name": "Slate Dye", "color": "#595959"},
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 732 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 KiB

BIN
static/images/maps/Genesis1.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
static/images/maps/Genesis2.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
static/images/maps/Ragnarok.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 KiB

BIN
static/images/maps/TheIsland.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

BIN
static/images/maps/Valguero.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 KiB

182
static/index.html Normal file
View File

@@ -0,0 +1,182 @@
<html>
<head>
<link rel="stylesheet" type="text/css"
href="https://cdn.jsdelivr.net/npm/bootswatch@5/dist/yeti/bootstrap.min.css">
<link rel="stylesheet" type="text/css"
href="https://cdn.datatables.net/v/bs5/jq-3.3.1/dt-1.10.25/sb-1.1.0/sl-1.3.3/datatables.min.css"/>
<script type="text/javascript"
src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.0.1/js/bootstrap.bundle.min.js"></script>
<script type="text/javascript"
src="https://cdn.datatables.net/v/bs5/jq-3.3.1/dt-1.10.25/sb-1.1.0/sl-1.3.3/datatables.min.js"></script>
<script type="text/javascript" src="colors.js"></script>
<script type="text/javascript" src="main.js"></script>
<script type="text/javascript">
$(document).ready( function () {
table = $('#dinos').DataTable(tableOptions);
table.searchBuilder.container().prependTo( $('#searchBuilderDiv') );
$( '#searchInput' ).on( 'keyup', function () {
table.search( this.value ).draw();
} );
$( '#showBase' ).on( 'click', showStats );
$( '#showTamed' ).on( 'click', showStats );
$( '#showTotal' ).on( 'click', showStats );
$( '#showCurrent' ).on( 'click', showStats );
$( '#showColor' ).on( 'click', showStats );
table.on( "select", function () {
row = table.row({"selected":true}).data();
if (row.name) {
$( '#dinoName' ).html(row.name);
} else {
$( '#dinoName' ).html("Unnamed " + row.class_name);
}
$( '#dinoWorldName' ).html(row.world);
$( '#dinoId' ).html(row.dino_id);
$( '#dinoClass' ).html(row.class_name);
if (row.is_cryo) {
if (row.parent_name) {
$( '#dinoStored' ).html(row.parent_name);
} else {
$( '#dinoStored' ).html(row.parent_class);
}
} else {
$( '#dinoStored' ).html("");
}
for (var i = 0; i < 6; i++) {
var id = "#dinoColor" + i;
var color = row["color" + i];
if (color >= 0) {
c = colors[color];
} else {
c = dyes[color+55];
}
if (c === undefined || c === null) {
$( id ).css("background-color", "");
$( id ).css("border", "");
} else {
$( id ).css("background-color", c.color);
$( id ).css("border", "1px solid black");
}
}
var canvas = document.getElementById("dinoWorldMap");
drawMap(canvas, row.world, row.x, row.y);
});
});
</script>
<style>
.swatch {
float: left;
height: 18px;
width: 18px;
margin-right: 10px;
border: 1px solid black;
}
.dinoInfo {
font-size: smaller;
}
</style>
</head>
<body>
<div class="container-fluid p-4">
<div class="row gx-5">
<div class="col-8 rounded-3 m-3 p-1 bg-light">
<table id="dinos" class="table table-striped nowrap" width="100%"></table>
</div>
<div class="col-3 m-3 p-3">
<div class="row">
<div class="btn-group" width="100%" role="group">
<input type="radio" class="btn-check" name="show" id="showBase" autocomplete="off">
<label class="btn btn-outline-primary" for="showBase">Base Stats</label>
<input type="radio" class="btn-check" name="show" id="showTamed" autocomplete="off">
<label class="btn btn-outline-primary" for="showTamed">Tamed Points</label>
<input type="radio" class="btn-check" name="show" id="showTotal" checked autocomplete="off">
<label class="btn btn-outline-primary" for="showTotal">Total Stats</label>
<input type="radio" class="btn-check" name="show" id="showCurrent" autocomplete="off">
<label class="btn btn-outline-primary" for="showCurrent">Current Stats</label>
<input type="radio" class="btn-check" name="show" id="showColor" autocomplete="off">
<label class="btn btn-outline-primary" for="showColor">Colors</label>
</div>
</div>
<div class="row my-2 p-2">
<input type="text" class="form-control" id="searchInput" placeholder="Search">
</div>
<div class="row my-2 p-2 rounded bg-light" id="searchBuilderDiv">
</div>
<div class="row my-2 p-2 rounded bg-light" id="dinoInfo">
<h3 id="dinoName">No Selection</h3>
<div class="p-3">
<table width="100%" class="dinoInfo table">
<tr>
<th>World</th>
<td id="dinoWorldName"></td>
</tr>
<tr>
<th>Class</th>
<td id="dinoClass"></td>
</tr>
<tr>
<th>Dino ID</th>
<td id="dinoId"></td>
</tr>
<tr>
<th>Stored In</th>
<td id="dinoStored"></td>
</tr>
</table>
</div>
<div class="p-3">
<table width="100%">
<tr>
<th>0</th>
<th>1</th>
<th>2</th>
<th>3</th>
<th>4</th>
<th>5</th>
</tr>
<tr>
<td id="dinoColor0">&nbsp;</td>
<td id="dinoColor1">&nbsp;</td>
<td id="dinoColor2">&nbsp;</td>
<td id="dinoColor3">&nbsp;</td>
<td id="dinoColor4">&nbsp;</td>
<td id="dinoColor5">&nbsp;</td>
</tr>
<canvas id="dinoWorldMap"></canvas>
</div>
</div>
</div>
</div>
</body>
</html>

169
static/main.js Normal file
View File

@@ -0,0 +1,169 @@
var colorFunc = function (data, type, row, meta) {
var c = null;
if (data < 0) {
c = dyes[data+55]
} else {
c = colors[data];
}
if (c) {
return "<div class='swatch' style='background-color: "
+ c.color + ";'></div> " + c.id + ": " + c.name;
} else {
if (data != 0) {
console.log("Unknown color value: " + data + " for slot " + index);
}
return "<div class='swatch'></div> " + data + ": Unused";
}
}
var showStats = function () {
var showBase = $( '#showBase' )[0].checked;
var showTamed = $( '#showTamed' )[0].checked;
var showTotal = $( '#showTotal' )[0].checked;
var showCurrent = $( '#showCurrent' )[0].checked;
var showColor = $( '#showColor' )[0].checked;
table.columns(3).visible(showBase);
table.columns(4).visible(showTamed);
table.columns(5).visible(showTotal);
table.columns([12, 13, 14, 15, 16, 17]).visible(showColor);
table.columns([18, 19, 20, 21, 22, 23, 24, 25]).visible(showCurrent);
table.columns([26, 27, 28, 29, 30, 31, 32, 33]).visible(showBase);
table.columns([34, 35, 36, 37, 38, 39, 40, 41]).visible(showTamed);
table.columns([42, 43, 44, 45, 46, 47, 48, 49]).visible(showTotal);
};
var fixedFloat = function (data, type, row, meta) {
return data.toFixed(0);
};
var maps = {
"TheIsland": {"shiftx": 0.5, "shifty": 0.5, "mulx": 800000, "muly": 800000},
"ScorchedEarth": {"shiftx": 0.5, "shifty": 0.5, "mulx": 800000, "muly": 800000},
"Aberration": {"shiftx": 0.5, "shifty": 0.5, "mulx": 800000, "muly": 800000},
"Extinction": {"shiftx": 0.5, "shifty": 0.5, "mulx": 800000, "muly": 800000},
"Ragnarok": {"shiftx": 0.5, "shifty": 0.5, "mulx": 1310000, "muly": 1310000},
"Valguero": {"shiftx": 0.5, "shifty": 0.5, "mulx": 816000, "muly": 816000},
"CrystalIsles": {"shiftx": 0.4875, "shifty": 0.5, "mulx": 1600000, "muly": 1700000},
"Genesis1": {"shiftx": 0.5, "shifty": 0.5, "mulx": 1050000, "muly": 1050000},
"Genesis2": {"shiftx": 0.49655, "shifty": 0.49655, "mulx": 1450000, "muly": 1450000},
};
var drawMap = function (canvas, world, x, y) {
var mapInfo = maps[world];
if (mapInfo === undefined) {
return;
}
var mapImage = new Image();
mapImage.onload = function () {
canvas.height = mapImage.height;
canvas.width = mapImage.width;
ctx = canvas.getContext("2d");
ctx.drawImage(mapImage, 0, 0);
mx = (x / mapInfo["mulx"] + mapInfo["shiftx"]) * canvas.width;
my = (y / mapInfo["muly"] + mapInfo["shifty"]) * canvas.height;
pointSize = canvas.width / 125;
ctx.fillStyle = "#ff2020";
ctx.beginPath();
ctx.arc(mx, my, pointSize, 0, 2 * Math.PI, true);
ctx.fill();
canvas.style.width = "100%";
};
mapImage.src = "images/maps/" + world + ".webp";
};
var columns = [
{"data": "name", "title": "Name"},
{"data": "world", "title": "World", "visible": false},
{"data": "class_name", "title": "Class"},
{"data": "levels_wild", "title": "Base Lvl", "visible": false},
{"data": "levels_tamed", "title": "Tame Lvl", "visible": false},
{"data": "levels_total", "title": "Total Lvl"},
{"data": "is_cryo", "title": "Stored?", "visible": false},
{"data": "parent_class", "title": "Container Type", "visible": false},
{"data": "parent_name", "title": "Container Name", "visible": false},
{"data": "x", "visible": false},
{"data": "y", "visible": false},
{"data": "z", "visible": false},
{"data": "color0", "render": colorFunc, "title": "C0", "searchBuilderTitle": "Color 0", "visible": false},
{"data": "color1", "render": colorFunc, "title": "C1", "searchBuilderTitle": "Color 1", "visible": false},
{"data": "color2", "render": colorFunc, "title": "C2", "searchBuilderTitle": "Color 2", "visible": false},
{"data": "color3", "render": colorFunc, "title": "C3", "searchBuilderTitle": "Color 3", "visible": false},
{"data": "color4", "render": colorFunc, "title": "C4", "searchBuilderTitle": "Color 4", "visible": false},
{"data": "color5", "render": colorFunc, "title": "C5", "searchBuilderTitle": "Color 5", "visible": false},
{"data": "health_current", "render": fixedFloat, "title": "H", "searchBuilderTitle": "Current Health", "visible": false},
{"data": "stamina_current", "render": fixedFloat, "title": "St", "searchBuilderTitle": "Current Stamina", "visible": false},
{"data": "torpor_current", "render": fixedFloat, "title": "T", "searchBuilderTitle": "Current Torpor", "visible": false},
{"data": "oxygen_current", "render": fixedFloat, "title": "O", "searchBuilderTitle": "Current Oxygen", "visible": false},
{"data": "food_current", "render": fixedFloat, "title": "F", "searchBuilderTitle": "Current Food", "visible": false},
{"data": "weight_current", "render": fixedFloat, "title": "W", "searchBuilderTitle": "Current Weight", "visible": false},
{"data": "melee_current", "render": fixedFloat, "title": "M", "searchBuilderTitle": "Current Melee", "visible": false},
{"data": "speed_current", "render": fixedFloat, "title": "Sp", "searchBuilderTitle": "Current Speed", "visible": false},
{"data": "health_wild", "title": "H", "searchBuilderTitle": "Base Health", "visible": false},
{"data": "stamina_wild", "title": "St", "searchBuilderTitle": "Base Stamina", "visible": false},
{"data": "torpor_wild", "title": "T", "searchBuilderTitle": "Base Torpor", "visible": false},
{"data": "oxygen_wild", "title": "O", "searchBuilderTitle": "Base Oxygen", "visible": false},
{"data": "food_wild", "title": "F", "searchBuilderTitle": "Base Food", "visible": false},
{"data": "weight_wild", "title": "W", "searchBuilderTitle": "Base Weight", "visible": false},
{"data": "melee_wild", "title": "M", "searchBuilderTitle": "Base Melee", "visible": false},
{"data": "speed_wild", "title": "Sp", "searchBuilderTitle": "Base Speed", "visible": false},
{"data": "health_tamed", "title": "H", "searchBuilderTitle": "Health Tamed Points", "visible": false},
{"data": "stamina_tamed", "title": "St", "searchBuilderTitle": "Stamina Tamed Points", "visible": false},
{"data": "torpor_tamed", "title": "T", "searchBuilderTitle": "Torpor Tamed Points", "visible": false},
{"data": "oxygen_tamed", "title": "O", "searchBuilderTitle": "Oxygen Tamed Points", "visible": false},
{"data": "food_tamed", "title": "F", "searchBuilderTitle": "Food Tamed Points", "visible": false},
{"data": "weight_tamed", "title": "W", "searchBuilderTitle": "Weight Tamed Points", "visible": false},
{"data": "melee_tamed", "title": "M", "searchBuilderTitle": "Melee Tamed Points", "visible": false},
{"data": "speed_tamed", "title": "Sp", "searchBuilderTitle": "Speed Tamed Points", "visible": false},
{"data": "health_total", "title":"H", "searchBuilderTitle": "Total Health"},
{"data": "stamina_total", "title":"St", "searchBuilderTitle": "Total Stamina"},
{"data": "torpor_total", "title":"T", "searchBuilderTitle": "Total Torpor"},
{"data": "oxygen_total", "title":"O", "searchBuilderTitle": "Total Oxygen"},
{"data": "food_total", "title":"F", "searchBuilderTitle": "Total Food"},
{"data": "weight_total", "title":"W", "searchBuilderTitle": "Total Weight"},
{"data": "melee_total", "title":"M", "searchBuilderTitle": "Total Melee"},
{"data": "speed_total", "title":"Sp", "searchBuilderTitle": "Total Speed"}
];
var tableOptions = {
"ajax": {"url":"api/dinos", "dataSrc":""},
"columns": columns,
"dom": "rtpil",
"pageLength": 50,
"scrollX": true,
"language": {
"searchBuilder": {
"title": ""
},
},
"select": {
"info": false,
"style": "single",
},
"searchBuilder": {
"columns": [0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 13, 14, 15, 16, 17,
18, 19, 20, 21, 22, 23, 24, 25,
26, 27, 28, 29, 30, 31, 32, 33,
34, 35, 36, 37, 38, 39, 40, 41,
42, 43, 44, 45, 46, 47, 48, 49]
}
};