This commit is contained in:
41666 2024-06-13 22:33:29 -04:00
commit c5cc245e25
29 changed files with 926 additions and 0 deletions

View file

@ -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)
}

View file

@ -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
}

View file

@ -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",
}
)

65
cmd/ws/event_handler.go Normal file
View file

@ -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) {
}

View file

@ -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)
}

22
cmd/ws/event_names.go Normal file
View file

@ -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
}

View file

@ -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")
}

14
cmd/ws/ingest.go Normal file
View file

@ -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) {
}

42
cmd/ws/pop_event.go Normal file
View file

@ -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
}

71
cmd/ws/ws.go Normal file
View file

@ -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, "")
}