finish aggpop
This commit is contained in:
parent
dea89d9a30
commit
e47389854f
34 changed files with 1277 additions and 0 deletions
2
.envrc
Normal file
2
.envrc
Normal file
|
@ -0,0 +1,2 @@
|
|||
dotenv;
|
||||
use flake;
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -21,3 +21,4 @@
|
|||
# Go workspace file
|
||||
go.work
|
||||
|
||||
.direnv/
|
57
clients/agg.ps2.live/client.go
Normal file
57
clients/agg.ps2.live/client.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package aggps2live
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.sapphic.engineer/ps2.live/ps2.live/clients"
|
||||
"git.sapphic.engineer/ps2.live/ps2.live/types"
|
||||
)
|
||||
|
||||
type AggPopClient interface {
|
||||
GetWorld(ctx context.Context, id string) (*types.AggPopWorld, error)
|
||||
GetAllWorlds(ctx context.Context) ([]*types.AggPopWorld, error)
|
||||
}
|
||||
|
||||
// Client implements AggClient.
|
||||
type Client struct {
|
||||
clients.CommonClient
|
||||
}
|
||||
|
||||
func NewAggPopClient(baseURL ...string) AggPopClient {
|
||||
u := "https://agg.ps2.live/population"
|
||||
if len(baseURL) != 0 {
|
||||
u = baseURL[0]
|
||||
}
|
||||
|
||||
c := &Client{}
|
||||
c.SetBaseURL(u)
|
||||
c.SetClient(&http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
})
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Client) GetWorld(ctx context.Context, id string) (w *types.AggPopWorld, err error) {
|
||||
res, err := c.Request(ctx, "/"+id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetWorld request failed, %w", err)
|
||||
}
|
||||
|
||||
err = json.NewDecoder(res.Body).Decode(&w)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) GetAllWorlds(ctx context.Context) (ws []*types.AggPopWorld, err error) {
|
||||
res, err := c.Request(ctx, "/")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetAllWorlds request failed, %w", err)
|
||||
}
|
||||
|
||||
err = json.NewDecoder(res.Body).Decode(&ws)
|
||||
return
|
||||
}
|
49
clients/common.go
Normal file
49
clients/common.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package clients
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type CommonClient struct {
|
||||
HttpClient *http.Client
|
||||
BaseURL string
|
||||
}
|
||||
|
||||
func (c *CommonClient) Request(ctx context.Context, path string) (*http.Response, error) {
|
||||
url, err := url.Parse(c.BaseURL + path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse URL, %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), http.NoBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("crafting http request failed, %w", err)
|
||||
}
|
||||
|
||||
res, err := c.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("http request failed, %w", err)
|
||||
}
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
body = []byte(fmt.Sprintf("<< unable to read (%s) >>", err))
|
||||
}
|
||||
return res, fmt.Errorf("http request not 200, got: %d\nresponse body: %s", res.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *CommonClient) SetBaseURL(baseURL string) {
|
||||
c.BaseURL = baseURL
|
||||
}
|
||||
|
||||
func (c *CommonClient) SetClient(client *http.Client) {
|
||||
c.HttpClient = client
|
||||
}
|
61
clients/metagame.ps2.live/client.go
Normal file
61
clients/metagame.ps2.live/client.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
package metagameps2live
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.sapphic.engineer/ps2.live/ps2.live/clients"
|
||||
"git.sapphic.engineer/ps2.live/ps2.live/types"
|
||||
)
|
||||
|
||||
type MetagameClient interface {
|
||||
GetWorld(ctx context.Context, id string) (*types.MetagameWorld, error)
|
||||
GetAllWorlds(ctx context.Context) ([]*types.MetagameWorld, error)
|
||||
}
|
||||
|
||||
// Client implements MetagameClient.
|
||||
type Client struct {
|
||||
clients.CommonClient
|
||||
}
|
||||
|
||||
type MetagameClientConfig struct {
|
||||
BaseURL string
|
||||
}
|
||||
|
||||
func NewMetagameClient(baseURL ...string) MetagameClient {
|
||||
u := "https://metagame.ps2.live"
|
||||
if len(baseURL) != 0 {
|
||||
u = baseURL[0]
|
||||
}
|
||||
|
||||
c := &Client{}
|
||||
c.SetBaseURL(u)
|
||||
c.SetClient(&http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
})
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Client) GetWorld(ctx context.Context, id string) (w *types.MetagameWorld, err error) {
|
||||
res, err := c.Request(ctx, "/"+id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetWorld request failed, %w", err)
|
||||
}
|
||||
|
||||
err = json.NewDecoder(res.Body).Decode(&w)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) GetAllWorlds(ctx context.Context) (ws []*types.MetagameWorld, err error) {
|
||||
res, err := c.Request(ctx, "/")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetWorld request failed, %w", err)
|
||||
}
|
||||
|
||||
err = json.NewDecoder(res.Body).Decode(&ws)
|
||||
return
|
||||
}
|
9
clients/saerro.ps2.live/README.md
Normal file
9
clients/saerro.ps2.live/README.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
# Saerro Go API Client
|
||||
|
||||
This only fulfills the goals of rendering the ps2.live website directly.
|
||||
|
||||
A consumer may use `saerrops2live.Query(...)` to use GraphQL directly.
|
||||
|
||||
## Mocks
|
||||
|
||||
a stretchr mock is available at ./mock
|
129
clients/saerro.ps2.live/client.go
Normal file
129
clients/saerro.ps2.live/client.go
Normal file
|
@ -0,0 +1,129 @@
|
|||
package saerrops2live
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"git.sapphic.engineer/ps2.live/ps2.live/types"
|
||||
)
|
||||
|
||||
// SaerroClient connects to https://saerro.ps2.live and returns some results from its GraphQL API.
|
||||
// btw this is mockable in stretchr :>
|
||||
type SaerroClient interface {
|
||||
Query(ctx context.Context, query string, out interface{}) error
|
||||
GetWorldPopulation(ctx context.Context, worldID uint32) (*types.Factions[uint32], error)
|
||||
}
|
||||
|
||||
// Client implements SaerroClient.
|
||||
type Client struct {
|
||||
Config SaerroClientConfig
|
||||
HttpClient *http.Client
|
||||
}
|
||||
|
||||
type SaerroClientConfig struct {
|
||||
BaseURL string
|
||||
}
|
||||
|
||||
func NewSaerroClient(config ...SaerroClientConfig) SaerroClient {
|
||||
c := SaerroClientConfig{
|
||||
BaseURL: "https://saerro.ps2.live",
|
||||
}
|
||||
|
||||
if len(config) != 0 {
|
||||
c0 := config[0]
|
||||
c.BaseURL = c0.BaseURL
|
||||
}
|
||||
|
||||
return &Client{
|
||||
Config: c,
|
||||
HttpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type graphqlQuery struct {
|
||||
Query string `json:"query,omitempty"`
|
||||
OperationName string `json:"operation_name,omitempty"`
|
||||
Variables map[string]interface{} `json:"variables,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Client) Query(ctx context.Context, query string, out interface{}) error {
|
||||
url, err := url.Parse(c.Config.BaseURL + "/graphql")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error constructing query URL, %w", err)
|
||||
}
|
||||
|
||||
q := graphqlQuery{
|
||||
Query: query,
|
||||
}
|
||||
buf := bytes.Buffer{}
|
||||
err = json.NewEncoder(&buf).Encode(q)
|
||||
if err != nil {
|
||||
return fmt.Errorf("saerro client: query: query encode failed, %w", err)
|
||||
}
|
||||
|
||||
req := &http.Request{
|
||||
URL: url,
|
||||
Method: http.MethodPost,
|
||||
Body: io.NopCloser(&buf),
|
||||
Header: http.Header{},
|
||||
}
|
||||
req.Header.Add("content-type", "application/json")
|
||||
|
||||
res, err := c.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("saerro client: query: http request failed, %w", err)
|
||||
}
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
buf := bytes.Buffer{}
|
||||
io.Copy(&buf, res.Body)
|
||||
return fmt.Errorf("saerro client: query: status code not 200, got %d --\ntext: %s", res.StatusCode, buf.String())
|
||||
}
|
||||
|
||||
err = json.NewDecoder(res.Body).Decode(out)
|
||||
if err != nil {
|
||||
return fmt.Errorf("saerro client: query: response decode failed, %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type SaerroResponse[T interface{}] struct {
|
||||
Data T `json:"data"`
|
||||
}
|
||||
|
||||
type SaerroWorld[T interface{}] struct {
|
||||
World T `json:"world"`
|
||||
}
|
||||
|
||||
type SaerroPopulation struct {
|
||||
Population types.Factions[uint32] `json:"population"`
|
||||
}
|
||||
|
||||
func (c *Client) GetWorldPopulation(ctx context.Context, worldID uint32) (*types.Factions[uint32], error) {
|
||||
query := fmt.Sprintf(`{
|
||||
world(by: { id: %d }) {
|
||||
population {
|
||||
nc
|
||||
tr
|
||||
vs
|
||||
}
|
||||
}
|
||||
}`, worldID)
|
||||
|
||||
var data SaerroResponse[SaerroWorld[SaerroPopulation]]
|
||||
err := c.Query(ctx, query, &data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &data.Data.World.Population, nil
|
||||
}
|
27
clients/saerro.ps2.live/mock/mock.go
Normal file
27
clients/saerro.ps2.live/mock/mock.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package mock
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
saerrops2live "git.sapphic.engineer/ps2.live/ps2.live/clients/saerro.ps2.live"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type MockSaerroClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockSaerroClient) Query(ctx context.Context, query string, out interface{}) error {
|
||||
c := m.Called(ctx, query, out)
|
||||
return c.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockSaerroClient) GetWorldPopulation(ctx context.Context, worldID uint32) (*saerrops2live.SaerroPopulation, error) {
|
||||
c := m.Called(ctx, worldID)
|
||||
rv := c.Get(0)
|
||||
if rv == nil {
|
||||
return nil, c.Error(1)
|
||||
}
|
||||
|
||||
return rv.(*saerrops2live.SaerroPopulation), c.Error(1)
|
||||
}
|
11
cmd/agg.ps2.live/fetcher/errors.go
Normal file
11
cmd/agg.ps2.live/fetcher/errors.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package fetcher
|
||||
|
||||
import "fmt"
|
||||
|
||||
var (
|
||||
ErrNotImplemented = fmt.Errorf("fetcher: not implemented")
|
||||
ErrNotSupported = fmt.Errorf("fetcher: not supported")
|
||||
ErrFailedRequest = fmt.Errorf("fetcher: request failed")
|
||||
ErrUnexpectedResponse = fmt.Errorf("fetcher: unexpected response structure")
|
||||
ErrLikelyFailed = fmt.Errorf("fetcher: likely failed service")
|
||||
)
|
81
cmd/agg.ps2.live/fetcher/fetcher.go
Normal file
81
cmd/agg.ps2.live/fetcher/fetcher.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
package fetcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
saerrops2live "git.sapphic.engineer/ps2.live/ps2.live/clients/saerro.ps2.live"
|
||||
"git.sapphic.engineer/ps2.live/ps2.live/shared/cache"
|
||||
"git.sapphic.engineer/ps2.live/ps2.live/types"
|
||||
)
|
||||
|
||||
type Fetcher struct {
|
||||
Client *http.Client
|
||||
Saerro saerrops2live.SaerroClient
|
||||
Cache cache.ICache[uint32, types.AggPopServiceMap]
|
||||
}
|
||||
|
||||
func NewFetcher() Fetcher {
|
||||
return Fetcher{
|
||||
Client: &http.Client{
|
||||
Timeout: time.Second * 30,
|
||||
},
|
||||
Saerro: saerrops2live.NewSaerroClient(),
|
||||
Cache: cache.NewCache[uint32, types.AggPopServiceMap](time.Minute * 3),
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Fetcher) FetchByWorld(ctx context.Context, worldID uint32) (types.AggPopServiceMap, error) {
|
||||
cached, ok := f.Cache.Get(worldID)
|
||||
if ok {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
res := types.AggPopServiceMap{}
|
||||
var err error
|
||||
|
||||
x := func(e error) {
|
||||
if e != nil {
|
||||
if errors.Is(e, ErrNotSupported) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("FetchAll: errored, ", err)
|
||||
}
|
||||
}
|
||||
|
||||
res.Fisu, err = f.FetchFisu(ctx, worldID)
|
||||
x(err)
|
||||
|
||||
res.Honu, err = f.FetchHonu(ctx, worldID)
|
||||
x(err)
|
||||
|
||||
res.Saerro, err = f.FetchSaerro(ctx, worldID)
|
||||
x(err)
|
||||
|
||||
res.Sanctuary, err = f.FetchSanctuary(ctx, worldID)
|
||||
x(err)
|
||||
|
||||
res.Voidwell, err = f.FetchVoidwell(ctx, worldID)
|
||||
x(err)
|
||||
|
||||
f.Cache.Put(worldID, res)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (f *Fetcher) FetchAllWorlds(ctx context.Context, worldIDs []uint32) (map[uint32]types.AggPopServiceMap, error) {
|
||||
out := map[uint32]types.AggPopServiceMap{}
|
||||
var err error
|
||||
|
||||
for _, worldID := range worldIDs {
|
||||
out[worldID], err = f.FetchByWorld(ctx, worldID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
67
cmd/agg.ps2.live/fetcher/fisu.go
Normal file
67
cmd/agg.ps2.live/fetcher/fisu.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
package fetcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"git.sapphic.engineer/ps2.live/ps2.live/types"
|
||||
)
|
||||
|
||||
func (f *Fetcher) FetchFisu(ctx context.Context, worldID uint32) (*types.Factions[uint32], error) {
|
||||
url := f.makeFisuURL(worldID)
|
||||
|
||||
res, err := f.Client.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("FetchFisu: get to %s failed, %w", url, err)
|
||||
}
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return nil, ErrFailedRequest
|
||||
}
|
||||
|
||||
var out fisuRoot
|
||||
err = json.NewDecoder(res.Body).Decode(&out)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("FetchFisu: failed to decode json, %w", err)
|
||||
}
|
||||
|
||||
if len(out.Result) == 0 {
|
||||
return nil, ErrUnexpectedResponse
|
||||
}
|
||||
|
||||
fa := out.Result[0].ToFactions()
|
||||
return &fa, nil
|
||||
}
|
||||
|
||||
func (f *Fetcher) makeFisuURL(worldID uint32) string {
|
||||
subdomain := "ps2"
|
||||
switch worldID {
|
||||
case 1000:
|
||||
subdomain = "ps4us.ps2"
|
||||
case 2000:
|
||||
subdomain = "ps4eu.ps2"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("https://%s.fisu.pw/api/population/?world=%d", subdomain, worldID)
|
||||
}
|
||||
|
||||
type fisuRoot struct {
|
||||
Result []fisuFactions `json:"result"`
|
||||
}
|
||||
|
||||
type fisuFactions struct {
|
||||
VS uint32 `json:"vs"`
|
||||
TR uint32 `json:"tr"`
|
||||
NC uint32 `json:"nc"`
|
||||
NS uint32 `json:"ns"`
|
||||
}
|
||||
|
||||
func (fr fisuFactions) ToFactions() types.Factions[uint32] {
|
||||
return types.Factions[uint32]{
|
||||
NC: fr.NC,
|
||||
TR: fr.TR,
|
||||
VS: fr.VS,
|
||||
Total: fr.NC + fr.TR + fr.VS + fr.NS,
|
||||
}
|
||||
}
|
51
cmd/agg.ps2.live/fetcher/honu.go
Normal file
51
cmd/agg.ps2.live/fetcher/honu.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package fetcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"git.sapphic.engineer/ps2.live/ps2.live/types"
|
||||
)
|
||||
|
||||
func (f *Fetcher) FetchHonu(ctx context.Context, worldID uint32) (*types.Factions[uint32], error) {
|
||||
url := fmt.Sprintf("https://wt.honu.pw/api/population/%d", worldID)
|
||||
|
||||
res, err := f.Client.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("FetchHonu: get to %s failed, %w", url, err)
|
||||
}
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return nil, ErrFailedRequest
|
||||
}
|
||||
|
||||
var out honuResponse
|
||||
err = json.NewDecoder(res.Body).Decode(&out)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("FetchHonu: failed to decode json, %w", err)
|
||||
}
|
||||
|
||||
fr := out.ToFactions()
|
||||
return &fr, nil
|
||||
}
|
||||
|
||||
type honuResponse struct {
|
||||
NC uint32 `json:"nc"`
|
||||
TR uint32 `json:"tr"`
|
||||
VS uint32 `json:"vs"`
|
||||
NS_NC uint32 `json:"ns_nc"`
|
||||
NS_TR uint32 `json:"ns_tr"`
|
||||
NS_VS uint32 `json:"ns_vs"`
|
||||
}
|
||||
|
||||
func (hr honuResponse) ToFactions() types.Factions[uint32] {
|
||||
return types.Factions[uint32]{
|
||||
NC: hr.NC + hr.NS_NC,
|
||||
TR: hr.TR + hr.NS_TR,
|
||||
VS: hr.VS + hr.NS_VS,
|
||||
Total: hr.NC + hr.NS_NC +
|
||||
hr.TR + hr.NS_TR +
|
||||
hr.VS + hr.NS_VS,
|
||||
}
|
||||
}
|
28
cmd/agg.ps2.live/fetcher/saerro.go
Normal file
28
cmd/agg.ps2.live/fetcher/saerro.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package fetcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.sapphic.engineer/ps2.live/ps2.live/types"
|
||||
)
|
||||
|
||||
func (f *Fetcher) FetchSaerro(ctx context.Context, worldID uint32) (*types.Factions[uint32], error) {
|
||||
sr, err := f.Saerro.GetWorldPopulation(ctx, worldID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("FetchSaerro: error querying, %w", err)
|
||||
}
|
||||
|
||||
r := &types.Factions[uint32]{
|
||||
NC: sr.NC,
|
||||
TR: sr.TR,
|
||||
VS: sr.NC,
|
||||
Total: sr.NC + sr.TR + sr.VS,
|
||||
}
|
||||
|
||||
if worldID != 19 && r.Total == 0 {
|
||||
return nil, ErrLikelyFailed
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
71
cmd/agg.ps2.live/fetcher/sanctuary.go
Normal file
71
cmd/agg.ps2.live/fetcher/sanctuary.go
Normal file
|
@ -0,0 +1,71 @@
|
|||
package fetcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.sapphic.engineer/ps2.live/ps2.live/types"
|
||||
)
|
||||
|
||||
func (f *Fetcher) FetchSanctuary(ctx context.Context, worldID uint32) (*types.Factions[uint32], error) {
|
||||
// PS4 and Jaeger aren't supported on Sanctuary
|
||||
if worldID == 1000 || worldID == 2000 || worldID == 19 {
|
||||
return nil, ErrNotSupported
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("https://census.lithafalcon.cc/get/ps2/world_population?c:censusJSON=false&world_id=%d", worldID)
|
||||
|
||||
res, err := f.Client.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("FetchSanctuary: get to %s failed, %w", url, err)
|
||||
}
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return nil, ErrFailedRequest
|
||||
}
|
||||
|
||||
var out sanctuaryRoot
|
||||
err = json.NewDecoder(res.Body).Decode(&out)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("FetchSanctuary: failed to decode json, %w", err)
|
||||
}
|
||||
|
||||
if len(out.WorldPopulationList) == 0 {
|
||||
return nil, ErrUnexpectedResponse
|
||||
}
|
||||
|
||||
timestamp := time.Unix(int64(out.WorldPopulationList[0].Timestamp), 0)
|
||||
if timestamp.Before(time.Now().Add(-time.Minute * 15)) {
|
||||
return nil, ErrUnexpectedResponse
|
||||
}
|
||||
|
||||
fr := out.WorldPopulationList[0].Population.ToFactions()
|
||||
return &fr, nil
|
||||
}
|
||||
|
||||
type sanctuaryRoot struct {
|
||||
WorldPopulationList []sanctuaryWorld `json:"world_population_list"`
|
||||
}
|
||||
|
||||
type sanctuaryWorld struct {
|
||||
Population sanctuaryPopulation `json:"population"`
|
||||
Timestamp uint64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
type sanctuaryPopulation struct {
|
||||
VS uint32 `json:"VS"`
|
||||
TR uint32 `json:"TR"`
|
||||
NC uint32 `json:"NC"`
|
||||
NSO uint32 `json:"NSO"`
|
||||
}
|
||||
|
||||
func (sp sanctuaryPopulation) ToFactions() types.Factions[uint32] {
|
||||
return types.Factions[uint32]{
|
||||
NC: sp.NC,
|
||||
TR: sp.TR,
|
||||
VS: sp.VS,
|
||||
Total: sp.NC + sp.TR + sp.VS + sp.NSO,
|
||||
}
|
||||
}
|
77
cmd/agg.ps2.live/fetcher/voidwell.go
Normal file
77
cmd/agg.ps2.live/fetcher/voidwell.go
Normal file
|
@ -0,0 +1,77 @@
|
|||
package fetcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"git.sapphic.engineer/ps2.live/ps2.live/types"
|
||||
)
|
||||
|
||||
func (f *Fetcher) FetchVoidwell(ctx context.Context, worldID uint32) (*types.Factions[uint32], error) {
|
||||
// PS4 isn't supported on Voidwell
|
||||
if worldID >= 1000 {
|
||||
return nil, ErrNotSupported
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("https://api.voidwell.com/ps2/worldstate/%d?platform=pc", worldID)
|
||||
res, err := f.Client.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("FetchVoidwell: get to %s failed, %w", url, err)
|
||||
}
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return nil, ErrFailedRequest
|
||||
}
|
||||
|
||||
var out voidwellRoot
|
||||
err = json.NewDecoder(res.Body).Decode(&out)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("FetchVoidwell: failed to decode json, %w", err)
|
||||
}
|
||||
|
||||
vp := out.FlattenZonePopulations()
|
||||
fr := vp.ToFactions()
|
||||
fr.Total = out.OnlineCharacters
|
||||
|
||||
return &fr, nil
|
||||
}
|
||||
|
||||
type voidwellRoot struct {
|
||||
OnlineCharacters uint32 `json:"onlineCharacters"`
|
||||
ZoneStates []voidwellZone `json:"zoneStates"`
|
||||
}
|
||||
|
||||
func (vr voidwellRoot) FlattenZonePopulations() voidwellPopulation {
|
||||
pop := voidwellPopulation{}
|
||||
|
||||
for _, zone := range vr.ZoneStates {
|
||||
pop.NC += zone.Population.NC
|
||||
pop.TR += zone.Population.TR
|
||||
pop.VS += zone.Population.VS
|
||||
pop.NS += zone.Population.NS
|
||||
}
|
||||
|
||||
return pop
|
||||
}
|
||||
|
||||
type voidwellZone struct {
|
||||
Population voidwellPopulation `json:"population"`
|
||||
}
|
||||
|
||||
type voidwellPopulation struct {
|
||||
VS uint32 `json:"vs"`
|
||||
TR uint32 `json:"tr"`
|
||||
NC uint32 `json:"nc"`
|
||||
NS uint32 `json:"ns"`
|
||||
}
|
||||
|
||||
func (vp *voidwellPopulation) ToFactions() types.Factions[uint32] {
|
||||
nsAvg := vp.NS / 3
|
||||
return types.Factions[uint32]{
|
||||
NC: vp.NC + nsAvg,
|
||||
TR: vp.TR + nsAvg,
|
||||
VS: vp.VS + nsAvg,
|
||||
Total: vp.NC + vp.TR + vp.VS + vp.NS,
|
||||
}
|
||||
}
|
54
cmd/agg.ps2.live/html/index.html
Normal file
54
cmd/agg.ps2.live/html/index.html
Normal file
|
@ -0,0 +1,54 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset="utf-8" />
|
||||
<title>Aggregate PlanetSide 2 Population API</title>
|
||||
<meta name="description" content="Multi-source population API" />
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
flex-direction: column;
|
||||
font-weight: lighter;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.big-header {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.api-list {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.api-item {
|
||||
display: block;
|
||||
background-color: #334;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
margin: 0.5rem;
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
<main>
|
||||
<div>
|
||||
<b>tl;dr:</b><br />
|
||||
<span class="big-header"
|
||||
>( fisu + honu + saerro + sanctuary + voidwell ) / 5</span
|
||||
>
|
||||
</div>
|
||||
<div class="api-list">
|
||||
<a class="api-item" href="/population/1">GET /population/{worldID} ▶️</a>
|
||||
<a class="api-item" href="/population/all">GET /population/all ▶️</a>
|
||||
</div>
|
||||
<p>Results are cached for 3 minutes. Requesting faster is a dumb idea.</p>
|
||||
</main>
|
162
cmd/agg.ps2.live/main.go
Normal file
162
cmd/agg.ps2.live/main.go
Normal file
|
@ -0,0 +1,162 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.sapphic.engineer/ps2.live/ps2.live/cmd/agg.ps2.live/fetcher"
|
||||
"git.sapphic.engineer/ps2.live/ps2.live/types"
|
||||
)
|
||||
|
||||
//go:embed html/index.html
|
||||
var indexHtml []byte
|
||||
|
||||
var (
|
||||
AllWorlds []uint32
|
||||
Fetcher fetcher.Fetcher = fetcher.NewFetcher()
|
||||
)
|
||||
|
||||
func main() {
|
||||
addr := os.Getenv("ADDR")
|
||||
if addr == "" {
|
||||
addr = ":8001"
|
||||
}
|
||||
|
||||
allWorldsEnv := os.Getenv("ALL_WORLDS")
|
||||
if allWorldsEnv == "" {
|
||||
allWorldsEnv = "1,10,17,19,40,1000,2000"
|
||||
}
|
||||
AllWorlds = envListToU32(allWorldsEnv)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.Header().Add("content-type", "text/html")
|
||||
rw.Write(indexHtml)
|
||||
})
|
||||
|
||||
mux.HandleFunc("/population/{id}", handlePop)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*60)
|
||||
Fetcher.FetchAllWorlds(ctx, AllWorlds)
|
||||
|
||||
time.Sleep(time.Minute * 3)
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
|
||||
err := http.ListenAndServe(addr, mux)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
|
||||
func envListToU32(input string) []uint32 {
|
||||
ls := strings.Split(input, ",")
|
||||
o := make([]uint32, len(ls))
|
||||
|
||||
for i, s := range ls {
|
||||
v, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
log.Println("error parsing ALL_WORLDS number, got: ", s)
|
||||
continue
|
||||
}
|
||||
o[i] = uint32(v)
|
||||
}
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
func handlePop(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.Header().Add("content-type", "application/json")
|
||||
|
||||
worldIDStr := req.PathValue("id")
|
||||
|
||||
if worldIDStr == "all" {
|
||||
handlePopAll(rw, req)
|
||||
return
|
||||
}
|
||||
|
||||
worldID, err := strconv.Atoi(worldIDStr)
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
rw.Write([]byte(`{ "err":"bad request", "msg": "world ID did not parse as a number" }`))
|
||||
log.Println("handlePop: worldID failed to parse, got: ", worldIDStr)
|
||||
return
|
||||
}
|
||||
|
||||
handlePopSingle(rw, req, uint32(worldID))
|
||||
}
|
||||
|
||||
func handlePopSingle(rw http.ResponseWriter, req *http.Request, worldID uint32) {
|
||||
ctx, cancel := context.WithTimeout(req.Context(), time.Second*30)
|
||||
defer cancel()
|
||||
|
||||
services, err := Fetcher.FetchByWorld(ctx, worldID)
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
rw.Write([]byte(`{ "err":"bad request", "msg": "fetching world failed" }`))
|
||||
log.Println("handlePopOne: FetchByWorld failed, got: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
factions := services.ToFactions()
|
||||
|
||||
world := types.AggPopWorld{
|
||||
ID: worldID,
|
||||
Services: services,
|
||||
Factions: factions,
|
||||
Average: factions.Total,
|
||||
}
|
||||
|
||||
err = json.NewEncoder(rw).Encode(world)
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
rw.Write([]byte(`{ "err":"bad request", "msg": "rendering world failed" }`))
|
||||
log.Println("handlePopOne: json encode failed, got: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
func handlePopAll(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(req.Context(), time.Second*30)
|
||||
defer cancel()
|
||||
|
||||
worldMap, err := Fetcher.FetchAllWorlds(ctx, AllWorlds)
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
rw.Write([]byte(`{ "err":"bad request", "msg": "fetching all worlds failed" }`))
|
||||
log.Println("handlePopAll: FetchAllWorlds failed, got: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
out := []types.AggPopWorld{}
|
||||
|
||||
for worldID, services := range worldMap {
|
||||
factions := services.ToFactions()
|
||||
|
||||
world := types.AggPopWorld{
|
||||
ID: worldID,
|
||||
Services: services,
|
||||
Factions: factions,
|
||||
Average: factions.Total,
|
||||
}
|
||||
|
||||
out = append(out, world)
|
||||
}
|
||||
|
||||
err = json.NewEncoder(rw).Encode(out)
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
rw.Write([]byte(`{ "err":"bad request", "msg": "rendering all worlds failed" }`))
|
||||
log.Println("handlePopAll: json encode failed, got: ", err)
|
||||
}
|
||||
}
|
5
cmd/metagame.ps2.live/main.go
Normal file
5
cmd/metagame.ps2.live/main.go
Normal file
|
@ -0,0 +1,5 @@
|
|||
package main
|
||||
|
||||
func main() {
|
||||
|
||||
}
|
14
cmd/ps2.live/assets/global.css
Normal file
14
cmd/ps2.live/assets/global.css
Normal file
|
@ -0,0 +1,14 @@
|
|||
main {
|
||||
display: flex;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
|
||||
.center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.v-center {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
0
cmd/ps2.live/assets/global.js
Normal file
0
cmd/ps2.live/assets/global.js
Normal file
5
cmd/ps2.live/main.go
Normal file
5
cmd/ps2.live/main.go
Normal file
|
@ -0,0 +1,5 @@
|
|||
package main
|
||||
|
||||
func main() {
|
||||
|
||||
}
|
6
cmd/ps2.live/static_embeds.go
Normal file
6
cmd/ps2.live/static_embeds.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
package main
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed assets templates
|
||||
var StaticFileFS embed.FS
|
4
cmd/ps2.live/templates/404.html.tmpl
Normal file
4
cmd/ps2.live/templates/404.html.tmpl
Normal file
|
@ -0,0 +1,4 @@
|
|||
<main class="center v-center">
|
||||
<h1>404 Not Found</h1>
|
||||
<p>{{ or .PageData.RandomText "why are you here" }}</p>
|
||||
</main>
|
14
cmd/ps2.live/templates/base.html.tmpl
Normal file
14
cmd/ps2.live/templates/base.html.tmpl
Normal file
|
@ -0,0 +1,14 @@
|
|||
<!DOCTYPE html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>
|
||||
{{ or .Meta.Title "ps2.live" }}
|
||||
</title>
|
||||
<link rel="stylesheet" href="/assets/global.css" />
|
||||
<script src="/assets/global.js" async defer></script>
|
||||
</head>
|
||||
<body>
|
||||
{{ with $page := or .TemplateName "404" }}
|
||||
{{ template $page . }}
|
||||
{{ end }}
|
||||
</body>
|
1
cmd/ps2.live/templates/index.html.tmpl
Normal file
1
cmd/ps2.live/templates/index.html.tmpl
Normal file
|
@ -0,0 +1 @@
|
|||
<h1>hello world!!!</h1>
|
58
flake.lock
generated
Normal file
58
flake.lock
generated
Normal file
|
@ -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
|
||||
}
|
15
flake.nix
Normal file
15
flake.nix
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
description = "ps2.live";
|
||||
|
||||
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; };
|
||||
};
|
||||
};
|
||||
}
|
12
go.mod
Normal file
12
go.mod
Normal file
|
@ -0,0 +1,12 @@
|
|||
module git.sapphic.engineer/ps2.live/ps2.live
|
||||
|
||||
go 1.22.3
|
||||
|
||||
require github.com/stretchr/testify v1.9.0
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
11
go.sum
Normal file
11
go.sum
Normal file
|
@ -0,0 +1,11 @@
|
|||
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
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=
|
||||
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=
|
45
shared/cache/cache.go
vendored
Normal file
45
shared/cache/cache.go
vendored
Normal file
|
@ -0,0 +1,45 @@
|
|||
package cache
|
||||
|
||||
import "time"
|
||||
|
||||
type Object[V interface{}] struct {
|
||||
Value V
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
type Cache[K comparable, V interface{}] struct {
|
||||
data map[K]Object[V]
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
type ICache[K comparable, V interface{}] interface {
|
||||
Get(K) (V, bool)
|
||||
Put(K, V)
|
||||
}
|
||||
|
||||
func NewCache[K comparable, V interface{}](timeout time.Duration) Cache[K, V] {
|
||||
return Cache[K, V]{
|
||||
data: map[K]Object[V]{},
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
func (c Cache[K, V]) Get(key K) (out V, ok bool) {
|
||||
o, ok := c.data[key]
|
||||
if !ok {
|
||||
return out, false
|
||||
}
|
||||
|
||||
if o.ExpiresAt.Before(time.Now()) {
|
||||
return out, false
|
||||
}
|
||||
|
||||
return o.Value, true
|
||||
}
|
||||
|
||||
func (c Cache[K, V]) Put(key K, value V) {
|
||||
c.data[key] = Object[V]{
|
||||
Value: value,
|
||||
ExpiresAt: time.Now().Add(c.timeout),
|
||||
}
|
||||
}
|
40
shared/cache/cache_test.go
vendored
Normal file
40
shared/cache/cache_test.go
vendored
Normal file
|
@ -0,0 +1,40 @@
|
|||
package cache_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.sapphic.engineer/ps2.live/ps2.live/shared/cache"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCacheHit(t *testing.T) {
|
||||
c := cache.NewCache[uint32, string](time.Second * 1)
|
||||
|
||||
c.Put(1, "1")
|
||||
r, ok := c.Get(1)
|
||||
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "1", r)
|
||||
}
|
||||
|
||||
func TestCacheMissTimeout(t *testing.T) {
|
||||
c := cache.NewCache[uint32, string](time.Second * 1)
|
||||
|
||||
c.Put(1, "1")
|
||||
|
||||
time.Sleep(time.Second*1 + time.Millisecond)
|
||||
r, ok := c.Get(1)
|
||||
|
||||
assert.False(t, ok)
|
||||
assert.Empty(t, r)
|
||||
}
|
||||
|
||||
func TestCacheMissNonexisting(t *testing.T) {
|
||||
c := cache.NewCache[uint32, string](time.Second * 1)
|
||||
|
||||
r, ok := c.Get(1)
|
||||
|
||||
assert.False(t, ok)
|
||||
assert.Empty(t, r)
|
||||
}
|
6
shell.nix
Normal file
6
shell.nix
Normal file
|
@ -0,0 +1,6 @@
|
|||
{ pkgs ? import <nixpkgs> {} }:
|
||||
pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
go
|
||||
];
|
||||
}
|
70
types/aggpop.go
Normal file
70
types/aggpop.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package types
|
||||
|
||||
type AggPopWorld struct {
|
||||
ID uint32 `json:"id"`
|
||||
Average uint32 `json:"average"`
|
||||
Factions Factions[uint32] `json:"factions"`
|
||||
Services AggPopServiceMap `json:"services"`
|
||||
}
|
||||
|
||||
type AggPopServiceMap struct {
|
||||
Fisu *Factions[uint32] `json:"fisu,omitempty"`
|
||||
Honu *Factions[uint32] `json:"honu,omitempty"`
|
||||
Saerro *Factions[uint32] `json:"saerro,omitempty"`
|
||||
Sanctuary *Factions[uint32] `json:"sanctuary,omitempty"`
|
||||
Voidwell *Factions[uint32] `json:"voidwell,omitempty"`
|
||||
}
|
||||
|
||||
// Average the 5 services by faction
|
||||
func (sm AggPopServiceMap) ToFactions() Factions[uint32] {
|
||||
var servicesCount, tTR, tNC, tVS, tTotal uint32
|
||||
|
||||
if sm.Fisu != nil {
|
||||
tTR += sm.Fisu.TR
|
||||
tNC += sm.Fisu.NC
|
||||
tVS += sm.Fisu.VS
|
||||
tTotal += sm.Fisu.Total
|
||||
servicesCount++
|
||||
}
|
||||
|
||||
if sm.Honu != nil {
|
||||
tTR += sm.Honu.TR
|
||||
tNC += sm.Honu.NC
|
||||
tVS += sm.Honu.VS
|
||||
tTotal += sm.Honu.Total
|
||||
servicesCount++
|
||||
}
|
||||
|
||||
if sm.Saerro != nil {
|
||||
tTR += sm.Saerro.TR
|
||||
tNC += sm.Saerro.NC
|
||||
tVS += sm.Saerro.VS
|
||||
tTotal += sm.Saerro.Total
|
||||
servicesCount++
|
||||
}
|
||||
|
||||
if sm.Sanctuary != nil {
|
||||
tTR += sm.Sanctuary.TR
|
||||
tNC += sm.Sanctuary.NC
|
||||
tVS += sm.Sanctuary.VS
|
||||
tTotal += sm.Sanctuary.Total
|
||||
servicesCount++
|
||||
}
|
||||
|
||||
if sm.Voidwell != nil {
|
||||
tTR += sm.Voidwell.TR
|
||||
tNC += sm.Voidwell.NC
|
||||
tVS += sm.Voidwell.VS
|
||||
tTotal += sm.Voidwell.Total
|
||||
servicesCount++
|
||||
}
|
||||
|
||||
f := Factions[uint32]{
|
||||
NC: tNC / servicesCount,
|
||||
TR: tTR / servicesCount,
|
||||
VS: tVS / servicesCount,
|
||||
Total: tTotal / servicesCount,
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
34
types/metagame.go
Normal file
34
types/metagame.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
type MetagameWorld struct {
|
||||
ID uint32 `json:"id"`
|
||||
Zones []MetagameZone `json:"zones"`
|
||||
CachedAt time.Time `json:"cached_at"`
|
||||
}
|
||||
|
||||
type MetagameZone struct {
|
||||
ID uint32 `json:"id"`
|
||||
Locked bool `json:"locked"`
|
||||
Alert *MetagameAlert `json:"alert,omitempty"`
|
||||
Territory Factions[float32] `json:"territory"`
|
||||
LockedSince time.Time `json:"locked_since,omitempty"`
|
||||
}
|
||||
|
||||
type MetagameAlert struct {
|
||||
ID uint32 `json:"id"`
|
||||
Zone uint32 `json:"zone"`
|
||||
EndTime time.Time `json:"end_time,omitempty"`
|
||||
StartTime time.Time `json:"start_time,omitempty"`
|
||||
AlertType string `json:"alert_type"`
|
||||
PS2Alerts string `json:"ps2alerts"`
|
||||
Percentages Factions[float32] `json:"percentages"`
|
||||
}
|
||||
|
||||
type Factions[T float32 | uint32] struct {
|
||||
TR T `json:"tr"`
|
||||
NC T `json:"nc"`
|
||||
VS T `json:"vs"`
|
||||
Total T `json:"total"`
|
||||
}
|
Loading…
Add table
Reference in a new issue