Select Git revision
pluginapi.go
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)
}
}