298 lines
5.5 KiB
Go
298 lines
5.5 KiB
Go
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"github.com/hpcloud/tail"
|
|
"github.com/tystuyfzand/mcgorcon"
|
|
"log"
|
|
"math"
|
|
"os"
|
|
"path"
|
|
"regexp"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
timeThreadRegexp = "^\\[.*?\\]\\s\\[.*?\\/INFO\\]:\\s"
|
|
|
|
tickDuration = time.Millisecond * 50
|
|
)
|
|
|
|
var (
|
|
serverPath string
|
|
rconPort int
|
|
rconPassword string
|
|
|
|
client *mcgorcon.Client
|
|
|
|
messageRegexp = regexp.MustCompile(timeThreadRegexp + "<(.*?)>\\s(.*)")
|
|
joinedRegexp = regexp.MustCompile(timeThreadRegexp + "(.*?) joined the game$")
|
|
leftRegexp = regexp.MustCompile(timeThreadRegexp + "(.*?) left the game$")
|
|
stoppingRegexp = regexp.MustCompile(timeThreadRegexp + "Stopping server")
|
|
rconRegexp = regexp.MustCompile(timeThreadRegexp + "RCON running on")
|
|
|
|
sleepRegexp = regexp.MustCompile("^z{3,}$")
|
|
timeRegexp = regexp.MustCompile("(\\d+)$")
|
|
userRegexp = regexp.MustCompile("There are (\\d+) of a max (\\d+) players online: (.*?)$")
|
|
|
|
configRegexp = regexp.MustCompile("^(.*?)=(.*)$")
|
|
)
|
|
|
|
func init() {
|
|
flag.StringVar(&serverPath, "dir", "", "server directory")
|
|
}
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
|
|
// Load properties
|
|
configPath := path.Join(serverPath, "server.properties")
|
|
|
|
cfg, err := loadServerConfig(configPath)
|
|
|
|
if err != nil {
|
|
log.Fatalln("Unable to load config:", err)
|
|
}
|
|
|
|
if rconEnabled, ok := cfg["enable-rcon"]; !ok || rconEnabled != "true" {
|
|
log.Fatalln("RCON not enabled.")
|
|
}
|
|
|
|
var rconPortStr string
|
|
var ok bool
|
|
|
|
if rconPortStr, ok = cfg["rcon.port"]; !ok {
|
|
log.Fatalln("RCON is not enabled: No port set")
|
|
}
|
|
|
|
rconPort, _ = strconv.Atoi(rconPortStr)
|
|
|
|
if rconPassword, ok = cfg["rcon.password"]; !ok {
|
|
log.Fatalln("RCON is not enabled: No password set")
|
|
}
|
|
|
|
go queryPlayersLoop()
|
|
|
|
logPath := path.Join(serverPath, "logs/latest.log")
|
|
|
|
log.Println("Starting rcon connection")
|
|
|
|
ensureConnection()
|
|
|
|
logParser(logPath)
|
|
}
|
|
|
|
func logParser(logPath string) {
|
|
log.Println("Watching log path ", logPath)
|
|
|
|
stat, err := os.Stat(logPath)
|
|
|
|
if err != nil {
|
|
log.Fatalln("Unable to open log file:", err)
|
|
}
|
|
|
|
seek := &tail.SeekInfo{
|
|
Offset: stat.Size(),
|
|
}
|
|
|
|
// Start parsing file
|
|
t, err := tail.TailFile(logPath, tail.Config{Location: seek, Follow: true, ReOpen: true})
|
|
|
|
if err != nil {
|
|
log.Fatalln("Unable to open file:", err)
|
|
}
|
|
|
|
var m []string
|
|
|
|
for line := range t.Lines {
|
|
|
|
if m = messageRegexp.FindStringSubmatch(line.Text); m != nil {
|
|
handleMessage(m[1], m[2])
|
|
} else if m = joinedRegexp.FindStringSubmatch(line.Text); m != nil {
|
|
userJoined(m[1])
|
|
} else if m = leftRegexp.FindStringSubmatch(line.Text); m != nil {
|
|
userLeft(m[1])
|
|
} else if stoppingRegexp.MatchString(line.Text) {
|
|
log.Println("Server closing")
|
|
|
|
if client != nil {
|
|
// Close the server connection to allow it to exit normally
|
|
client.Close()
|
|
}
|
|
} else if rconRegexp.MatchString(line.Text) {
|
|
log.Println("Rcon started, connecting")
|
|
|
|
if client != nil {
|
|
client.Close()
|
|
}
|
|
|
|
// Reconnect, as we got a new rcon start line
|
|
ensureConnection()
|
|
}
|
|
}
|
|
}
|
|
|
|
var (
|
|
votes = make(map[string]bool)
|
|
voteLock sync.RWMutex
|
|
|
|
onlinePlayers int
|
|
expireTime time.Time
|
|
)
|
|
|
|
func queryPlayersLoop() {
|
|
t := time.NewTicker(30 * time.Second)
|
|
|
|
for {
|
|
if client == nil {
|
|
<- t.C
|
|
continue
|
|
}
|
|
|
|
online, _, _, err := onlineUsers()
|
|
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
onlinePlayers = online
|
|
|
|
<- t.C
|
|
}
|
|
}
|
|
|
|
func handleMessage(user, message string) {
|
|
if !sleepRegexp.MatchString(message) {
|
|
return
|
|
}
|
|
|
|
// Query time from server
|
|
// Add seconds
|
|
t, err := queryTime()
|
|
|
|
if err != nil {
|
|
sendMessage(user, "Something went wrong and the time couldn't be retrieved.")
|
|
return
|
|
}
|
|
|
|
if t < 12000 {
|
|
sendMessage(user, "It's not night time, go mine some more.")
|
|
return
|
|
}
|
|
|
|
difference := 24000 - t
|
|
|
|
if expireTime.IsZero() || time.Now().After(expireTime) {
|
|
expDuration := time.Duration(difference) * tickDuration
|
|
|
|
expireTime = time.Now().Add(expDuration)
|
|
|
|
// Reset the time after
|
|
time.AfterFunc(expDuration, func() {
|
|
voteLock.Lock()
|
|
votes = make(map[string]bool)
|
|
voteLock.Unlock()
|
|
})
|
|
}
|
|
|
|
voteLock.RLock()
|
|
if _, exists := votes[user]; exists {
|
|
voteLock.RUnlock()
|
|
return
|
|
}
|
|
voteLock.RUnlock()
|
|
|
|
voteLock.Lock()
|
|
votes[user] = true
|
|
voteLock.Unlock()
|
|
|
|
requiredVotes := int(math.Ceil(float64(onlinePlayers) * 0.30))
|
|
|
|
if len(votes) >= requiredVotes {
|
|
mimicSleeping(t)
|
|
} else {
|
|
serverMessage(fmt.Sprintf("%s wants to sleep (%d of %d votes)", user, len(votes), requiredVotes))
|
|
}
|
|
}
|
|
|
|
func mimicSleeping(t int) {
|
|
var err error
|
|
|
|
if t == -1 {
|
|
t, err = queryTime()
|
|
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
voteLock.Lock()
|
|
votes = make(map[string]bool)
|
|
voteLock.Unlock()
|
|
|
|
difference := 24000 - t
|
|
|
|
log.Println("Adding time", difference)
|
|
|
|
t, err = addTime(difference)
|
|
|
|
if err != nil || t > 12000 {
|
|
serverMessage("Could not set the time, sorry")
|
|
return
|
|
}
|
|
|
|
serverMessage("Good morning everyone!")
|
|
|
|
// Force the vote to expire
|
|
expireTime = time.Now()
|
|
}
|
|
|
|
func userJoined(user string) {
|
|
onlinePlayers++
|
|
|
|
log.Println("Player joined:", user)
|
|
}
|
|
|
|
func userLeft(user string) {
|
|
onlinePlayers--
|
|
|
|
log.Println("Player left:", user)
|
|
|
|
voteLock.Lock()
|
|
defer voteLock.Unlock()
|
|
|
|
if _, exists := votes[user]; exists {
|
|
delete(votes, user)
|
|
}
|
|
|
|
requiredVotes := int(math.Ceil(float64(onlinePlayers) * 0.30))
|
|
|
|
if len(votes) >= requiredVotes {
|
|
mimicSleeping(-1)
|
|
}
|
|
}
|
|
|
|
func ensureConnection() *mcgorcon.Client {
|
|
if client == nil {
|
|
c, err := mcgorcon.Dial("localhost", rconPort, rconPassword)
|
|
|
|
tries := 0
|
|
|
|
for err != nil {
|
|
c, err = mcgorcon.Dial("localhost", rconPort, rconPassword)
|
|
|
|
tries++
|
|
|
|
if tries >= 10 {
|
|
log.Fatalln("Unable to connect to rcon, giving up.")
|
|
}
|
|
}
|
|
|
|
client = &c
|
|
}
|
|
|
|
return client
|
|
} |