init
This commit is contained in:
commit
c5cc245e25
29 changed files with 926 additions and 0 deletions
100
cmd/codegen-vehicles/main.go
Normal file
100
cmd/codegen-vehicles/main.go
Normal 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)
|
||||
}
|
39
cmd/codegen-vehicles/template.go
Normal file
39
cmd/codegen-vehicles/template.go
Normal 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
|
||||
}
|
27
cmd/codegen-vehicles/vehicle_list.go
Normal file
27
cmd/codegen-vehicles/vehicle_list.go
Normal 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
65
cmd/ws/event_handler.go
Normal 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) {
|
||||
|
||||
}
|
72
cmd/ws/event_handler_test.go
Normal file
72
cmd/ws/event_handler_test.go
Normal 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
22
cmd/ws/event_names.go
Normal 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
|
||||
}
|
14
cmd/ws/event_names_test.go
Normal file
14
cmd/ws/event_names_test.go
Normal 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
14
cmd/ws/ingest.go
Normal 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
42
cmd/ws/pop_event.go
Normal 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
71
cmd/ws/ws.go
Normal 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, "")
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue