From e9d3b367142c7d7f1395c4856dac654e42fc7983 Mon Sep 17 00:00:00 2001 From: Marcel Schramm Date: Mon, 6 Jan 2025 00:55:03 +0100 Subject: [PATCH] wip --- internal/api/http.go | 1 + internal/api/v1.go | 25 +- internal/api/v1_easyjson.go | 570 ++--------------------------- internal/api/ws.go | 8 +- internal/frontend/lobby.js | 66 +++- internal/game/data.go | 97 +++-- internal/game/data_easyjson.go | 23 ++ internal/game/data_test.go | 6 +- internal/game/lobby.go | 231 +++++++----- internal/game/lobby_test.go | 50 +-- internal/game/shared.go | 34 +- internal/game/shared_easyjson.go | 211 ++++++++--- internal/game/words.go | 10 +- internal/game/words_test.go | 63 ++-- internal/state/lobbies.go | 21 ++ internal/state/lobbies_easyjson.go | 110 +----- 16 files changed, 615 insertions(+), 911 deletions(-) create mode 100644 internal/game/data_easyjson.go diff --git a/internal/api/http.go b/internal/api/http.go index b8b0408e..cf7a87a9 100644 --- a/internal/api/http.go +++ b/internal/api/http.go @@ -31,6 +31,7 @@ func (handler *V1Handler) SetupRoutes(rootPath string, register func(string, str // We support both path parameter and cookie. register("GET", path.Join(v1, "lobby", "ws"), handler.websocketUpgrade) + register("POST", path.Join(v1, "lobby", "resurrect"), handler.resurrectLobby) register("POST", path.Join(v1, "lobby", "{lobby_id}", "player"), handler.postPlayer) } diff --git a/internal/api/v1.go b/internal/api/v1.go index 7f5d671b..35345a2d 100644 --- a/internal/api/v1.go +++ b/internal/api/v1.go @@ -5,6 +5,7 @@ package api import ( + "encoding/base64" "errors" "fmt" "log" @@ -71,7 +72,7 @@ func (handler *V1Handler) getLobbies(writer http.ResponseWriter, _ *http.Request MaxClientsPerIP: lobby.ClientsPerIPLimit, Wordpack: lobby.Wordpack, State: lobby.State, - Scoring: lobby.ScoreCalculation.Identifier(), + Scoring: lobby.ScoreCalculationIdentifier, }) } @@ -83,6 +84,28 @@ func (handler *V1Handler) getLobbies(writer http.ResponseWriter, _ *http.Request } } +func (handler *V1Handler) resurrectLobby(writer http.ResponseWriter, request *http.Request) { + var data game.LobbyRestoreData + base64Decoder := base64.NewDecoder(base64.StdEncoding, request.Body) + if err := easyjson.UnmarshalFromReader(base64Decoder, &data); err != nil { + log.Println("Error unmarshalling lobby resurrection data:", err) + http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + lobby := data.Lobby + // We add the lobby, while the lobby mutex is aqcuired. This prevents us + // from attempting to connect to the lobby, before the internal state has + // been restored correctly. + lobby.Synchronized(func() { + if state.ResurrectLobby(lobby) { + lobby.WriteObject = WriteObject + lobby.WritePreparedMessage = WritePreparedMessage + lobby.ResurrectUnsynchronized(&data) + } + }) +} + func (handler *V1Handler) postLobby(writer http.ResponseWriter, request *http.Request) { if err := request.ParseForm(); err != nil { http.Error(writer, err.Error(), http.StatusBadRequest) diff --git a/internal/api/v1_easyjson.go b/internal/api/v1_easyjson.go index b77084e9..22dd4893 100644 --- a/internal/api/v1_easyjson.go +++ b/internal/api/v1_easyjson.go @@ -1,557 +1,37 @@ -// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. +// TEMPORARY AUTOGENERATED FILE: easyjson stub code to make the package +// compilable during generation. -package api +package api import ( - json "encoding/json" - easyjson "github.com/mailru/easyjson" - jlexer "github.com/mailru/easyjson/jlexer" - jwriter "github.com/mailru/easyjson/jwriter" - game "github.com/scribble-rs/scribble.rs/internal/game" + "github.com/mailru/easyjson/jwriter" + "github.com/mailru/easyjson/jlexer" ) -// suppress unused package warning -var ( - _ *json.RawMessage - _ *jlexer.Lexer - _ *jwriter.Writer - _ easyjson.Marshaler -) - -func easyjson102f8a2fDecodeGithubComScribbleRsScribbleRsInternalApi(in *jlexer.Lexer, out *LobbyEntry) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - if in.IsNull() { - in.Skip() - in.WantComma() - continue - } - switch key { - case "lobbyId": - out.LobbyID = string(in.String()) - case "wordpack": - out.Wordpack = string(in.String()) - case "scoring": - out.Scoring = string(in.String()) - case "state": - out.State = game.State(in.String()) - case "playerCount": - out.PlayerCount = int(in.Int()) - case "maxPlayers": - out.MaxPlayers = int(in.Int()) - case "round": - out.Round = int(in.Int()) - case "rounds": - out.Rounds = int(in.Int()) - case "drawingTime": - out.DrawingTime = int(in.Int()) - case "maxClientsPerIp": - out.MaxClientsPerIP = int(in.Int()) - case "customWords": - out.CustomWords = bool(in.Bool()) - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjson102f8a2fEncodeGithubComScribbleRsScribbleRsInternalApi(out *jwriter.Writer, in LobbyEntry) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"lobbyId\":" - out.RawString(prefix[1:]) - out.String(string(in.LobbyID)) - } - { - const prefix string = ",\"wordpack\":" - out.RawString(prefix) - out.String(string(in.Wordpack)) - } - { - const prefix string = ",\"scoring\":" - out.RawString(prefix) - out.String(string(in.Scoring)) - } - { - const prefix string = ",\"state\":" - out.RawString(prefix) - out.String(string(in.State)) - } - { - const prefix string = ",\"playerCount\":" - out.RawString(prefix) - out.Int(int(in.PlayerCount)) - } - { - const prefix string = ",\"maxPlayers\":" - out.RawString(prefix) - out.Int(int(in.MaxPlayers)) - } - { - const prefix string = ",\"round\":" - out.RawString(prefix) - out.Int(int(in.Round)) - } - { - const prefix string = ",\"rounds\":" - out.RawString(prefix) - out.Int(int(in.Rounds)) - } - { - const prefix string = ",\"drawingTime\":" - out.RawString(prefix) - out.Int(int(in.DrawingTime)) - } - { - const prefix string = ",\"maxClientsPerIp\":" - out.RawString(prefix) - out.Int(int(in.MaxClientsPerIP)) - } - { - const prefix string = ",\"customWords\":" - out.RawString(prefix) - out.Bool(bool(in.CustomWords)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v LobbyEntry) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjson102f8a2fEncodeGithubComScribbleRsScribbleRsInternalApi(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v LobbyEntry) MarshalEasyJSON(w *jwriter.Writer) { - easyjson102f8a2fEncodeGithubComScribbleRsScribbleRsInternalApi(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *LobbyEntry) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjson102f8a2fDecodeGithubComScribbleRsScribbleRsInternalApi(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *LobbyEntry) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson102f8a2fDecodeGithubComScribbleRsScribbleRsInternalApi(l, v) -} -func easyjson102f8a2fDecodeGithubComScribbleRsScribbleRsInternalApi1(in *jlexer.Lexer, out *LobbyEntries) { - isTopLevel := in.IsStart() - if in.IsNull() { - in.Skip() - *out = nil - } else { - in.Delim('[') - if *out == nil { - if !in.IsDelim(']') { - *out = make(LobbyEntries, 0, 8) - } else { - *out = LobbyEntries{} - } - } else { - *out = (*out)[:0] - } - for !in.IsDelim(']') { - var v1 *LobbyEntry - if in.IsNull() { - in.Skip() - v1 = nil - } else { - if v1 == nil { - v1 = new(LobbyEntry) - } - (*v1).UnmarshalEasyJSON(in) - } - *out = append(*out, v1) - in.WantComma() - } - in.Delim(']') - } - if isTopLevel { - in.Consumed() - } -} -func easyjson102f8a2fEncodeGithubComScribbleRsScribbleRsInternalApi1(out *jwriter.Writer, in LobbyEntries) { - if in == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { - out.RawString("null") - } else { - out.RawByte('[') - for v2, v3 := range in { - if v2 > 0 { - out.RawByte(',') - } - if v3 == nil { - out.RawString("null") - } else { - (*v3).MarshalEasyJSON(out) - } - } - out.RawByte(']') - } -} - -// MarshalJSON supports json.Marshaler interface -func (v LobbyEntries) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjson102f8a2fEncodeGithubComScribbleRsScribbleRsInternalApi1(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v LobbyEntries) MarshalEasyJSON(w *jwriter.Writer) { - easyjson102f8a2fEncodeGithubComScribbleRsScribbleRsInternalApi1(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *LobbyEntries) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjson102f8a2fDecodeGithubComScribbleRsScribbleRsInternalApi1(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *LobbyEntries) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson102f8a2fDecodeGithubComScribbleRsScribbleRsInternalApi1(l, v) -} -func easyjson102f8a2fDecodeGithubComScribbleRsScribbleRsInternalApi2(in *jlexer.Lexer, out *LobbyData) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - out.GameConstants = new(GameConstants) - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - if in.IsNull() { - in.Skip() - in.WantComma() - continue - } - switch key { - case "drawingBoardBaseWidth": - out.DrawingBoardBaseWidth = uint16(in.Uint16()) - case "drawingBoardBaseHeight": - out.DrawingBoardBaseHeight = uint16(in.Uint16()) - case "minBrushSize": - out.MinBrushSize = uint8(in.Uint8()) - case "maxBrushSize": - out.MaxBrushSize = uint8(in.Uint8()) - case "canvasColor": - out.CanvasColor = uint8(in.Uint8()) - case "suggestedBrushSizes": - if in.IsNull() { - in.Skip() - } else { - copy(out.SuggestedBrushSizes[:], in.Bytes()) - } - case "public": - out.Public = bool(in.Bool()) - case "maxPlayers": - out.MaxPlayers = int(in.Int()) - case "customWordsPerTurn": - out.CustomWordsPerTurn = int(in.Int()) - case "clientsPerIpLimit": - out.ClientsPerIPLimit = int(in.Int()) - case "rounds": - out.Rounds = int(in.Int()) - case "drawingTime": - out.DrawingTime = int(in.Int()) - case "minDrawingTime": - out.MinDrawingTime = int(in.Int()) - case "maxDrawingTime": - out.MaxDrawingTime = int(in.Int()) - case "minRounds": - out.MinRounds = int(in.Int()) - case "maxRounds": - out.MaxRounds = int(in.Int()) - case "minMaxPlayers": - out.MinMaxPlayers = int(in.Int()) - case "maxMaxPlayers": - out.MaxMaxPlayers = int(in.Int()) - case "minClientsPerIpLimit": - out.MinClientsPerIPLimit = int(in.Int()) - case "maxClientsPerIpLimit": - out.MaxClientsPerIPLimit = int(in.Int()) - case "minCustomWordsPerTurn": - out.MinCustomWordsPerTurn = int(in.Int()) - case "maxCustomWordsPerTurn": - out.MaxCustomWordsPerTurn = int(in.Int()) - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjson102f8a2fEncodeGithubComScribbleRsScribbleRsInternalApi2(out *jwriter.Writer, in LobbyData) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"drawingBoardBaseWidth\":" - out.RawString(prefix[1:]) - out.Uint16(uint16(in.DrawingBoardBaseWidth)) - } - { - const prefix string = ",\"drawingBoardBaseHeight\":" - out.RawString(prefix) - out.Uint16(uint16(in.DrawingBoardBaseHeight)) - } - { - const prefix string = ",\"minBrushSize\":" - out.RawString(prefix) - out.Uint8(uint8(in.MinBrushSize)) - } - { - const prefix string = ",\"maxBrushSize\":" - out.RawString(prefix) - out.Uint8(uint8(in.MaxBrushSize)) - } - { - const prefix string = ",\"canvasColor\":" - out.RawString(prefix) - out.Uint8(uint8(in.CanvasColor)) - } - { - const prefix string = ",\"suggestedBrushSizes\":" - out.RawString(prefix) - out.Base64Bytes(in.SuggestedBrushSizes[:]) - } - { - const prefix string = ",\"public\":" - out.RawString(prefix) - out.Bool(bool(in.Public)) - } - { - const prefix string = ",\"maxPlayers\":" - out.RawString(prefix) - out.Int(int(in.MaxPlayers)) - } - { - const prefix string = ",\"customWordsPerTurn\":" - out.RawString(prefix) - out.Int(int(in.CustomWordsPerTurn)) - } - { - const prefix string = ",\"clientsPerIpLimit\":" - out.RawString(prefix) - out.Int(int(in.ClientsPerIPLimit)) - } - { - const prefix string = ",\"rounds\":" - out.RawString(prefix) - out.Int(int(in.Rounds)) - } - { - const prefix string = ",\"drawingTime\":" - out.RawString(prefix) - out.Int(int(in.DrawingTime)) - } - { - const prefix string = ",\"minDrawingTime\":" - out.RawString(prefix) - out.Int(int(in.MinDrawingTime)) - } - { - const prefix string = ",\"maxDrawingTime\":" - out.RawString(prefix) - out.Int(int(in.MaxDrawingTime)) - } - { - const prefix string = ",\"minRounds\":" - out.RawString(prefix) - out.Int(int(in.MinRounds)) - } - { - const prefix string = ",\"maxRounds\":" - out.RawString(prefix) - out.Int(int(in.MaxRounds)) - } - { - const prefix string = ",\"minMaxPlayers\":" - out.RawString(prefix) - out.Int(int(in.MinMaxPlayers)) - } - { - const prefix string = ",\"maxMaxPlayers\":" - out.RawString(prefix) - out.Int(int(in.MaxMaxPlayers)) - } - { - const prefix string = ",\"minClientsPerIpLimit\":" - out.RawString(prefix) - out.Int(int(in.MinClientsPerIPLimit)) - } - { - const prefix string = ",\"maxClientsPerIpLimit\":" - out.RawString(prefix) - out.Int(int(in.MaxClientsPerIPLimit)) - } - { - const prefix string = ",\"minCustomWordsPerTurn\":" - out.RawString(prefix) - out.Int(int(in.MinCustomWordsPerTurn)) - } - { - const prefix string = ",\"maxCustomWordsPerTurn\":" - out.RawString(prefix) - out.Int(int(in.MaxCustomWordsPerTurn)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v LobbyData) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjson102f8a2fEncodeGithubComScribbleRsScribbleRsInternalApi2(&w, v) - return w.Buffer.BuildBytes(), w.Error -} +func ( GameConstants ) MarshalJSON() ([]byte, error) { return nil, nil } +func (* GameConstants ) UnmarshalJSON([]byte) error { return nil } +func ( GameConstants ) MarshalEasyJSON(w *jwriter.Writer) {} +func (* GameConstants ) UnmarshalEasyJSON(l *jlexer.Lexer) {} -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v LobbyData) MarshalEasyJSON(w *jwriter.Writer) { - easyjson102f8a2fEncodeGithubComScribbleRsScribbleRsInternalApi2(w, v) -} +type EasyJSON_exporter_GameConstants *GameConstants -// UnmarshalJSON supports json.Unmarshaler interface -func (v *LobbyData) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjson102f8a2fDecodeGithubComScribbleRsScribbleRsInternalApi2(&r, v) - return r.Error() -} +func ( LobbyData ) MarshalJSON() ([]byte, error) { return nil, nil } +func (* LobbyData ) UnmarshalJSON([]byte) error { return nil } +func ( LobbyData ) MarshalEasyJSON(w *jwriter.Writer) {} +func (* LobbyData ) UnmarshalEasyJSON(l *jlexer.Lexer) {} -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *LobbyData) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson102f8a2fDecodeGithubComScribbleRsScribbleRsInternalApi2(l, v) -} -func easyjson102f8a2fDecodeGithubComScribbleRsScribbleRsInternalApi3(in *jlexer.Lexer, out *GameConstants) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - if in.IsNull() { - in.Skip() - in.WantComma() - continue - } - switch key { - case "drawingBoardBaseWidth": - out.DrawingBoardBaseWidth = uint16(in.Uint16()) - case "drawingBoardBaseHeight": - out.DrawingBoardBaseHeight = uint16(in.Uint16()) - case "minBrushSize": - out.MinBrushSize = uint8(in.Uint8()) - case "maxBrushSize": - out.MaxBrushSize = uint8(in.Uint8()) - case "canvasColor": - out.CanvasColor = uint8(in.Uint8()) - case "suggestedBrushSizes": - if in.IsNull() { - in.Skip() - } else { - copy(out.SuggestedBrushSizes[:], in.Bytes()) - } - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjson102f8a2fEncodeGithubComScribbleRsScribbleRsInternalApi3(out *jwriter.Writer, in GameConstants) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"drawingBoardBaseWidth\":" - out.RawString(prefix[1:]) - out.Uint16(uint16(in.DrawingBoardBaseWidth)) - } - { - const prefix string = ",\"drawingBoardBaseHeight\":" - out.RawString(prefix) - out.Uint16(uint16(in.DrawingBoardBaseHeight)) - } - { - const prefix string = ",\"minBrushSize\":" - out.RawString(prefix) - out.Uint8(uint8(in.MinBrushSize)) - } - { - const prefix string = ",\"maxBrushSize\":" - out.RawString(prefix) - out.Uint8(uint8(in.MaxBrushSize)) - } - { - const prefix string = ",\"canvasColor\":" - out.RawString(prefix) - out.Uint8(uint8(in.CanvasColor)) - } - { - const prefix string = ",\"suggestedBrushSizes\":" - out.RawString(prefix) - out.Base64Bytes(in.SuggestedBrushSizes[:]) - } - out.RawByte('}') -} +type EasyJSON_exporter_LobbyData *LobbyData -// MarshalJSON supports json.Marshaler interface -func (v GameConstants) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjson102f8a2fEncodeGithubComScribbleRsScribbleRsInternalApi3(&w, v) - return w.Buffer.BuildBytes(), w.Error -} +func ( LobbyEntries ) MarshalJSON() ([]byte, error) { return nil, nil } +func (* LobbyEntries ) UnmarshalJSON([]byte) error { return nil } +func ( LobbyEntries ) MarshalEasyJSON(w *jwriter.Writer) {} +func (* LobbyEntries ) UnmarshalEasyJSON(l *jlexer.Lexer) {} -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v GameConstants) MarshalEasyJSON(w *jwriter.Writer) { - easyjson102f8a2fEncodeGithubComScribbleRsScribbleRsInternalApi3(w, v) -} +type EasyJSON_exporter_LobbyEntries *LobbyEntries -// UnmarshalJSON supports json.Unmarshaler interface -func (v *GameConstants) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjson102f8a2fDecodeGithubComScribbleRsScribbleRsInternalApi3(&r, v) - return r.Error() -} +func ( LobbyEntry ) MarshalJSON() ([]byte, error) { return nil, nil } +func (* LobbyEntry ) UnmarshalJSON([]byte) error { return nil } +func ( LobbyEntry ) MarshalEasyJSON(w *jwriter.Writer) {} +func (* LobbyEntry ) UnmarshalEasyJSON(l *jlexer.Lexer) {} -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *GameConstants) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson102f8a2fDecodeGithubComScribbleRsScribbleRsInternalApi3(l, v) -} +type EasyJSON_exporter_LobbyEntry *LobbyEntry diff --git a/internal/api/ws.go b/internal/api/ws.go index 3f8fff53..64c153ce 100644 --- a/internal/api/ws.go +++ b/internal/api/ws.go @@ -51,13 +51,19 @@ func (handler *V1Handler) websocketUpgrade(writer http.ResponseWriter, request * lobby := state.GetLobby(lobbyId) if lobby == nil { - http.Error(writer, ErrLobbyNotExistent.Error(), http.StatusNotFound) + socket, err := upgrader.Upgrade(writer, request) + if err != nil { + http.Error(writer, err.Error(), http.StatusInternalServerError) + return + } + socket.WriteClose(1000, []byte("lobby_gone")) return } lobby.Synchronized(func() { player := lobby.GetPlayer(userSession) if player == nil { + log.Println("no sesss") http.Error(writer, "you don't have access to this lobby;usersession unknown", http.StatusUnauthorized) return } diff --git a/internal/frontend/lobby.js b/internal/frontend/lobby.js index 2a240f2e..f1d509d1 100644 --- a/internal/frontend/lobby.js +++ b/internal/frontend/lobby.js @@ -10,22 +10,34 @@ let hasSocketEverConnected = false; let socket; function connectToWebsocket() { if (socketIsConnecting === true) { + console.log("aborting connection attempt."); return; } socketIsConnecting = true; - socket = new WebSocket(`${rootPath}/v1/lobby/ws`); + try { + socket = new WebSocket(`${rootPath}/v1/lobby/ws`); + } catch (exception) { + console.log("Connection error:" + exception) + socketIsConnecting = false; + connectToWebsocket(); + return; + } socket.onerror = error => { //Is not connected and we haven't yet said that we are done trying to //connect, this means that we could never even establish a connection. - if (socket.readyState != 1 && !hasSocketEverConnected) { + if (socket.readyState != 1) { socketIsConnecting = false; - showTextDialog("connection-error-dialog", - '{{.Translation.Get "error-connecting"}}', - `{{.Translation.Get "error-connecting-text"}}`); - console.log("Error establishing connection: ", error); + if (!hasSocketEverConnected) { + showTextDialog("connection-error-dialog", + '{{.Translation.Get "error-connecting"}}', + `{{.Translation.Get "error-connecting-text"}}`); + console.log("Error establishing connection: ", error); + } else { + connectToWebsocket(); + } } else { console.log("Socket error: ", error) } @@ -38,13 +50,32 @@ function connectToWebsocket() { socketIsConnecting = false; socket.onclose = event => { - //We want to avoid handling the error multiple times and showing the incorrect dialogs. + //We w to avoid handling the error multiple times and showing the incorrect dialogs. socket.onerror = null; console.log("Socket Closed Connection: ", event); - console.log("Attempting to reestablish socket connection."); - showReconnectDialogIfNotShown(); - connectToWebsocket(); + + if (restoreData && event.reason === "lobby_gone") { + console.log("Resurrecting lobby ...",); + fetch('/v1/lobby/resurrect', { + method: 'POST', + body: restoreData, + }).then(() => { + console.log("Attempting to reestablish socket connection after resurrection ..."); + socketIsConnecting = false; + connectToWebsocket(); + }); + + return + } + + if (event.reason !== "lobby_gone" && event.reason !== "server_restart") { + console.log("Attempting to reestablish socket connection."); + showReconnectDialogIfNotShown(); + } + if (event.reason === "server_restart") { + connectToWebsocket(); + } }; registerMessageHandler(socket); @@ -833,6 +864,7 @@ let rounds = 0; let roundEndTime = 0; let gameState = "unstarted"; let drawingTimeSetting = "∞"; +let restoreData; function registerMessageHandler(targetSocket) { targetSocket.onmessage = event => { @@ -985,10 +1017,15 @@ function registerMessageHandler(targetSocket) { + '{{.Translation.Get "custom-words-per-turn-setting"}}: ' + parsed.data.customWordsPerTurn + "%\n" + '{{.Translation.Get "players-per-ip-limit-setting"}}: ' + parsed.data.clientsPerIpLimit); } else if (parsed.type === "shutdown") { - socket.onclose = null; - socket.close(); - showDialog("shutdown-info", "Server shutting down", - document.createTextNode("Sorry, but the server is about to shut down. Please come back at a later time.")); + if (parsed.data) { + restoreData = parsed.data; + // FIXMe Text anpassen! + showDialog("shutdown-info", "Server shutting down", + document.createTextNode("Sorry, but the server is about to shut down. Please come back at a later time.")); + } else { + showDialog("shutdown-info", "Server shutting down", + document.createTextNode("Sorry, but the server is about to shut down. Please come back at a later time.")); + } } } }; @@ -1033,6 +1070,7 @@ function setRoundTimeLeft(timeLeftMs) { } function handleReadyEvent(ready) { + restoreData = null; ownerID = ready.ownerId; ownID = ready.playerId; diff --git a/internal/game/data.go b/internal/game/data.go index fae809b0..e5564e3c 100644 --- a/internal/game/data.go +++ b/internal/game/data.go @@ -1,3 +1,5 @@ +//go:generate easyjson ${GOFILE} + package game import ( @@ -18,11 +20,13 @@ const slotReservationTime = time.Minute * 1 // Lobby represents a game session. It must not be sent via the API, as it // exposes gameplay relevant information. +// +//easyjson:json type Lobby struct { // ID uniquely identified the Lobby. LobbyID string - EditableLobbySettings + LobbySettings // DrawingTimeNew is the new value of the drawing time. If a round is // already ongoing, we can't simply change the drawing time, as it would @@ -30,10 +34,10 @@ type Lobby struct { DrawingTimeNew int CustomWords []string - words []string + Words []string - // players references all participants of the Lobby. - players []*Player + // Players references all participants of the Lobby. + Players []*Player // Whether the game has started, is ongoing or already over. State State @@ -42,19 +46,19 @@ type Lobby struct { OwnerID uuid.UUID // ScoreCalculation decides how scores for both guessers and drawers are // determined. - ScoreCalculation ScoreCalculation + ScoreCalculation ScoreCalculation `json:"-"` // CurrentWord represents the word that was last selected. If no word has // been selected yet or the round is already over, this should be empty. CurrentWord string - // wordHints for the current word. - wordHints []*WordHint - // wordHintsShown are the same as wordHints with characters visible. - wordHintsShown []*WordHint - // hintsLeft is the amount of hints still available for revelation. - hintsLeft int - // hintCount is the amount of hints that were initially available + // WordHints for the current word. + WordHints []*WordHint + // WordHintsShown are the same as wordHints with characters visible. + WordHintsShown []*WordHint + // HintsLeft is the amount of hints still available for revelation. + HintsLeft int + // HintCount is the amount of hints that were initially available // for revelation. - hintCount int + HintCount int // Round is the round that the Lobby is currently in. This is a number // between 0 and Rounds. 0 indicates that it hasn't started yet. Round int @@ -62,17 +66,16 @@ type Lobby struct { preSelectedWord int // wordChoice represents the current choice of words present to the drawer. wordChoice []string - Wordpack string - // roundEndTime represents the time at which the current round will end. + // RoundEndTime represents the time at which the current round will end. // This is a UTC unix-timestamp in milliseconds. - roundEndTime int64 + RoundEndTime int64 timeLeftTicker *time.Ticker - // currentDrawing represents the state of the current canvas. The elements + // CurrentDrawing represents the state of the current canvas. The elements // consist of LineEvent and FillEvent. Please do not modify the contents // of this array an only move AppendLine and AppendFill on the respective // lobby object. - currentDrawing []any + CurrentDrawing []any // These variables are used to define the ranges of connected drawing events. // For example a line that has been drawn or a fill that has been executed. @@ -82,18 +85,44 @@ type Lobby struct { // connected, but that could technically undo a whole drawing. lastDrawEvent time.Time - connectedDrawEventsIndexStack []int + ConnectedDrawEventsIndexStack []int lowercaser cases.Caser // LastPlayerDisconnectTime is used to know since when a lobby is empty, in case - // it is empty. + // it is empty. If the time is nil, it's treated the same as when the + // timelimit has been reached. LastPlayerDisconnectTime *time.Time mutex sync.Mutex - WriteObject func(*Player, easyjson.Marshaler) error - WritePreparedMessage func(*Player, *gws.Broadcaster) error + WriteObject func(*Player, easyjson.Marshaler) error `json:"-"` + WritePreparedMessage func(*Player, *gws.Broadcaster) error `json:"-"` +} + +//easyjson:json +type LobbyRestoreData struct { + ShutdownTime time.Time + Lobby *Lobby +} + +func (lobby *Lobby) ResurrectUnsynchronized(restoreData *LobbyRestoreData) { + lobby.lowercaser = WordlistData[lobby.Wordpack].Lowercaser() + + // Since we don't know how long the restart took, we extend all timers.\ + // We add an additional second for good measure. + now := time.Now() + timeDiff := now.Sub(restoreData.ShutdownTime).Milliseconds() + 1000 + + lobby.RoundEndTime = lobby.RoundEndTime + int64(timeDiff) + + if lobby.CurrentWord != "" { + lobby.timeLeftTicker = time.NewTicker(1 * time.Second) + go startTurnTimeTicker(lobby, lobby.timeLeftTicker) + } else if len(lobby.wordChoice) > 0 { + lobby.wordChoiceEndTime = lobby.wordChoiceEndTime + int64(timeDiff) + go lobby.startWordChoiceTimer(lobby.wordChoiceEndTime - now.UTC().UnixMilli()) + } } // MaxPlayerNameLength defines how long a string can be at max when used @@ -125,7 +154,7 @@ func (player *Player) SetWebsocket(socket *gws.Conn) { // GetUserSession returns the players current user session. func (player *Player) GetUserSession() uuid.UUID { - return player.userSession + return player.UserSession } type PlayerState string @@ -140,8 +169,8 @@ const ( // GetPlayer searches for a player, identifying them by usersession. func (lobby *Lobby) GetPlayer(userSession uuid.UUID) *Player { - for _, player := range lobby.players { - if player.userSession == userSession { + for _, player := range lobby.Players { + if player.UserSession == userSession { return player } } @@ -154,22 +183,22 @@ func (lobby *Lobby) GetOwner() *Player { } func (lobby *Lobby) ClearDrawing() { - lobby.currentDrawing = make([]any, 0) - lobby.connectedDrawEventsIndexStack = nil + lobby.CurrentDrawing = make([]any, 0) + lobby.ConnectedDrawEventsIndexStack = nil } // AppendLine adds a line direction to the current drawing. This exists in order // to prevent adding arbitrary elements to the drawing, as the backing array is // an empty interface type. func (lobby *Lobby) AppendLine(line *LineEvent) { - lobby.currentDrawing = append(lobby.currentDrawing, line) + lobby.CurrentDrawing = append(lobby.CurrentDrawing, line) } // AppendFill adds a fill direction to the current drawing. This exists in order // to prevent adding arbitrary elements to the drawing, as the backing array is // an empty interface type. func (lobby *Lobby) AppendFill(fill *FillEvent) { - lobby.currentDrawing = append(lobby.currentDrawing, fill) + lobby.CurrentDrawing = append(lobby.CurrentDrawing, fill) } // SanitizeName removes invalid characters from the players name, resolves @@ -195,7 +224,7 @@ func SanitizeName(name string) string { // established a socket connection. func (lobby *Lobby) GetConnectedPlayerCount() int { var count int - for _, player := range lobby.players { + for _, player := range lobby.Players { if player.Connected { count++ } @@ -208,7 +237,7 @@ func (lobby *Lobby) HasConnectedPlayers() bool { lobby.mutex.Lock() defer lobby.mutex.Unlock() - for _, otherPlayer := range lobby.players { + for _, otherPlayer := range lobby.Players { if otherPlayer.Connected { return true } @@ -239,7 +268,7 @@ func (lobby *Lobby) IsPublic() bool { } func (lobby *Lobby) GetPlayers() []*Player { - return lobby.players + return lobby.Players } // GetOccupiedPlayerSlots counts the available slots which can be taken by new @@ -250,7 +279,7 @@ func (lobby *Lobby) GetPlayers() []*Player { func (lobby *Lobby) GetOccupiedPlayerSlots() int { var occupiedPlayerSlots int now := time.Now() - for _, player := range lobby.players { + for _, player := range lobby.Players { if player.Connected { occupiedPlayerSlots++ } else { @@ -273,7 +302,7 @@ func (lobby *Lobby) GetOccupiedPlayerSlots() int { // will be preserved for 5 minutes. This function should be used over // Lobby.GetOccupiedPlayerSlots, as it is potentially faster. func (lobby *Lobby) HasFreePlayerSlot() bool { - if len(lobby.players) < lobby.MaxPlayers { + if len(lobby.Players) < lobby.MaxPlayers { return true } diff --git a/internal/game/data_easyjson.go b/internal/game/data_easyjson.go new file mode 100644 index 00000000..3fc53788 --- /dev/null +++ b/internal/game/data_easyjson.go @@ -0,0 +1,23 @@ +// TEMPORARY AUTOGENERATED FILE: easyjson stub code to make the package +// compilable during generation. + +package game + +import ( + "github.com/mailru/easyjson/jwriter" + "github.com/mailru/easyjson/jlexer" +) + +func ( Lobby ) MarshalJSON() ([]byte, error) { return nil, nil } +func (* Lobby ) UnmarshalJSON([]byte) error { return nil } +func ( Lobby ) MarshalEasyJSON(w *jwriter.Writer) {} +func (* Lobby ) UnmarshalEasyJSON(l *jlexer.Lexer) {} + +type EasyJSON_exporter_Lobby *Lobby + +func ( LobbyRestoreData ) MarshalJSON() ([]byte, error) { return nil, nil } +func (* LobbyRestoreData ) UnmarshalJSON([]byte) error { return nil } +func ( LobbyRestoreData ) MarshalEasyJSON(w *jwriter.Writer) {} +func (* LobbyRestoreData ) UnmarshalEasyJSON(l *jlexer.Lexer) {} + +type EasyJSON_exporter_LobbyRestoreData *LobbyRestoreData diff --git a/internal/game/data_test.go b/internal/game/data_test.go index 10ef6936..f45a23c3 100644 --- a/internal/game/data_test.go +++ b/internal/game/data_test.go @@ -14,12 +14,12 @@ func TestOccupiedPlayerCount(t *testing.T) { } // While disconnect, there's no disconnect time, which we count as occupied. - lobby.players = append(lobby.players, &Player{}) + lobby.Players = append(lobby.Players, &Player{}) if lobby.GetOccupiedPlayerSlots() != 1 { t.Errorf("Occupied player count expected to be 1, but was %d", lobby.GetOccupiedPlayerSlots()) } - lobby.players = append(lobby.players, &Player{ + lobby.Players = append(lobby.Players, &Player{ Connected: true, }) if lobby.GetOccupiedPlayerSlots() != 2 { @@ -29,7 +29,7 @@ func TestOccupiedPlayerCount(t *testing.T) { disconnectedPlayer := &Player{ Connected: false, } - lobby.players = append(lobby.players, disconnectedPlayer) + lobby.Players = append(lobby.Players, disconnectedPlayer) if lobby.GetOccupiedPlayerSlots() != 3 { t.Errorf("Occupied player count expected to be 3, but was %d", lobby.GetOccupiedPlayerSlots()) } diff --git a/internal/game/lobby.go b/internal/game/lobby.go index cb207d61..fecb36be 100644 --- a/internal/game/lobby.go +++ b/internal/game/lobby.go @@ -8,6 +8,7 @@ import ( "math/rand/v2" "sort" "strings" + "sync" "time" "unicode/utf8" @@ -42,6 +43,7 @@ const ( DrawingBoardBaseHeight = 900 MinBrushSize = 8 MaxBrushSize = 32 + wordChoiceDurationMs = 30000 ) // SettingBounds defines the lower and upper bounds for the user-specified @@ -88,7 +90,7 @@ func (lobby *Lobby) HandleEvent(eventType string, payload []byte, player *Player player.SpectateToggleRequested = false } - lobby.Broadcast(&Event{Type: EventTypeUpdatePlayers, Data: lobby.players}) + lobby.Broadcast(&Event{Type: EventTypeUpdatePlayers, Data: lobby.Players}) } else if eventType == EventTypeMessage { var message StringDataEvent if err := easyjson.Unmarshal(payload, &message); err != nil { @@ -113,7 +115,7 @@ func (lobby *Lobby) HandleEvent(eventType string, payload []byte, player *Player now := time.Now() if now.Sub(lobby.lastDrawEvent) > 150*time.Millisecond || lobby.wasLastDrawEventFill() { - lobby.connectedDrawEventsIndexStack = append(lobby.connectedDrawEventsIndexStack, len(lobby.currentDrawing)) + lobby.ConnectedDrawEventsIndexStack = append(lobby.ConnectedDrawEventsIndexStack, len(lobby.CurrentDrawing)) } lobby.lastDrawEvent = now @@ -129,7 +131,7 @@ func (lobby *Lobby) HandleEvent(eventType string, payload []byte, player *Player return fmt.Errorf("error decoding data: %w", err) } - lobby.connectedDrawEventsIndexStack = append(lobby.connectedDrawEventsIndexStack, len(lobby.currentDrawing)) + lobby.ConnectedDrawEventsIndexStack = append(lobby.ConnectedDrawEventsIndexStack, len(lobby.CurrentDrawing)) lobby.lastDrawEvent = time.Now() lobby.AppendFill(&fill) @@ -138,19 +140,19 @@ func (lobby *Lobby) HandleEvent(eventType string, payload []byte, player *Player lobby.broadcastConditional(&fill, ExcludePlayer(player)) } } else if eventType == EventTypeClearDrawingBoard { - if lobby.canDraw(player) && len(lobby.currentDrawing) > 0 { + if lobby.canDraw(player) && len(lobby.CurrentDrawing) > 0 { lobby.ClearDrawing() lobby.broadcastConditional( EventTypeOnly{Type: EventTypeClearDrawingBoard}, ExcludePlayer(player)) } } else if eventType == EventTypeUndo { - if lobby.canDraw(player) && len(lobby.currentDrawing) > 0 && len(lobby.connectedDrawEventsIndexStack) > 0 { - undoFrom := lobby.connectedDrawEventsIndexStack[len(lobby.connectedDrawEventsIndexStack)-1] - lobby.connectedDrawEventsIndexStack = lobby.connectedDrawEventsIndexStack[:len(lobby.connectedDrawEventsIndexStack)-1] - if undoFrom < len(lobby.currentDrawing) { - lobby.currentDrawing = lobby.currentDrawing[:undoFrom] - lobby.Broadcast(&Event{Type: EventTypeDrawing, Data: lobby.currentDrawing}) + if lobby.canDraw(player) && len(lobby.CurrentDrawing) > 0 && len(lobby.ConnectedDrawEventsIndexStack) > 0 { + undoFrom := lobby.ConnectedDrawEventsIndexStack[len(lobby.ConnectedDrawEventsIndexStack)-1] + lobby.ConnectedDrawEventsIndexStack = lobby.ConnectedDrawEventsIndexStack[:len(lobby.ConnectedDrawEventsIndexStack)-1] + if undoFrom < len(lobby.CurrentDrawing) { + lobby.CurrentDrawing = lobby.CurrentDrawing[:undoFrom] + lobby.Broadcast(&Event{Type: EventTypeDrawing, Data: lobby.CurrentDrawing}) } } } else if eventType == EventTypeChooseWord { @@ -191,8 +193,8 @@ func (lobby *Lobby) HandleEvent(eventType string, payload []byte, player *Player } else if eventType == EventTypeRequestDrawing { // Since the client shouldn't be blocking to wait for the drawing, it's // fine to emit the event if there's no drawing. - if len(lobby.currentDrawing) != 0 { - _ = lobby.WriteObject(player, Event{Type: EventTypeDrawing, Data: lobby.currentDrawing}) + if len(lobby.CurrentDrawing) != 0 { + _ = lobby.WriteObject(player, Event{Type: EventTypeDrawing, Data: lobby.CurrentDrawing}) } } @@ -210,7 +212,7 @@ func (lobby *Lobby) handleToggleReadinessEvent(player *Player) { if lobby.readyToStart() { lobby.startGame() } else { - lobby.Broadcast(&Event{Type: EventTypeUpdatePlayers, Data: lobby.players}) + lobby.Broadcast(&Event{Type: EventTypeUpdatePlayers, Data: lobby.Players}) } } } @@ -220,7 +222,7 @@ func (lobby *Lobby) readyToStart() bool { // if a lobby is created and the owner refreshes. var hasConnectedPlayers bool - for _, otherPlayer := range lobby.players { + for _, otherPlayer := range lobby.Players { if !otherPlayer.Connected { continue } @@ -282,9 +284,9 @@ func handleMessage(message string, sender *Player, lobby *Lobby) { advanceLobby(lobby) } else { // Since the word has been guessed correctly, we reveal it. - _ = lobby.WriteObject(sender, Event{Type: EventTypeUpdateWordHint, Data: lobby.wordHintsShown}) + _ = lobby.WriteObject(sender, Event{Type: EventTypeUpdateWordHint, Data: lobby.WordHintsShown}) recalculateRanks(lobby) - lobby.Broadcast(&Event{Type: EventTypeUpdatePlayers, Data: lobby.players}) + lobby.Broadcast(&Event{Type: EventTypeUpdatePlayers, Data: lobby.Players}) } } case CloseGuess: @@ -301,15 +303,15 @@ func handleMessage(message string, sender *Player, lobby *Lobby) { } func (lobby *Lobby) wasLastDrawEventFill() bool { - if len(lobby.currentDrawing) == 0 { + if len(lobby.CurrentDrawing) == 0 { return false } - _, isFillEvent := lobby.currentDrawing[len(lobby.currentDrawing)-1].(*FillEvent) + _, isFillEvent := lobby.CurrentDrawing[len(lobby.CurrentDrawing)-1].(*FillEvent) return isFillEvent } func (lobby *Lobby) isAnyoneStillGuessing() bool { - for _, otherPlayer := range lobby.players { + for _, otherPlayer := range lobby.Players { if otherPlayer.State == Guessing && otherPlayer.Connected { return true } @@ -359,7 +361,7 @@ func (lobby *Lobby) Broadcast(data easyjson.Marshaler) { func (lobby *Lobby) broadcastConditional(data easyjson.Marshaler, condition func(*Player) bool) { var message *gws.Broadcaster - for _, player := range lobby.players { + for _, player := range lobby.Players { if condition(player) { if message == nil { bytes, err := easyjson.Marshal(data) @@ -382,7 +384,7 @@ func (lobby *Lobby) startGame() { // We are reseting each players score, since players could // technically be player a second game after the last one // has already ended. - for _, otherPlayer := range lobby.players { + for _, otherPlayer := range lobby.Players { otherPlayer.Score = 0 otherPlayer.LastScore = 0 // Everyone has the same score and therefore the same rank. @@ -407,7 +409,7 @@ func handleKickVoteEvent(lobby *Lobby, player *Player, toKickID uuid.UUID) { } playerToKickIndex := -1 - for index, otherPlayer := range lobby.players { + for index, otherPlayer := range lobby.Players { if otherPlayer.ID == toKickID { playerToKickIndex = index break @@ -419,11 +421,11 @@ func handleKickVoteEvent(lobby *Lobby, player *Player, toKickID uuid.UUID) { return } - playerToKick := lobby.players[playerToKickIndex] + playerToKick := lobby.Players[playerToKickIndex] player.votedForKick[toKickID] = true var voteKickCount int - for _, otherPlayer := range lobby.players { + for _, otherPlayer := range lobby.Players { if otherPlayer.Connected && otherPlayer.votedForKick[toKickID] { voteKickCount++ } @@ -459,13 +461,13 @@ func kickPlayer(lobby *Lobby, playerToKick *Player, playerToKickIndex int) { } // Since the player is already kicked, we first clean up the kicking information related to that player - for _, otherPlayer := range lobby.players { + for _, otherPlayer := range lobby.Players { delete(otherPlayer.votedForKick, playerToKick.ID) } // If the owner is kicked, we choose the next best person as the owner. if lobby.OwnerID == playerToKick.ID { - for _, otherPlayer := range lobby.players { + for _, otherPlayer := range lobby.Players { potentialOwner := otherPlayer if potentialOwner.Connected { lobby.OwnerID = potentialOwner.ID @@ -483,25 +485,25 @@ func kickPlayer(lobby *Lobby, playerToKick *Player, playerToKickIndex int) { if playerToKick.State == Drawing { newDrawer, roundOver := determineNextDrawer(lobby) - lobby.players = append(lobby.players[:playerToKickIndex], lobby.players[playerToKickIndex+1:]...) + lobby.Players = append(lobby.Players[:playerToKickIndex], lobby.Players[playerToKickIndex+1:]...) lobby.Broadcast(&EventTypeOnly{Type: EventTypeDrawerKicked}) // Since the drawer has been kicked, that probably means that they were // probably trolling, therefore we redact everyones last earned score. - for _, otherPlayer := range lobby.players { + for _, otherPlayer := range lobby.Players { otherPlayer.Score -= otherPlayer.LastScore otherPlayer.LastScore = 0 } advanceLobbyPredefineDrawer(lobby, roundOver, newDrawer) } else { - lobby.players = append(lobby.players[:playerToKickIndex], lobby.players[playerToKickIndex+1:]...) + lobby.Players = append(lobby.Players[:playerToKickIndex], lobby.Players[playerToKickIndex+1:]...) if lobby.isAnyoneStillGuessing() { // This isn't necessary in case we need to advanced the lobby, as it has // to happen anyways and sending events twice would be wasteful. recalculateRanks(lobby) - lobby.Broadcast(&Event{Type: EventTypeUpdatePlayers, Data: lobby.players}) + lobby.Broadcast(&Event{Type: EventTypeUpdatePlayers, Data: lobby.Players}) } else { advanceLobby(lobby) } @@ -509,7 +511,7 @@ func kickPlayer(lobby *Lobby, playerToKick *Player, playerToKickIndex int) { } func (lobby *Lobby) Drawer() *Player { - for _, player := range lobby.players { + for _, player := range lobby.Players { if player.State == Drawing { return player } @@ -584,13 +586,13 @@ func advanceLobbyPredefineDrawer(lobby *Lobby, roundOver bool, newDrawer *Player // client to know which word was previously supposed to be guessed. previousWord := lobby.CurrentWord lobby.CurrentWord = "" - lobby.wordHints = nil + lobby.WordHints = nil if lobby.DrawingTimeNew != 0 { lobby.DrawingTime = lobby.DrawingTimeNew } - for _, otherPlayer := range lobby.players { + for _, otherPlayer := range lobby.Players { // If the round ends and people are still guessing, that means the // "LastScore" value for the next turn has to be "no score earned". // We also reset spectating players, to prevent any score fuckups. @@ -626,7 +628,7 @@ func advanceLobbyPredefineDrawer(lobby *Lobby, roundOver bool, newDrawer *Player if lobby.Round == lobby.Rounds || newDrawer == nil { lobby.State = GameOver - for _, player := range lobby.players { + for _, player := range lobby.Players { readyData := generateReadyData(lobby, player) // The drawing is always available on the client, as the // game-over event is only sent to already connected players. @@ -654,31 +656,34 @@ func advanceLobbyPredefineDrawer(lobby *Lobby, roundOver bool, newDrawer *Player lobby.wordChoice = GetRandomWords(3, lobby) lobby.preSelectedWord = rand.IntN(len(lobby.wordChoice)) - wordChoiceDuration := 30 lobby.Broadcast(&Event{ Type: EventTypeNextTurn, Data: &NextTurn{ Round: lobby.Round, - Players: lobby.players, - ChoiceTimeLeft: wordChoiceDuration * 1000, + Players: lobby.Players, + ChoiceTimeLeft: wordChoiceDurationMs, PreviousWord: previousWord, }, }) - lobby.wordChoiceEndTime = getTimeAsMillis() + int64(wordChoiceDuration)*1000 - go func() { - timer := time.NewTimer(time.Duration(wordChoiceDuration) * time.Second) - <-timer.C + lobby.wordChoiceEndTime = getTimeAsMillis() + wordChoiceDurationMs + go lobby.startWordChoiceTimer(wordChoiceDurationMs) - lobby.mutex.Lock() - defer lobby.mutex.Unlock() + lobby.SendYourTurnEvent(newDrawer) +} - // We let the timer run out as long as it doesn't seem to cause any - // issues and make sure it doesn't fire when it would break stuff. - lobby.selectWord(int(lobby.preSelectedWord)) - }() +func (lobby *Lobby) startWordChoiceTimer(durationMs int64) { + timer := time.NewTimer(time.Duration(wordChoiceDurationMs) * time.Second) + <-timer.C - lobby.SendYourTurnEvent(newDrawer) + lobby.mutex.Lock() + defer lobby.mutex.Unlock() + + // We let the timer run out as long as it doesn't seem to cause any + // issues and make sure it doesn't fire when it would break stuff. + if err := lobby.selectWord(int(lobby.preSelectedWord)); err != nil { + log.Println("Error automatically selecting word:", err) + } } // advanceLobby will either start the game or jump over to the next turn. @@ -700,11 +705,11 @@ func (player *Player) desiresToDraw() bool { // doesn't tell the lobby yet. The boolean signals whether the current round // is over. func determineNextDrawer(lobby *Lobby) (*Player, bool) { - for index, player := range lobby.players { + for index, player := range lobby.Players { if player.State == Drawing { // If we have someone that's drawing, take the next one - for i := index + 1; i < len(lobby.players); i++ { - nextPlayer := lobby.players[i] + for i := index + 1; i < len(lobby.Players); i++ { + nextPlayer := lobby.Players[i] if !nextPlayer.desiresToDraw() || !nextPlayer.Connected { continue } @@ -719,7 +724,7 @@ func determineNextDrawer(lobby *Lobby) (*Player, bool) { } // We prefer the first connected player and non-spectating. - for _, player := range lobby.players { + for _, player := range lobby.Players { if !player.desiresToDraw() || !player.Connected { continue } @@ -758,35 +763,35 @@ func (lobby *Lobby) tickLogic(expectedTicker *time.Ticker) bool { } currentTime := getTimeAsMillis() - if currentTime >= lobby.roundEndTime { + if currentTime >= lobby.RoundEndTime { expectedTicker.Stop() advanceLobby(lobby) // Kill outer goroutine and therefore avoid executing hint logic. return false } - if lobby.hintsLeft > 0 && lobby.wordHints != nil { - revealHintEveryXMilliseconds := int64(lobby.DrawingTime * 1000 / (lobby.hintCount + 1)) + if lobby.HintsLeft > 0 && lobby.WordHints != nil { + revealHintEveryXMilliseconds := int64(lobby.DrawingTime * 1000 / (lobby.HintCount + 1)) // If you have a drawingtime of 120 seconds and three hints, you // want to reveal a hint every 40 seconds, so that the two hints // are visible for at least a third of the time. //If the word // was chosen at 60 seconds, we'll still reveal one hint // instantly, as the time is already lower than 80. - revealHintAtXOrLower := revealHintEveryXMilliseconds * int64(lobby.hintsLeft) - timeLeft := lobby.roundEndTime - currentTime + revealHintAtXOrLower := revealHintEveryXMilliseconds * int64(lobby.HintsLeft) + timeLeft := lobby.RoundEndTime - currentTime if timeLeft <= revealHintAtXOrLower { - lobby.hintsLeft-- + lobby.HintsLeft-- // We are trying til we find a yet unshown wordhint. Since we have // thread safety and have already checked that there's a hint // left, this loop can never spin forever. for { - randomIndex := rand.Int() % len(lobby.wordHints) - if lobby.wordHints[randomIndex].Character == 0 { - lobby.wordHints[randomIndex].Character = []rune(lobby.CurrentWord)[randomIndex] + randomIndex := rand.Int() % len(lobby.WordHints) + if lobby.WordHints[randomIndex].Character == 0 { + lobby.WordHints[randomIndex].Character = []rune(lobby.CurrentWord)[randomIndex] wordHintData := &Event{ Type: EventTypeUpdateWordHint, - Data: lobby.wordHints, + Data: lobby.WordHints, } lobby.broadcastConditional(wordHintData, IsAllowedToSeeHints) break @@ -807,8 +812,8 @@ func getTimeAsMillis() int64 { func recalculateRanks(lobby *Lobby) { // We don't directly sort the players, since the order determines in which // order the players will have to draw. - sortedPlayers := make([]*Player, len(lobby.players)) - copy(sortedPlayers, lobby.players) + sortedPlayers := make([]*Player, len(lobby.Players)) + copy(sortedPlayers, lobby.Players) sort.Slice(sortedPlayers, func(a, b int) bool { return sortedPlayers[a].Score > sortedPlayers[b].Score }) @@ -853,20 +858,20 @@ func (lobby *Lobby) selectWord(index int) error { // would be too easy or too hard. runeCount := utf8.RuneCountInString(lobby.CurrentWord) if runeCount <= 2 { - lobby.hintCount = 0 + lobby.HintCount = 0 } else if runeCount <= 4 { - lobby.hintCount = 1 + lobby.HintCount = 1 } else if runeCount <= 9 { - lobby.hintCount = 2 + lobby.HintCount = 2 } else { - lobby.hintCount = 3 + lobby.HintCount = 3 } - lobby.hintsLeft = lobby.hintCount + lobby.HintsLeft = lobby.HintCount // We generate both the "empty" word hints and the hints for the // drawer. Since the length is the same, we do it in one run. - lobby.wordHints = make([]*WordHint, 0, runeCount) - lobby.wordHintsShown = make([]*WordHint, 0, runeCount) + lobby.WordHints = make([]*WordHint, 0, runeCount) + lobby.WordHintsShown = make([]*WordHint, 0, runeCount) for _, char := range lobby.CurrentWord { // These characters are part of the word, but aren't relevant for the @@ -878,40 +883,40 @@ func (lobby *Lobby) selectWord(index int) error { // The hints for the drawer are always visible, therefore they // don't require any handling of different cases. - lobby.wordHintsShown = append(lobby.wordHintsShown, &WordHint{ + lobby.WordHintsShown = append(lobby.WordHintsShown, &WordHint{ Character: char, Underline: !isAlwaysVisibleCharacter, }) if isAlwaysVisibleCharacter { - lobby.wordHints = append(lobby.wordHints, &WordHint{ + lobby.WordHints = append(lobby.WordHints, &WordHint{ Character: char, Underline: false, }) } else { - lobby.wordHints = append(lobby.wordHints, &WordHint{ + lobby.WordHints = append(lobby.WordHints, &WordHint{ Underline: true, }) } } // We use milliseconds for higher accuracy - lobby.roundEndTime = getTimeAsMillis() + int64(lobby.DrawingTime)*1000 + lobby.RoundEndTime = getTimeAsMillis() + int64(lobby.DrawingTime)*1000 lobby.timeLeftTicker = time.NewTicker(1 * time.Second) go startTurnTimeTicker(lobby, lobby.timeLeftTicker) wordHintData := &Event{ Type: EventTypeWordChosen, Data: &WordChosen{ - Hints: lobby.wordHints, - TimeLeft: int(lobby.roundEndTime - getTimeAsMillis()), + Hints: lobby.WordHints, + TimeLeft: int(lobby.RoundEndTime - getTimeAsMillis()), }, } lobby.broadcastConditional(wordHintData, IsAllowedToSeeHints) wordHintDataRevealed := &Event{ Type: EventTypeWordChosen, Data: &WordChosen{ - Hints: lobby.wordHintsShown, - TimeLeft: int(lobby.roundEndTime - getTimeAsMillis()), + Hints: lobby.WordHintsShown, + TimeLeft: int(lobby.RoundEndTime - getTimeAsMillis()), }, } lobby.broadcastConditional(wordHintDataRevealed, IsAllowedToSeeRevealedHints) @@ -934,16 +939,19 @@ func CreateLobby( } lobby := &Lobby{ LobbyID: desiredLobbyId, - EditableLobbySettings: EditableLobbySettings{ - Rounds: rounds, - DrawingTime: drawingTime, - MaxPlayers: maxPlayers, - CustomWordsPerTurn: customWordsPerTurn, - ClientsPerIPLimit: clientsPerIPLimit, - Public: publicLobby, + LobbySettings: LobbySettings{ + ScoreCalculationIdentifier: scoringCalculation.Identifier(), + EditableLobbySettings: EditableLobbySettings{ + Rounds: rounds, + DrawingTime: drawingTime, + MaxPlayers: maxPlayers, + CustomWordsPerTurn: customWordsPerTurn, + ClientsPerIPLimit: clientsPerIPLimit, + Public: publicLobby, + }, }, CustomWords: customWords, - currentDrawing: make([]any, 0), + CurrentDrawing: make([]any, 0), State: Unstarted, ScoreCalculation: scoringCalculation, } @@ -995,15 +1003,15 @@ func generateReadyData(lobby *Lobby, player *Player) *ReadyEvent { Rounds: lobby.Rounds, DrawingTimeSetting: lobby.DrawingTime, WordHints: lobby.GetAvailableWordHints(player), - Players: lobby.players, - CurrentDrawing: lobby.currentDrawing, + Players: lobby.Players, + CurrentDrawing: lobby.CurrentDrawing, } if lobby.State != Ongoing { // Clients should interpret 0 as "time over", unless the gamestate isn't "ongoing" ready.TimeLeft = 0 } else { - ready.TimeLeft = int(lobby.roundEndTime - getTimeAsMillis()) + ready.TimeLeft = int(lobby.RoundEndTime - getTimeAsMillis()) } return ready @@ -1037,7 +1045,7 @@ func (lobby *Lobby) OnPlayerConnectUnsynchronized(player *Player) { // that player and update event for players. lobby.broadcastConditional(&Event{ Type: EventTypeUpdatePlayers, - Data: lobby.players, + Data: lobby.Players, }, ExcludePlayer(player)) } @@ -1078,7 +1086,7 @@ func (lobby *Lobby) OnPlayerDisconnect(player *Player) { // points when disconnecting, they shouldn't preserve their ranking. Upon // reconnecting, the ranking will be recalculated though. recalculateRanks(lobby) - lobby.Broadcast(&Event{Type: EventTypeUpdatePlayers, Data: lobby.players}) + lobby.Broadcast(&Event{Type: EventTypeUpdatePlayers, Data: lobby.Players}) } // GetAvailableWordHints returns a WordHint array depending on the players @@ -1089,19 +1097,21 @@ func (lobby *Lobby) GetAvailableWordHints(player *Player) []*WordHint { // the hints for displaying the word, instead of having yet another GUI // element that wastes space. if player.State != Guessing { - return lobby.wordHintsShown + return lobby.WordHintsShown } - return lobby.wordHints + return lobby.WordHints } // JoinPlayer creates a new player object using the given name and adds it // to the lobbies playerlist. The new players is returned. func (lobby *Lobby) JoinPlayer(name string) *Player { player := &Player{ - Name: SanitizeName(name), - ID: uuid.Must(uuid.NewV4()), - userSession: uuid.Must(uuid.NewV4()), + PlayerPublic: &PlayerPublic{ + Name: SanitizeName(name), + ID: uuid.Must(uuid.NewV4()), + }, + UserSession: uuid.Must(uuid.NewV4()), votedForKick: make(map[uuid.UUID]bool), } @@ -1112,7 +1122,7 @@ func (lobby *Lobby) JoinPlayer(name string) *Player { } else { player.State = Standby } - lobby.players = append(lobby.players, player) + lobby.Players = append(lobby.Players, player) return player } @@ -1129,7 +1139,28 @@ func (lobby *Lobby) Shutdown() { defer lobby.mutex.Unlock() log.Println("Lobby Shutdown: Mutex acquired") - lobby.Broadcast(&EventTypeOnly{Type: EventTypeShutdown}) + state, err := easyjson.Marshal(LobbyRestoreData{ + ShutdownTime: time.Now(), + Lobby: lobby, + }) + if err != nil { + log.Println("Error marshalling lobby:", err) + lobby.Broadcast(&EventTypeOnly{Type: EventTypeShutdown}) + } else { + lobby.Broadcast(&Event{Type: EventTypeShutdown, Data: state}) + } + + // We gotta make sure we wait for all sockets to be shut down correctly. + // Otherwise the palyers receive the close before the message. + var waitGroup sync.WaitGroup + waitGroup.Add(len(lobby.Players)) + for _, player := range lobby.Players { + player.ws.Async(func() { + defer waitGroup.Done() + player.ws.WriteClose(1012, []byte("server_restart")) + }) + } + waitGroup.Wait() } // ScoreCalculation allows having different scoring systems for a lobby. @@ -1168,7 +1199,7 @@ func (s *adjustableScoringAlgorithm) Identifier() string { } func (s *adjustableScoringAlgorithm) CalculateGuesserScore(lobby *Lobby) int { - return s.CalculateGuesserScoreInternal(lobby.hintCount, lobby.hintsLeft, lobby.DrawingTime, lobby.roundEndTime) + return s.CalculateGuesserScoreInternal(lobby.HintCount, lobby.HintsLeft, lobby.DrawingTime, lobby.RoundEndTime) } func (s *adjustableScoringAlgorithm) MaxScore() int { diff --git a/internal/game/lobby_test.go b/internal/game/lobby_test.go index 954607d9..0c0ebc18 100644 --- a/internal/game/lobby_test.go +++ b/internal/game/lobby_test.go @@ -20,7 +20,7 @@ func createLobbyWithDemoPlayers(playercount int) *Lobby { OwnerID: owner.ID, } for range playercount { - lobby.players = append(lobby.players, &Player{ + lobby.Players = append(lobby.Players, &Player{ Connected: true, }) } @@ -146,40 +146,40 @@ func Test_recalculateRanks(t *testing.T) { t.Parallel() lobby := &Lobby{} - lobby.players = append(lobby.players, &Player{ + lobby.Players = append(lobby.Players, &Player{ ID: uuid.Must(uuid.NewV4()), Score: 1, Connected: true, }) - lobby.players = append(lobby.players, &Player{ + lobby.Players = append(lobby.Players, &Player{ ID: uuid.Must(uuid.NewV4()), Score: 1, Connected: true, }) recalculateRanks(lobby) - rankPlayerA := lobby.players[0].Rank - rankPlayerB := lobby.players[1].Rank + rankPlayerA := lobby.Players[0].Rank + rankPlayerB := lobby.Players[1].Rank if rankPlayerA != 1 || rankPlayerB != 1 { t.Errorf("With equal score, ranks should be equal. (A: %d; B: %d)", rankPlayerA, rankPlayerB) } - lobby.players = append(lobby.players, &Player{ + lobby.Players = append(lobby.Players, &Player{ ID: uuid.Must(uuid.NewV4()), Score: 0, Connected: true, }) recalculateRanks(lobby) - rankPlayerA = lobby.players[0].Rank - rankPlayerB = lobby.players[1].Rank + rankPlayerA = lobby.Players[0].Rank + rankPlayerB = lobby.Players[1].Rank if rankPlayerA != 1 || rankPlayerB != 1 { t.Errorf("With equal score, ranks should be equal. (A: %d; B: %d)", rankPlayerA, rankPlayerB) } - rankPlayerC := lobby.players[2].Rank + rankPlayerC := lobby.Players[2].Rank if rankPlayerC != 2 { t.Errorf("new player should be rank 2, since the previous two players had the same rank. (C: %d)", rankPlayerC) } @@ -234,11 +234,13 @@ func Test_wordSelectionEvent(t *testing.T) { t.Parallel() lobby := &Lobby{ - EditableLobbySettings: EditableLobbySettings{ - DrawingTime: 10, - Rounds: 10, + LobbySettings: LobbySettings{ + EditableLobbySettings: EditableLobbySettings{ + DrawingTime: 10, + Rounds: 10, + }, }, - words: []string{"abc", "def", "ghi"}, + Words: []string{"abc", "def", "ghi"}, } wordHintEvents := make(map[uuid.UUID][]*WordHint) var wordChoice []string @@ -331,12 +333,14 @@ func Test_kickDrawer(t *testing.T) { t.Parallel() lobby := &Lobby{ - EditableLobbySettings: EditableLobbySettings{ - DrawingTime: 10, - Rounds: 10, + LobbySettings: LobbySettings{ + EditableLobbySettings: EditableLobbySettings{ + DrawingTime: 10, + Rounds: 10, + }, }, ScoreCalculation: ChillScoring, - words: []string{"a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a"}, + Words: []string{"a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a", "a"}, } lobby.WriteObject = noOpWriteObject lobby.WritePreparedMessage = noOpWritePreparedMessage @@ -394,7 +398,7 @@ func Test_lobby_calculateDrawerScore(t *testing.T) { t.Parallel() drawer := &Player{State: Drawing} lobby := Lobby{ - players: []*Player{ + Players: []*Player{ drawer, { Connected: false, @@ -414,7 +418,7 @@ func Test_lobby_calculateDrawerScore(t *testing.T) { t.Parallel() drawer := &Player{State: Drawing} lobby := Lobby{ - players: []*Player{ + Players: []*Player{ drawer, { Connected: false, @@ -434,7 +438,7 @@ func Test_lobby_calculateDrawerScore(t *testing.T) { t.Parallel() drawer := &Player{State: Drawing} lobby := Lobby{ - players: []*Player{ + Players: []*Player{ drawer, { Connected: true, @@ -454,7 +458,7 @@ func Test_lobby_calculateDrawerScore(t *testing.T) { t.Parallel() drawer := &Player{State: Drawing} lobby := Lobby{ - players: []*Player{ + Players: []*Player{ drawer, { Connected: true, @@ -474,7 +478,7 @@ func Test_lobby_calculateDrawerScore(t *testing.T) { t.Parallel() drawer := &Player{State: Drawing} lobby := Lobby{ - players: []*Player{ + Players: []*Player{ drawer, { Connected: true, @@ -502,7 +506,7 @@ func Test_lobby_calculateDrawerScore(t *testing.T) { t.Parallel() drawer := &Player{State: Drawing} lobby := Lobby{ - players: []*Player{ + Players: []*Player{ drawer, { Connected: true, diff --git a/internal/game/shared.go b/internal/game/shared.go index e048e639..d15422b6 100644 --- a/internal/game/shared.go +++ b/internal/game/shared.go @@ -208,18 +208,7 @@ type ReadyEvent struct { AllowDrawing bool `json:"allowDrawing"` } -// Player represents a participant in a Lobby. -type Player struct { - // userSession uniquely identifies the player. - userSession uuid.UUID - ws *gws.Conn - // disconnectTime is used to kick a player in case the lobby doesn't have - // space for new players. The player with the oldest disconnect.Time will - // get kicked. - disconnectTime *time.Time - votedForKick map[uuid.UUID]bool - lastKnownAddress string - +type PlayerPublic struct { // Name is the players displayed name Name string `json:"name"` State PlayerState `json:"state"` @@ -243,6 +232,27 @@ type Player struct { ID uuid.UUID `json:"id"` } +// Player represents a participant in a Lobby. +type Player struct { + *PlayerPublic + + // UserSession uniquely identifies the player. + UserSession uuid.UUID + ws *gws.Conn + // disconnectTime is used to kick a player in case the lobby doesn't have + // space for new players. The player with the oldest disconnect.Time will + // get kicked. + disconnectTime *time.Time + votedForKick map[uuid.UUID]bool + lastKnownAddress string +} + +type LobbySettings struct { + Wordpack string + ScoreCalculationIdentifier string `json:"scoreCalculationIdentifier"` + EditableLobbySettings +} + // EditableLobbySettings represents all lobby settings that are editable by // the lobby owner after the lobby has already been opened. type EditableLobbySettings struct { diff --git a/internal/game/shared_easyjson.go b/internal/game/shared_easyjson.go index 5ea0ebcc..0e89bdc6 100644 --- a/internal/game/shared_easyjson.go +++ b/internal/game/shared_easyjson.go @@ -1144,7 +1144,122 @@ func (v *NameChangeEvent) UnmarshalJSON(data []byte) error { func (v *NameChangeEvent) UnmarshalEasyJSON(l *jlexer.Lexer) { easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame9(l, v) } -func easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame10(in *jlexer.Lexer, out *LineEvent) { +func easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame10(in *jlexer.Lexer, out *LobbySettings) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "Wordpack": + out.Wordpack = string(in.String()) + case "scoreCalculationIdentifier": + out.ScoreCalculationIdentifier = string(in.String()) + case "public": + out.Public = bool(in.Bool()) + case "maxPlayers": + out.MaxPlayers = int(in.Int()) + case "customWordsPerTurn": + out.CustomWordsPerTurn = int(in.Int()) + case "clientsPerIpLimit": + out.ClientsPerIPLimit = int(in.Int()) + case "rounds": + out.Rounds = int(in.Int()) + case "drawingTime": + out.DrawingTime = int(in.Int()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame10(out *jwriter.Writer, in LobbySettings) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"Wordpack\":" + out.RawString(prefix[1:]) + out.String(string(in.Wordpack)) + } + { + const prefix string = ",\"scoreCalculationIdentifier\":" + out.RawString(prefix) + out.String(string(in.ScoreCalculationIdentifier)) + } + { + const prefix string = ",\"public\":" + out.RawString(prefix) + out.Bool(bool(in.Public)) + } + { + const prefix string = ",\"maxPlayers\":" + out.RawString(prefix) + out.Int(int(in.MaxPlayers)) + } + { + const prefix string = ",\"customWordsPerTurn\":" + out.RawString(prefix) + out.Int(int(in.CustomWordsPerTurn)) + } + { + const prefix string = ",\"clientsPerIpLimit\":" + out.RawString(prefix) + out.Int(int(in.ClientsPerIPLimit)) + } + { + const prefix string = ",\"rounds\":" + out.RawString(prefix) + out.Int(int(in.Rounds)) + } + { + const prefix string = ",\"drawingTime\":" + out.RawString(prefix) + out.Int(int(in.DrawingTime)) + } + out.RawByte('}') +} + +// MarshalJSON supports json.Marshaler interface +func (v LobbySettings) MarshalJSON() ([]byte, error) { + w := jwriter.Writer{} + easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame10(&w, v) + return w.Buffer.BuildBytes(), w.Error +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v LobbySettings) MarshalEasyJSON(w *jwriter.Writer) { + easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame10(w, v) +} + +// UnmarshalJSON supports json.Unmarshaler interface +func (v *LobbySettings) UnmarshalJSON(data []byte) error { + r := jlexer.Lexer{Data: data} + easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame10(&r, v) + return r.Error() +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *LobbySettings) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame10(l, v) +} +func easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame11(in *jlexer.Lexer, out *LineEvent) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1177,7 +1292,7 @@ func easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame10(in *jlexe in.Consumed() } } -func easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame10(out *jwriter.Writer, in LineEvent) { +func easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame11(out *jwriter.Writer, in LineEvent) { out.RawByte('{') first := true _ = first @@ -1197,25 +1312,25 @@ func easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame10(out *jwri // MarshalJSON supports json.Marshaler interface func (v LineEvent) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame10(&w, v) + easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame11(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v LineEvent) MarshalEasyJSON(w *jwriter.Writer) { - easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame10(w, v) + easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame11(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *LineEvent) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame10(&r, v) + easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame11(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *LineEvent) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame10(l, v) + easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame11(l, v) } func easyjson9aa6bd57Decode(in *jlexer.Lexer, out *struct { X int16 `json:"x"` @@ -1308,7 +1423,7 @@ func easyjson9aa6bd57Encode(out *jwriter.Writer, in struct { } out.RawByte('}') } -func easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame11(in *jlexer.Lexer, out *KickVote) { +func easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame12(in *jlexer.Lexer, out *KickVote) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1347,7 +1462,7 @@ func easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame11(in *jlexe in.Consumed() } } -func easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame11(out *jwriter.Writer, in KickVote) { +func easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame12(out *jwriter.Writer, in KickVote) { out.RawByte('{') first := true _ = first @@ -1377,27 +1492,27 @@ func easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame11(out *jwri // MarshalJSON supports json.Marshaler interface func (v KickVote) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame11(&w, v) + easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame12(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v KickVote) MarshalEasyJSON(w *jwriter.Writer) { - easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame11(w, v) + easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame12(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *KickVote) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame11(&r, v) + easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame12(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *KickVote) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame11(l, v) + easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame12(l, v) } -func easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame12(in *jlexer.Lexer, out *IntDataEvent) { +func easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame13(in *jlexer.Lexer, out *IntDataEvent) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1428,7 +1543,7 @@ func easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame12(in *jlexe in.Consumed() } } -func easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame12(out *jwriter.Writer, in IntDataEvent) { +func easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame13(out *jwriter.Writer, in IntDataEvent) { out.RawByte('{') first := true _ = first @@ -1443,27 +1558,27 @@ func easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame12(out *jwri // MarshalJSON supports json.Marshaler interface func (v IntDataEvent) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame12(&w, v) + easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame13(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v IntDataEvent) MarshalEasyJSON(w *jwriter.Writer) { - easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame12(w, v) + easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame13(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *IntDataEvent) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame12(&r, v) + easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame13(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *IntDataEvent) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame12(l, v) + easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame13(l, v) } -func easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame13(in *jlexer.Lexer, out *GameOverEvent) { +func easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame14(in *jlexer.Lexer, out *GameOverEvent) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1608,7 +1723,7 @@ func easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame13(in *jlexe in.Consumed() } } -func easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame13(out *jwriter.Writer, in GameOverEvent) { +func easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame14(out *jwriter.Writer, in GameOverEvent) { out.RawByte('{') first := true _ = first @@ -1730,27 +1845,27 @@ func easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame13(out *jwri // MarshalJSON supports json.Marshaler interface func (v GameOverEvent) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame13(&w, v) + easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame14(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v GameOverEvent) MarshalEasyJSON(w *jwriter.Writer) { - easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame13(w, v) + easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame14(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *GameOverEvent) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame13(&r, v) + easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame14(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *GameOverEvent) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame13(l, v) + easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame14(l, v) } -func easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame14(in *jlexer.Lexer, out *FillEvent) { +func easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame15(in *jlexer.Lexer, out *FillEvent) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1795,7 +1910,7 @@ func easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame14(in *jlexe in.Consumed() } } -func easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame14(out *jwriter.Writer, in FillEvent) { +func easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame15(out *jwriter.Writer, in FillEvent) { out.RawByte('{') first := true _ = first @@ -1819,25 +1934,25 @@ func easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame14(out *jwri // MarshalJSON supports json.Marshaler interface func (v FillEvent) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame14(&w, v) + easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame15(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v FillEvent) MarshalEasyJSON(w *jwriter.Writer) { - easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame14(w, v) + easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame15(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *FillEvent) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame14(&r, v) + easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame15(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *FillEvent) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame14(l, v) + easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame15(l, v) } func easyjson9aa6bd57Decode1(in *jlexer.Lexer, out *struct { X uint16 `json:"x"` @@ -1903,7 +2018,7 @@ func easyjson9aa6bd57Encode1(out *jwriter.Writer, in struct { } out.RawByte('}') } -func easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame15(in *jlexer.Lexer, out *EventTypeOnly) { +func easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame16(in *jlexer.Lexer, out *EventTypeOnly) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1934,7 +2049,7 @@ func easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame15(in *jlexe in.Consumed() } } -func easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame15(out *jwriter.Writer, in EventTypeOnly) { +func easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame16(out *jwriter.Writer, in EventTypeOnly) { out.RawByte('{') first := true _ = first @@ -1949,27 +2064,27 @@ func easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame15(out *jwri // MarshalJSON supports json.Marshaler interface func (v EventTypeOnly) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame15(&w, v) + easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame16(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v EventTypeOnly) MarshalEasyJSON(w *jwriter.Writer) { - easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame15(w, v) + easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame16(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *EventTypeOnly) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame15(&r, v) + easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame16(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *EventTypeOnly) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame15(l, v) + easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame16(l, v) } -func easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame16(in *jlexer.Lexer, out *Event) { +func easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame17(in *jlexer.Lexer, out *Event) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -2008,7 +2123,7 @@ func easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame16(in *jlexe in.Consumed() } } -func easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame16(out *jwriter.Writer, in Event) { +func easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame17(out *jwriter.Writer, in Event) { out.RawByte('{') first := true _ = first @@ -2034,27 +2149,27 @@ func easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame16(out *jwri // MarshalJSON supports json.Marshaler interface func (v Event) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame16(&w, v) + easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame17(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v Event) MarshalEasyJSON(w *jwriter.Writer) { - easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame16(w, v) + easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame17(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *Event) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame16(&r, v) + easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame17(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *Event) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame16(l, v) + easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame17(l, v) } -func easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame17(in *jlexer.Lexer, out *EditableLobbySettings) { +func easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame18(in *jlexer.Lexer, out *EditableLobbySettings) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -2095,7 +2210,7 @@ func easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame17(in *jlexe in.Consumed() } } -func easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame17(out *jwriter.Writer, in EditableLobbySettings) { +func easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame18(out *jwriter.Writer, in EditableLobbySettings) { out.RawByte('{') first := true _ = first @@ -2135,23 +2250,23 @@ func easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame17(out *jwri // MarshalJSON supports json.Marshaler interface func (v EditableLobbySettings) MarshalJSON() ([]byte, error) { w := jwriter.Writer{} - easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame17(&w, v) + easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame18(&w, v) return w.Buffer.BuildBytes(), w.Error } // MarshalEasyJSON supports easyjson.Marshaler interface func (v EditableLobbySettings) MarshalEasyJSON(w *jwriter.Writer) { - easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame17(w, v) + easyjson9aa6bd57EncodeGithubComScribbleRsScribbleRsInternalGame18(w, v) } // UnmarshalJSON supports json.Unmarshaler interface func (v *EditableLobbySettings) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} - easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame17(&r, v) + easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame18(&r, v) return r.Error() } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *EditableLobbySettings) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame17(l, v) + easyjson9aa6bd57DecodeGithubComScribbleRsScribbleRsInternalGame18(l, v) } diff --git a/internal/game/words.go b/internal/game/words.go index bdfca026..19bd8ce0 100644 --- a/internal/game/words.go +++ b/internal/game/words.go @@ -149,9 +149,9 @@ func popCustomWord(lobby *Lobby) string { // popCustomWords is, that the wordlist gets reset and reshuffeled once every // item has been popped. func popWordpackWord(lobby *Lobby, reloadWords func(lobby *Lobby) ([]string, error)) string { - if len(lobby.words) == 0 { + if len(lobby.Words) == 0 { var err error - lobby.words, err = reloadWords(lobby) + lobby.Words, err = reloadWords(lobby) if err != nil { // Since this list should've been successfully read once before, we // can "safely" panic if this happens, assuming that there's a @@ -159,9 +159,9 @@ func popWordpackWord(lobby *Lobby, reloadWords func(lobby *Lobby) ([]string, err panic(err) } } - lastIndex := len(lobby.words) - 1 - lastWord := lobby.words[lastIndex] - lobby.words = lobby.words[:lastIndex] + lastIndex := len(lobby.Words) - 1 + lastWord := lobby.Words[lastIndex] + lobby.Words = lobby.Words[:lastIndex] return lastWord } diff --git a/internal/game/words_test.go b/internal/game/words_test.go index 65aa7c94..a03d518c 100644 --- a/internal/game/words_test.go +++ b/internal/game/words_test.go @@ -84,14 +84,16 @@ func Test_getRandomWords(t *testing.T) { lobby := &Lobby{ CurrentWord: "", - EditableLobbySettings: EditableLobbySettings{ - CustomWordsPerTurn: 0, + LobbySettings: LobbySettings{ + EditableLobbySettings: EditableLobbySettings{ + CustomWordsPerTurn: 0, + }, }, - words: []string{"a", "b", "c"}, + Words: []string{"a", "b", "c"}, } randomWords := GetRandomWords(3, lobby) - for _, lobbyWord := range lobby.words { + for _, lobbyWord := range lobby.Words { if !arrayContains(randomWords, lobbyWord) { t.Errorf("Random words %s, didn't contain lobbyWord %s", randomWords, lobbyWord) } @@ -103,16 +105,17 @@ func Test_getRandomWords(t *testing.T) { lobby := &Lobby{ CurrentWord: "", - words: []string{"a", "b", "c"}, - EditableLobbySettings: EditableLobbySettings{ - CustomWordsPerTurn: 0, + Words: []string{"a", "b", "c"}, + LobbySettings: LobbySettings{ + EditableLobbySettings: EditableLobbySettings{ + CustomWordsPerTurn: 0, + }, }, - CustomWords: []string{"d", "e", "f"}, } randomWords := GetRandomWords(3, lobby) - for _, lobbyWord := range lobby.words { + for _, lobbyWord := range lobby.Words { if !arrayContains(randomWords, lobbyWord) { t.Errorf("Random words %s, didn't contain lobbyWord %s", randomWords, lobbyWord) } @@ -124,15 +127,17 @@ func Test_getRandomWords(t *testing.T) { lobby := &Lobby{ CurrentWord: "", - words: []string{"a", "b", "c"}, - EditableLobbySettings: EditableLobbySettings{ - CustomWordsPerTurn: 3, + Words: []string{"a", "b", "c"}, + LobbySettings: LobbySettings{ + EditableLobbySettings: EditableLobbySettings{ + CustomWordsPerTurn: 3, + }, }, CustomWords: nil, } randomWords := GetRandomWords(3, lobby) - for _, lobbyWord := range lobby.words { + for _, lobbyWord := range lobby.Words { if !arrayContains(randomWords, lobbyWord) { t.Errorf("Random words %s, didn't contain lobbyWord %s", randomWords, lobbyWord) } @@ -144,9 +149,11 @@ func Test_getRandomWords(t *testing.T) { lobby := &Lobby{ CurrentWord: "", - words: []string{"a", "b", "c"}, - EditableLobbySettings: EditableLobbySettings{ - CustomWordsPerTurn: 3, + Words: []string{"a", "b", "c"}, + LobbySettings: LobbySettings{ + EditableLobbySettings: EditableLobbySettings{ + CustomWordsPerTurn: 3, + }, }, CustomWords: []string{"d", "e", "f"}, } @@ -171,9 +178,11 @@ func Test_getRandomWordsReloading(t *testing.T) { t.Parallel() lobby := &Lobby{ - words: wordList, - EditableLobbySettings: EditableLobbySettings{ - CustomWordsPerTurn: 0, + Words: wordList, + LobbySettings: LobbySettings{ + EditableLobbySettings: EditableLobbySettings{ + CustomWordsPerTurn: 0, + }, }, CustomWords: nil, } @@ -192,9 +201,11 @@ func Test_getRandomWordsReloading(t *testing.T) { t.Parallel() lobby := &Lobby{ - words: wordList, - EditableLobbySettings: EditableLobbySettings{ - CustomWordsPerTurn: 3, + Words: wordList, + LobbySettings: LobbySettings{ + EditableLobbySettings: EditableLobbySettings{ + CustomWordsPerTurn: 3, + }, }, CustomWords: nil, } @@ -213,9 +224,11 @@ func Test_getRandomWordsReloading(t *testing.T) { t.Parallel() lobby := &Lobby{ - words: wordList, - EditableLobbySettings: EditableLobbySettings{ - CustomWordsPerTurn: 3, + Words: wordList, + LobbySettings: LobbySettings{ + EditableLobbySettings: EditableLobbySettings{ + CustomWordsPerTurn: 3, + }, }, CustomWords: []string{"a"}, } diff --git a/internal/state/lobbies.go b/internal/state/lobbies.go index a5c44aa7..f2d8cbee 100644 --- a/internal/state/lobbies.go +++ b/internal/state/lobbies.go @@ -65,15 +65,36 @@ func AddLobby(lobby *game.Lobby) { globalStateMutex.Lock() defer globalStateMutex.Unlock() + addLobby(lobby) +} + +func addLobby(lobby *game.Lobby) { lobbies = append(lobbies, lobby) } +func ResurrectLobby(lobby *game.Lobby) bool { + globalStateMutex.RLock() + defer globalStateMutex.RUnlock() + + existingLobby := getLobby(lobby.LobbyID) + if existingLobby == nil { + addLobby(lobby) + return true + } + + return false +} + // GetLobby returns a Lobby that has a matching ID or no Lobby if none could // be found. func GetLobby(id string) *game.Lobby { globalStateMutex.RLock() defer globalStateMutex.RUnlock() + return getLobby(id) +} + +func getLobby(id string) *game.Lobby { for _, lobby := range lobbies { if lobby.LobbyID == id { return lobby diff --git a/internal/state/lobbies_easyjson.go b/internal/state/lobbies_easyjson.go index 5a96defe..17cc5f68 100644 --- a/internal/state/lobbies_easyjson.go +++ b/internal/state/lobbies_easyjson.go @@ -1,106 +1,16 @@ -// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. +// TEMPORARY AUTOGENERATED FILE: easyjson stub code to make the package +// compilable during generation. -package state +package state import ( - json "encoding/json" - easyjson "github.com/mailru/easyjson" - jlexer "github.com/mailru/easyjson/jlexer" - jwriter "github.com/mailru/easyjson/jwriter" + "github.com/mailru/easyjson/jwriter" + "github.com/mailru/easyjson/jlexer" ) -// suppress unused package warning -var ( - _ *json.RawMessage - _ *jlexer.Lexer - _ *jwriter.Writer - _ easyjson.Marshaler -) - -func easyjson48287f42DecodeGithubComScribbleRsScribbleRsInternalState(in *jlexer.Lexer, out *PageStats) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeFieldName(false) - in.WantColon() - if in.IsNull() { - in.Skip() - in.WantComma() - continue - } - switch key { - case "activeLobbyCount": - out.ActiveLobbyCount = int(in.Int()) - case "playersCount": - out.PlayersCount = uint64(in.Uint64()) - case "occupiedPlayerSlotCount": - out.OccupiedPlayerSlotCount = uint64(in.Uint64()) - case "connectedPlayersCount": - out.ConnectedPlayersCount = uint64(in.Uint64()) - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjson48287f42EncodeGithubComScribbleRsScribbleRsInternalState(out *jwriter.Writer, in PageStats) { - out.RawByte('{') - first := true - _ = first - { - const prefix string = ",\"activeLobbyCount\":" - out.RawString(prefix[1:]) - out.Int(int(in.ActiveLobbyCount)) - } - { - const prefix string = ",\"playersCount\":" - out.RawString(prefix) - out.Uint64(uint64(in.PlayersCount)) - } - { - const prefix string = ",\"occupiedPlayerSlotCount\":" - out.RawString(prefix) - out.Uint64(uint64(in.OccupiedPlayerSlotCount)) - } - { - const prefix string = ",\"connectedPlayersCount\":" - out.RawString(prefix) - out.Uint64(uint64(in.ConnectedPlayersCount)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v PageStats) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjson48287f42EncodeGithubComScribbleRsScribbleRsInternalState(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v PageStats) MarshalEasyJSON(w *jwriter.Writer) { - easyjson48287f42EncodeGithubComScribbleRsScribbleRsInternalState(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *PageStats) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjson48287f42DecodeGithubComScribbleRsScribbleRsInternalState(&r, v) - return r.Error() -} +func ( PageStats ) MarshalJSON() ([]byte, error) { return nil, nil } +func (* PageStats ) UnmarshalJSON([]byte) error { return nil } +func ( PageStats ) MarshalEasyJSON(w *jwriter.Writer) {} +func (* PageStats ) UnmarshalEasyJSON(l *jlexer.Lexer) {} -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *PageStats) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjson48287f42DecodeGithubComScribbleRsScribbleRsInternalState(l, v) -} +type EasyJSON_exporter_PageStats *PageStats