2024-01-22 20:16:10 -06:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2024-02-01 21:38:44 -06:00
|
|
|
"encoding/json"
|
2024-01-22 20:16:10 -06:00
|
|
|
"errors"
|
2024-01-27 13:51:20 -06:00
|
|
|
"log/slog"
|
2024-01-22 20:16:10 -06:00
|
|
|
"net/http"
|
|
|
|
"time"
|
|
|
|
|
2024-02-04 09:25:09 -06:00
|
|
|
"github.com/google/uuid"
|
2024-01-22 20:16:10 -06:00
|
|
|
"nhooyr.io/websocket"
|
2024-01-27 13:24:42 -06:00
|
|
|
"nhooyr.io/websocket/wsjson"
|
2024-01-22 20:16:10 -06:00
|
|
|
|
2024-01-27 13:24:42 -06:00
|
|
|
"git.sunturtle.xyz/studio/shotgun/game"
|
2024-01-22 20:16:10 -06:00
|
|
|
"git.sunturtle.xyz/studio/shotgun/lobby"
|
|
|
|
"git.sunturtle.xyz/studio/shotgun/player"
|
|
|
|
"git.sunturtle.xyz/studio/shotgun/serve"
|
|
|
|
)
|
|
|
|
|
|
|
|
type Server struct {
|
2024-02-04 09:42:42 -06:00
|
|
|
l *lobby.Lobby[uuid.UUID, matchingPerson]
|
2024-01-22 20:16:10 -06:00
|
|
|
|
2024-02-01 21:38:44 -06:00
|
|
|
creds db
|
|
|
|
sessions db
|
2024-01-22 20:16:10 -06:00
|
|
|
}
|
|
|
|
|
2024-02-01 21:38:44 -06:00
|
|
|
type db interface {
|
2024-02-01 21:02:30 -06:00
|
|
|
player.RowQuerier
|
|
|
|
player.Execer
|
|
|
|
}
|
|
|
|
|
2024-01-22 20:16:10 -06:00
|
|
|
type person struct {
|
|
|
|
conn *websocket.Conn
|
|
|
|
id player.ID
|
|
|
|
}
|
|
|
|
|
2024-02-04 09:42:42 -06:00
|
|
|
type matchingPerson struct {
|
|
|
|
sync chan struct{}
|
|
|
|
person
|
2024-01-22 20:16:10 -06:00
|
|
|
}
|
|
|
|
|
2024-01-27 13:24:42 -06:00
|
|
|
func (s *Server) accept(w http.ResponseWriter, r *http.Request, p player.ID) (person, error) {
|
|
|
|
conn, err := websocket.Accept(w, r, nil)
|
|
|
|
if err != nil {
|
|
|
|
return person{}, err
|
|
|
|
}
|
2024-01-27 13:51:20 -06:00
|
|
|
slog.Debug("upgraded", "player", p)
|
2024-01-27 13:24:42 -06:00
|
|
|
return person{conn: conn, id: p}, nil
|
2024-01-22 20:16:10 -06:00
|
|
|
}
|
|
|
|
|
2024-02-01 21:02:30 -06:00
|
|
|
func (s *Server) Register(w http.ResponseWriter, r *http.Request) {
|
|
|
|
ctx := r.Context()
|
|
|
|
slog := slog.With(
|
|
|
|
slog.String("remote", r.RemoteAddr),
|
|
|
|
slog.String("forwarded-for", r.Header.Get("X-Forwarded-For")),
|
|
|
|
slog.String("user-agent", r.UserAgent()),
|
|
|
|
)
|
|
|
|
if err := r.ParseForm(); err != nil {
|
|
|
|
slog.WarnContext(ctx, "error parsing form on register", "err", err.Error())
|
|
|
|
http.Error(w, "what", http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
user, pass := r.PostFormValue("user"), r.PostFormValue("pass")
|
|
|
|
if user == "" || pass == "" {
|
|
|
|
slog.WarnContext(ctx, "missing user or pass on register")
|
|
|
|
http.Error(w, "missing credentials", http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
err := player.Register(ctx, s.creds, user, pass)
|
|
|
|
if err != nil {
|
|
|
|
slog.ErrorContext(ctx, "registration failed", "err", err.Error())
|
|
|
|
http.Error(w, "something went wrong, maybe someone already has that username, idk", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
2024-02-02 13:14:00 -06:00
|
|
|
p, err := player.Login(ctx, s.creds, user, pass)
|
|
|
|
if err != nil {
|
|
|
|
slog.ErrorContext(ctx, "login failed", "err", err.Error())
|
|
|
|
http.Error(w, "no", http.StatusUnauthorized)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
id, err := player.StartSession(ctx, s.sessions, p, time.Now())
|
|
|
|
if err != nil {
|
|
|
|
slog.ErrorContext(ctx, "failed to create session", "player", p, "err", err.Error())
|
|
|
|
http.Error(w, "something went wrong", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
serve.SetSession(w, id)
|
2024-02-01 21:02:30 -06:00
|
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
|
|
slog.InfoContext(ctx, "registered", "user", user)
|
|
|
|
}
|
|
|
|
|
2024-02-01 20:40:59 -06:00
|
|
|
func (s *Server) Login(w http.ResponseWriter, r *http.Request) {
|
|
|
|
ctx := r.Context()
|
|
|
|
slog := slog.With(
|
|
|
|
slog.String("remote", r.RemoteAddr),
|
|
|
|
slog.String("forwarded-for", r.Header.Get("X-Forwarded-For")),
|
|
|
|
slog.String("user-agent", r.UserAgent()),
|
|
|
|
)
|
|
|
|
if err := r.ParseForm(); err != nil {
|
|
|
|
slog.WarnContext(ctx, "error parsing form on login", "err", err.Error())
|
|
|
|
http.Error(w, "what", http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
user, pass := r.PostFormValue("user"), r.PostFormValue("pass")
|
|
|
|
if user == "" || pass == "" {
|
|
|
|
slog.WarnContext(ctx, "missing user or pass on login")
|
|
|
|
http.Error(w, "missing credentials", http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
p, err := player.Login(ctx, s.creds, user, pass)
|
|
|
|
if err != nil {
|
|
|
|
slog.ErrorContext(ctx, "login failed", "err", err.Error())
|
|
|
|
http.Error(w, "no", http.StatusUnauthorized)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
id, err := player.StartSession(ctx, s.sessions, p, time.Now())
|
|
|
|
if err != nil {
|
|
|
|
slog.ErrorContext(ctx, "failed to create session", "player", p, "err", err.Error())
|
|
|
|
http.Error(w, "something went wrong", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
serve.SetSession(w, id)
|
2024-02-01 21:02:30 -06:00
|
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
|
|
slog.InfoContext(ctx, "logged in", "player", p, "id", id)
|
2024-02-01 20:40:59 -06:00
|
|
|
}
|
|
|
|
|
2024-02-02 17:30:35 -06:00
|
|
|
func (s *Server) Logout(w http.ResponseWriter, r *http.Request) {
|
|
|
|
ctx := r.Context()
|
|
|
|
slog := slog.With(
|
|
|
|
slog.String("remote", r.RemoteAddr),
|
|
|
|
slog.String("forwarded-for", r.Header.Get("X-Forwarded-For")),
|
|
|
|
slog.String("user-agent", r.UserAgent()),
|
|
|
|
)
|
|
|
|
id := serve.ReqSession(ctx)
|
|
|
|
if id == (player.Session{}) {
|
|
|
|
slog.WarnContext(ctx, "no session on logout")
|
|
|
|
http.Error(w, "what", http.StatusUnauthorized)
|
|
|
|
panic("unreachable")
|
|
|
|
}
|
|
|
|
err := player.Logout(ctx, s.sessions, id)
|
|
|
|
if err != nil {
|
|
|
|
slog.ErrorContext(ctx, "logout failed", "err", err.Error())
|
|
|
|
http.Error(w, "something went wrong", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
serve.RemoveSession(w)
|
|
|
|
slog.InfoContext(ctx, "logged out", "session", id)
|
|
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
|
|
}
|
|
|
|
|
2024-02-01 21:38:44 -06:00
|
|
|
func (s *Server) Me(w http.ResponseWriter, r *http.Request) {
|
2024-02-02 13:03:05 -06:00
|
|
|
id := serve.ReqPlayer(r.Context())
|
|
|
|
if id == (player.ID{}) {
|
2024-02-01 21:38:44 -06:00
|
|
|
panic("missing player ID")
|
|
|
|
}
|
|
|
|
q := struct {
|
|
|
|
ID player.ID `json:"id"`
|
2024-02-02 13:03:05 -06:00
|
|
|
}{id}
|
2024-02-01 21:38:44 -06:00
|
|
|
json.NewEncoder(w).Encode(q)
|
|
|
|
}
|
|
|
|
|
2024-01-22 20:16:10 -06:00
|
|
|
// Queue connects players to games. The connection immediately upgrades.
|
|
|
|
// This handler MUST be wrapped in [serve.WithPlayerID].
|
|
|
|
func (s *Server) Queue(w http.ResponseWriter, r *http.Request) {
|
2024-02-02 13:03:05 -06:00
|
|
|
p := serve.ReqPlayer(r.Context())
|
2024-01-27 13:24:42 -06:00
|
|
|
person, err := s.accept(w, r, p)
|
2024-01-22 20:16:10 -06:00
|
|
|
if err != nil {
|
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
2024-01-27 13:24:42 -06:00
|
|
|
s.joinAndServe(person)
|
2024-01-22 20:16:10 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Server) joinAndServe(p person) {
|
2024-01-27 13:51:20 -06:00
|
|
|
slog.Debug("joining", "player", p.id)
|
2024-02-03 22:40:56 -06:00
|
|
|
ctx, stop := context.WithTimeoutCause(context.Background(), 3*time.Minute, errQueueEmpty)
|
2024-02-02 21:18:03 -06:00
|
|
|
ch := make(chan struct{})
|
2024-02-02 20:48:45 -06:00
|
|
|
go func() {
|
2024-02-02 21:18:03 -06:00
|
|
|
defer close(ch)
|
2024-02-02 21:29:13 -06:00
|
|
|
_, _, err := p.conn.Read(ctx)
|
|
|
|
if err != nil {
|
|
|
|
slog.ErrorContext(ctx, "player dropped on hello", "player", p.id, "err", err.Error())
|
2024-02-03 15:39:59 -06:00
|
|
|
stop()
|
2024-02-02 20:48:45 -06:00
|
|
|
}
|
|
|
|
}()
|
2024-02-04 09:42:42 -06:00
|
|
|
mp := matchingPerson{sync: make(chan struct{}), person: p}
|
|
|
|
id, chall, deal := s.l.Queue(ctx, uuid.New, mp)
|
2024-02-02 21:18:03 -06:00
|
|
|
<-ch
|
2024-02-04 09:42:42 -06:00
|
|
|
close(mp.sync)
|
2024-02-02 21:33:50 -06:00
|
|
|
stop()
|
2024-02-04 09:25:09 -06:00
|
|
|
if id == uuid.Nil {
|
2024-01-22 20:16:10 -06:00
|
|
|
// Context canceled.
|
2024-01-27 13:24:42 -06:00
|
|
|
p.conn.Close(websocket.StatusTryAgainLater, "sorry, queue is empty...")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if deal {
|
2024-02-04 09:42:42 -06:00
|
|
|
g := game.New(p.id, chall.id)
|
|
|
|
<-chall.sync
|
2024-02-04 09:52:14 -06:00
|
|
|
go gameActor(context.TODO(), g, p, chall.person, nil)
|
2024-01-27 13:24:42 -06:00
|
|
|
}
|
|
|
|
// Reply with the game ID so they can share.
|
2024-01-29 21:23:55 -06:00
|
|
|
r := serve.GameStart{
|
|
|
|
ID: id,
|
|
|
|
Dealer: deal,
|
2024-01-27 13:24:42 -06:00
|
|
|
}
|
|
|
|
if err := wsjson.Write(context.TODO(), p.conn, r); err != nil {
|
2024-01-27 13:51:20 -06:00
|
|
|
slog.WarnContext(ctx, "got a game but player dropped", "game", id, "player", p.id)
|
2024-02-04 09:42:42 -06:00
|
|
|
p.conn.Close(websocket.StatusNormalClosure, "looks like you dropped")
|
2024-01-22 20:16:10 -06:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var errQueueEmpty = errors.New("sorry, queue is empty")
|