Add server-authoritative PvP main.go

This commit is contained in:
Admin 2026-09-21 01:52:42 +00:00
parent 91616b653d
commit 689562aadd

View File

@ -6,11 +6,12 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "hash/fnv"
"log" "log"
"math" "math"
"net/http" "net/http"
"os" "os"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -20,6 +21,27 @@ import (
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
) )
const (
worldSize = 3600.0
spawnX = 1200.0
spawnY = 1550.0
playerMaxHull = 100
playerMaxSpeed = 180.0
playerReverseSpeed = -55.0
playerAcceleration = 92.0
playerBrake = 118.0
playerTurnRate = 76.0
playerCoastDrag = 0.992
broadsideReload = 850 * time.Millisecond
shotSpeed = 520.0
shotLifetime = 900 * time.Millisecond
shotDamage = 16
shipHitRadius = 44.0
respawnDelay = 2200 * time.Millisecond
simTick = time.Second / 30
snapshotTick = 50 * time.Millisecond
)
type config struct { type config struct {
Port string Port string
DatabaseURL string DatabaseURL string
@ -53,6 +75,7 @@ type ticketStore struct {
} }
func newTicketStore() *ticketStore { return &ticketStore{items: map[string]ticket{}} } func newTicketStore() *ticketStore { return &ticketStore{items: map[string]ticket{}} }
func (s *ticketStore) issue(subject, name string) (string, error) { func (s *ticketStore) issue(subject, name string) (string, error) {
buf := make([]byte, 24) buf := make([]byte, 24)
if _, err := rand.Read(buf); err != nil { if _, err := rand.Read(buf); err != nil {
@ -64,6 +87,7 @@ func (s *ticketStore) issue(subject, name string) (string, error) {
s.mu.Unlock() s.mu.Unlock()
return value, nil return value, nil
} }
func (s *ticketStore) consume(value string) (ticket, bool) { func (s *ticketStore) consume(value string) (ticket, bool) {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
@ -74,6 +98,7 @@ func (s *ticketStore) consume(value string) (ticket, bool) {
} }
return item, true return item, true
} }
func (s *ticketStore) prune() { func (s *ticketStore) prune() {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
@ -86,35 +111,67 @@ func (s *ticketStore) prune() {
} }
type playerState struct { type playerState struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
X float64 `json:"x"` X float64 `json:"x"`
Y float64 `json:"y"` Y float64 `json:"y"`
Angle float64 `json:"angle"` Angle float64 `json:"angle"`
Hull int `json:"hull"` Speed float64 `json:"speed"`
Online bool `json:"online"` Hull int `json:"hull"`
MaxHull int `json:"maxHull"`
Alive bool `json:"alive"`
Online bool `json:"online"`
Kills int `json:"kills"`
Deaths int `json:"deaths"`
InputSeq int64 `json:"inputSeq"`
RespawnAt int64 `json:"respawnAt,omitempty"`
LastKiller string `json:"lastKiller,omitempty"`
}
type inputState struct {
Throttle float64
Steer float64
Stop bool
Docked bool
Seq int64
}
type projectileState struct {
ID string `json:"id"`
OwnerID string `json:"ownerId"`
X float64 `json:"x"`
Y float64 `json:"y"`
VX float64 `json:"vx"`
VY float64 `json:"vy"`
Damage int `json:"damage"`
bornAt time.Time
} }
type clientMessage struct { type clientMessage struct {
Type string `json:"type"` Type string `json:"type"`
X float64 `json:"x"` Seq int64 `json:"seq"`
Y float64 `json:"y"` Throttle float64 `json:"throttle"`
Angle float64 `json:"angle"` Steer float64 `json:"steer"`
Hull int `json:"hull"` Stop bool `json:"stop"`
Docked bool `json:"docked"`
Side string `json:"side"`
} }
type serverMessage struct { type serverMessage struct {
Type string `json:"type"` Type string `json:"type"`
PlayerID string `json:"playerId,omitempty"` PlayerID string `json:"playerId,omitempty"`
WorldSeed string `json:"worldSeed,omitempty"` WorldSeed string `json:"worldSeed,omitempty"`
Players []playerState `json:"players,omitempty"` Players []playerState `json:"players,omitempty"`
ServerTime int64 `json:"serverTime,omitempty"` Projectiles []projectileState `json:"projectiles,omitempty"`
ServerTime int64 `json:"serverTime,omitempty"`
} }
type client struct { type client struct {
conn *websocket.Conn conn *websocket.Conn
writeMu sync.Mutex writeMu sync.Mutex
state playerState state playerState
input inputState
lastFire map[string]time.Time
} }
func (c *client) writeJSON(v any) error { func (c *client) writeJSON(v any) error {
@ -125,12 +182,39 @@ func (c *client) writeJSON(v any) error {
} }
type hub struct { type hub struct {
mu sync.RWMutex mu sync.RWMutex
clients map[string]*client clients map[string]*client
worldSeed string projectiles map[string]projectileState
nextProjectile uint64
worldSeed string
}
func newHub(seed string) *hub {
return &hub{
clients: map[string]*client{},
projectiles: map[string]projectileState{},
worldSeed: seed,
}
}
func newPlayerState(id, name string, kills, deaths int) playerState {
x, y := spawnFor(id)
return playerState{
ID: id, Name: name, X: x, Y: y, Angle: 0, Speed: 0,
Hull: playerMaxHull, MaxHull: playerMaxHull, Alive: true, Online: true,
Kills: kills, Deaths: deaths,
}
}
func spawnFor(id string) (float64, float64) {
h := fnv.New32a()
_, _ = h.Write([]byte(id))
v := h.Sum32()
dx := float64(int(v%241) - 120)
dy := float64(int((v/241)%181) - 90)
return clampFloat(spawnX+dx, 90, worldSize-90), clampFloat(spawnY+dy, 90, worldSize-90)
} }
func newHub(seed string) *hub { return &hub{clients: map[string]*client{}, worldSeed: seed} }
func (h *hub) add(c *client) { func (h *hub) add(c *client) {
h.mu.Lock() h.mu.Lock()
if previous := h.clients[c.state.ID]; previous != nil && previous != c { if previous := h.clients[c.state.ID]; previous != nil && previous != c {
@ -139,6 +223,7 @@ func (h *hub) add(c *client) {
h.clients[c.state.ID] = c h.clients[c.state.ID] = c
h.mu.Unlock() h.mu.Unlock()
} }
func (h *hub) remove(id string, c *client) { func (h *hub) remove(id string, c *client) {
h.mu.Lock() h.mu.Lock()
if h.clients[id] == c { if h.clients[id] == c {
@ -146,37 +231,208 @@ func (h *hub) remove(id string, c *client) {
} }
h.mu.Unlock() h.mu.Unlock()
} }
func (h *hub) update(id string, msg clientMessage) {
h.mu.Lock() func (h *hub) count() int {
if c := h.clients[id]; c != nil { h.mu.RLock()
c.state.X = clampFloat(msg.X, 0, 3600) defer h.mu.RUnlock()
c.state.Y = clampFloat(msg.Y, 0, 3600) return len(h.clients)
c.state.Angle = normalizeAngle(msg.Angle)
c.state.Hull = clampInt(msg.Hull, 0, 500)
}
h.mu.Unlock()
} }
func (h *hub) snapshot() ([]*client, []playerState) {
func (h *hub) updateInput(id string, msg clientMessage) {
h.mu.Lock()
defer h.mu.Unlock()
c := h.clients[id]
if c == nil || msg.Seq <= c.input.Seq {
return
}
c.input = inputState{
Throttle: clampFloat(msg.Throttle, -1, 1),
Steer: clampFloat(msg.Steer, -1, 1),
Stop: msg.Stop,
Docked: msg.Docked,
Seq: msg.Seq,
}
}
func (h *hub) fire(id, side string, now time.Time) bool {
h.mu.Lock()
defer h.mu.Unlock()
c := h.clients[id]
if c == nil || !c.state.Alive || (side != "port" && side != "starboard") {
return false
}
if c.lastFire == nil {
c.lastFire = map[string]time.Time{}
}
if last := c.lastFire[side]; !last.IsZero() && now.Sub(last) < broadsideReload {
return false
}
c.lastFire[side] = now
sideSign := 1.0
if side == "port" {
sideSign = -1
}
base := headingRadians(c.state.Angle) + sideSign*math.Pi/2
shipVX := math.Cos(headingRadians(c.state.Angle)) * c.state.Speed
shipVY := math.Sin(headingRadians(c.state.Angle)) * c.state.Speed
for _, offset := range []float64{-7, 0, 7} {
angle := base + offset*math.Pi/180
h.nextProjectile++
projectileID := id + ":" + strconv.FormatUint(h.nextProjectile, 10)
h.projectiles[projectileID] = projectileState{
ID: projectileID, OwnerID: id,
X: c.state.X + math.Cos(base)*34,
Y: c.state.Y + math.Sin(base)*34,
VX: math.Cos(angle)*shotSpeed + shipVX*0.25,
VY: math.Sin(angle)*shotSpeed + shipVY*0.25,
Damage: shotDamage,
bornAt: now,
}
}
return true
}
func (h *hub) step(now time.Time, dt float64) {
h.mu.Lock()
defer h.mu.Unlock()
for _, c := range h.clients {
stepPlayer(&c.state, c.input, now, dt)
}
for id, shot := range h.projectiles {
if now.Sub(shot.bornAt) >= shotLifetime {
delete(h.projectiles, id)
continue
}
shot.X += shot.VX * dt
shot.Y += shot.VY * dt
if shot.X < -80 || shot.X > worldSize+80 || shot.Y < -80 || shot.Y > worldSize+80 {
delete(h.projectiles, id)
continue
}
hit := false
for _, target := range h.clients {
if target.state.ID == shot.OwnerID || !target.state.Alive {
continue
}
dx := target.state.X - shot.X
dy := target.state.Y - shot.Y
if dx*dx+dy*dy > shipHitRadius*shipHitRadius {
continue
}
target.state.Hull = clampInt(target.state.Hull-shot.Damage, 0, target.state.MaxHull)
if target.state.Hull <= 0 {
target.state.Alive = false
target.state.Speed = 0
target.state.Deaths++
target.state.RespawnAt = now.Add(respawnDelay).UnixMilli()
if owner := h.clients[shot.OwnerID]; owner != nil {
owner.state.Kills++
target.state.LastKiller = owner.state.Name
}
}
delete(h.projectiles, id)
hit = true
break
}
if hit {
continue
}
h.projectiles[id] = shot
}
}
func stepPlayer(state *playerState, input inputState, now time.Time, dt float64) {
state.InputSeq = input.Seq
if !state.Alive {
if state.RespawnAt > 0 && now.UnixMilli() >= state.RespawnAt {
x, y := spawnFor(state.ID)
state.X = x
state.Y = y
state.Angle = 0
state.Speed = 0
state.Hull = state.MaxHull
state.Alive = true
state.RespawnAt = 0
state.LastKiller = ""
}
return
}
if input.Docked {
state.Speed = 0
return
}
if input.Throttle > 0 {
state.Speed += playerAcceleration * input.Throttle * dt
} else if input.Throttle < 0 {
state.Speed += playerBrake * input.Throttle * dt
} else if !input.Stop {
state.Speed *= math.Pow(playerCoastDrag, dt*60)
}
if input.Stop {
state.Speed *= math.Pow(0.025, dt)
}
state.Speed = clampFloat(state.Speed, playerReverseSpeed, playerMaxSpeed)
turnStrength := clampFloat(math.Abs(state.Speed)/100, 0.28, 1)
directionSign := 1.0
if state.Speed < 0 {
directionSign = -1
}
state.Angle = normalizeAngle(state.Angle + playerTurnRate*turnStrength*directionSign*input.Steer*dt)
heading := headingRadians(state.Angle)
state.X = clampFloat(state.X+math.Cos(heading)*state.Speed*dt, 0, worldSize)
state.Y = clampFloat(state.Y+math.Sin(heading)*state.Speed*dt, 0, worldSize)
}
func headingRadians(angle float64) float64 { return (angle - 90) * math.Pi / 180 }
func (h *hub) snapshot() ([]*client, []playerState, []projectileState) {
h.mu.RLock() h.mu.RLock()
defer h.mu.RUnlock() defer h.mu.RUnlock()
clients := make([]*client, 0, len(h.clients)) clients := make([]*client, 0, len(h.clients))
players := make([]playerState, 0, len(h.clients)) players := make([]playerState, 0, len(h.clients))
projectiles := make([]projectileState, 0, len(h.projectiles))
for _, c := range h.clients { for _, c := range h.clients {
clients = append(clients, c) clients = append(clients, c)
players = append(players, c.state) players = append(players, c.state)
} }
return clients, players for _, shot := range h.projectiles {
projectiles = append(projectiles, shot)
}
return clients, players, projectiles
} }
func (h *hub) run(ctx context.Context) { func (h *hub) run(ctx context.Context) {
ticker := time.NewTicker(100 * time.Millisecond) simTicker := time.NewTicker(simTick)
defer ticker.Stop() snapshotTicker := time.NewTicker(snapshotTick)
defer simTicker.Stop()
defer snapshotTicker.Stop()
lastSim := time.Now()
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return
case now := <-ticker.C: case now := <-simTicker.C:
clients, players := h.snapshot() dt := now.Sub(lastSim).Seconds()
msg := serverMessage{Type: "snapshot", Players: players, ServerTime: now.UnixMilli()} if dt <= 0 || dt > 0.1 {
dt = simTick.Seconds()
}
lastSim = now
h.step(now, dt)
case now := <-snapshotTicker.C:
clients, players, projectiles := h.snapshot()
msg := serverMessage{
Type: "snapshot", Players: players, Projectiles: projectiles,
ServerTime: now.UnixMilli(),
}
for _, c := range clients { for _, c := range clients {
if err := c.writeJSON(msg); err != nil { if err := c.writeJSON(msg); err != nil {
_ = c.conn.Close() _ = c.conn.Close()
@ -198,6 +454,7 @@ func clampFloat(v, min, max float64) float64 {
} }
return v return v
} }
func clampInt(v, min, max int) int { func clampInt(v, min, max int) int {
if v < min { if v < min {
return min return min
@ -207,6 +464,7 @@ func clampInt(v, min, max int) int {
} }
return v return v
} }
func normalizeAngle(v float64) float64 { func normalizeAngle(v float64) float64 {
if math.IsNaN(v) || math.IsInf(v, 0) { if math.IsNaN(v) || math.IsInf(v, 0) {
return 0 return 0
@ -236,6 +494,7 @@ func loadConfig() (config, error) {
} }
return cfg, nil return cfg, nil
} }
func env(key, fallback string) string { func env(key, fallback string) string {
if v := os.Getenv(key); v != "" { if v := os.Getenv(key); v != "" {
return v return v
@ -251,13 +510,18 @@ func migrate(ctx context.Context, db *pgxpool.Pool) error {
credits INTEGER NOT NULL DEFAULT 650, credits INTEGER NOT NULL DEFAULT 650,
scrap INTEGER NOT NULL DEFAULT 0, scrap INTEGER NOT NULL DEFAULT 0,
progression JSONB NOT NULL DEFAULT '{}'::jsonb, progression JSONB NOT NULL DEFAULT '{}'::jsonb,
pvp_kills INTEGER NOT NULL DEFAULT 0,
pvp_deaths INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now() last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now()
) );
ALTER TABLE player_profiles ADD COLUMN IF NOT EXISTS pvp_kills INTEGER NOT NULL DEFAULT 0;
ALTER TABLE player_profiles ADD COLUMN IF NOT EXISTS pvp_deaths INTEGER NOT NULL DEFAULT 0
`) `)
return err return err
} }
func ensurePlayer(ctx context.Context, db *pgxpool.Pool, sub, name string) error { func ensurePlayer(ctx context.Context, db *pgxpool.Pool, sub, name string) error {
_, err := db.Exec(ctx, ` _, err := db.Exec(ctx, `
INSERT INTO player_profiles (keycloak_sub, display_name) INSERT INTO player_profiles (keycloak_sub, display_name)
@ -270,6 +534,24 @@ func ensurePlayer(ctx context.Context, db *pgxpool.Pool, sub, name string) error
return err return err
} }
func loadPvPStats(ctx context.Context, db *pgxpool.Pool, sub string) (int, int, error) {
var kills, deaths int
err := db.QueryRow(ctx, `SELECT pvp_kills, pvp_deaths FROM player_profiles WHERE keycloak_sub = $1`, sub).Scan(&kills, &deaths)
return kills, deaths, err
}
func savePvPStats(ctx context.Context, db *pgxpool.Pool, state playerState) error {
_, err := db.Exec(ctx, `
UPDATE player_profiles SET
pvp_kills = $2,
pvp_deaths = $3,
updated_at = now(),
last_seen_at = now()
WHERE keycloak_sub = $1
`, state.ID, state.Kills, state.Deaths)
return err
}
type app struct { type app struct {
cfg config cfg config
db *pgxpool.Pool db *pgxpool.Pool
@ -314,6 +596,7 @@ func (a *app) authenticate(next http.Handler) http.Handler {
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), claimsKey, c))) next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), claimsKey, c)))
}) })
} }
func audienceMatches(c claims, clientID string) bool { func audienceMatches(c claims, clientID string) bool {
if c.AuthorizedParty == clientID { if c.AuthorizedParty == clientID {
return true return true
@ -330,6 +613,7 @@ func audienceMatches(c claims, clientID string) bool {
} }
return false return false
} }
func (a *app) cors(next http.Handler) http.Handler { func (a *app) cors(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin") origin := r.Header.Get("Origin")
@ -346,11 +630,13 @@ func (a *app) cors(next http.Handler) http.Handler {
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
}) })
} }
func jsonResponse(w http.ResponseWriter, status int, value any) { func jsonResponse(w http.ResponseWriter, status int, value any) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status) w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(value) _ = json.NewEncoder(w).Encode(value)
} }
func (a *app) health(w http.ResponseWriter, r *http.Request) { func (a *app) health(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), time.Second) ctx, cancel := context.WithTimeout(r.Context(), time.Second)
defer cancel() defer cancel()
@ -358,12 +644,17 @@ func (a *app) health(w http.ResponseWriter, r *http.Request) {
jsonResponse(w, 503, map[string]any{"ok": false, "database": false}) jsonResponse(w, 503, map[string]any{"ok": false, "database": false})
return return
} }
jsonResponse(w, 200, map[string]any{"ok": true, "database": true, "players": len(a.hub.clients), "worldSeed": a.cfg.WorldSeed}) jsonResponse(w, 200, map[string]any{
"ok": true, "database": true, "players": a.hub.count(),
"worldSeed": a.cfg.WorldSeed, "authority": "movement+pvp",
})
} }
func (a *app) me(w http.ResponseWriter, r *http.Request) { func (a *app) me(w http.ResponseWriter, r *http.Request) {
c := r.Context().Value(claimsKey).(claims) c := r.Context().Value(claimsKey).(claims)
jsonResponse(w, 200, map[string]any{"id": c.Subject, "name": c.PreferredUsername, "worldSeed": a.cfg.WorldSeed}) jsonResponse(w, 200, map[string]any{"id": c.Subject, "name": c.PreferredUsername, "worldSeed": a.cfg.WorldSeed})
} }
func (a *app) wsTicket(w http.ResponseWriter, r *http.Request) { func (a *app) wsTicket(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed) http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
@ -377,6 +668,7 @@ func (a *app) wsTicket(w http.ResponseWriter, r *http.Request) {
} }
jsonResponse(w, 201, map[string]any{"ticket": value, "expiresIn": 30}) jsonResponse(w, 201, map[string]any{"ticket": value, "expiresIn": 30})
} }
func (a *app) ws(w http.ResponseWriter, r *http.Request) { func (a *app) ws(w http.ResponseWriter, r *http.Request) {
value := r.URL.Query().Get("ticket") value := r.URL.Query().Get("ticket")
t, ok := a.tickets.consume(value) t, ok := a.tickets.consume(value)
@ -388,7 +680,16 @@ func (a *app) ws(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
return return
} }
c := &client{conn: conn, state: playerState{ID: t.Subject, Name: t.Name, X: 1200, Y: 1550, Angle: 0, Hull: 100, Online: true}}
kills, deaths, err := loadPvPStats(r.Context(), a.db, t.Subject)
if err != nil {
kills, deaths = 0, 0
}
c := &client{
conn: conn,
state: newPlayerState(t.Subject, t.Name, kills, deaths),
lastFire: map[string]time.Time{},
}
a.hub.add(c) a.hub.add(c)
defer func() { defer func() {
a.hub.remove(t.Subject, c) a.hub.remove(t.Subject, c)
@ -396,8 +697,13 @@ func (a *app) ws(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() defer cancel()
_ = ensurePlayer(ctx, a.db, t.Subject, t.Name) _ = ensurePlayer(ctx, a.db, t.Subject, t.Name)
_ = savePvPStats(ctx, a.db, c.state)
}() }()
_ = c.writeJSON(serverMessage{Type: "welcome", PlayerID: t.Subject, WorldSeed: a.cfg.WorldSeed, ServerTime: time.Now().UnixMilli()})
_ = c.writeJSON(serverMessage{
Type: "welcome", PlayerID: t.Subject, WorldSeed: a.cfg.WorldSeed,
ServerTime: time.Now().UnixMilli(),
})
conn.SetReadLimit(4096) conn.SetReadLimit(4096)
for { for {
_ = conn.SetReadDeadline(time.Now().Add(45 * time.Second)) _ = conn.SetReadDeadline(time.Now().Add(45 * time.Second))
@ -405,8 +711,11 @@ func (a *app) ws(w http.ResponseWriter, r *http.Request) {
if err := conn.ReadJSON(&msg); err != nil { if err := conn.ReadJSON(&msg); err != nil {
return return
} }
if msg.Type == "state" { switch msg.Type {
a.hub.update(t.Subject, msg) case "input":
a.hub.updateInput(t.Subject, msg)
case "fire":
a.hub.fire(t.Subject, strings.ToLower(msg.Side), time.Now())
} }
} }
} }
@ -433,7 +742,10 @@ func main() {
log.Fatalf("oidc provider: %v", err) log.Fatalf("oidc provider: %v", err)
} }
verifier := provider.Verifier(&oidc.Config{SkipClientIDCheck: true}) verifier := provider.Verifier(&oidc.Config{SkipClientIDCheck: true})
application := &app{cfg: cfg, db: db, verifier: verifier, tickets: newTicketStore(), hub: newHub(cfg.WorldSeed)} application := &app{
cfg: cfg, db: db, verifier: verifier,
tickets: newTicketStore(), hub: newHub(cfg.WorldSeed),
}
application.upgrader = websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { application.upgrader = websocket.Upgrader{CheckOrigin: func(r *http.Request) bool {
origin := r.Header.Get("Origin") origin := r.Header.Get("Origin")
return origin == "" || cfg.AllowedOrigins[origin] return origin == "" || cfg.AllowedOrigins[origin]
@ -446,14 +758,16 @@ func main() {
application.tickets.prune() application.tickets.prune()
} }
}() }()
mux := http.NewServeMux() mux := http.NewServeMux()
mux.HandleFunc("/health", application.health) mux.HandleFunc("/health", application.health)
mux.Handle("/api/me", application.cors(application.authenticate(http.HandlerFunc(application.me)))) mux.Handle("/api/me", application.cors(application.authenticate(http.HandlerFunc(application.me))))
mux.Handle("/api/ws-ticket", application.cors(application.authenticate(http.HandlerFunc(application.wsTicket)))) mux.Handle("/api/ws-ticket", application.cors(application.authenticate(http.HandlerFunc(application.wsTicket))))
mux.HandleFunc("/ws", application.ws) mux.HandleFunc("/ws", application.ws)
server := &http.Server{Addr: ":" + cfg.Port, Handler: mux, ReadHeaderTimeout: 5 * time.Second, IdleTimeout: 60 * time.Second} server := &http.Server{
log.Printf("airship game server listening on :%s world=%s", cfg.Port, cfg.WorldSeed) Addr: ":" + cfg.Port, Handler: mux,
ReadHeaderTimeout: 5 * time.Second, IdleTimeout: 60 * time.Second,
}
log.Printf("airship game server listening on :%s world=%s authority=movement+pvp", cfg.Port, cfg.WorldSeed)
log.Fatal(server.ListenAndServe()) log.Fatal(server.ListenAndServe())
} }
func init() { _ = fmt.Sprintf }