Skip to content
Snippets Groups Projects
Select Git revision
  • master
  • slidy
  • wobbly
3 results

pluginapi.go

Blame
  • pluginapi.go 4.76 KiB
    package main
    
    import (
    	"bufio"
    	"encoding/json"
    	"fmt"
    	"io/ioutil"
    	"log"
    	"net"
    	"strings"
    	"time"
    )
    
    //
    // BEGIN PUSH MESSAGE API
    //
    
    // PApiStatusCode allows the external plugin to determine the
    // type of PApiMessage.data
    type PApiStatusCode int
    
    const (
    	pApiPlayerConnected    PApiStatusCode = iota // Data = player.Id int
    	pApiPlayerDisconnected                       // Data = player.Id int
    )
    
    type PApiMessage struct {
    	Code PApiStatusCode
    	Data interface{}
    }
    
    type PApiMessageJson []byte
    
    type LobbyServerInfo struct {
    	ActivePlayerCount int
    	SpectatorCount    int
    	MaxPlayerCount    int
    }
    
    func NewPApiMessageConnected(p *Player) PApiMessageJson {
    	return newPApiMessage(pApiPlayerConnected, p)
    }
    
    func NewPApiMessageDisconnected(p *Player) PApiMessageJson {
    	return newPApiMessage(pApiPlayerDisconnected, p)
    }
    
    func newPApiMessage(code PApiStatusCode, data interface{}) PApiMessageJson {
    	m := PApiMessage{code, data}
    	j, err := json.Marshal(m)
    	if err != nil {
    		// Marshal fails iff its parameters contain non serialisable data
    		// We must never let that happen anyway
    		panic(err)
    	}
    	return PApiMessageJson(j)
    }
    
    //
    // END PUSH MESSAGE API
    //
    
    type PType int
    
    const (
    	pNotify PType = iota
    	pBot
    )
    
    type Plugin interface {
    	IsFailed() bool
    }
    
    type NotifyPlugin struct {
    	failed bool
    	writer net.Conn
    }
    
    func (p *NotifyPlugin) IsFailed() bool {
    	return p.failed
    }
    
    func (p *NotifyPlugin) disconnect() {
    	p.writer.Close()
    }
    
    var (
    	pluginChan = make(chan Plugin, 10)
    	notifyPlugins    = make([]*NotifyPlugin, 0)
    	botPlugins    = make([]*BotPlugin, 0)
    	// executes func synchronous in the next game loop
    	queryClosureChan = make(chan func())
    )
    
    func PluginServerLoop(port int) {
    	addr, err := net.ResolveTCPAddr("tcp", ":"+fmt.Sprintf("%d", port))
    	if err != nil {
    		log.Fatal(err)
    		return
    	}
    	ln, err := net.ListenTCP("tcp", addr)
    	if err != nil {
    		log.Fatal(err)
    		return
    	}
    	fmt.Println("Plugin server started")
    	for {
    		conn, err := ln.AcceptTCP()
    		go func() {
    			conn.SetKeepAlive(true)
    			if err != nil {
    				log.Println(err)
    				return
    			}
    			conn.SetReadDeadline(time.Now().Add(connectionConfig.PluginTimeout * time.Millisecond))
    			reader := bufio.NewReader(conn)
    			initialQuery, err := reader.ReadString('\n')
    			if err != nil {
    				log.Println(err)
    				return
    			}
    
    			switch initialQuery {
    			case "getserverinfo\n":
    				fin := make(chan struct{}, 1)
    				info := LobbyServerInfo{0, 0, gameConfig.MaxPlayerCount}
    				queryClosureChan <- func() {
    					info.ActivePlayerCount = activePlayerCount
    					info.SpectatorCount = len(players) - info.ActivePlayerCount
    					fin <- struct{}{}
    				}
    				<-fin
    				// ignore error, marshal can only fail if data types can not be marshalled
    				infoJson, _ := json.Marshal(info)
    				conn.Write(infoJson)
    				conn.Write([]byte{'\n'})
    				conn.Close()
    			case "gethelp\n":
    				help, err := ioutil.ReadFile("help.txt")
    				if err != nil {
    					log.Println("PluginAPI: Failed to read help")
    					log.Println(err.Error())
    					conn.Write([]byte("Help unavailable"))
    					conn.Close()
    				}
    				conn.Write(help)
    				conn.Close()
    			case "register\n":
    				typeString, err := reader.ReadString('\n')
    				if err != nil {
    					log.Println(err)
    					return
    				}
    				typeString = strings.TrimSpace(typeString)
    				var plugin Plugin
    				switch typeString {
    				case "notify":
    					plugin = &NotifyPlugin{
    						failed: false,
    						writer: conn,
    					}
    					pluginChan <- plugin
    				default:
    					// "bot" or "bot_<name>"
    					if typeString == "bot" {
    						typeString = "bot_unknown"
    					}
    					if len(typeString) < 5 {
    						return;
    					}
    					if typeString[:4] != "bot_" {
    						return;
    					}
    					j, _ := json.Marshal(botAPIVersion)
    					j = append(j, '\n')
    					conn.Write(j)
    					// TODO use player idle timeouts for bots
    					conn.SetDeadline(time.Time{})
    					registerChan<-NewBotPlayer(conn, typeString[4:])
    				}
    			}
    		}()
    	}
    }
    
    func DeregisterFailedPlugins() {
    	tmpNPlugins := notifyPlugins[:0]
    	for i, p := range notifyPlugins {
    		if p.IsFailed() {
    			p.disconnect()
    			notifyPlugins[i] = nil
    			continue
    		}
    		tmpNPlugins = append(tmpNPlugins, p)
    	}
    	notifyPlugins = tmpNPlugins
    
    	tmpPPlugins := botPlugins[:0]
    	for i, p := range botPlugins {
    		if p.IsFailed() {
    			p.player.disconnect("")
    			botPlugins[i] = nil
    			continue
    		}
    		tmpPPlugins = append(tmpPPlugins, p)
    	}
    	botPlugins = tmpPPlugins
    }
    
    // Synchronous with game-loop
    func RegisterPlugin(plugin Plugin) {
    	switch plugin.(type) {
    	case *NotifyPlugin:
    		notifyPlugins = append(notifyPlugins, plugin.(*NotifyPlugin))
    	}
    }
    
    // Synchronous with game-loop
    func (j PApiMessageJson) Send() {
    	for _, p := range notifyPlugins {
    		go func(p *NotifyPlugin) {
    			p.writer.SetDeadline(time.Now().Add(connectionConfig.PluginTimeout * time.Millisecond))
    			_, err := p.writer.Write(append(j, '\n'))
    			if err != nil {
    				p.failed = true
    			}
    		}(p)
    	}
    }