netris/pkg/game/server.go

513 lines
9.7 KiB
Go

package game
import (
"encoding/json"
"fmt"
"log"
"net"
"sort"
"strings"
"sync"
"time"
"gitlab.com/tslocum/netris/pkg/event"
)
const (
DefaultPort = 1984
DefaultServer = "netris.rocketnine.space"
)
type Server struct {
I []ServerInterface
In chan GameCommandInterface
Out chan GameCommandInterface
Games map[int]*Game
Logger chan string
listeners []net.Listener
NewPlayers chan *IncomingPlayer
created time.Time
logLevel int
sync.RWMutex
}
type IncomingPlayer struct {
Name string
Conn *Conn
}
type ServerInterface interface {
// Load config
Host(newPlayers chan<- *IncomingPlayer)
Shutdown(reason string)
}
func NewServer(si []ServerInterface, logLevel int) *Server {
in := make(chan GameCommandInterface, CommandQueueSize)
out := make(chan GameCommandInterface, CommandQueueSize)
s := &Server{I: si, In: in, Out: out, Games: make(map[int]*Game), created: time.Now(), logLevel: logLevel}
var (
g *Game
err error
)
g, err = s.NewGame()
if err != nil {
log.Fatal(err)
}
g.Eternal = true
g.Name = "No speed limit"
g, err = s.NewGame()
if err != nil {
log.Fatal(err)
}
g.Eternal = true
g.Name = "Speed limit 100"
g.SpeedLimit = 100
g, err = s.NewGame()
if err != nil {
log.Fatal(err)
}
g.Eternal = true
g.Name = "Speed limit 40"
g.SpeedLimit = 40
s.NewPlayers = make(chan *IncomingPlayer, CommandQueueSize)
go s.accept()
go s.handle()
for _, serverInterface := range si {
serverInterface.Host(s.NewPlayers)
}
return s
}
func (s *Server) NewGame() (*Game, error) {
s.Lock()
defer s.Unlock()
gameID := 1
for {
if _, ok := s.Games[gameID]; !ok {
break
}
gameID++
}
draw := make(chan event.DrawObject)
go func() {
for range draw {
}
}()
logger := make(chan string, LogQueueSize)
go func() {
for msg := range logger {
s.Log(fmt.Sprintf("Game %d: %s", gameID, msg))
}
}()
g, err := NewGame(4, nil, logger, draw)
if err != nil {
return nil, err
}
g.ID = gameID
g.LogLevel = s.logLevel
s.Games[gameID] = g
return g, nil
}
func (s *Server) handle() {
for {
time.Sleep(1 * time.Minute)
s.removeTerminatedGames()
}
}
func (s *Server) removeTerminatedGames() {
s.Lock()
defer s.Unlock()
for gameID, g := range s.Games {
g.Lock()
if !g.Terminated {
g.Unlock()
continue
}
delete(s.Games, gameID)
g.Unlock()
}
}
func (s *Server) FindGame(p *Player, gameID int, newGame ListedGame) *Game {
var (
g *Game
err error
)
if newGame.Name != "" {
// Create a custom game
g, err = s.NewGame()
if err != nil {
log.Fatalf("failed to create custom game: %s", err)
}
g.Lock()
g.Name = GameName(newGame.Name)
g.MaxPlayers = newGame.MaxPlayers
if g.MaxPlayers < 0 {
g.MaxPlayers = 0
} else if g.MaxPlayers > 999 {
g.MaxPlayers = 999
}
g.SpeedLimit = newGame.SpeedLimit
if g.SpeedLimit < 0 {
g.SpeedLimit = 0
} else if g.SpeedLimit > 999 {
g.SpeedLimit = 999
}
g.Unlock()
} else if gameID > 0 {
// Join a game by its ID
s.Lock()
gm := s.Games[gameID]
s.Unlock()
if gm != nil {
gm.Lock()
canJoin := !gm.Terminated && (gm.MaxPlayers == 0 || len(gm.Players) < gm.MaxPlayers)
gm.Unlock()
if canJoin {
g = gm
} else {
p.Write(&GameCommandMessage{Message: "Failed to join game - Player limit reached"})
return nil
}
} else {
p.Write(&GameCommandMessage{Message: "Failed to join game - Invalid game ID"})
return nil
}
} else if gameID == 0 {
// Join any game
s.Lock()
for _, gm := range s.Games {
gm.Lock()
if !gm.Terminated && (gm.MaxPlayers == 0 || len(gm.Players) < gm.MaxPlayers) {
gm.Unlock()
g = gm
break
}
gm.Unlock()
}
s.Unlock()
} else {
// Create a local game
g, err = s.NewGame()
if err != nil {
log.Fatalf("failed to create local game: %s", err)
}
g.Local = true
}
if g == nil {
p.Write(&GameCommandMessage{Message: "Failed to join game"})
return nil
}
g.Lock()
g.AddPlayerL(p)
if gameID == event.GameIDNewLocal {
go g.Start(0)
} else if len(g.Players) > 1 {
go s.initiateAutoStart(g)
} else if !g.Started {
p.Write(&GameCommandMessage{Message: "Waiting for at least two players to join..."})
}
g.Unlock()
return g
}
func (s *Server) accept() {
for {
np := <-s.NewPlayers
p := NewPlayer(np.Name, np.Conn)
go s.handleNewPlayer(p)
}
}
func (s *Server) handleNewPlayer(pl *Player) {
handled := false
go func() {
time.Sleep(10 * time.Second)
if !handled {
pl.Close()
}
}()
for e := range pl.In {
switch e.Command() {
case CommandListGames:
if _, ok := e.(*GameCommandListGames); ok {
var gl []*ListedGame
s.Lock()
for _, g := range s.Games {
g.Lock()
if g.Terminated {
g.Unlock()
continue
}
gl = append(gl, &ListedGame{ID: g.ID, Name: g.Name, Players: len(g.Players), MaxPlayers: g.MaxPlayers, SpeedLimit: g.SpeedLimit})
g.Unlock()
}
s.Unlock()
sort.Slice(gl, func(i, j int) bool {
if gl[i].Players == gl[j].Players {
return gl[i].Name < gl[j].Name
}
return gl[i].Players > gl[j].Players
})
pl.Write(&GameCommandListGames{Games: gl})
}
case CommandJoinGame:
if p, ok := e.(*GameCommandJoinGame); ok {
pl.Name = Nickname(p.Name)
g := s.FindGame(pl, p.GameID, p.Listing)
if g == nil {
return
}
if p.Listing.Name == "" {
g.Logf(LogStandard, "Player %s joined %s", pl.Name, g.Name)
} else {
g.Logf(LogStandard, "Player %s created new game %s", pl.Name, g.Name)
}
go s.handleGameCommands(pl, g)
handled = true
return
}
}
}
}
func (s *Server) initiateAutoStart(g *Game) {
g.Lock()
defer g.Unlock()
if g.Starting || g.Started {
return
}
g.Starting = true
go func() {
g.WriteMessage("Starting game...")
time.Sleep(2 * time.Second)
g.Start(0)
}()
}
func (s *Server) handleGameCommands(pl *Player, g *Game) {
var (
msgJSON []byte
err error
)
for e := range pl.In {
c := e.Command()
if (c != CommandPing && c != CommandPong && c != CommandUpdateMatrix) || g.LogLevel >= LogVerbose {
msgJSON, err = json.Marshal(e)
if err != nil {
log.Fatal(err)
}
g.Logf(LogStandard, "%d -> %s %s", e.Source(), e.Command(), msgJSON)
}
g.Lock()
switch p := e.(type) {
case *GameCommandDisconnect:
g.RemovePlayerL(p.SourcePlayer)
case *GameCommandMessage:
if player, ok := g.Players[p.SourcePlayer]; ok {
s.Logf("<%s> %s", player.Name, p.Message)
msg := strings.ReplaceAll(strings.TrimSpace(p.Message), "\n", "")
if msg != "" {
g.WriteAllL(&GameCommandMessage{Player: p.SourcePlayer, Message: msg})
}
}
case *GameCommandNickname:
if player, ok := g.Players[p.SourcePlayer]; ok {
newNick := Nickname(p.Nickname)
if newNick != "" && newNick != player.Name {
oldNick := player.Name
player.Name = newNick
g.Logf(LogStandard, "* %s is now known as %s", oldNick, newNick)
g.WriteAllL(&GameCommandNickname{Player: p.SourcePlayer, Nickname: newNick})
}
}
case *GameCommandUpdateMatrix:
if pl, ok := g.Players[p.SourcePlayer]; ok {
for _, m := range p.Matrixes {
pl.Matrix.Replace(m)
if g.SpeedLimit > 0 && m.Speed > g.SpeedLimit+5 && time.Since(g.TimeStarted) > 7*time.Second {
pl.Matrix.SetGameOver()
g.WriteMessage(fmt.Sprintf("%s went too fast and crashed", pl.Name))
g.WriteAllL(&GameCommandGameOver{Player: p.SourcePlayer})
}
}
m := pl.Matrix
spawn := m.SpawnLocation(m.P)
if m.P != nil && spawn.X >= 0 && spawn.Y >= 0 && m.P.X != spawn.X {
pl.Moved = time.Now()
pl.Idle = 0
}
}
case *GameCommandGameOver:
g.Players[p.SourcePlayer].Matrix.SetGameOver()
g.WriteMessage(fmt.Sprintf("%s was knocked out", g.Players[p.SourcePlayer].Name))
g.WriteAllL(&GameCommandGameOver{Player: p.SourcePlayer})
case *GameCommandSendGarbage:
leastGarbagePlayer := -1
leastGarbage := -1
for playerID, player := range g.Players {
if playerID == p.SourcePlayer || player.Matrix.GameOver {
continue
}
if leastGarbage == -1 || player.totalGarbageReceived < leastGarbage {
leastGarbagePlayer = playerID
leastGarbage = player.totalGarbageReceived
}
}
if leastGarbagePlayer != -1 {
g.Players[leastGarbagePlayer].totalGarbageReceived += p.Lines
g.Players[leastGarbagePlayer].pendingGarbage += p.Lines
g.Players[p.SourcePlayer].totalGarbageSent += p.Lines
}
case *GameCommandStats:
go func(p *Player) {
players := 0
games := 0
s.Lock()
for _, g := range s.Games {
players += len(g.Players)
games++
}
s.Unlock()
p.Write(&GameCommandStats{Created: s.created, Players: players, Games: games})
}(g.Players[p.SourcePlayer])
}
g.Unlock()
}
}
func (s *Server) Listen(address string) {
var network string
network, address = NetworkAndAddress(address)
listener, err := net.Listen(network, address)
if err != nil {
log.Fatalf("failed to listen on %s: %s", address, err)
}
s.listeners = append(s.listeners, listener)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
s.NewPlayers <- &IncomingPlayer{Name: "Anonymous", Conn: NewServerConn(conn, nil)}
}
}
func (s *Server) StopListening() {
for i := range s.listeners {
s.listeners[i].Close()
}
}
func (s *Server) Log(a ...interface{}) {
if s.Logger == nil {
return
}
s.Logger <- fmt.Sprint(a...)
}
func (s *Server) Logf(format string, a ...interface{}) {
if s.Logger == nil {
return
}
s.Logger <- fmt.Sprintf(format, a...)
}
func NetworkAndAddress(address string) (string, string) {
var network string
if strings.ContainsAny(address, `\/`) {
network = "unix"
} else {
network = "tcp"
if !strings.Contains(address, `:`) {
address = fmt.Sprintf("%s:%d", address, DefaultPort)
}
}
return network, address
}