365 lines
8.5 KiB
Go
365 lines
8.5 KiB
Go
package lobby
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"log/slog"
|
|
"net"
|
|
"sync"
|
|
)
|
|
|
|
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 []byte
|
|
}
|
|
|
|
func CreateLobby() *Lobby {
|
|
externalMessageChan := make(chan ExternalMessage)
|
|
|
|
l := Lobby{
|
|
lobbyMembers: sync.Map{},
|
|
ExternalMessageChannel: externalMessageChan,
|
|
}
|
|
|
|
go func(lm *sync.Map) {
|
|
for {
|
|
msg := <-externalMessageChan
|
|
slog.Debug("forwarding external message")
|
|
|
|
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() {
|
|
b, err := Marshal(ErrorData{Message: fmt.Sprintf("Sorry player %s is not available...", msg.Target)}, Error)
|
|
if err != nil {
|
|
slog.Debug("Could not marshall error message for missing player", slog.Any("error", err))
|
|
}
|
|
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))
|
|
lm.Delete(msg.Target)
|
|
continue
|
|
}
|
|
go func() {
|
|
c.Conn.Write(msg.Message)
|
|
}()
|
|
|
|
}
|
|
}(&l.lobbyMembers)
|
|
|
|
return &l
|
|
}
|
|
|
|
func (l *Lobby) HandleLobbyConnection(client Client) {
|
|
messageBytes := make([]byte, 4096)
|
|
|
|
ingress := make(chan []byte)
|
|
egress := make(chan []byte)
|
|
|
|
// Network Reader
|
|
go func() {
|
|
for {
|
|
n, err := client.Conn.Read(messageBytes)
|
|
if err != nil {
|
|
client.Conn.Close()
|
|
log.Printf("Error reading message %v", err)
|
|
l.lobbyMembers.Delete(client.Username)
|
|
|
|
// Server receives a disconnect message of the user
|
|
msg, err := Marshal(DisconnectData{
|
|
From: client.Username,
|
|
}, Disconnect)
|
|
if err != nil {
|
|
slog.Error("error marshalling responsive disconnect of EOF error", "error", err)
|
|
} else {
|
|
ingress <- msg
|
|
}
|
|
return
|
|
}
|
|
ingress <- messageBytes[:n]
|
|
}
|
|
}()
|
|
|
|
// Network Writer
|
|
go func() {
|
|
for {
|
|
msg := <-egress
|
|
_, err := client.Conn.Write(msg)
|
|
if err != nil {
|
|
client.Conn.Close()
|
|
|
|
l.lobbyMembers.Delete(client.Username)
|
|
|
|
// Server receives a disconnect message of the user
|
|
msg, err := Marshal(DisconnectData{
|
|
From: client.Username,
|
|
}, Disconnect)
|
|
if err != nil {
|
|
slog.Error("error marshalling responsive disconnect of EOF error", "error", err)
|
|
} else {
|
|
ingress <- msg
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Client message handler
|
|
go func() {
|
|
for {
|
|
msg := <-ingress
|
|
slog.Debug("Received an ingress message", "message", msg)
|
|
|
|
resMsg, err := l.handleClientLobbyMessage(msg)
|
|
if err != nil {
|
|
resMsg, err = Marshal(ErrorData{
|
|
Message: err.Error(),
|
|
}, Error)
|
|
}
|
|
if len(resMsg) > 0 {
|
|
egress <- resMsg
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (l *Lobby) handleClientLobbyMessage(msg []byte) ([]byte, error) {
|
|
header := msg[0]
|
|
|
|
switch header {
|
|
case Chat:
|
|
l.BroadcastToLobby(msg)
|
|
return []byte{}, nil
|
|
case Invite:
|
|
i, err := Unmarshal[InviteData](msg)
|
|
if err != nil {
|
|
slog.Debug("error unmarshalling invite message", "error", err)
|
|
return []byte{}, err
|
|
}
|
|
|
|
msg, err := Marshal(InviteData{
|
|
From: i.From,
|
|
To: i.To,
|
|
}, Invite)
|
|
if err != nil {
|
|
slog.Error("error marshalling invite data...", "error", err)
|
|
return []byte{}, err
|
|
}
|
|
|
|
_, ok := l.lobbyMembers.Load(i.To)
|
|
if !ok {
|
|
return Marshal(ErrorData{
|
|
Message: fmt.Sprintf("Sorry, player %s is not available.", i.To),
|
|
}, Error)
|
|
} else {
|
|
l.ExternalMessageChannel <- ExternalMessage{
|
|
From: i.From,
|
|
Target: i.To,
|
|
Message: msg,
|
|
}
|
|
}
|
|
|
|
return Marshal(PendingInviteData{
|
|
Recipient: i.To,
|
|
}, PendingInvite)
|
|
|
|
// TODO: is pending invite really something that we need?
|
|
// case PendingInvite:
|
|
// pi, err := Unmarshal[PendingInviteData](msg)
|
|
// if err != nil {
|
|
// slog.Debug("error unmarshalling pending invite message", err)
|
|
// return
|
|
// }
|
|
|
|
case Accept:
|
|
a, err := Unmarshal[AcceptData](msg)
|
|
if err != nil {
|
|
slog.Debug("error unmarshalling accept message", "error", err)
|
|
return []byte{}, err
|
|
}
|
|
|
|
gID := a.GameID
|
|
|
|
msg, err := Marshal(StartGameData{
|
|
GameID: gID,
|
|
}, StartGame)
|
|
|
|
l.ExternalMessageChannel <- ExternalMessage{
|
|
From: a.From,
|
|
Target: a.To,
|
|
Message: msg,
|
|
}
|
|
|
|
slog.Debug("Sent start game message to inviter")
|
|
|
|
return Marshal(StartGameData{
|
|
To: a.From,
|
|
From: a.To,
|
|
GameID: gID,
|
|
}, StartGame)
|
|
|
|
// TODO: figure out the accepted and start game data situation... To field is a little hard to fill.
|
|
// case Accepted:
|
|
// a, err := Unmarshal[AcceptedData](msg)
|
|
// if err != nil {
|
|
// slog.Debug("error unmarshalling accpeted message", "error", err)
|
|
// return []byte{}, err
|
|
// }
|
|
// return Marshal(StartGameData{
|
|
// To: "",
|
|
// GameID: a.GameID,
|
|
// }, StartGame)
|
|
|
|
// TODO: Like pending invite, I think start game is only a client message
|
|
// case StartGame:
|
|
// sg, err := Unmarshal[StartGameData](msg)
|
|
// if err != nil {
|
|
// slog.Debug("error unmarshalling start game message", err)
|
|
// return []byte{}, err
|
|
// }
|
|
|
|
// TODO: Do we even want to support decline responses?
|
|
// case Decline:
|
|
// d, err := Unmarshal[DeclineData](msg)
|
|
// if err != nil {
|
|
// slog.Debug("error unmarshalling decline message", err)
|
|
// return []byte{}, err
|
|
// }
|
|
|
|
case Disconnect:
|
|
d, err := Unmarshal[DisconnectData](msg)
|
|
if err != nil {
|
|
slog.Debug("error unmarshalling disconnect message", "error", err)
|
|
return []byte{}, err
|
|
}
|
|
|
|
l.lobbyMembers.Delete(d.From)
|
|
|
|
msg, err := Marshal(DisconnectData{
|
|
From: d.From,
|
|
}, Disconnect)
|
|
|
|
l.BroadcastToLobby(msg)
|
|
|
|
// TODO: how do we handle a disconnect for the client's side
|
|
return []byte{}, nil
|
|
|
|
// TODO: This is just a client side message right...?
|
|
// case Connect:
|
|
// c, err := Unmarshal[ConnectData](msg)
|
|
// if err != nil {
|
|
// slog.Debug("error unmarshalling connect message", err)
|
|
// return
|
|
// }
|
|
|
|
// TODO: This is just a client side message right...?
|
|
// case Error:
|
|
// e, err := Unmarshal[ErrorData](msg)
|
|
// if err != nil {
|
|
// slog.Debug("error unmarshalling error message", err)
|
|
// return []byte{}, err
|
|
// }
|
|
}
|
|
return []byte{}, nil
|
|
}
|
|
|
|
func (l *Lobby) BroadcastToLobby(bytes []byte) {
|
|
var disconnectedUsers []string
|
|
l.lobbyMembers.Range(func(playerId, player interface{}) bool {
|
|
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)
|
|
}
|
|
}
|
|
func (l *Lobby) InitialConnectionHandler(conn net.Conn) (Client, []byte, error) {
|
|
msg := make([]byte, 256)
|
|
nb, err := conn.Read(msg)
|
|
if err != nil {
|
|
slog.Debug("error reading from initial connection")
|
|
}
|
|
msg = msg[:nb]
|
|
n, err := Unmarshal[NameData](msg)
|
|
if err != nil {
|
|
slog.Debug("error unmarshalling name message:", "error", err.Error(), "message", msg[1:nb])
|
|
|
|
msgOut, err := Marshal(ErrorData{
|
|
Message: "incorrectly formatted username in name message",
|
|
}, Error)
|
|
if err != nil {
|
|
slog.Error("error marshalling error message for incorrectly formatted username")
|
|
}
|
|
return Client{}, msgOut, err
|
|
}
|
|
_, ok := l.lobbyMembers.Load(n.Name)
|
|
if ok {
|
|
msg, merr := Marshal(ErrorData{
|
|
Message: "Sorry that name is already taken, please try a different name",
|
|
}, Error)
|
|
if merr != nil {
|
|
slog.Error("error marshalling error on name already taken msg")
|
|
}
|
|
return Client{}, msg, fmt.Errorf("unoriginal name :p")
|
|
}
|
|
h, err := Marshal(ConnectData{
|
|
From: n.Name,
|
|
}, Connect)
|
|
if err != nil {
|
|
slog.Debug("error marshalling broadcast connect message on player connect", "error", err)
|
|
return Client{Username: n.Name, Conn: conn}, h, err
|
|
}
|
|
l.BroadcastToLobby(h)
|
|
|
|
// Build current lobby list
|
|
var lobby []string
|
|
l.lobbyMembers.Range(func(lobbyUsername any, client any) bool {
|
|
usernameString, _ := lobbyUsername.(string)
|
|
lobby = append(lobby, usernameString)
|
|
return true
|
|
})
|
|
msgOut, err := Marshal(CurrentlyConnectedData{Players: lobby}, CurrentlyConnected)
|
|
if err != nil {
|
|
slog.Debug("Error marshalling currectly connected data on player connect")
|
|
}
|
|
|
|
client := Client{
|
|
Username: n.Name,
|
|
Conn: conn,
|
|
}
|
|
|
|
l.lobbyMembers.Store(n.Name, client)
|
|
|
|
return client, msgOut, err
|
|
}
|