residentsleeper/main.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
}