Newer
Older
wwwcats / lobby.go
package main

import (
	"log"
	"strings"
	"sync"

	"runtime/debug"
)

type Lobby struct {
	name string

	// Client management

	// We need a lock for clients, because although the map is never written concurrently,
	// it can be updated while a send operation is taking place, resulting in a race
	// condition -> send on closed channel
	// The reason for doing all this is so that we can make synchronous writes to the send
	// channel of clients, in order to send events in the order they occur.
	// Therefore: need to gain a lock when changing OR sending to clients!

	clients    map[*Client]bool
	clientsMu  sync.Mutex
	register   chan *Client
	unregister chan *Client

	currentGame *Game
}

func newLobby(name string) (lobby *Lobby) {
	lobby = &Lobby{
		name:    name,
		clients: make(map[*Client]bool),

		// We make channels with a small buffer, in case we need to
		// write to them from their own goroutine for convenience

		register:     make(chan *Client, 64),
		unregister:   make(chan *Client, 64),
	}
	lobby.currentGame = newGame(lobby)
	return
}

func (l *Lobby) run(lobbies map[string]*Lobby) {
	// Goroutine to deal with all the tasks of the lobby

	defer func() {
		if r := recover(); r != nil {
			// Recover a panicking lobby to avoid crashing the whole server
			for client := range l.clients {
				l.destroyClient(client)
			}

			delete(lobbies, l.name)

			log.Printf("!!! PANIC in lobby %s: %v !!!", l.name, r)
			debug.PrintStack()
		}
	}()

	for {
		select {

		case client := <-l.register:
			l.clients[client] = true

			// Sync the join to the game object
			l.clientsMu.Lock()
			l.currentGame.addPlayer(client)
			l.clientsMu.Unlock()

		case client := <-l.unregister:
			// Announce and sync
			// We need to do this before we close the channel
			l.currentGame.removePlayer(client)

			if _, ok := l.clients[client]; ok {
				l.destroyClient(client)
			}

			if len(l.clients) == 0 {
				// The lobby is finished
				delete(lobbies, l.name)
				return
			}

		}
	}
}

func (c *Client) joinToLobby(lobby_name string, player_name string, lobbies map[string]*Lobby) {
	var lobby *Lobby

	if lobbies[lobby_name] == nil {
		// Create the lobby, and start its goroutine
		lobby = newLobby(lobby_name)
		lobbies[lobby_name] = lobby
		go lobby.run(lobbies)
	} else {
		lobby = lobbies[lobby_name]
	}

	// Avoid nickname collisions
	for connected_client := range lobbies[lobby_name].clients {
		if connected_client.name == player_name {
			select {
			case c.send <- []byte("err username_exists"):
			default:
				close(c.send)
			}
			// Nobody is getting joined to the lobby today
			return
		}
	}

	c.name = player_name

	lobby.register <- c
	c.lobby = lobby
}

func (l *Lobby) readFromClient(c *Client, msg string) {
	fields := strings.Fields(msg)

	if len(fields) < 1 {
		return
	}

	// Lobby-wide commands

	if fields[0] == "chat" {
		l.sendBcast("chat " + c.name + " " + msg[5:])
		return
	}

	// Nothing to be done here, hand the message off to the game object
	l.currentGame.readFromClient(c, msg)
}

func (l *Lobby) sendBcast(msg string) {
	l.clientsMu.Lock()
	defer l.clientsMu.Unlock()

	l.sendBcastRaw(msg)
}

func (l *Lobby) sendBcastRaw(msg string) {
	for client := range l.clients {
		client.sendMsg(msg)
	}
}

func (l *Lobby) sendComplexBcast(text string, except map[*Client]bool) {
	l.clientsMu.Lock()
	defer l.clientsMu.Unlock()

	for client := range l.clients {
		_, ok := except[client]
		if ok {
			continue
		}

		client.sendMsg(text)
	}
}

func (l *Lobby) destroyClient(client *Client) {
	l.clientsMu.Lock()
	defer l.clientsMu.Unlock()

	client.conn.Close()
	delete(l.clients, client)
	close(client.send)
	log.Printf("DELETING %s", client.name)
}