From c5cc245e25ba9b024272034c1b86fdc64004f54c Mon Sep 17 00:00:00 2001 From: noe Date: Thu, 13 Jun 2024 22:33:29 -0400 Subject: [PATCH] init --- .envrc | 2 + .gitignore | 2 + cmd/codegen-vehicles/main.go | 100 +++++++++++++++++++++++++++ cmd/codegen-vehicles/template.go | 39 +++++++++++ cmd/codegen-vehicles/vehicle_list.go | 27 ++++++++ cmd/ws/event_handler.go | 65 +++++++++++++++++ cmd/ws/event_handler_test.go | 72 +++++++++++++++++++ cmd/ws/event_names.go | 22 ++++++ cmd/ws/event_names_test.go | 14 ++++ cmd/ws/ingest.go | 14 ++++ cmd/ws/pop_event.go | 42 +++++++++++ cmd/ws/ws.go | 71 +++++++++++++++++++ docker-compose.yaml | 10 +++ flake.lock | 58 ++++++++++++++++ flake.nix | 15 ++++ go.mod | 25 +++++++ go.sum | 57 +++++++++++++++ justfile | 2 + shell.nix | 8 +++ store/player.go | 48 +++++++++++++ translators/loadouts.go | 56 +++++++++++++++ translators/loadouts_test.go | 13 ++++ translators/vehicles.go | 33 +++++++++ translators/vehicles_map.gen.go | 54 +++++++++++++++ translators/vehicles_test.go | 13 ++++ types/census.go | 10 +++ types/payload.go | 23 ++++++ util/map.go | 11 +++ util/map_test.go | 20 ++++++ 29 files changed, 926 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 cmd/codegen-vehicles/main.go create mode 100644 cmd/codegen-vehicles/template.go create mode 100644 cmd/codegen-vehicles/vehicle_list.go create mode 100644 cmd/ws/event_handler.go create mode 100644 cmd/ws/event_handler_test.go create mode 100644 cmd/ws/event_names.go create mode 100644 cmd/ws/event_names_test.go create mode 100644 cmd/ws/ingest.go create mode 100644 cmd/ws/pop_event.go create mode 100644 cmd/ws/ws.go create mode 100644 docker-compose.yaml create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 go.mod create mode 100644 go.sum create mode 100644 justfile create mode 100644 shell.nix create mode 100644 store/player.go create mode 100644 translators/loadouts.go create mode 100644 translators/loadouts_test.go create mode 100644 translators/vehicles.go create mode 100644 translators/vehicles_map.gen.go create mode 100644 translators/vehicles_test.go create mode 100644 types/census.go create mode 100644 types/payload.go create mode 100644 util/map.go create mode 100644 util/map_test.go diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..91c714b --- /dev/null +++ b/.envrc @@ -0,0 +1,2 @@ +source .env; +use flake; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b016d04 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.env +.direnv diff --git a/cmd/codegen-vehicles/main.go b/cmd/codegen-vehicles/main.go new file mode 100644 index 0000000..649ed3d --- /dev/null +++ b/cmd/codegen-vehicles/main.go @@ -0,0 +1,100 @@ +// / Generate pkgs/translators/vehicles_map.gen.go +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "regexp" + "strings" + "time" +) + +type VehicleResponse struct { + VehicleList []Vehicle `json:"vehicle_list"` +} + +type Vehicle struct { + VehicleID string `json:"vehicle_id"` + Name LocaleString `json:"name"` +} + +type LocaleString struct { + En string `json:"en"` +} + +func fetchCensusVehicles() (VehicleResponse, error) { + var vehicleResponse VehicleResponse + + client := http.Client{ + Timeout: time.Second * 30, + } + + resp, err := client.Get("https://census.lithafalcon.cc/get/ps2/vehicle") + if err != nil { + return vehicleResponse, fmt.Errorf("census request failed: %w", err) + } + + err = json.NewDecoder(resp.Body).Decode(&vehicleResponse) + if err != nil { + return vehicleResponse, fmt.Errorf("census response decode failed: %w", err) + } + + return vehicleResponse, nil +} + +func generateRegexp(vehicles []string) *regexp.Regexp { + pipes := strings.Join(vehicles, "|") + expr := fmt.Sprintf("(%s)", pipes) + + log.Println(expr) + + return regexp.MustCompile(expr) +} + +func main() { + filterRegexp := generateRegexp(AllVehicles) + + censusVehicles, err := fetchCensusVehicles() + if err != nil { + log.Fatalln("fetch census failed", err) + } + + vehicles := []VehicleItem{} + for _, vehicle := range censusVehicles.VehicleList { + if vehicle.Name.En == "" || strings.Contains(vehicle.Name.En, "Turret") { + continue + } + + match := filterRegexp.FindString(strings.ToLower(vehicle.Name.En)) + if match == "" { + continue + } + + switch match { + case "wasp": + match = "valkyrie" + case "deliverer": + match = "ant" + case "lodestar": + match = "galaxy" + } + + enumName := fmt.Sprintf("%s%s", strings.ToUpper(match[0:1]), match[1:]) + + vehicles = append(vehicles, VehicleItem{ + VehicleID: vehicle.VehicleID, + VehicleEnumName: enumName, + }) + } + + output, err := renderTemplate(TemplateData{ + Vehicles: vehicles, + }) + if err != nil { + log.Fatalln("render failed", err) + } + + fmt.Println(output) +} diff --git a/cmd/codegen-vehicles/template.go b/cmd/codegen-vehicles/template.go new file mode 100644 index 0000000..1c60aaf --- /dev/null +++ b/cmd/codegen-vehicles/template.go @@ -0,0 +1,39 @@ +package main + +import ( + "bytes" + "fmt" + "text/template" +) + +var ( + vehicleMapTmpl = `package translators + +var ( + VehicleMap = map[string]Vehicle{ + {{ range .Vehicles }}"{{ .VehicleID }}": {{ .VehicleEnumName }}, + {{end}} + } +)` + + vehicleMapTemplate = template.Must(template.New("vehicle_map").Parse(vehicleMapTmpl)) +) + +type TemplateData struct { + Vehicles []VehicleItem +} + +type VehicleItem struct { + VehicleID string + VehicleEnumName string +} + +func renderTemplate(data TemplateData) (string, error) { + var buffer bytes.Buffer + err := vehicleMapTemplate.Execute(&buffer, data) + if err != nil { + return "", fmt.Errorf("template render failed, %w", err) + } + + return buffer.String(), nil +} diff --git a/cmd/codegen-vehicles/vehicle_list.go b/cmd/codegen-vehicles/vehicle_list.go new file mode 100644 index 0000000..38f18f6 --- /dev/null +++ b/cmd/codegen-vehicles/vehicle_list.go @@ -0,0 +1,27 @@ +package main + +var ( + AllVehicles = []string{ + "flash", + "sunderer", + "lightning", + "scythe", + "vanguard", + "prowler", + "reaver", + "mosquito", + "galaxy", + "valkyrie", + "wasp", + "deliverer", + "lodestar", + "liberator", + "ant", + "harasser", + "dervish", + "chimera", + "javelin", + "corsair", + "magrider", + } +) diff --git a/cmd/ws/event_handler.go b/cmd/ws/event_handler.go new file mode 100644 index 0000000..bb57c27 --- /dev/null +++ b/cmd/ws/event_handler.go @@ -0,0 +1,65 @@ +package main + +import ( + "context" + "log" + + "github.com/genudine/saerro-go/types" +) + +type EventHandler struct { + Ingest *Ingest +} + +func (eh *EventHandler) HandleEvent(ctx context.Context, event types.ESSEvent) { + if event.EventName == "" { + log.Println("invalid event; dropping") + } + + if event.EventName == "Death" || event.EventName == "VehicleDestroy" { + go eh.HandleDeath(ctx, event) + } else if event.EventName == "GainExperience" { + go eh.HandleExperience(ctx, event) + } + + go eh.HandleAnalytics(ctx, event) +} + +func (eh *EventHandler) HandleDeath(ctx context.Context, event types.ESSEvent) { + if event.CharacterID != "" && event.CharacterID != "0" { + log.Println("got pop event") + pe := PopEventFromESSEvent(event, false) + eh.Ingest.TrackPop(ctx, pe) + } + + if event.AttackerCharacterID != "" && event.AttackerCharacterID != "0" && event.AttackerTeamID != 0 { + log.Println("got attacker pop event") + pe := PopEventFromESSEvent(event, true) + eh.Ingest.TrackPop(ctx, pe) + } +} + +func (eh *EventHandler) HandleExperience(ctx context.Context, event types.ESSEvent) { + // Detect specific vehicles via related experience IDs + vehicleID := "" + switch event.ExperienceID { + case 201: // Galaxy Spawn Bonus + vehicleID = "11" + break + case 233: // Sunderer Spawn Bonus + vehicleID = "2" + break + case 674: // ANT stuff + case 675: + vehicleID = "160" + break + } + + event.VehicleID = vehicleID + pe := PopEventFromESSEvent(event, false) + eh.Ingest.TrackPop(ctx, pe) +} + +func (eh *EventHandler) HandleAnalytics(ctx context.Context, event types.ESSEvent) { + +} diff --git a/cmd/ws/event_handler_test.go b/cmd/ws/event_handler_test.go new file mode 100644 index 0000000..d9cc866 --- /dev/null +++ b/cmd/ws/event_handler_test.go @@ -0,0 +1,72 @@ +package main + +import ( + "context" + "database/sql" + "testing" + "time" + + "github.com/genudine/saerro-go/translators" + "github.com/genudine/saerro-go/types" + "github.com/stretchr/testify/assert" + + _ "modernc.org/sqlite" +) + +func getEventHandlerTestShim(t *testing.T) (EventHandler, *sql.DB) { + db, err := sql.Open("sqlite", ":memory:") + if err != nil { + t.Fatalf("test shim: sqlite open failed, %v", err) + } + + return EventHandler{ + Ingest: &Ingest{ + DB: db, + }, + }, db +} + +func TestHandleDeath(t *testing.T) { + eh, db := getEventHandlerTestShim(t) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) + defer cancel() + + event := types.ESSEvent{ + EventName: "Death", + WorldID: 17, + ZoneID: 2, + + CharacterID: "DollNC", + LoadoutID: "3", + TeamID: types.NC, + + AttackerCharacterID: "Lyyti", + AttackerLoadoutID: "3", + AttackerTeamID: types.TR, + } + + eh.HandleDeath(ctx, event) + + type player struct { + CharacterID string `json:"character_id"` + ClassName string `json:"class_name"` + } + + var player1 player + err := db.QueryRowContext(ctx, "SELECT * FROM players WHERE character_id = ?", event.CharacterID).Scan(&player1) + if err != nil { + t.Error(err) + } + + assert.Equal(t, event.CharacterID, player1.CharacterID) + assert.Equal(t, translators.ClassFromLoadout(event.LoadoutID), player1.ClassName) + + var player2 player + err = db.QueryRowContext(ctx, "SELECT * FROM players WHERE character_id = ?", event.AttackerCharacterID).Scan(&player2) + if err != nil { + t.Error(err) + } + + assert.Equal(t, event.AttackerCharacterID, player2.CharacterID) + assert.Equal(t, translators.ClassFromLoadout(event.AttackerLoadoutID), player2.ClassName) +} diff --git a/cmd/ws/event_names.go b/cmd/ws/event_names.go new file mode 100644 index 0000000..bf5e9ca --- /dev/null +++ b/cmd/ws/event_names.go @@ -0,0 +1,22 @@ +package main + +import ( + "fmt" + + "github.com/genudine/saerro-go/util" +) + +var experienceIDs = []int{ + 2, 3, 4, 5, 6, 7, 34, 51, 53, 55, 57, 86, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, + 100, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 201, 233, 293, + 294, 302, 303, 353, 354, 355, 438, 439, 503, 505, 579, 581, 584, 653, 656, 674, 675, +} + +func getEventNames() []string { + events := util.Map(experienceIDs, func(i int) string { + return fmt.Sprintf("GainExperience_experience_id_%d", i) + }) + events = append(events, "Death", "VehicleDestroy") + + return events +} diff --git a/cmd/ws/event_names_test.go b/cmd/ws/event_names_test.go new file mode 100644 index 0000000..c8eeb27 --- /dev/null +++ b/cmd/ws/event_names_test.go @@ -0,0 +1,14 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEventNames(t *testing.T) { + result := getEventNames() + assert.Contains(t, result, "GainExperience_experience_id_55") + assert.Contains(t, result, "Death") + assert.Contains(t, result, "VehicleDestroy") +} diff --git a/cmd/ws/ingest.go b/cmd/ws/ingest.go new file mode 100644 index 0000000..e698856 --- /dev/null +++ b/cmd/ws/ingest.go @@ -0,0 +1,14 @@ +package main + +import ( + "context" + "database/sql" +) + +type Ingest struct { + DB *sql.DB +} + +func (i *Ingest) TrackPop(ctx context.Context, event PopEvent) { + +} diff --git a/cmd/ws/pop_event.go b/cmd/ws/pop_event.go new file mode 100644 index 0000000..3af50b5 --- /dev/null +++ b/cmd/ws/pop_event.go @@ -0,0 +1,42 @@ +package main + +import ( + "github.com/genudine/saerro-go/translators" + "github.com/genudine/saerro-go/types" +) + +type PopEvent struct { + WorldID uint16 + ZoneID uint32 + CharacterID string + LoadoutID string + TeamID types.Faction + VehicleID string + + VehicleName translators.Vehicle + ClassName translators.Class +} + +func PopEventFromESSEvent(event types.ESSEvent, attacker bool) PopEvent { + pe := PopEvent{ + WorldID: event.WorldID, + ZoneID: event.ZoneID, + } + + if !attacker { + pe.CharacterID = event.CharacterID + pe.LoadoutID = event.LoadoutID + pe.TeamID = event.TeamID + pe.VehicleID = event.VehicleID + } else { + pe.CharacterID = event.AttackerCharacterID + pe.LoadoutID = event.AttackerLoadoutID + pe.TeamID = event.AttackerTeamID + pe.VehicleID = event.AttackerVehicleID + } + + pe.ClassName = translators.ClassFromLoadout(pe.LoadoutID) + pe.VehicleName = translators.VehicleNameFromID(pe.VehicleID) + + return pe +} diff --git a/cmd/ws/ws.go b/cmd/ws/ws.go new file mode 100644 index 0000000..d1f7eb8 --- /dev/null +++ b/cmd/ws/ws.go @@ -0,0 +1,71 @@ +package main + +import ( + "context" + "database/sql" + "log" + "os" + "time" + + "github.com/genudine/saerro-go/types" + "nhooyr.io/websocket" + "nhooyr.io/websocket/wsjson" +) + +func main() { + wsAddr := os.Getenv("WS_ADDR") + if wsAddr == "" { + log.Fatalln("WS_ADDR is not set.") + } + + db, err := sql.Open("sqlite", ":memory:") + if err != nil { + log.Fatalln("database connection failed", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + wsConn, _, err := websocket.Dial(ctx, wsAddr, nil) + if err != nil { + log.Fatalln("Connection to ESS failed.", err) + } + defer wsConn.Close(websocket.StatusInternalError, "internal error. bye") + + err = wsjson.Write(ctx, wsConn, map[string]interface{}{ + "action": "subscribe", + "worlds": "all", + "eventNames": getEventNames(), + "characters": []string{"all"}, + "service": "event", + + "logicalAndCharactersWithWorlds": true, + }) + if err != nil { + log.Fatalln("subscription write failed", err) + } + + log.Println("subscribe done") + + eventHandler := EventHandler{ + Ingest: &Ingest{ + DB: db, + }, + } + + for { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + + var event types.ESSData + err := wsjson.Read(ctx, wsConn, &event) + if err != nil { + log.Println("wsjson read failed", err) + cancel() + continue + } + + go eventHandler.HandleEvent(ctx, event.Payload) + } + + wsConn.Close(websocket.StatusNormalClosure, "") +} diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..bff71b7 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,10 @@ + +services: + tsdb: + image: docker.io/timescale/timescaledb:latest-pg14 + environment: + POSTGRES_PASSWORD: saerro321 + POSTGRES_USER: saerrouser + POSTGRES_DB: data + ports: + - 5432:5432 diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..7272e65 --- /dev/null +++ b/flake.lock @@ -0,0 +1,58 @@ +{ + "nodes": { + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1717285511, + "narHash": "sha256-iKzJcpdXih14qYVcZ9QC9XuZYnPc6T8YImb6dX166kw=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "2a55567fcf15b1b1c7ed712a2c6fadaec7412ea8", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1717786204, + "narHash": "sha256-4q0s6m0GUcN7q+Y2DqD27iLvbcd1G50T2lv08kKxkSI=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "051f920625ab5aabe37c920346e3e69d7d34400e", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1717284937, + "narHash": "sha256-lIbdfCsf8LMFloheeE6N31+BMIeixqyQWbSr2vk79EQ=", + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/eb9ceca17df2ea50a250b6b27f7bf6ab0186f198.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs/archive/eb9ceca17df2ea50a250b6b27f7bf6ab0186f198.tar.gz" + } + }, + "root": { + "inputs": { + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..b0b6c25 --- /dev/null +++ b/flake.nix @@ -0,0 +1,15 @@ +{ + description = "saerro"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-parts.url = "github:hercules-ci/flake-parts"; + }; + + outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } { + systems = [ "x86_64-linux" "aarch64-linux" ]; + perSystem = { config, self', pkgs, lib, system, ... }: { + devShells.default = import ./shell.nix { inherit pkgs; }; + }; + }; +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8bdda41 --- /dev/null +++ b/go.mod @@ -0,0 +1,25 @@ +module github.com/genudine/saerro-go + +go 1.22.3 + +require ( + github.com/glebarez/go-sqlite v1.22.0 + github.com/stretchr/testify v1.9.0 + nhooyr.io/websocket v1.8.11 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/sys v0.19.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.52.1 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.8.0 // indirect + modernc.org/sqlite v1.30.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..93e7497 --- /dev/null +++ b/go.sum @@ -0,0 +1,57 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= +github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= +golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= +golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.21.2 h1:dycHFB/jDc3IyacKipCNSDrjIC0Lm1hyoWOZTRR20Lk= +modernc.org/cc/v4 v4.21.2/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.17.10 h1:6wrtRozgrhCxieCeJh85QsxkX/2FFrT9hdaWPlbn4Zo= +modernc.org/ccgo/v4 v4.17.10/go.mod h1:0NBHgsqTTpm9cA5z2ccErvGZmtntSM9qD2kFAs6pjXM= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= +modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= +modernc.org/libc v1.52.1 h1:uau0VoiT5hnR+SpoWekCKbLqm7v6dhRL3hI+NQhgN3M= +modernc.org/libc v1.52.1/go.mod h1:HR4nVzFDSDizP620zcMCgjb1/8xk2lg5p/8yjfGv1IQ= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= +modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= +modernc.org/sqlite v1.30.1 h1:YFhPVfu2iIgUf9kuA1CR7iiHdcEEsI2i+yjRYHscyxk= +modernc.org/sqlite v1.30.1/go.mod h1:DUmsiWQDaAvU4abhc/N+djlom/L2o8f7gZ95RCvyoLU= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +nhooyr.io/websocket v1.8.11 h1:f/qXNc2/3DpoSZkHt1DQu6rj4zGC8JmkkLkWss0MgN0= +nhooyr.io/websocket v1.8.11/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= diff --git a/justfile b/justfile new file mode 100644 index 0000000..759891d --- /dev/null +++ b/justfile @@ -0,0 +1,2 @@ +codegen: + go run ./cmd/codegen-vehicles > pkg/translators/vehicles_map.gen.go \ No newline at end of file diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..f02fb90 --- /dev/null +++ b/shell.nix @@ -0,0 +1,8 @@ +{ pkgs ? import {} }: pkgs.mkShell { + buildInputs = with pkgs; [ + go + just + docker-compose + sqlite + ]; +} diff --git a/store/player.go b/store/player.go new file mode 100644 index 0000000..5a6c87d --- /dev/null +++ b/store/player.go @@ -0,0 +1,48 @@ +package store + +import ( + "context" + "database/sql" + "log" + "time" +) + +type PlayerStore struct { + DB *sql.DB +} + +func NewPlayerStore(db *sql.DB) *PlayerStore { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + ps := &PlayerStore{ + DB: db, + } + + ps.RunMigration(ctx, false) + + return ps +} + +func (ps *PlayerStore) RunMigration(ctx context.Context, force bool) { + if !force { + // check if migrated first... + + } + + log.Println("(Re)-creating players table") + ps.DB.ExecContext(ctx, ` + DROP TABLE IF EXISTS players; + + CREATE TABLE players ( + character_id TEXT NOT NULL PRIMARY KEY, + last_updated TIMESTAMPTZ NOT NULL, + world_id INT NOT NULL, + faction_id INT NOT NULL, + zone_id INT NOT NULL, + class_name TEXT NOT NULL + ); + `) + + log.Println("Done, players table is initialized.") +} diff --git a/translators/loadouts.go b/translators/loadouts.go new file mode 100644 index 0000000..c0f5990 --- /dev/null +++ b/translators/loadouts.go @@ -0,0 +1,56 @@ +package translators + +type Class string + +const ( + Infiltrator Class = "infiltrator" + LightAssault Class = "light_assault" + CombatMedic Class = "combat_medic" + Engineer Class = "engineer" + HeavyAssault Class = "heavy_assault" + MAX Class = "max" +) + +var ( + LoadoutMap = map[string]Class{ + "1": Infiltrator, + "8": Infiltrator, + "15": Infiltrator, + "28": Infiltrator, + + "3": LightAssault, + "10": LightAssault, + "17": LightAssault, + "29": LightAssault, + + "4": CombatMedic, + "11": CombatMedic, + "18": CombatMedic, + "30": CombatMedic, + + "5": Engineer, + "12": Engineer, + "19": Engineer, + "31": Engineer, + + "6": HeavyAssault, + "13": HeavyAssault, + "20": HeavyAssault, + "32": HeavyAssault, + + "7": MAX, + "14": MAX, + "21": MAX, + "45": MAX, + } +) + +func ClassFromLoadout(loadoutID string) Class { + c, ok := LoadoutMap[loadoutID] + + if !ok { + return "unknown" + } + + return c +} diff --git a/translators/loadouts_test.go b/translators/loadouts_test.go new file mode 100644 index 0000000..489a8e3 --- /dev/null +++ b/translators/loadouts_test.go @@ -0,0 +1,13 @@ +package translators_test + +import ( + "testing" + + "github.com/genudine/saerro-go/pkg/translators" + "github.com/stretchr/testify/assert" +) + +func TestLoadouts(t *testing.T) { + assert.Equal(t, translators.ClassFromLoadout("1"), translators.Infiltrator) + assert.Equal(t, translators.ClassFromLoadout("0"), translators.Class("unknown")) +} diff --git a/translators/vehicles.go b/translators/vehicles.go new file mode 100644 index 0000000..89aee8b --- /dev/null +++ b/translators/vehicles.go @@ -0,0 +1,33 @@ +package translators + +type Vehicle string + +const ( + Flash Vehicle = "flash" + Sunderer Vehicle = "sunderer" + Lightning Vehicle = "lightning" + Magrider Vehicle = "magrider" + Vanguard Vehicle = "vanguard" + Prowler Vehicle = "prowler" + Scythe Vehicle = "scythe" + Reaver Vehicle = "reaver" + Mosquito Vehicle = "mosquito" + Liberator Vehicle = "liberator" + Galaxy Vehicle = "galaxy" + Harasser Vehicle = "harasser" + Valkyrie Vehicle = "valkyrie" + Ant Vehicle = "ant" + Dervish Vehicle = "dervish" + Chimera Vehicle = "chimera" + Javelin Vehicle = "javelin" + Corsair Vehicle = "corsair" +) + +func VehicleNameFromID(id string) Vehicle { + v, ok := VehicleMap[id] + if !ok { + return "unknown" + } + + return v +} diff --git a/translators/vehicles_map.gen.go b/translators/vehicles_map.gen.go new file mode 100644 index 0000000..01acbe6 --- /dev/null +++ b/translators/vehicles_map.gen.go @@ -0,0 +1,54 @@ +package translators + +var ( + VehicleMap = map[string]Vehicle{ + "1": Flash, + "2": Sunderer, + "3": Lightning, + "4": Magrider, + "5": Vanguard, + "6": Prowler, + "7": Scythe, + "8": Reaver, + "9": Mosquito, + "10": Liberator, + "11": Galaxy, + "12": Harasser, + "14": Valkyrie, + "15": Ant, + "160": Ant, + "161": Ant, + "162": Ant, + "1001": Flash, + "1002": Sunderer, + "1004": Magrider, + "1005": Vanguard, + "1007": Scythe, + "1008": Reaver, + "1009": Mosquito, + "1010": Liberator, + "1011": Galaxy, + "1105": Vanguard, + "2010": Flash, + "2033": Javelin, + "2039": Ant, + "2040": Valkyrie, + "2122": Mosquito, + "2123": Reaver, + "2124": Scythe, + "2125": Javelin, + "2129": Javelin, + "2130": Sunderer, + "2131": Galaxy, + "2132": Valkyrie, + "2133": Magrider, + "2134": Vanguard, + "2135": Prowler, + "2136": Dervish, + "2137": Chimera, + "2139": Ant, + "2140": Galaxy, + "2141": Valkyrie, + "2142": Corsair, + } +) diff --git a/translators/vehicles_test.go b/translators/vehicles_test.go new file mode 100644 index 0000000..8696921 --- /dev/null +++ b/translators/vehicles_test.go @@ -0,0 +1,13 @@ +package translators_test + +import ( + "testing" + + "github.com/genudine/saerro-go/pkg/translators" + "github.com/stretchr/testify/assert" +) + +func TestVehicles(t *testing.T) { + assert.Equal(t, translators.VehicleNameFromID("12"), translators.Harasser) + assert.Equal(t, translators.VehicleNameFromID("0"), translators.Vehicle("unknown")) +} diff --git a/types/census.go b/types/census.go new file mode 100644 index 0000000..f3a1228 --- /dev/null +++ b/types/census.go @@ -0,0 +1,10 @@ +package types + +type Faction uint8 + +const ( + VS Faction = iota + NC + TR + NSO +) diff --git a/types/payload.go b/types/payload.go new file mode 100644 index 0000000..5c5cdc5 --- /dev/null +++ b/types/payload.go @@ -0,0 +1,23 @@ +package types + +type ESSData struct { + Payload ESSEvent +} + +type ESSEvent struct { + EventName string `json:"event_name"` + WorldID uint16 `json:"world_id"` + ZoneID uint32 `json:"zone_id"` + + CharacterID string `json:"character_id"` + LoadoutID string `json:"loadout_id"` + VehicleID string `json:"vehicle_id"` + TeamID Faction `json:"team_id"` + + AttackerCharacterID string `json:"attacker_character_id"` + AttackerLoadoutID string `json:"attacker_loadout_id"` + AttackerVehicleID string `json:"attacker_vehicle_id"` + AttackerTeamID Faction `json:"attacker_team_id"` + + ExperienceID uint32 +} diff --git a/util/map.go b/util/map.go new file mode 100644 index 0000000..b66582b --- /dev/null +++ b/util/map.go @@ -0,0 +1,11 @@ +package util + +func Map[In, Out any](inSlice []In, predicate func(In) Out) []Out { + outSlice := make([]Out, len(inSlice)) + + for i := range inSlice { + outSlice[i] = predicate(inSlice[i]) + } + + return outSlice +} diff --git a/util/map_test.go b/util/map_test.go new file mode 100644 index 0000000..7fb5cc8 --- /dev/null +++ b/util/map_test.go @@ -0,0 +1,20 @@ +package util_test + +import ( + "strconv" + "testing" + + "github.com/genudine/saerro-go/util" + "github.com/stretchr/testify/assert" +) + +func TestMap(t *testing.T) { + dolls := []int64{44203, 41666, 79579, 63741, 57213} + + result := util.Map(dolls, func(doll int64) string { + return strconv.FormatInt(doll, 16) + }) + + assert.Contains(t, result, "acab") + assert.Len(t, result, len(dolls)) +}