From f2d488ef515a5ea45fde74cde2fa0442c1954a97 Mon Sep 17 00:00:00 2001 From: Beric Bearnson <37596980+bericyb@users.noreply.github.com> Date: Thu, 26 Sep 2024 19:49:59 -0600 Subject: [PATCH] middle of the re-work --- cmd/client/main.go | 127 ++++++++--- cmd/server/main.go | 98 ++++++++- internal/client/client_utils.go | 192 +++++++++++----- internal/client/game.go | 316 +++++++++++++++++++++++++++ internal/lobby/lobby.go | 290 ++++++++++++++++++++++++ internal/lobby/lobby_messages.go | 244 +++++++++++++++++++++ internal/netwrk/client.go | 27 --- internal/netwrk/lobby.go | 185 ---------------- internal/netwrk/lobby_messages.pb.go | 162 -------------- internal/netwrk/netwrk.go | 127 ----------- internal/pong/pong.go | 201 +++++++++++------ internal/pong/state.go | 10 +- internal/renderer/renderer.go | 119 ++-------- main.go | 1 - makefile | 2 + 15 files changed, 1347 insertions(+), 754 deletions(-) create mode 100644 internal/client/game.go create mode 100644 internal/lobby/lobby.go create mode 100644 internal/lobby/lobby_messages.go delete mode 100644 internal/netwrk/client.go delete mode 100644 internal/netwrk/lobby.go delete mode 100644 internal/netwrk/lobby_messages.pb.go delete mode 100644 internal/netwrk/netwrk.go delete mode 100644 main.go create mode 100644 makefile diff --git a/cmd/client/main.go b/cmd/client/main.go index f753264..8aff24f 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -4,41 +4,45 @@ import ( "fmt" "io" "log" + "log/slog" + "net" "os" "sshpong/internal/client" - "sshpong/internal/netwrk" + "sshpong/internal/lobby" "strings" - - "google.golang.org/protobuf/proto" ) -var exit chan bool +var username string func main() { + slog.SetLogLoggerLevel(slog.LevelDebug) + slog.Debug("Debug logs active...") + fmt.Println("Welcome to sshpong!") fmt.Println("Please enter your username") - egress := make(chan *netwrk.LobbyMessage) - ingress := make(chan *netwrk.LobbyMessage) + egress := make(chan lobby.LobbyMessage) + ingress := make(chan lobby.LobbyMessage) interrupter := make(chan client.InterrupterMessage, 100) + exit := make(chan string) buf := make([]byte, 1024) n, err := os.Stdin.Read(buf) if err != nil { log.Panic("Bro your input is no good...") } - username := string(buf[:n-1]) + username = string(buf[:n-1]) - conn, err := netwrk.ConnectToLobby(username) + fmt.Println("username is...", username) + conn, err := ConnectToLobby(username) if err != nil { log.Panic(err) } // User input handler - go func(egress chan *netwrk.LobbyMessage) { + go func(egress chan lobby.LobbyMessage) { buf := make([]byte, 1024) for { - n, err := os.Stdin.Read(buf) if err != nil { log.Panic("Bro your input wack as fuck") @@ -47,42 +51,57 @@ func main() { input := string(buf[:n-1]) args := strings.Fields(input) - userMessage := &netwrk.LobbyMessage{} + userMessage := lobby.LobbyMessage{} select { case msg := <-interrupter: - userMessage, err := client.HandleInterruptInput(msg, args) + userMessage, err := client.HandleInterruptInput(msg, args, username) if err != nil { - userMessage, err = client.HandleUserInput(args) + userMessage, err = client.HandleUserInput(args, username) if err == io.EOF { - exit <- true + exit <- "" } if err != nil { fmt.Println(err) continue } } - userMessage.PlayerId = username 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)) + return + } + if userMessage.MessageType == "start_game" { + 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 + return + } default: - userMessage, err = client.HandleUserInput(args) + userMessage, err = client.HandleUserInput(args, username) if err == io.EOF { - exit <- true + exit <- "" } if err != nil { fmt.Println(err) continue } - userMessage.PlayerId = username egress <- userMessage } + } }(egress) // Ingress Handler - go func(oc chan *netwrk.LobbyMessage) { + go func(oc chan lobby.LobbyMessage) { for { msg := <-ingress @@ -98,10 +117,10 @@ func main() { }(ingress) // Network writer - go func(userMessages chan *netwrk.LobbyMessage) { + go func(userMessages chan lobby.LobbyMessage) { for { msg := <-userMessages - bytes, err := proto.Marshal(msg) + bytes, err := lobby.Marshal(msg) if err != nil { log.Panic("Malformed proto message", err) } @@ -111,31 +130,79 @@ func main() { } else if err != nil { log.Panic("Error reading from server connection...") } + if msg.MessageType == "start_game" || msg.MessageType == "disconnect" { + slog.Debug("closing network writer ") + return + } } }(egress) // Network reader - go func(serverMessages chan *netwrk.LobbyMessage) { + go func(serverMessages chan lobby.LobbyMessage) { buf := make([]byte, 1024) for { n, err := conn.Read(buf) if err == io.EOF { - log.Panic("Server disconnected sorry...") + fmt.Println("disconnected from lobby") } else if err != nil { log.Panic("Error reading from server connection...", err) } - message := &netwrk.LobbyMessage{} - - err = proto.Unmarshal(buf[:n], message) + message, err := lobby.Unmarshal(buf[:n]) if err != nil { - log.Panic("Error reading message from server") + log.Panic("Error reading message from server", err) } - serverMessages <- message - } }(ingress) - _ = <-exit + fmt.Println("Waiting for an exit message") + isStartGame := <-exit + if isStartGame != "" { + fmt.Println("Connecting to game", isStartGame) + gameConn, err := ConnectToGame(username, isStartGame) + + if err != nil { + log.Panic("Failed to connect to game server...", err) + } + + client.Game(gameConn) + + } else { + return + } +} + +func ConnectToLobby(username string) (net.Conn, error) { + conn, err := net.Dial("tcp", "127.0.0.1:12345") + if err != nil { + return nil, fmt.Errorf("Sorry, failed to connect to server...") + } + + loginMsg, err := lobby.Marshal(lobby.LobbyMessage{MessageType: "name", Message: lobby.Name{Name: username}}) + if err != nil { + return nil, fmt.Errorf("Sorry bro but your username is wack AF...") + } + + _, err = conn.Write(loginMsg) + if err != nil { + return nil, fmt.Errorf("Sorry, could not communicate with server...") + } + + return conn, nil +} + +func ConnectToGame(username, gameID string) (net.Conn, error) { + conn, err := net.Dial("tcp", "127.0.0.1:42069") + if err != nil { + return nil, err + + } + + _, err = conn.Write([]byte(fmt.Sprintf("%s:%s", gameID, username))) + if err != nil { + return nil, err + } + + return conn, nil } diff --git a/cmd/server/main.go b/cmd/server/main.go index c630667..693f1d5 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -2,16 +2,106 @@ package main import ( "fmt" - "sshpong/internal/netwrk" + "log" + "log/slog" + "net" + "sshpong/internal/lobby" + "sync" ) var exit chan bool +var games sync.Map func main() { - fmt.Println("Starting sshpong server!") + slog.SetLogLoggerLevel(slog.LevelDebug) + fmt.Println("Starting sshpong lobby...") + go LobbyListen() + fmt.Println("Lobby started") - netwrk.LobbyListen() - netwrk.GamesListen() + // fmt.Println("Starting game listener...") + // go GamesListen() + // fmt.Println("Game listener started") _ = <-exit } + +// Starts listening on port 12345 for TCP connections +// Also creates client pool and game connection singletons +func LobbyListen() { + + listener, err := net.Listen("tcp", "127.0.0.1:12345") + if err != nil { + slog.Error("Error setting up listener for lobby. Exiting...") + } + + defer listener.Close() + + l := lobby.CreateLobby() + + for { + conn, err := listener.Accept() + if err != nil { + log.Println(err) + continue + } + go l.HandleLobbyConnection(conn) + } +} + +// func GamesListen() { +// +// slog.SetLogLoggerLevel(slog.LevelDebug) +// slog.Debug("Debug level logs are active") +// +// gameListener, err := net.Listen("tcp", "127.0.0.1:42069") +// if err != nil { +// log.Fatal(err) +// } +// +// for { +// defer gameListener.Close() +// conn, err := gameListener.Accept() +// if err != nil { +// log.Println(err) +// continue +// } +// +// slog.Debug("Received game connection") +// +// go func(conn net.Conn) { +// messageBytes := make([]byte, 126) +// +// n, err := conn.Read(messageBytes) +// if err != nil { +// log.Printf("Error reading game ID on connection %s", err) +// } +// +// gInfo := strings.SplitAfter(string(messageBytes[:n]), ":") +// if err != nil { +// log.Printf("Game id was not a string? %s", err) +// } +// +// slog.Debug("Game request data", slog.Any("game info", gInfo)) +// +// game, ok := games.Load(gInfo[0]) +// if !ok { +// games.Store(gInfo[0], GameClients{Client1: Client{ +// Username: gInfo[1], +// Conn: conn, +// }, Client2: Client{}}) +// } else { +// gameclients, _ := game.(GameClients) +// client2 := Client{ +// Username: gInfo[1], +// Conn: conn, +// } +// +// games.Store(gInfo[0], GameClients{ +// Client1: gameclients.Client1, +// Client2: client2}) +// +// go pong.StartGame(gameclients.Client1.Conn, client2.Conn, gameclients.Client1.Username, client2.Username) +// } +// }(conn) +// } +// } diff --git a/internal/client/client_utils.go b/internal/client/client_utils.go index bc748fd..61d9361 100644 --- a/internal/client/client_utils.go +++ b/internal/client/client_utils.go @@ -1,9 +1,11 @@ package client import ( + "errors" "fmt" "io" - "sshpong/internal/netwrk" + "log/slog" + "sshpong/internal/lobby" "strings" ) @@ -14,113 +16,197 @@ type InterrupterMessage struct { var help = fmt.Errorf("use invite to invite a player\nchat or / to send a message to the lobby\nq or quit to leave the game") -func HandleUserInput(args []string) (*netwrk.LobbyMessage, error) { +var red = "\x1b[31m" +var normal = "\033[0m" + +func HandleUserInput(args []string, username string) (lobby.LobbyMessage, error) { if len(args) == 0 { - return nil, help + return lobby.LobbyMessage{}, help } switch args[0] { case "invite": if args[1] != "" { - return &netwrk.LobbyMessage{ - Type: "invite", - Content: args[1], - }, nil + return lobby.LobbyMessage{ + MessageType: "invite", + Message: lobby.Invite{From: username, To: args[1]}}, nil } else { fmt.Println("Please provide a player to invite ") } case "chat": if args[1] != "" { - return &netwrk.LobbyMessage{ - Type: "chat", - Content: strings.Join(args[1:], " "), + return lobby.LobbyMessage{ + MessageType: "chat", + Message: lobby.Chat{ + From: username, + Message: args[1], + }, }, nil } case "/": if args[1] != "" { - return &netwrk.LobbyMessage{ - Type: "chat", - Content: strings.Join(args[1:], " "), + return lobby.LobbyMessage{ + MessageType: "chat", + Message: lobby.Chat{ + From: username, + Message: args[1], + }, }, nil } case "quit": - return nil, io.EOF + return lobby.LobbyMessage{}, io.EOF case "q": - return nil, io.EOF + return lobby.LobbyMessage{}, io.EOF case "help": - return nil, help + return lobby.LobbyMessage{}, help case "h": - return nil, help + return lobby.LobbyMessage{}, help default: if strings.Index(args[0], "/") == 0 { - return &netwrk.LobbyMessage{ - Type: "chat", - Content: strings.Join(args, " ")[1:], + return lobby.LobbyMessage{ + MessageType: "chat", + Message: lobby.Chat{ + From: username, + Message: args[1], + }, }, nil } - return nil, help + return lobby.LobbyMessage{}, help } - return nil, nil + return lobby.LobbyMessage{}, nil } -func HandleInterruptInput(incoming InterrupterMessage, args []string) (*netwrk.LobbyMessage, error) { +func HandleInterruptInput(incoming InterrupterMessage, args []string, username string) (lobby.LobbyMessage, error) { switch incoming.InterruptType { case "invite": if len(args) < 1 { - return &netwrk.LobbyMessage{ - Type: "decline", - Content: incoming.Content, + return lobby.LobbyMessage{ + MessageType: "decline", + Message: lobby.Decline{ + From: username, + To: incoming.Content, + }, }, nil } else { if strings.ToLower(args[0]) == "y" || strings.ToLower(args[0]) == "yes" { - return &netwrk.LobbyMessage{Type: "accept", Content: incoming.Content}, nil + return lobby.LobbyMessage{MessageType: "accept", Message: lobby.Accept{ + From: username, + To: incoming.Content, + }, + }, nil } } - // Cancel waiting for invite? - case "decline": - + // // Cancel waiting for invite? we aren't doing this I guess. + // case "decline": + // return nil, // Disconnect and connect to game case "accepted": - return &netwrk.LobbyMessage{ - Type: "disconnect", - Content: "", + return lobby.LobbyMessage{ + MessageType: "disconnect", + Message: lobby.Disconnect{ + From: incoming.Content, + }, + }, nil + case "start_game": + return lobby.LobbyMessage{ + MessageType: "start_game", + Message: lobby.StartGame{GameID: incoming.Content}, }, nil - default: - return nil, fmt.Errorf("received a interrupt message that could not be handled %v", incoming) } - return nil, nil + return lobby.LobbyMessage{}, fmt.Errorf("received a interrupt message that could not be handled %v", incoming) } -func HandleServerMessage(message *netwrk.LobbyMessage) (InterrupterMessage, error) { - switch message.Type { +func HandleServerMessage(message lobby.LobbyMessage) (InterrupterMessage, error) { + + msg := message.Message + switch message.MessageType { case "name": - fmt.Printf("Current Players\n%s\n", message.Content) + + 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": - fmt.Println(message.PlayerId, "is inviting you to a game\nType y to accept...") + + imsg, ok := msg.(lobby.Invite) + if !ok { + return InterrupterMessage{}, errors.New("Not a propertly formatted invite message") + } + fmt.Println(imsg.From, "is inviting you to a game\nType y to accept...") return InterrupterMessage{ InterruptType: "invite", - Content: message.PlayerId, + Content: imsg.From, }, nil case "pending_invite": - fmt.Println("Invite sent to", message.Content, "\nWaiting for response...") + + pimsg, ok := msg.(lobby.PendingInvite) + if !ok { + return InterrupterMessage{}, errors.New("Not a properly formatted pending invite message") + } + fmt.Println("Invite sent to", pimsg.Recipient, "\nWaiting for response...") case "accepted": - fmt.Println(message.PlayerId, "accepted your invite.\n", "Starting game...") - case "game_start": - fmt.Println("Invited accepted\n", "Starting game...") - case "text": - fmt.Println(message.PlayerId, ":", message.Content) - case "decline_game": - fmt.Println(message.Content, "declined your game invite") + + amsg, ok := msg.(lobby.Accepted) + if !ok { + return InterrupterMessage{}, errors.New("Not a properly formatted accepted message") + } + fmt.Println(amsg.Accepter, "accepted your invite.", "Press Enter to connect to game...") + return InterrupterMessage{ + InterruptType: "start_game", + Content: amsg.GameID, + }, nil + case "start_game": + + sgmsg, ok := msg.(lobby.StartGame) + if !ok { + 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 { + 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 { + return InterrupterMessage{}, errors.New("Not a properly formatted decline message") + } + + fmt.Println(dmsg.From, "declined your game invite") case "disconnect": - fmt.Println(message.Content, "has disconnected") + + dmsg, ok := msg.(lobby.Disconnect) + if !ok { + return InterrupterMessage{}, errors.New("Not a properly formatted disconnect message") + } + + fmt.Println(dmsg.From, "has disconnected") case "connect": - fmt.Println(message.Content, "has connected") + + cmsg, ok := msg.(lobby.Connect) + if !ok { + return InterrupterMessage{}, errors.New("Not a properly formated connect message") + } + fmt.Println(cmsg.From, "has connected") case "pong": - fmt.Println("Received", message.Content) + fmt.Println("Received pong") + case "error": + em, ok := msg.(lobby.Error) + if !ok { + slog.Debug("Received an indecipherable error message...", slog.Any("msg", msg)) + } + fmt.Println(red, em.Message, normal) default: - fmt.Println("Received", message.Content) + fmt.Println("Received", message.MessageType, message.Message) } return InterrupterMessage{}, nil } diff --git a/internal/client/game.go b/internal/client/game.go new file mode 100644 index 0000000..51b5667 --- /dev/null +++ b/internal/client/game.go @@ -0,0 +1,316 @@ +package client + +import ( + "encoding/json" + "fmt" + "log/slog" + "net" + "os" + "sshpong/internal/ansii" + "sshpong/internal/pong" + "sshpong/internal/renderer" + "strings" +) + +var state pong.GameState +var quit chan int +var egress chan pong.StateUpdate +var isPlayer1 bool = false + +func Game(conn net.Conn) { + fmt.Println("Connected to game!") + + egress = make(chan pong.StateUpdate) + quit = make(chan int) + + // Network reader + go func() { + bytes := make([]byte, 512) + for { + n, err := conn.Read(bytes) + if err != nil { + slog.Debug("failed to read from game connection...") + quit <- 1 + } + stateUpdateHandler(bytes[:n]) + } + }() + + // Network writer + go func() { + for { + update := <-egress + bytes, err := json.Marshal(update) + if err != nil { + slog.Debug("failed to unmarhal game update message from server") + } + _, err = conn.Write(bytes) + if err != nil { + slog.Debug("failed to write to game connection...") + } + } + }() + + prev, err := ansii.MakeTermRaw() + if err != nil { + fmt.Println("Failed to make terminal raw") + return + } + defer ansii.RestoreTerm(prev) + + os.Stdout.WriteString(string(ansii.Screen.HideCursor)) + defer os.Stdout.WriteString(string(ansii.Screen.ShowCursor)) + + // Input handler + go func() { + buf := make([]byte, 3) + for { + + n, err := os.Stdin.Read(buf) + if err != nil { + fmt.Println("Error reading from stdin", err) + return + } + + handleGameInput(buf[:n]) + } + }() + + <-quit + return +} + +func stateUpdateHandler(bytes []byte) { + update := pong.StateUpdate{} + err := json.Unmarshal(bytes, &update) + if err != nil { + slog.Debug("error unmarshalling server json", slog.Any("Unmarshal error", err)) + update.FieldPath = "Message" + update.Value = []byte("An error has occured ") + } + + fields := strings.Split(update.FieldPath, ".") + + // type GameState struct { + // Message string + // Winner string + // Score map[string]int + // Player1 Player + // Player2 Player + // Ball Ball + // } + + // For now let's just send the whole field from a top level + // of the state. If things are slow we can optimize that later + val := update.Value + + switch fields[0] { + case "All": + + ns := pong.GameState{} + err = json.Unmarshal(val, &ns) + if err != nil { + slog.Debug("error unmarshalling whole state update") + return + } + + state = ns + case "Message": + state.Message = string(update.Value) + case "Winner": + state.Winner = string(update.Value) + case "Score": + + sc := map[string]int{} + err = json.Unmarshal(val, &sc) + if err != nil { + slog.Debug("error unmarshalling score update") + return + } + state.Score = sc + case "Player1": + p1 := pong.Player{} + err = json.Unmarshal(val, &p1) + if err != nil { + slog.Debug("error unmarshalling player1 update") + } + state.Player1 = p1 + case "Player2": + p2 := pong.Player{} + err = json.Unmarshal(val, &p2) + if err != nil { + slog.Debug("error unmarshalling player2 update") + } + state.Player2 = p2 + case "Ball": + + b := pong.Ball{} + err = json.Unmarshal(val, &b) + if err != nil { + slog.Debug("error unmarshalling ball update") + } + state.Ball = b + // Special update message that determines if the client is player1 or player2 + case "isPlayer1": + if update.Value[0] != 0 { + isPlayer1 = true + } + } + + renderer.Render(state) + +} + +func handleGameInput(bytes []byte) { + switch bytes[0] { + // Up + case 'w': + if isPlayer1 { + state.Player1.Pos.Y = state.Player1.Pos.Y + 1 + v, err := json.Marshal(pong.Vector{ + X: 0, Y: state.Player1.Pos.Y, + }) + if err != nil { + slog.Debug("error marshalling player movement", slog.Any("error", err)) + } + update := pong.StateUpdate{ + FieldPath: "Player1.Pos", + Value: v, + } + + egress <- update + } else { + state.Player2.Pos.Y = state.Player2.Pos.Y + 1 + v, err := json.Marshal(pong.Vector{ + X: 0, Y: state.Player2.Pos.Y, + }) + if err != nil { + slog.Debug("error marshalling Player2 movement", slog.Any("error", err)) + } + update := pong.StateUpdate{ + FieldPath: "Player2.Pos", + Value: v, + } + egress <- update + } + return + // Down + case 's': + if isPlayer1 { + state.Player1.Pos.Y = state.Player1.Pos.Y - 1 + v, err := json.Marshal(pong.Vector{ + X: 0, Y: state.Player1.Pos.Y, + }) + if err != nil { + slog.Debug("error marshalling player movement", slog.Any("error", err)) + } + update := pong.StateUpdate{ + FieldPath: "Player1.Pos", + Value: v, + } + + egress <- update + } else { + state.Player2.Pos.Y = state.Player2.Pos.Y - 1 + v, err := json.Marshal(pong.Vector{ + X: 0, Y: state.Player2.Pos.Y, + }) + if err != nil { + slog.Debug("error marshalling player movement", slog.Any("error", err)) + } + update := pong.StateUpdate{ + FieldPath: "Player2.Pos", + Value: v, + } + egress <- update + } + return + + // Quit + case 'q': + // Acts as a forfeit. Other player wins + if isPlayer1 { + update := pong.StateUpdate{ + FieldPath: "Winner", + Value: []byte("Player2"), + } + egress <- update + } else { + update := pong.StateUpdate{ + FieldPath: "Winner", + Value: []byte("Player1"), + } + egress <- update + } + quit <- 1 + return + + // Esc char + case 27: + // Arrow Keys + switch bytes[1] { + // Up + case 65: + if isPlayer1 { + state.Player1.Pos.Y = state.Player1.Pos.Y + 1 + v, err := json.Marshal(pong.Vector{ + X: 0, Y: state.Player1.Pos.Y, + }) + if err != nil { + slog.Debug("error marshalling player movement", slog.Any("error", err)) + } + update := pong.StateUpdate{ + FieldPath: "Player1.Pos", + Value: v, + } + + egress <- update + } else { + state.Player2.Pos.Y = state.Player2.Pos.Y + 1 + v, err := json.Marshal(pong.Vector{ + X: 0, Y: state.Player2.Pos.Y, + }) + if err != nil { + slog.Debug("error marshalling Player2 movement", slog.Any("error", err)) + } + update := pong.StateUpdate{ + FieldPath: "Player2.Pos", + Value: v, + } + egress <- update + } + return + // Down + case 66: + if isPlayer1 { + state.Player1.Pos.Y = state.Player1.Pos.Y - 1 + v, err := json.Marshal(pong.Vector{ + X: 0, Y: state.Player1.Pos.Y, + }) + if err != nil { + slog.Debug("error marshalling player movement", slog.Any("error", err)) + } + update := pong.StateUpdate{ + FieldPath: "Player1.Pos", + Value: v, + } + + egress <- update + } else { + state.Player2.Pos.Y = state.Player2.Pos.Y - 1 + v, err := json.Marshal(pong.Vector{ + X: 0, Y: state.Player2.Pos.Y, + }) + if err != nil { + slog.Debug("error marshalling player movement", slog.Any("error", err)) + } + update := pong.StateUpdate{ + FieldPath: "Player2.Pos", + Value: v, + } + egress <- update + } + return + } + } +} diff --git a/internal/lobby/lobby.go b/internal/lobby/lobby.go new file mode 100644 index 0000000..5f6c0f7 --- /dev/null +++ b/internal/lobby/lobby.go @@ -0,0 +1,290 @@ +package lobby + +import ( + "fmt" + "io" + "log" + "log/slog" + "net" + "sync" + + "github.com/google/uuid" +) + +type Lobby struct { + lobbyMembers sync.Map + ExternalMessageChannel chan ExternalMessage +} + +type Client struct { + Username string + Conn net.Conn +} + +type ExternalMessage struct { + From string + Target string + Message LobbyMessage +} + +func CreateLobby() *Lobby { + externalMessageChan := make(chan ExternalMessage) + + l := Lobby{ + lobbyMembers: sync.Map{}, + ExternalMessageChannel: externalMessageChan, + } + + go func(lm *sync.Map) { + for { + msg := <-externalMessageChan + + tc, ok := lm.Load(msg.Target) + if !ok { + slog.Debug("Target not found in lobby map") + sc, ok := lm.Load(msg.From) + if !ok { + slog.Debug("Sender was also not found in lobby map and cannot send error message") + continue + } + c, ok := sc.(Client) + if !ok { + 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) + if err != nil { + slog.Debug("Could not marshall error message for missing player", slog.Any("error", err)) + } + c.Conn.Write(b) + }() + continue + } + c, ok := tc.(Client) + if !ok { + + slog.Debug("Item that was not a client found in the lobby map...", slog.Any("key", msg.From)) + 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) + }() + + } + }(&l.lobbyMembers) + + return &l +} + +func (l *Lobby) HandleLobbyConnection(conn net.Conn) { + messageBytes := make([]byte, 4096) + + ingress := make(chan LobbyMessage) + egress := make(chan LobbyMessage) + + // Network Reader + go func() { + for { + n, err := conn.Read(messageBytes) + if err == io.EOF { + conn.Close() + return + } + if err != nil { + conn.Close() + log.Printf("Error reading message %v", err) + return + } + + message := LobbyMessage{} + + message, err = Unmarshal(messageBytes[:n]) + if err != nil { + log.Println("Invalid message received from client", err) + } + ingress <- message + } + }() + + // Network Writer + go func() { + for { + msg := <-egress + bytes, err := Marshal(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) + + // 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) + } + } + }() + + // Client message handler + go func() { + for { + msg := <-ingress + serverMsg, err := l.handleClientLobbyMessage(&msg, conn) + if err != nil { + log.Println("Error handling client lobby message...", err) + } + if serverMsg.MessageType != "" { + egress <- serverMsg + } + } + }() +} + +// 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 + } + + 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 + } + + 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{}, + } + + return LobbyMessage{MessageType: "pending_invite", Message: PendingInvite{ + Recipient: i.To, + }}, nil + + // 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() + + 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 + } + + slog.Debug("incoming accept message", slog.Any("From", am.From), slog.Any("To", am.To)) + 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 + } + 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 + } + 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 + + // Ping and pong + default: + return LobbyMessage{MessageType: "pong", Message: "pong"}, nil + + } +} + +func (l *Lobby) broadcastToLobby(message LobbyMessage) { + 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) + if err != nil { + log.Println("Error broadcasting to clients...", err) + disconnectedUsers = append(disconnectedUsers, playerId.(string)) + + } + return true + }) + + for _, player := range disconnectedUsers { + l.lobbyMembers.Delete(player) + } +} diff --git a/internal/lobby/lobby_messages.go b/internal/lobby/lobby_messages.go new file mode 100644 index 0000000..17805ad --- /dev/null +++ b/internal/lobby/lobby_messages.go @@ -0,0 +1,244 @@ +package lobby + +import ( + "encoding/base64" + "encoding/json" + "errors" + "log/slog" + "reflect" + "strings" +) + +type LobbyMessage struct { + MessageType string `json:"message_type"` + Message any `json:"message"` +} + +type Name struct { + Name string `json:"name"` +} + +type Chat struct { + From string `json:"from"` + Message string `json:"message"` +} + +type Invite struct { + From string `json:"from"` + To string `json:"to"` +} + +type PendingInvite struct { + Recipient string `json:"recipient"` +} + +type Accept struct { + From string `json:"from"` + To string `json:"to"` +} + +type Accepted struct { + Accepter string `json:"accepter"` + GameID string `json:"game_id"` +} + +type StartGame struct { + To string `json:"to"` + GameID string `json:"game_id"` +} + +type Decline struct { + From string `json:"from"` + To string `json:"to"` +} + +type Disconnect struct { + From string `json:"from"` +} + +type Connect struct { + From string `json:"from"` +} + +type Error 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) + if err != nil { + return nil, err + } + + a.Message = bm + return json.Marshal(a) +} + +// 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) + if err != nil { + return lm, 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)) + + jsonBytes, err := base64.StdEncoding.DecodeString(smsg) + lm.Message = jsonBytes + + 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") +} diff --git a/internal/netwrk/client.go b/internal/netwrk/client.go deleted file mode 100644 index 562110f..0000000 --- a/internal/netwrk/client.go +++ /dev/null @@ -1,27 +0,0 @@ -package netwrk - -import ( - "fmt" - "net" - - "google.golang.org/protobuf/proto" -) - -func ConnectToLobby(username string) (net.Conn, error) { - conn, err := net.Dial("tcp", "127.0.0.1:12345") - if err != nil { - return nil, fmt.Errorf("Sorry, failed to connect to server...") - } - - loginMsg, err := proto.Marshal(&LobbyMessage{Type: "name", Content: username}) - if err != nil { - return nil, fmt.Errorf("Sorry bro but your username is wack AF...") - } - - _, err = conn.Write(loginMsg) - if err != nil { - return nil, fmt.Errorf("Sorry, could not communicate with server...") - } - - return conn, nil -} diff --git a/internal/netwrk/lobby.go b/internal/netwrk/lobby.go deleted file mode 100644 index ad66889..0000000 --- a/internal/netwrk/lobby.go +++ /dev/null @@ -1,185 +0,0 @@ -package netwrk - -import ( - "io" - "log" - "net" - "strings" - - "google.golang.org/protobuf/proto" -) - -func handleLobbyConnection(conn net.Conn) { - messageBytes := make([]byte, 4096) - - ingress := make(chan *LobbyMessage) - egress := make(chan *LobbyMessage) - - // Network Reader - go func() { - for { - n, err := conn.Read(messageBytes) - if err == io.EOF { - conn.Close() - return - } - if err != nil { - conn.Close() - log.Printf("Error reading message %v", err) - return - } - - message := LobbyMessage{} - - err = proto.Unmarshal(messageBytes[:n], &message) - if err != nil { - log.Println("Invalid message received from client", err) - } - ingress <- &message - } - }() - - // Network Writer - go func() { - for { - msg := <-egress - bytes, err := proto.Marshal(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) - ingress <- &LobbyMessage{ - Type: "disconnect", - Content: msg.PlayerId, - } - } - if err != nil { - log.Println("Error writing to user...", err) - } - } - }() - - // Client message handler - go func() { - for { - msg := <-ingress - serverMsg, err := handleClientLobbyMessage(msg, conn) - if err != nil { - log.Println("Error handling client lobby message...", err) - } - if serverMsg != nil { - egress <- serverMsg - } - } - }() -} - -// Returns a bool of whether the player has disconnected from the lobby and an error -func handleClientLobbyMessage(message *LobbyMessage, conn net.Conn) (*LobbyMessage, error) { - switch message.Type { - - // Handle an name/login message from a player - // Store the new player in the lobbyMembers - // Send a connection message for each of the lobbyMembers to the new player - // Send a connection message to all members in the lobby - case "name": - _, ok := lobbyMembers.Load(message.Content) - if ok { - return &LobbyMessage{Type: "name_error", Content: "Sorry, that name is already taken, please try a different name"}, nil - } - username := message.Content - - lobbyMembers.Store(username, Client{Username: username, Conn: conn}) - - // Build current lobby list - var lobby []string - lobbyMembers.Range(func(lobbyUsername any, client any) bool { - usernameString, _ := lobbyUsername.(string) - lobby = append(lobby, usernameString) - return true - }) - - broadcastToLobby(&LobbyMessage{PlayerId: "", Type: "connect", Content: username}) - - return &LobbyMessage{PlayerId: username, Type: "name", Content: strings.Join(lobby, "\n")}, 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": - externalMessageChan <- ExternalMessage{ - Target: message.Content, - Message: message, - } - - return &LobbyMessage{Type: "pending_invite", Content: message.Content}, nil - - // Handle a accept message from a player that was invited - // Send a game_start message back to the player: message.PlayerId - // Send an accepted message back to the inviter: message.Content - case "accept": - externalMessageChan <- ExternalMessage{ - Target: message.Content, - Message: &LobbyMessage{Type: "game_start", Content: ""}, - } - - return &LobbyMessage{PlayerId: message.PlayerId, Type: "accepted", Content: ""}, nil - - // Handle a chat message from a player with PlayerId - case "chat": - broadcastToLobby(&LobbyMessage{PlayerId: message.PlayerId, Type: "text", Content: message.Content}) - return nil, nil - - // Handle a decline_game message from a player that was invited - // Send an ack message back to the invitee: message.PlayerId - // Send an ack message to the inviter: message.Content - case "decline_game": - externalMessageChan <- ExternalMessage{ - Target: message.Content, - Message: &LobbyMessage{Type: "decline", Content: ""}, - } - - return &LobbyMessage{Type: "decline_game", Content: message.PlayerId}, nil - - // Handle a quit message from a player that was connected - // broadcast the player quit to the lobby - case "quit": - lobbyMembers.Delete(message.PlayerId) - broadcastToLobby(&LobbyMessage{Type: "disconnect", Content: message.PlayerId}) - return nil, nil - - // Ping and pong - case "ping": - return &LobbyMessage{Type: "pong", Content: "pong"}, nil - - // Ping and pong - default: - return &LobbyMessage{Type: "pong", Content: "pong"}, nil - } -} - -func broadcastToLobby(message *LobbyMessage) { - var disconnectedUsers []string - lobbyMembers.Range(func(playerId, player interface{}) bool { - bytes, err := proto.Marshal(message) - if err != nil { - log.Println("Error marshalling broadcast message", err) - } - - client := player.(Client) - _, err = client.Conn.Write(bytes) - if err != nil { - log.Println("Error broadcasting to clients...", err) - disconnectedUsers = append(disconnectedUsers, playerId.(string)) - - } - return true - }) - - for _, player := range disconnectedUsers { - lobbyMembers.Delete(player) - } -} diff --git a/internal/netwrk/lobby_messages.pb.go b/internal/netwrk/lobby_messages.pb.go deleted file mode 100644 index 0f27cdb..0000000 --- a/internal/netwrk/lobby_messages.pb.go +++ /dev/null @@ -1,162 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.34.2 -// protoc v5.27.3 -// source: proto/lobby_messages.proto - -package netwrk - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type LobbyMessage struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - PlayerId string `protobuf:"bytes,1,opt,name=player_id,json=playerId,proto3" json:"player_id,omitempty"` - Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` - Content string `protobuf:"bytes,3,opt,name=content,proto3" json:"content,omitempty"` -} - -func (x *LobbyMessage) Reset() { - *x = LobbyMessage{} - if protoimpl.UnsafeEnabled { - mi := &file_proto_lobby_messages_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *LobbyMessage) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*LobbyMessage) ProtoMessage() {} - -func (x *LobbyMessage) ProtoReflect() protoreflect.Message { - mi := &file_proto_lobby_messages_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use LobbyMessage.ProtoReflect.Descriptor instead. -func (*LobbyMessage) Descriptor() ([]byte, []int) { - return file_proto_lobby_messages_proto_rawDescGZIP(), []int{0} -} - -func (x *LobbyMessage) GetPlayerId() string { - if x != nil { - return x.PlayerId - } - return "" -} - -func (x *LobbyMessage) GetType() string { - if x != nil { - return x.Type - } - return "" -} - -func (x *LobbyMessage) GetContent() string { - if x != nil { - return x.Content - } - return "" -} - -var File_proto_lobby_messages_proto protoreflect.FileDescriptor - -var file_proto_lobby_messages_proto_rawDesc = []byte{ - 0x0a, 0x1a, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x6c, 0x6f, 0x62, 0x62, 0x79, 0x5f, 0x6d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x6e, 0x65, - 0x74, 0x77, 0x72, 0x6b, 0x2e, 0x76, 0x31, 0x22, 0x59, 0x0a, 0x0c, 0x4c, 0x6f, 0x62, 0x62, 0x79, - 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x6c, 0x61, 0x79, 0x65, - 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x79, - 0x65, 0x72, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, - 0x65, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, - 0x6e, 0x74, 0x42, 0x0a, 0x5a, 0x08, 0x2e, 0x2f, 0x6e, 0x65, 0x74, 0x77, 0x72, 0x6b, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} - -var ( - file_proto_lobby_messages_proto_rawDescOnce sync.Once - file_proto_lobby_messages_proto_rawDescData = file_proto_lobby_messages_proto_rawDesc -) - -func file_proto_lobby_messages_proto_rawDescGZIP() []byte { - file_proto_lobby_messages_proto_rawDescOnce.Do(func() { - file_proto_lobby_messages_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_lobby_messages_proto_rawDescData) - }) - return file_proto_lobby_messages_proto_rawDescData -} - -var file_proto_lobby_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 1) -var file_proto_lobby_messages_proto_goTypes = []any{ - (*LobbyMessage)(nil), // 0: netwrk.v1.LobbyMessage -} -var file_proto_lobby_messages_proto_depIdxs = []int32{ - 0, // [0:0] is the sub-list for method output_type - 0, // [0:0] is the sub-list for method input_type - 0, // [0:0] is the sub-list for extension type_name - 0, // [0:0] is the sub-list for extension extendee - 0, // [0:0] is the sub-list for field type_name -} - -func init() { file_proto_lobby_messages_proto_init() } -func file_proto_lobby_messages_proto_init() { - if File_proto_lobby_messages_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_proto_lobby_messages_proto_msgTypes[0].Exporter = func(v any, i int) any { - switch v := v.(*LobbyMessage); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_proto_lobby_messages_proto_rawDesc, - NumEnums: 0, - NumMessages: 1, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_proto_lobby_messages_proto_goTypes, - DependencyIndexes: file_proto_lobby_messages_proto_depIdxs, - MessageInfos: file_proto_lobby_messages_proto_msgTypes, - }.Build() - File_proto_lobby_messages_proto = out.File - file_proto_lobby_messages_proto_rawDesc = nil - file_proto_lobby_messages_proto_goTypes = nil - file_proto_lobby_messages_proto_depIdxs = nil -} diff --git a/internal/netwrk/netwrk.go b/internal/netwrk/netwrk.go deleted file mode 100644 index a1534f8..0000000 --- a/internal/netwrk/netwrk.go +++ /dev/null @@ -1,127 +0,0 @@ -package netwrk - -import ( - "log" - "net" - "sshpong/internal/pong" - "strings" - sync "sync" - - "google.golang.org/protobuf/proto" -) - -type Client struct { - Username string - Conn net.Conn -} - -type ExternalMessage struct { - Target string - Message *LobbyMessage -} - -type GameClients struct { - Client1 Client - Client2 Client -} - -var externalMessageChan chan ExternalMessage - -var lobbyMembers sync.Map -var games sync.Map - -func init() { - externalMessageChan = make(chan ExternalMessage) - - lobbyMembers = sync.Map{} - games = sync.Map{} - - go func() { - for { - msg := <-externalMessageChan - player, ok := lobbyMembers.Load(msg.Target) - if !ok { - log.Println("failed to send to target", msg.Target) - continue - } - client, _ := player.(Client) - bytes, _ := proto.Marshal(msg.Message) - _, err := client.Conn.Write(bytes) - if err != nil { - log.Println("Could not write to target", msg.Target, err) - } - - } - }() -} - -// Starts listening on port 12345 for TCP connections -// Also creates client pool and game connection singletons -func LobbyListen() { - - listener, err := net.Listen("tcp", "127.0.0.1:12345") - if err != nil { - log.Fatal(err) - } - - defer listener.Close() - - for { - conn, err := listener.Accept() - if err != nil { - log.Println(err) - continue - } - go handleLobbyConnection(conn) - } -} - -func GamesListen() { - gameListener, err := net.Listen("tcp", "127.0.0.1:42069") - if err != nil { - log.Fatal(err) - } - - defer gameListener.Close() - for { - conn, err := gameListener.Accept() - if err != nil { - log.Println(err) - continue - } - - go func(conn net.Conn) { - messageBytes := make([]byte, 126) - - n, err := conn.Read(messageBytes) - if err != nil { - log.Printf("Error reading game ID on connection %s", err) - } - - gInfo := strings.SplitAfter(string(messageBytes[:n]), ":") - if err != nil { - log.Printf("Game id was not a string? %s", err) - } - - game, ok := games.Load(gInfo[0]) - if !ok { - games.Store(gInfo[0], GameClients{Client1: Client{ - Username: gInfo[1], - Conn: conn, - }, Client2: Client{}}) - } else { - gameclients, _ := game.(GameClients) - client2 := Client{ - Username: gInfo[1], - Conn: conn, - } - - games.Store(gInfo[0], GameClients{ - Client1: gameclients.Client1, - Client2: client2}) - - go pong.StartGame(gameclients.Client1.Conn, client2.Conn, gameclients.Client1.Username, client2.Username) - } - }(conn) - } -} diff --git a/internal/pong/pong.go b/internal/pong/pong.go index 1bb05f3..f7f3f39 100644 --- a/internal/pong/pong.go +++ b/internal/pong/pong.go @@ -1,15 +1,15 @@ package pong import ( + "encoding/json" "fmt" "log" + "log/slog" "net" - "strconv" "strings" "time" "golang.org/x/exp/rand" - "google.golang.org/protobuf/proto" ) type GameClient struct { @@ -20,38 +20,71 @@ type GameClient struct { var player1 GameClient var player2 GameClient -var ingress chan *ClientUpdateRequest -var egress chan *ServerUpdateMessage +var ingress chan StateUpdate +var egress chan StateUpdate -const posXBound = 50 +const posXBound = 100 const negXBound = posXBound * -1 const posYBound = 50 const negYBound = posYBound * -1 func StartGame(conn1, conn2 net.Conn, username1, username2 string) { + + egress = make(chan StateUpdate) + ingress = make(chan StateUpdate) + player1 = GameClient{ Username: username1, Conn: conn1, } + + p1msg := StateUpdate{ + FieldPath: "isPlayer1", + Value: []byte{1}, + } + b, err := json.Marshal(p1msg) + if err != nil { + return + } + _, err = player1.Conn.Write(b) + if err != nil { + fmt.Println("Error writing player1 msg to player1") + return + } + player2 = GameClient{ Username: username2, Conn: conn2, } + p2msg := StateUpdate{ + FieldPath: "isPlayer1", + Value: []byte{0}, + } + b, err = json.Marshal(p2msg) + if err != nil { + return + } + _, err = player2.Conn.Write(b) + if err != nil { + fmt.Println("Error writing player1 msg to player2") + return + } + time.Sleep(1 * time.Second) - broadcastUpdate(&ServerUpdateMessage{ - Type: "message", - Value: "Ready...", + broadcastUpdate(StateUpdate{ + FieldPath: "Message", + Value: []byte("Ready..."), }) time.Sleep(1 * time.Second) - broadcastUpdate(&ServerUpdateMessage{ - Type: "message", - Value: "Set...", + broadcastUpdate(StateUpdate{ + FieldPath: "Message", + Value: []byte("Set..."), }) time.Sleep(1 * time.Second) - broadcastUpdate(&ServerUpdateMessage{ - Type: "message", - Value: "Go!", + broadcastUpdate(StateUpdate{ + FieldPath: "Message", + Value: []byte("Go!"), }) time.Sleep(1 * time.Second) bv := float32(rand.Intn(2)*2 - 1) @@ -68,7 +101,6 @@ func StartGame(conn1, conn2 net.Conn, username1, username2 string) { X: 1, Y: 10, }, - Speed: 0, }, Player2: Player{ client: player2, @@ -80,7 +112,6 @@ func StartGame(conn1, conn2 net.Conn, username1, username2 string) { X: 1, Y: 10, }, - Speed: 0, }, Ball: Ball{ Pos: Vector{ @@ -93,23 +124,22 @@ func StartGame(conn1, conn2 net.Conn, username1, username2 string) { }, }, } - go gameLoop(state) + go gameLoop(&state) } -func gameLoop(state GameState) { +func gameLoop(state *GameState) { // Player 1 read loop go func() { for { bytes := make([]byte, 512) n, err := state.Player1.client.Conn.Read(bytes) - msg := &ClientUpdateRequest{} - err = proto.Unmarshal(bytes[:n], msg) + msg := StateUpdate{} + err = json.Unmarshal(bytes[:n], &msg) if err != nil { log.Println("error reading player 1's update request:", err) return } - msg.Player = 1 ingress <- msg } }() @@ -120,13 +150,12 @@ func gameLoop(state GameState) { bytes := make([]byte, 512) n, err := state.Player2.client.Conn.Read(bytes) - msg := &ClientUpdateRequest{} - err = proto.Unmarshal(bytes[:n], msg) + msg := StateUpdate{} + err = json.Unmarshal(bytes[:n], &msg) if err != nil { log.Println("error reading player 2's update request:", err) return } - msg.Player = 2 ingress <- msg } }() @@ -143,20 +172,38 @@ func gameLoop(state GameState) { for { select { case msg := <-ingress: - err := handlePlayerRequest(&state, msg) + err := handlePlayerRequest(msg, state) if err != nil { fmt.Println("FUCK!~", err) } case _ = <-ticker.C: - update := process(&state) - egress <- &update + update := process(state) + egress <- update } } } -func process(state *GameState) ServerUpdateMessage { +func process(state *GameState) StateUpdate { + + // Move players + // Check if player edge is out of bounds + // If out of bounds reset velocity to zero and position to edge + + if state.Player1.Pos.Y+state.Player1.Size.Y/2 > posYBound { + state.Player1.Pos.Y = posYBound - state.Player1.Size.Y/2 - 1 + } + if state.Player1.Pos.Y-state.Player1.Size.Y/2 < negYBound { + state.Player1.Pos.Y = negYBound + state.Player1.Size.Y/2 + 1 + } + + if state.Player2.Pos.Y+state.Player2.Size.Y/2 > posYBound { + state.Player2.Pos.Y = posYBound - state.Player2.Size.Y/2 - 1 + } + if state.Player2.Pos.Y-state.Player2.Size.Y/2 < negYBound { + state.Player2.Pos.Y = negYBound + state.Player2.Size.Y/2 + 1 + } // Move ball // Check if ball is out of bounds @@ -192,9 +239,9 @@ func process(state *GameState) ServerUpdateMessage { state.Ball.Vel.X = 1 state.Ball.Vel.Y = 0 if state.Score[player2.Username] >= 9 { - return ServerUpdateMessage{ - Type: "gameover", - Value: player2.Username, + return StateUpdate{ + FieldPath: "Winner", + Value: []byte(player2.Username), } } state.Score[player2.Username] = state.Score[player2.Username] + 1 @@ -215,49 +262,79 @@ func process(state *GameState) ServerUpdateMessage { state.Ball.Vel.X = -1 state.Ball.Vel.Y = 0 if state.Score[player1.Username] >= 9 { - return ServerUpdateMessage{ - Type: "gameover", - Value: player1.Username, + return StateUpdate{ + FieldPath: "Winner", + Value: []byte(player1.Username), } } state.Score[player1.Username] = state.Score[player1.Username] + 1 } } - return ServerUpdateMessage{} -} - -func handlePlayerRequest(state *GameState, msg *ClientUpdateRequest) error { - - switch msg.Type { - case "player_pos": - if msg.Player == 1 { - pos := strings.Split(msg.Value, " ") - x, err := strconv.ParseFloat(pos[0], 32) - if err != nil { - fmt.Println("Got weird position update for x", err) - } - y, err := strconv.ParseFloat(pos[1], 32) - if err != nil { - fmt.Println("Got weird position update for y", err) - } - - state.Player1.Pos = Vector{ - X: float32(x), - Y: float32(y), - } - } - default: - fmt.Println("Got unhandled update", msg.Type) + ns, err := json.Marshal(state) + if err != nil { + slog.Debug("error marshalling entire state update", slog.Any("error", err)) } + return StateUpdate{ + FieldPath: "All", + Value: ns, + } +} + +func handlePlayerRequest(update StateUpdate, state *GameState) error { + + fields := strings.Split(update.FieldPath, ".") + + // type GameState struct { + // Message string + // Winner string + // Score map[string]int + // Player1 Player + // Player2 Player + // Ball Ball + // } + + switch fields[0] { + case "Message": + state.Message = string(update.Value) + case "Winner": + state.Winner = string(update.Value) + case "Player1": + switch fields[1] { + case "Pos": + v1 := Vector{} + err := json.Unmarshal(update.Value, &v1) + if err != nil { + slog.Debug("error unmarshalling player1 update") + } + state.Player1.Pos = v1 + } + case "Player2": + switch fields[1] { + case "Pos": + v2 := Vector{} + err := json.Unmarshal(update.Value, &v2) + if err != nil { + slog.Debug("error unmarshalling player2 update") + } + state.Player2.Pos = v2 + } + case "Ball": + b := Ball{} + err := json.Unmarshal(update.Value, &b) + if err != nil { + slog.Debug("error unmarshalling ball update") + } + state.Ball = b + } return nil } -func broadcastUpdate(update *ServerUpdateMessage) error { - msg, err := proto.Marshal(update) +func broadcastUpdate(update StateUpdate) error { + msg, err := json.Marshal(update) if err != nil { - return fmt.Errorf("malformed server update message %v", err) + return err } _, err = player1.Conn.Write(msg) if err != nil { diff --git a/internal/pong/state.go b/internal/pong/state.go index 58f5a5e..c5444ce 100644 --- a/internal/pong/state.go +++ b/internal/pong/state.go @@ -1,6 +1,8 @@ package pong type GameState struct { + Message string + Winner string Score map[string]int Player1 Player Player2 Player @@ -16,10 +18,16 @@ type Player struct { client GameClient Pos Vector Size Vector - Speed float32 } type Ball struct { Pos Vector Vel Vector } + +type StateUpdate struct { + // The field to update on the state object dot separated + // I.e Player1.Speed = the speed field on Player1 + FieldPath string + Value []byte +} diff --git a/internal/renderer/renderer.go b/internal/renderer/renderer.go index 08c53ad..b015fc0 100644 --- a/internal/renderer/renderer.go +++ b/internal/renderer/renderer.go @@ -4,9 +4,9 @@ import ( "fmt" "os" "sshpong/internal/ansii" + "sshpong/internal/pong" "strings" "time" - "unicode/utf8" ) var ( @@ -19,17 +19,11 @@ var ( playerY int = 10 ) -func Render() { - now := time.Now() - err := doFpsTest() - if err != nil { - fmt.Println("Error ", err) - return - } - then := time.Now() - - total := float64(float64(then.UnixMicro()-now.UnixMicro()) / 1000.0) - fmt.Printf("\n\nTook %.3f milliseconds\n", total) +func Render(state pong.GameState) { + // drawScreen(state) + fmt.Print("\033c") + fmt.Println("Player 1", state.Player1.Pos.X, state.Player1.Pos.Y) + fmt.Println("Player 2", state.Player2.Pos.X, state.Player2.Pos.Y) } func writeCheckerBoard(height int, width int, builder *strings.Builder) { @@ -53,25 +47,22 @@ func writeCheckerBoard(height int, width int, builder *strings.Builder) { } } -func drawScreen(frameNum int, startMs float64) (frameTimeMs float64) { - _ = frameNum - _, height := ansii.GetTermSize() +func drawScreen(state pong.GameState) { + // width := 100 + // height := 50 var builder = strings.Builder{} builder.WriteString(string(ansii.Screen.ClearScreen)) - // writeCheckerBoard(height, width, &builder) - // var xOffset = frameNum % width - // ansii.DrawBox(&builder, ansii.Offset{X: 0, Y: 0}, 5, 8, ansii.Colors.Purple) - ansii.DrawBox(&builder, ansii.Offset{X: playerX, Y: playerY}, 5, 1, ansii.Colors.Cyan) - ansii.DrawPixelStyle(&builder, ansii.Offset{X: playerX, Y: playerY}, ansii.Colors.Purple) - ansii.DrawPixelStyle(&builder, ansii.Offset{X: playerX, Y: playerY + 5}, ansii.Colors.Purple) + ansii.DrawBox(&builder, ansii.Offset{X: int(state.Player1.Pos.X), Y: int(state.Player1.Pos.Y)}, 5, 1, ansii.Colors.Cyan) + ansii.DrawPixelStyle(&builder, ansii.Offset{X: int(state.Player1.Pos.X), Y: int(state.Player1.Pos.Y)}, ansii.Colors.Purple) + ansii.DrawPixelStyle(&builder, ansii.Offset{X: int(state.Player1.Pos.X), Y: int(state.Player1.Pos.Y) + 5}, ansii.Colors.Purple) + ansii.DrawBox(&builder, ansii.Offset{X: int(state.Player2.Pos.X), Y: int(state.Player2.Pos.Y)}, 5, 1, ansii.Colors.Cyan) + ansii.DrawPixelStyle(&builder, ansii.Offset{X: int(state.Player2.Pos.X), Y: int(state.Player2.Pos.Y)}, ansii.Colors.Purple) + ansii.DrawPixelStyle(&builder, ansii.Offset{X: int(state.Player2.Pos.X), Y: int(state.Player2.Pos.Y) + 5}, ansii.Colors.Purple) // Quit instructions - builder.WriteString(string(ansii.Screen.PlaceCursor(ansii.Offset{X: 0, Y: height}))) - builder.WriteString("q to quit") + // builder.WriteString(string(ansii.Screen.PlaceCursor(ansii.Offset{X: 0, Y: height}))) + // builder.WriteString("q to quit") os.Stdout.WriteString(builder.String()) - var frameTimeMilli = (float64(time.Now().UnixNano()) / 1_000_000.0) - startMs - // builder.WriteString(string(ansii.Screen.Coordinate(0, 0))) - return frameTimeMilli } func drawFrameStats(frameNum int, frameTimeMs float64) { @@ -117,79 +108,3 @@ func waitForFpsLock(startMs float64) { } } } - -func doFpsTest() error { - prev, err := ansii.MakeTermRaw() - if err != nil { - return err - } - defer ansii.RestoreTerm(prev) - quit = make(chan bool, 1) - userInput = make(chan rune, 1) - - // User input loop - go func() { - for { - buf := make([]byte, 3) - n, err := os.Stdin.Read(buf) - if err != nil { - fmt.Fprintf(os.Stderr, "Error reading from stdin: %v\n", err) - continue - } - - if n > 0 { - if buf[0] == 0x1b { // ESC - if n > 1 && buf[1] == '[' { // ESC [ - switch buf[2] { - case 'A': - userInput <- '↑' // Up arrow - case 'B': - userInput <- '↓' // Down arrow - case 'C': - userInput <- '→' // Right arrow - case 'D': - userInput <- '←' // Left arrow - default: - userInput <- '?' - } - } else { - userInput <- '?' - } - } else { - r, _ := utf8.DecodeRune(buf) - userInput <- r - } - } - } - }() - // Rendering loop - go func() { - for i := 0; i <= 10_000; i++ { - startMs := float64(time.Now().UnixNano()) / 1_000_000.0 - select { - case <-quit: - return - default: - frameTimeMs := drawScreen(i, startMs) - drawFrameStats(i, frameTimeMs) - waitForFpsLock(startMs) - } - } - close(quit) - }() - - os.Stdout.WriteString(string(ansii.Screen.HideCursor)) - defer os.Stdout.WriteString(string(ansii.Screen.ShowCursor)) - for { - select { - case <-quit: - fmt.Println("Exiting") - return nil - // case ui := <-userInput: - case input := <-userInput: - handleInput(input) - // fmt.Println(ui) - default: - } - } -} diff --git a/main.go b/main.go deleted file mode 100644 index 8b13789..0000000 --- a/main.go +++ /dev/null @@ -1 +0,0 @@ - diff --git a/makefile b/makefile new file mode 100644 index 0000000..e43f730 --- /dev/null +++ b/makefile @@ -0,0 +1,2 @@ +lint: + golangci-lint run