291 lines
7.5 KiB
Go
291 lines
7.5 KiB
Go
|
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)
|
||
|
}
|
||
|
}
|