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 }