first commit
This commit is contained in:
commit
afb79eaceb
|
@ -0,0 +1,2 @@
|
||||||
|
.idea/
|
||||||
|
*.iml
|
|
@ -0,0 +1,25 @@
|
||||||
|
# ResidentSleeper
|
||||||
|
|
||||||
|
A plugin-less, mod-less sleep voting system using log parsing, only available on Linux.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
Plugins exist that let you define a certain percentage of users that need to sleep, but they're plugins. They don't work on snapshots/etc.
|
||||||
|
|
||||||
|
## How
|
||||||
|
|
||||||
|
Log files are generated by the Minecraft server under `logs/latest.log`, which can be read in a fashion similar to `tail -F` to constantly receive updates. Commands are then executed via RCON that's connected using a configuration parser.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Requirements: Linux, RCON
|
||||||
|
|
||||||
|
Use supervisord or similar to run the program as your Minecraft user, with the flag `-dir /path/to/server`.
|
||||||
|
|
||||||
|
The following configuration values are required in server.properties:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
rcon.port=25575
|
||||||
|
rcon.password=password
|
||||||
|
enable-rcon=true
|
||||||
|
```
|
|
@ -0,0 +1,76 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func sendMessage(user, message string) {
|
||||||
|
_, err := client.SendCommand(fmt.Sprintf("msg %s %s", user, message))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
sendMessage(user, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func serverMessage(message string) {
|
||||||
|
_, err := client.SendCommand("say " + message)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
serverMessage(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func onlineUsers() (int, int, []string, error) {
|
||||||
|
res, err := client.SendCommand("list")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return -1, -1, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
m := userRegexp.FindStringSubmatch(res)
|
||||||
|
|
||||||
|
if m == nil {
|
||||||
|
return -1, -1, nil, errors.New("unexpected response")
|
||||||
|
}
|
||||||
|
|
||||||
|
online, _ := strconv.Atoi(m[1])
|
||||||
|
max, _ := strconv.Atoi(m[2])
|
||||||
|
names := strings.Split(m[3], ", ")
|
||||||
|
|
||||||
|
return online, max, names, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryTime() (int, error) {
|
||||||
|
res, err := client.SendCommand("time query daytime")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
m := timeRegexp.FindStringSubmatch(res)
|
||||||
|
|
||||||
|
if m == nil {
|
||||||
|
return -1, errors.New("no time found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return strconv.Atoi(m[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func addTime(ticks int) (int, error) {
|
||||||
|
res, err := client.SendCommand(fmt.Sprintf("time add %d", ticks))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
m := timeRegexp.FindStringSubmatch(res)
|
||||||
|
|
||||||
|
if m == nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return strconv.Atoi(m[1])
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func loadServerConfig(configPath string) (map[string]string, error) {
|
||||||
|
f, err := os.Open(configPath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
|
||||||
|
var text string
|
||||||
|
var m []string
|
||||||
|
|
||||||
|
ret := make(map[string]string)
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
text = scanner.Text()
|
||||||
|
|
||||||
|
if text[0] == '#' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
m = configRegexp.FindStringSubmatch(text)
|
||||||
|
|
||||||
|
if m == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ret[strings.TrimSpace(m[1])] = strings.TrimSpace(m[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
module sleepvote
|
||||||
|
|
||||||
|
go 1.12
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7 // indirect
|
||||||
|
github.com/hpcloud/tail v1.0.0
|
||||||
|
github.com/tystuyfzand/mcgorcon v0.0.0-20190416171454-d0d528ef5548
|
||||||
|
golang.org/x/sys v0.0.0-20190415145633-3fd5a3612ccd // indirect
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7 // indirect
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||||
|
)
|
|
@ -0,0 +1,12 @@
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/tystuyfzand/mcgorcon v0.0.0-20190416171454-d0d528ef5548 h1:ho5QHzfPALI0+v7sPzJFtSHDH/5amyuG88+3ycp70Rk=
|
||||||
|
github.com/tystuyfzand/mcgorcon v0.0.0-20190416171454-d0d528ef5548/go.mod h1:MpDGxcw1VVpnQrSbEjy5ZRc+RVgO3j68N8RuI8snkF4=
|
||||||
|
golang.org/x/sys v0.0.0-20190415145633-3fd5a3612ccd h1:MNN7PRW7zYXd8upVO5qfKeOnQG74ivRNv7sz4k4cQMs=
|
||||||
|
golang.org/x/sys v0.0.0-20190415145633-3fd5a3612ccd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
|
@ -0,0 +1,298 @@
|
||||||
|
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
|
||||||
|
}
|
Loading…
Reference in New Issue