middle of the re-work

This commit is contained in:
Beric Bearnson 2024-09-26 19:49:59 -06:00
parent e31dbef6ef
commit f2d488ef51
15 changed files with 1347 additions and 754 deletions

View File

@ -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
}

View File

@ -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)
// }
// }

View File

@ -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
View 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
View 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)
}
}

View 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")
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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:
}
}
}

View File

@ -1 +0,0 @@

2
makefile Normal file
View File

@ -0,0 +1,2 @@
lint:
golangci-lint run