From 15e7f20f1a608c627591d417d487be699eb1507d Mon Sep 17 00:00:00 2001 From: Beric Bearnson Date: Sat, 28 Sep 2024 23:26:06 -0600 Subject: [PATCH] networking protocol refactor --- cmd/client/main.go | 65 +++--- cmd/server/main.go | 18 +- internal/client/client_utils.go | 207 +++++++++--------- internal/lobby/lobby.go | 355 ++++++++++++++++++------------- internal/lobby/lobby_messages.go | 232 ++++---------------- 5 files changed, 397 insertions(+), 480 deletions(-) diff --git a/cmd/client/main.go b/cmd/client/main.go index 8aff24f..6348e36 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -21,8 +21,8 @@ func main() { fmt.Println("Welcome to sshpong!") fmt.Println("Please enter your username") - egress := make(chan lobby.LobbyMessage) - ingress := make(chan lobby.LobbyMessage) + egress := make(chan []byte) + ingress := make(chan []byte) interrupter := make(chan client.InterrupterMessage, 100) exit := make(chan string) @@ -40,7 +40,7 @@ func main() { } // User input handler - go func(egress chan lobby.LobbyMessage) { + go func() { buf := make([]byte, 1024) for { n, err := os.Stdin.Read(buf) @@ -51,7 +51,7 @@ func main() { input := string(buf[:n-1]) args := strings.Fields(input) - userMessage := lobby.LobbyMessage{} + userMessage := []byte{} select { case msg := <-interrupter: @@ -67,20 +67,14 @@ func main() { } } egress <- userMessage - if userMessage.MessageType == "accept" || userMessage.MessageType == "disconect" { - slog.Debug("Closing input handler with accept or disconnect message", slog.Any("message content", userMessage.Message)) + if userMessage[0] == lobby.Accept || userMessage[0] == lobby.Disconnect { + slog.Debug("Closing input handler with accept or disconnect message") return } - if userMessage.MessageType == "start_game" { + if userMessage[0] == lobby.StartGame { slog.Debug("closing input handler with start_game message and sending exit signal") - // TODO: This is a wierd one... - sg, ok := userMessage.Message.(lobby.StartGame) - if !ok { - slog.Debug("Start game interrupt message was improperly formatted... Could be indicative of an error in the HandleinterruptInput method") - continue - } - exit <- sg.GameID + exit <- msg.Content return } @@ -98,10 +92,10 @@ func main() { } } - }(egress) + }() // Ingress Handler - go func(oc chan lobby.LobbyMessage) { + go func() { for { msg := <-ingress @@ -114,47 +108,36 @@ func main() { } } - }(ingress) + }() // Network writer - go func(userMessages chan lobby.LobbyMessage) { + go func() { for { - msg := <-userMessages - bytes, err := lobby.Marshal(msg) - if err != nil { - log.Panic("Malformed proto message", err) - } - _, err = conn.Write(bytes) + msg := <-egress + slog.Debug("writing egress message to server", "message", msg) + + _, err = conn.Write(msg) if err == io.EOF { - log.Panic("Server disconnected sorry...") - } else if err != nil { - log.Panic("Error reading from server connection...") + log.Panic("Server disconnected, sorry...") } - if msg.MessageType == "start_game" || msg.MessageType == "disconnect" { + if msg[0] == lobby.StartGame || msg[0] == lobby.Disconnect { slog.Debug("closing network writer ") return } } - }(egress) + }() // Network reader - go func(serverMessages chan lobby.LobbyMessage) { + go func() { buf := make([]byte, 1024) for { n, err := conn.Read(buf) if err == io.EOF { - fmt.Println("disconnected from lobby") - } else if err != nil { - log.Panic("Error reading from server connection...", err) + log.Panic("disconnected from lobby") } - - message, err := lobby.Unmarshal(buf[:n]) - if err != nil { - log.Panic("Error reading message from server", err) - } - serverMessages <- message + ingress <- buf[:n] } - }(ingress) + }() fmt.Println("Waiting for an exit message") isStartGame := <-exit @@ -179,7 +162,7 @@ func ConnectToLobby(username string) (net.Conn, error) { return nil, fmt.Errorf("Sorry, failed to connect to server...") } - loginMsg, err := lobby.Marshal(lobby.LobbyMessage{MessageType: "name", Message: lobby.Name{Name: username}}) + loginMsg, err := lobby.Marshal(lobby.NameData{Name: username}, lobby.Name) if err != nil { return nil, fmt.Errorf("Sorry bro but your username is wack AF...") } diff --git a/cmd/server/main.go b/cmd/server/main.go index 693f1d5..d46bfa8 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -44,7 +44,23 @@ func LobbyListen() { log.Println(err) continue } - go l.HandleLobbyConnection(conn) + + go func() { + client, msgOut := l.InitialConnectionHandler(conn) + _, err = conn.Write(msgOut) + if err != nil { + slog.Debug("error writing to new player... disconnecting") + msg, err := lobby.Marshal(lobby.DisconnectData{ + From: client.Username, + }, lobby.Disconnect) + if err != nil { + slog.Error("error marshalling disconnect message on player connect") + } + l.BroadcastToLobby(msg) + } + + go l.HandleLobbyConnection(client) + }() } } diff --git a/internal/client/client_utils.go b/internal/client/client_utils.go index 61d9361..86a7057 100644 --- a/internal/client/client_utils.go +++ b/internal/client/client_utils.go @@ -19,121 +19,116 @@ var help = fmt.Errorf("use invite to invite a player\nchat or / to var red = "\x1b[31m" var normal = "\033[0m" -func HandleUserInput(args []string, username string) (lobby.LobbyMessage, error) { +func HandleUserInput(args []string, username string) ([]byte, error) { if len(args) == 0 { - return lobby.LobbyMessage{}, help + return []byte{}, help } switch args[0] { case "invite": if args[1] != "" { - return lobby.LobbyMessage{ - MessageType: "invite", - Message: lobby.Invite{From: username, To: args[1]}}, nil + msg, err := lobby.Marshal(lobby.InviteData{From: username, To: args[1]}, lobby.Invite) + if err != nil { + slog.Debug("invite message was not properly marshalled", "error", err) + } + return msg, err } else { fmt.Println("Please provide a player to invite ") } case "chat": if args[1] != "" { - return lobby.LobbyMessage{ - MessageType: "chat", - Message: lobby.Chat{ - From: username, - Message: args[1], - }, - }, nil + msg, err := lobby.Marshal(lobby.ChatData{ + From: username, + Message: strings.Join(args[1:], " "), + }, lobby.Chat) + if err != nil { + slog.Debug("chat message was not properly marshalled", "error", err) + } + return msg, err } case "/": if args[1] != "" { - return lobby.LobbyMessage{ - MessageType: "chat", - Message: lobby.Chat{ - From: username, - Message: args[1], - }, - }, nil + msg, err := lobby.Marshal(lobby.ChatData{ + From: username, + Message: strings.Join(args[1:], " "), + }, lobby.Chat) + if err != nil { + slog.Debug("chat slash message was not properly marshalled", "error", err) + } + return msg, err } case "quit": - return lobby.LobbyMessage{}, io.EOF + return []byte{}, io.EOF case "q": - return lobby.LobbyMessage{}, io.EOF + return []byte{}, io.EOF case "help": - return lobby.LobbyMessage{}, help + return []byte{}, help case "h": - return lobby.LobbyMessage{}, help + return []byte{}, help default: if strings.Index(args[0], "/") == 0 { - return lobby.LobbyMessage{ - MessageType: "chat", - Message: lobby.Chat{ - From: username, - Message: args[1], - }, - }, nil + msg, err := lobby.Marshal(lobby.ChatData{ + From: username, + Message: strings.Join(args, " ")[1:], + }, lobby.Chat) + if err != nil { + slog.Debug("chat slash default message was not properly marshalled", "error", err) + } + return msg, err } - return lobby.LobbyMessage{}, help + return []byte{}, help } - return lobby.LobbyMessage{}, nil + return []byte{}, nil } -func HandleInterruptInput(incoming InterrupterMessage, args []string, username string) (lobby.LobbyMessage, error) { - +func HandleInterruptInput(incoming InterrupterMessage, args []string, username string) ([]byte, error) { switch incoming.InterruptType { + // Respond with yes if you accept game case "invite": if len(args) < 1 { - return lobby.LobbyMessage{ - MessageType: "decline", - Message: lobby.Decline{ - From: username, - To: incoming.Content, - }, - }, nil + return []byte{}, nil } else { if strings.ToLower(args[0]) == "y" || strings.ToLower(args[0]) == "yes" { - return lobby.LobbyMessage{MessageType: "accept", Message: lobby.Accept{ + msg, err := lobby.Marshal(lobby.AcceptData{ From: username, To: incoming.Content, - }, - }, nil + }, lobby.Accept) + if err != nil { + slog.Debug("accept message was not properly marshalled", "error", err) + } + return msg, err } } - // // Cancel waiting for invite? we aren't doing this I guess. - // case "decline": - // return nil, // Disconnect and connect to game case "accepted": - return lobby.LobbyMessage{ - MessageType: "disconnect", - Message: lobby.Disconnect{ - From: incoming.Content, - }, - }, nil + msg, err := lobby.Marshal(lobby.DisconnectData{ + From: incoming.Content, + }, lobby.Disconnect) + if err != nil { + slog.Debug("disconnect message was not properly marshalled", "error", err) + } + return msg, err case "start_game": - return lobby.LobbyMessage{ - MessageType: "start_game", - Message: lobby.StartGame{GameID: incoming.Content}, - }, nil + msg, err := lobby.Marshal(lobby.StartGameData{ + To: "", + GameID: incoming.Content, + }, lobby.Chat) + if err != nil { + slog.Debug("start game message was not properly marshalled", "error", err) + } + return msg, err } - return lobby.LobbyMessage{}, fmt.Errorf("received a interrupt message that could not be handled %v", incoming) + return []byte{}, fmt.Errorf("received a interrupt message that could not be handled %v", incoming) } -func HandleServerMessage(message lobby.LobbyMessage) (InterrupterMessage, error) { +func HandleServerMessage(msg []byte) (InterrupterMessage, error) { + header := msg[0] - msg := message.Message - switch message.MessageType { - case "name": - - nmsg, ok := msg.(lobby.Name) - if !ok { - return InterrupterMessage{}, errors.New("Not a properly formatted name message") - } - - fmt.Printf("Current Players\n%s\n", nmsg) - case "invite": - - imsg, ok := msg.(lobby.Invite) - if !ok { + switch header { + case lobby.Invite: + imsg, err := lobby.Unmarshal[lobby.InviteData](msg) + if err != nil { return InterrupterMessage{}, errors.New("Not a propertly formatted invite message") } fmt.Println(imsg.From, "is inviting you to a game\nType y to accept...") @@ -141,17 +136,17 @@ func HandleServerMessage(message lobby.LobbyMessage) (InterrupterMessage, error) InterruptType: "invite", Content: imsg.From, }, nil - case "pending_invite": - pimsg, ok := msg.(lobby.PendingInvite) - if !ok { + case lobby.PendingInvite: + pimsg, err := lobby.Unmarshal[lobby.PendingInviteData](msg) + if err != nil { return InterrupterMessage{}, errors.New("Not a properly formatted pending invite message") } fmt.Println("Invite sent to", pimsg.Recipient, "\nWaiting for response...") - case "accepted": - amsg, ok := msg.(lobby.Accepted) - if !ok { + case lobby.Accepted: + amsg, err := lobby.Unmarshal[lobby.AcceptedData](msg) + if err != nil { return InterrupterMessage{}, errors.New("Not a properly formatted accepted message") } fmt.Println(amsg.Accepter, "accepted your invite.", "Press Enter to connect to game...") @@ -159,54 +154,60 @@ func HandleServerMessage(message lobby.LobbyMessage) (InterrupterMessage, error) InterruptType: "start_game", Content: amsg.GameID, }, nil - case "start_game": - sgmsg, ok := msg.(lobby.StartGame) - if !ok { + case lobby.StartGame: + sgmsg, err := lobby.Unmarshal[lobby.StartGameData](msg) + if err != nil { return InterrupterMessage{}, errors.New("Not a properly formatted start game message") } return InterrupterMessage{ InterruptType: "start_game", Content: sgmsg.GameID, }, nil - case "chat": - cmsg, ok := msg.(lobby.Chat) - if !ok { + + case lobby.Chat: + cmsg, err := lobby.Unmarshal[lobby.ChatData](msg) + if err != nil { return InterrupterMessage{}, errors.New("Not a properly formatted chat message") } fmt.Println(cmsg.From, ":", cmsg.Message) - case "decline": - dmsg, ok := msg.(lobby.Decline) - if !ok { + + case lobby.Decline: + dmsg, err := lobby.Unmarshal[lobby.DeclineData](msg) + if err != nil { return InterrupterMessage{}, errors.New("Not a properly formatted decline message") } fmt.Println(dmsg.From, "declined your game invite") - case "disconnect": - dmsg, ok := msg.(lobby.Disconnect) - if !ok { + case lobby.Disconnect: + dmsg, err := lobby.Unmarshal[lobby.DisconnectData](msg) + if err != nil { return InterrupterMessage{}, errors.New("Not a properly formatted disconnect message") } - fmt.Println(dmsg.From, "has disconnected") - case "connect": - cmsg, ok := msg.(lobby.Connect) - if !ok { + case lobby.Connect: + cmsg, err := lobby.Unmarshal[lobby.ConnectData](msg) + if err != nil { return InterrupterMessage{}, errors.New("Not a properly formated connect message") } fmt.Println(cmsg.From, "has connected") - case "pong": - fmt.Println("Received pong") - case "error": - em, ok := msg.(lobby.Error) - if !ok { - slog.Debug("Received an indecipherable error message...", slog.Any("msg", msg)) + + case lobby.CurrentlyConnected: + ccmsg, err := lobby.Unmarshal[lobby.CurrentlyConnectedData](msg) + if err != nil { + return InterrupterMessage{}, errors.New("Not a properly formated connect message") + } + fmt.Printf("Current Players\n%s\n", ccmsg.Players) + + case lobby.Error: + em, err := lobby.Unmarshal[lobby.ErrorData](msg) + if err != nil { + slog.Debug("Received an indecipherable error message...", slog.Any("msg", msg[1:])) } fmt.Println(red, em.Message, normal) - default: - fmt.Println("Received", message.MessageType, message.Message) + } return InterrupterMessage{}, nil } diff --git a/internal/lobby/lobby.go b/internal/lobby/lobby.go index 5f6c0f7..a92d328 100644 --- a/internal/lobby/lobby.go +++ b/internal/lobby/lobby.go @@ -2,7 +2,6 @@ package lobby import ( "fmt" - "io" "log" "log/slog" "net" @@ -24,7 +23,7 @@ type Client struct { type ExternalMessage struct { From string Target string - Message LobbyMessage + Message []byte } func CreateLobby() *Lobby { @@ -52,13 +51,7 @@ func CreateLobby() *Lobby { slog.Debug("Item that was not a client found in the lobby map...", slog.Any("key", msg.From)) } go func() { - em := LobbyMessage{ - MessageType: "error", - Message: Error{ - Message: fmt.Sprintf("Sorry, player %s is not available...", msg.Target), - }, - } - b, err := Marshal(em) + b, err := Marshal(ErrorData{Message: fmt.Sprintf("Sorry player %s is not available...", msg.Target)}, Error) if err != nil { slog.Debug("Could not marshall error message for missing player", slog.Any("error", err)) } @@ -68,16 +61,12 @@ func CreateLobby() *Lobby { } c, ok := tc.(Client) if !ok { - slog.Debug("Item that was not a client found in the lobby map...", slog.Any("key", msg.From)) + lm.Delete(msg.Target) continue } go func() { - b, err := Marshal(msg.Message) - if err != nil { - slog.Debug("Could not marshal external message...", slog.Any("error", err)) - } - c.Conn.Write(b) + c.Conn.Write(msg.Message) }() } @@ -86,33 +75,33 @@ func CreateLobby() *Lobby { return &l } -func (l *Lobby) HandleLobbyConnection(conn net.Conn) { +func (l *Lobby) HandleLobbyConnection(client Client) { messageBytes := make([]byte, 4096) - ingress := make(chan LobbyMessage) - egress := make(chan LobbyMessage) + ingress := make(chan []byte) + egress := make(chan []byte) // Network Reader go func() { for { - n, err := conn.Read(messageBytes) - if err == io.EOF { - conn.Close() - return - } + n, err := client.Conn.Read(messageBytes) if err != nil { - conn.Close() + client.Conn.Close() log.Printf("Error reading message %v", err) + l.lobbyMembers.Delete(client.Username) + + // Server receives a disconnect message of the user + msg, err := Marshal(DisconnectData{ + From: client.Username, + }, Disconnect) + if err != nil { + slog.Error("error marshalling responsive disconnect of EOF error", "error", err) + } else { + ingress <- msg + } return } - - message := LobbyMessage{} - - message, err = Unmarshal(messageBytes[:n]) - if err != nil { - log.Println("Invalid message received from client", err) - } - ingress <- message + ingress <- messageBytes[:n] } }() @@ -120,21 +109,21 @@ func (l *Lobby) HandleLobbyConnection(conn net.Conn) { go func() { for { msg := <-egress - bytes, err := Marshal(msg) + _, err := client.Conn.Write(msg) if err != nil { - log.Println("Error marshalling message to send to user...", err) - } - _, err = conn.Write(bytes) - if err == io.EOF { - conn.Close() - log.Println("User has disconnected", err) + client.Conn.Close() - // TODO: write message for disconnect to everyone? - slog.Debug("Sending bad disconnect message") - ingress <- LobbyMessage{MessageType: "disconnect", Message: Disconnect{}} - } - if err != nil { - log.Println("Error writing to user...", err) + l.lobbyMembers.Delete(client.Username) + + // Server receives a disconnect message of the user + msg, err := Marshal(DisconnectData{ + From: client.Username, + }, Disconnect) + if err != nil { + slog.Error("error marshalling responsive disconnect of EOF error", "error", err) + } else { + ingress <- msg + } } } }() @@ -143,139 +132,158 @@ func (l *Lobby) HandleLobbyConnection(conn net.Conn) { go func() { for { msg := <-ingress - serverMsg, err := l.handleClientLobbyMessage(&msg, conn) + slog.Debug("Received an ingress message", "message", msg) + + resMsg, err := l.handleClientLobbyMessage(msg) if err != nil { - log.Println("Error handling client lobby message...", err) + resMsg, err = Marshal(ErrorData{ + Message: err.Error(), + }, Error) } - if serverMsg.MessageType != "" { - egress <- serverMsg + if len(resMsg) > 0 { + egress <- resMsg } } }() } -// Returns a bool of whether the player has disconnected from the lobby and an error -func (l *Lobby) handleClientLobbyMessage(message *LobbyMessage, conn net.Conn) (LobbyMessage, error) { - switch message.MessageType { - // Handle an name/login message from a player - // Store the new player in the l.lobbyMembers - // Send a connection message for each of the l.lobbyMembers to the new player - // Send a connection message to all members in the lobby - case "name": - _, ok := l.lobbyMembers.Load(message.Message) - if ok { - return LobbyMessage{MessageType: "error", Message: Error{Message: "Sorry, that name is already taken, please try a different name"}}, nil +func (l *Lobby) handleClientLobbyMessage(msg []byte) ([]byte, error) { + header := msg[0] + + switch header { + case Chat: + l.BroadcastToLobby(msg) + return []byte{}, nil + case Invite: + i, err := Unmarshal[InviteData](msg) + if err != nil { + slog.Debug("error unmarshalling invite message", "error", err) + return []byte{}, err } - nm, ok := message.Message.(Name) - if !ok { - return LobbyMessage{MessageType: "error", Message: Error{Message: "Sorry the message value and type were not matching for name"}}, nil + msg, err := Marshal(InviteData{ + From: i.From, + To: i.To, + }, Invite) + if err != nil { + slog.Error("error marshalling invite data...", "error", err) + return []byte{}, err } - l.lobbyMembers.Store(nm.Name, Client{Username: nm.Name, Conn: conn}) - - // Build current lobby list - var lobby []string - l.lobbyMembers.Range(func(lobbyUsername any, client any) bool { - usernameString, _ := lobbyUsername.(string) - lobby = append(lobby, usernameString) - return true - }) - - l.broadcastToLobby(LobbyMessage{MessageType: "connect", Message: Name{Name: nm.Name}}) - - return LobbyMessage{MessageType: "name", Message: Name{ - Name: nm.Name, - }, - }, nil - - // Handle an invite message by sending a message to the target player - // Send an invite message to the invitee: message.Content - // Send an ack message to the inviter: message.PlayerId - case "invite": - - i, ok := message.Message.(Invite) - if !ok { - return LobbyMessage{MessageType: "error", Message: Error{Message: "Sorry the message value and type were not matching for invite"}}, nil - } - // TODO: figure out this shit l.ExternalMessageChannel <- ExternalMessage{ From: i.From, Target: i.To, - Message: LobbyMessage{}, + Message: msg, } - return LobbyMessage{MessageType: "pending_invite", Message: PendingInvite{ + return Marshal(PendingInviteData{ Recipient: i.To, - }}, nil + }, PendingInvite) - // Handle a accept message from a player that was invited - // Send a game_start message back to the player: message.Content - // Send an accepted message back to the inviter: message.PlayerId - case "accept": - gameID := uuid.NewString() + // TODO: is pending invite really something that we need? + // case PendingInvite: + // pi, err := Unmarshal[PendingInviteData](msg) + // if err != nil { + // slog.Debug("error unmarshalling pending invite message", err) + // return + // } - am, ok := message.Message.(Accept) - if !ok { - return LobbyMessage{MessageType: "error", Message: Error{Message: "Sorry the message value and type were not matching for accept"}}, nil + case Accept: + a, err := Unmarshal[AcceptData](msg) + if err != nil { + slog.Debug("error unmarshalling accept message", "error", err) + return []byte{}, err } - slog.Debug("incoming accept message", slog.Any("From", am.From), slog.Any("To", am.To)) + gID := uuid.NewString() + + msg, err := Marshal(AcceptedData{ + Accepter: a.From, + GameID: gID, + }, Accepted) + l.ExternalMessageChannel <- ExternalMessage{ - Target: am.To, - Message: LobbyMessage{MessageType: "accepted", Message: Accepted{ - Accepter: am.From, - GameID: gameID, - }, - }} - - return LobbyMessage{MessageType: "start_game", Message: StartGame{To: am.From, GameID: gameID}}, nil - // Handle a chat message from a player with PlayerId - case "chat": - c, ok := message.Message.(Chat) - if !ok { - return LobbyMessage{MessageType: "error", Message: Error{Message: "Sorry the message value and type were not matching for chat"}}, nil + From: a.From, + Target: a.To, + Message: msg, } - l.broadcastToLobby(LobbyMessage{MessageType: "text", Message: Chat{ - From: c.From, - Message: c.Message, - }}) - return LobbyMessage{}, nil - // Handle a quit message from a player that was connected - // broadcast the player quit to the lobby - case "quit": - q, ok := message.Message.(Disconnect) - if !ok { - return LobbyMessage{MessageType: "error", Message: Error{Message: "Sorry the message value and type were not matching for quit"}}, nil + return Marshal(StartGameData{ + To: a.From, + GameID: gID, + }, StartGame) + + case Accepted: + a, err := Unmarshal[AcceptedData](msg) + if err != nil { + slog.Debug("error unmarshalling accpeted message", "error", err) + return []byte{}, err } - l.lobbyMembers.Delete(q.From) - l.broadcastToLobby(LobbyMessage{MessageType: "disconnect", Message: Disconnect{ - From: q.From, - }}) - return LobbyMessage{}, nil - // Ping and pong - case "ping": - return LobbyMessage{MessageType: "pong", Message: "pong"}, nil + // TODO: figure out the accepted and start game data situation... To field is a little hard to fill. + return Marshal(StartGameData{ + To: "", + GameID: a.GameID, + }, StartGame) - // Ping and pong - default: - return LobbyMessage{MessageType: "pong", Message: "pong"}, nil + // TODO: Like pending invite, I think start game is only a client message + // case StartGame: + // sg, err := Unmarshal[StartGameData](msg) + // if err != nil { + // slog.Debug("error unmarshalling start game message", err) + // return []byte{}, err + // } + // TODO: Do we even want to support decline responses? + // case Decline: + // d, err := Unmarshal[DeclineData](msg) + // if err != nil { + // slog.Debug("error unmarshalling decline message", err) + // return []byte{}, err + // } + + case Disconnect: + d, err := Unmarshal[DisconnectData](msg) + if err != nil { + slog.Debug("error unmarshalling disconnect message", "error", err) + return []byte{}, err + } + + l.lobbyMembers.Delete(d.From) + + msg, err := Marshal(DisconnectData{ + From: d.From, + }, Disconnect) + + l.BroadcastToLobby(msg) + + // TODO: how do we handle a disconnect for the client's side + return []byte{}, nil + + // TODO: This is just a client side message right...? + // case Connect: + // c, err := Unmarshal[ConnectData](msg) + // if err != nil { + // slog.Debug("error unmarshalling connect message", err) + // return + // } + + // TODO: This is just a client side message right...? + // case Error: + // e, err := Unmarshal[ErrorData](msg) + // if err != nil { + // slog.Debug("error unmarshalling error message", err) + // return []byte{}, err + // } } + return []byte{}, nil } -func (l *Lobby) broadcastToLobby(message LobbyMessage) { +func (l *Lobby) BroadcastToLobby(bytes []byte) { var disconnectedUsers []string l.lobbyMembers.Range(func(playerId, player interface{}) bool { - bytes, err := Marshal(message) - if err != nil { - log.Println("Error marshalling broadcast message", err) - } - client := player.(Client) - _, err = client.Conn.Write(bytes) + _, err := client.Conn.Write(bytes) if err != nil { log.Println("Error broadcasting to clients...", err) disconnectedUsers = append(disconnectedUsers, playerId.(string)) @@ -288,3 +296,62 @@ func (l *Lobby) broadcastToLobby(message LobbyMessage) { l.lobbyMembers.Delete(player) } } +func (l *Lobby) InitialConnectionHandler(conn net.Conn) (Client, []byte) { + msg := make([]byte, 256) + nb, err := conn.Read(msg) + if err != nil { + slog.Debug("error reading from initial connection") + } + msg = msg[:nb] + n, err := Unmarshal[NameData](msg) + if err != nil { + slog.Debug("error unmarshalling name message:", "error", err.Error(), "message", msg[1:nb]) + + msgOut, err := Marshal(ErrorData{ + Message: "incorrectly formatted username in name message", + }, Error) + if err != nil { + slog.Error("error marshalling error message for incorrectly formatted username") + } + return Client{}, msgOut + } + _, ok := l.lobbyMembers.Load(n.Name) + if ok { + msg, err := Marshal(ErrorData{ + Message: "Sorry that name is already taken, please try a different name", + }, Error) + if err != nil { + slog.Error("error marshalling error on name already taken msg") + } + return Client{}, msg + } + h, err := Marshal(ConnectData{ + From: n.Name, + }, Connect) + if err != nil { + slog.Debug("error marshalling broadcast connect message on player connect", "error", err) + return Client{Username: n.Name, Conn: conn}, h + } + l.BroadcastToLobby(h) + + // Build current lobby list + var lobby []string + l.lobbyMembers.Range(func(lobbyUsername any, client any) bool { + usernameString, _ := lobbyUsername.(string) + lobby = append(lobby, usernameString) + return true + }) + msgOut, err := Marshal(CurrentlyConnectedData{Players: lobby}, CurrentlyConnected) + if err != nil { + slog.Debug("Error marshalling currectly connected data on player connect") + } + + client := Client{ + Username: n.Name, + Conn: conn, + } + + l.lobbyMembers.Store(n.Name, client) + + return client, msgOut +} diff --git a/internal/lobby/lobby_messages.go b/internal/lobby/lobby_messages.go index 17805ad..5256660 100644 --- a/internal/lobby/lobby_messages.go +++ b/internal/lobby/lobby_messages.go @@ -1,244 +1,94 @@ package lobby -import ( - "encoding/base64" - "encoding/json" - "errors" - "log/slog" - "reflect" - "strings" +import "encoding/json" + +const ( + Name = iota + Chat + Invite + PendingInvite + Accept + Accepted + StartGame + Decline + Disconnect + Connect + CurrentlyConnected + Error ) -type LobbyMessage struct { - MessageType string `json:"message_type"` - Message any `json:"message"` -} - -type Name struct { +type NameData struct { Name string `json:"name"` } -type Chat struct { +type ChatData struct { From string `json:"from"` Message string `json:"message"` } -type Invite struct { +type InviteData struct { From string `json:"from"` To string `json:"to"` } -type PendingInvite struct { +type PendingInviteData struct { Recipient string `json:"recipient"` } -type Accept struct { +type AcceptData struct { From string `json:"from"` To string `json:"to"` } -type Accepted struct { +type AcceptedData struct { Accepter string `json:"accepter"` GameID string `json:"game_id"` } -type StartGame struct { +type StartGameData struct { To string `json:"to"` GameID string `json:"game_id"` } -type Decline struct { +type DeclineData struct { From string `json:"from"` To string `json:"to"` } -type Disconnect struct { +type DisconnectData struct { From string `json:"from"` } -type Connect struct { +type ConnectData struct { From string `json:"from"` } -type Error struct { +type CurrentlyConnectedData struct { + Players []string `json:"players"` +} + +type ErrorData struct { Message string `json:"message"` } -func Marshal(a LobbyMessage) ([]byte, error) { - slog.Debug("Marshalling message", slog.Any("message type", a.MessageType)) - bm, err := json.Marshal(a.Message) +func Unmarshal[T NameData | ChatData | InviteData | PendingInviteData | AcceptData | AcceptedData | StartGameData | DeclineData | DisconnectData | ConnectData | CurrentlyConnectedData | ErrorData](msg []byte) (T, error) { + var d T + err := json.Unmarshal(msg[1:], &d) if err != nil { - return nil, err + return d, err } - - a.Message = bm - return json.Marshal(a) + return d, nil } -// Use this to get the appropriate message type into the message field then assert the -// right struct accordingly to safely access the fields you need. -func Unmarshal(b []byte) (LobbyMessage, error) { - lm := LobbyMessage{} - err := json.Unmarshal(b, &lm) +func Marshal[T NameData | ChatData | InviteData | PendingInviteData | AcceptData | AcceptedData | StartGameData | DeclineData | DisconnectData | ConnectData | CurrentlyConnectedData | ErrorData](msg T, header int) ([]byte, error) { + mb, err := json.Marshal(msg) if err != nil { - return lm, err + return mb, err } - smsg, ok := lm.Message.(string) - if !ok { - slog.Debug("error asserting message to string") - } - slog.Debug("type of message", slog.Any("type of message", reflect.TypeOf(smsg)), slog.String("message", smsg)) + b := []byte{byte(header)} - jsonBytes, err := base64.StdEncoding.DecodeString(smsg) - lm.Message = jsonBytes + b = append(b, mb...) - switch strings.ToLower(lm.MessageType) { - case "name": - n := Name{} - bs, ok := lm.Message.([]byte) - if !ok { - return lm, err - } - - err := json.Unmarshal(bs, &n) - if err != nil { - slog.Debug("Error", slog.Any("error", err)) - return lm, err - } - lm.Message = n - return lm, nil - case "chat": - c := Chat{} - bs, ok := lm.Message.([]byte) - if !ok { - return lm, err - } - err := json.Unmarshal(bs, &c) - if err != nil { - slog.Debug("chat", slog.Any("error", err)) - return lm, err - } - lm.Message = c - return lm, nil - case "invite": - i := Invite{} - bs, ok := lm.Message.([]byte) - if !ok { - return lm, err - } - err := json.Unmarshal(bs, &i) - if err != nil { - slog.Debug("invite", slog.Any("error", err)) - return lm, err - } - lm.Message = i - return lm, nil - case "pending_invite": - pi := PendingInvite{} - bs, ok := lm.Message.([]byte) - if !ok { - return lm, err - } - err := json.Unmarshal(bs, &pi) - if err != nil { - slog.Debug("pending_invite", slog.Any("error", err)) - return lm, err - } - lm.Message = pi - case "accept": - a := Accept{} - bs, ok := lm.Message.([]byte) - if !ok { - return lm, err - } - err := json.Unmarshal(bs, &a) - if err != nil { - slog.Debug("accept", slog.Any("error", err)) - return lm, err - } - lm.Message = a - return lm, nil - case "accepted": - a := Accepted{} - bs, ok := lm.Message.([]byte) - if !ok { - return lm, err - } - err := json.Unmarshal(bs, &a) - if err != nil { - slog.Debug("accepted", slog.Any("error", err)) - return lm, err - } - lm.Message = a - return lm, nil - case "start_game": - sg := StartGame{} - bs, ok := lm.Message.([]byte) - if !ok { - return lm, err - } - err := json.Unmarshal(bs, &sg) - if err != nil { - slog.Debug("start_game", slog.Any("error", err)) - return lm, err - } - lm.Message = sg - return lm, nil - case "decline": - d := Decline{} - bs, ok := lm.Message.([]byte) - if !ok { - return lm, err - } - err := json.Unmarshal(bs, &d) - if err != nil { - slog.Debug("decline", slog.Any("error", err)) - return lm, err - } - lm.Message = d - return lm, nil - case "disconnect": - di := Disconnect{} - bs, ok := lm.Message.([]byte) - if !ok { - return lm, err - } - err := json.Unmarshal(bs, &di) - if err != nil { - slog.Debug("disconnect", slog.Any("error", err)) - return lm, err - } - lm.Message = di - return lm, nil - case "connect": - co := Connect{} - bs, ok := lm.Message.([]byte) - if !ok { - return lm, err - } - err := json.Unmarshal(bs, &co) - if err != nil { - slog.Debug("connect", slog.Any("error", err)) - return lm, err - } - lm.Message = co - return lm, nil - case "error": - e := Error{} - bs, ok := lm.Message.([]byte) - if !ok { - return lm, err - } - err := json.Unmarshal(bs, &e) - if err != nil { - slog.Debug("error", slog.Any("error", err)) - return lm, err - } - lm.Message = e - return lm, nil - default: - return lm, errors.New("unknown message type") - } - return lm, errors.New("unknown message type") + return b, nil }