middle of the re-work
This commit is contained in:
parent
e31dbef6ef
commit
f2d488ef51
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
// }
|
||||
// }
|
||||
|
|
|
@ -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 <player name> 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
|
||||
}
|
||||
|
|
316
internal/client/game.go
Normal file
316
internal/client/game.go
Normal file
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
290
internal/lobby/lobby.go
Normal file
290
internal/lobby/lobby.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
244
internal/lobby/lobby_messages.go
Normal file
244
internal/lobby/lobby_messages.go
Normal file
|
@ -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")
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user