Major updates/patches, functionality, api
continuous-integration/drone/push Build is failing Details

This commit is contained in:
Tyler 2020-07-09 21:01:29 -04:00
parent afb79eaceb
commit b52b38179a
32 changed files with 1798 additions and 302 deletions

16
.drone.yml Normal file
View File

@ -0,0 +1,16 @@
kind: pipeline
type: docker
name: default
steps:
- name: docker
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: registry.meow.tf/tyler/residentsleeper
registry: registry.meow.tf
tags:
- latest

3
.gitignore vendored
View File

@ -1,2 +1,3 @@
.idea/ .idea/
*.iml *.iml
*.log

9
Dockerfile Normal file
View File

@ -0,0 +1,9 @@
FROM golang:alpine AS builder
RUN go build -o sleeper
FROM alpine
COPY --from=builder sleeper /sleeper
CMD ["/sleeper"]

View File

@ -1,76 +0,0 @@
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])
}

88
commands/arguments.go Normal file
View File

@ -0,0 +1,88 @@
package commands
import "strings"
const (
ArgumentTypeBasic = iota
ArgumentTypeUserMention
ArgumentTypeChannelMention
)
func parseCommandUsage(prefix string) *CommandHolder {
holder := &CommandHolder{}
holder.Name = prefix
holder.Usage = prefix
if idx := strings.Index(prefix, " "); idx != -1 {
holder.Name = prefix[0:idx]
prefix = prefix[idx+1:]
// Parse out command arguments, example:
// test <arg1> [optional arg2]
// Walk through string, match < and >, [ and ]
holder.Arguments = make(map[string]*CommandArgument)
str := prefix
var name string
var index int
for {
if len(str) == 0 {
break
}
ch := str[0]
if ch == '<' || ch == '[' {
// Scan until closing arrow or end of string
for i := 1; i < len(str); i++ {
if (str[i] == '>' || str[i] == ']') && str[i-1] != '\\' {
name = str[1:i]
if i+2 < len(str) {
str = str[i+2:]
} else {
str = ""
}
required := false
if ch == '<' {
required = true
holder.RequiredArgumentCount++
}
t := ArgumentTypeBasic
if name[0] == '@' {
t = ArgumentTypeUserMention
name = name[1:]
} else if name[0] == '#' {
t = ArgumentTypeChannelMention
name = name[1:]
}
holder.Arguments[name] = &CommandArgument{
Index: index,
Name: name,
Required: required,
Type: t,
}
index++
break
}
}
}
}
holder.ArgumentCount = len(holder.Arguments)
}
return holder
}

15
commands/context.go Normal file
View File

@ -0,0 +1,15 @@
package commands
type CommandContext struct {
holder *CommandHolder
User string
Prefix string
Command string
ArgumentString string
Arguments []string
ArgumentCount int
Reply func(text string)
}

69
commands/definition.go Normal file
View File

@ -0,0 +1,69 @@
package commands
import "errors"
type CommandHolder struct {
Name string
Usage string
Handler CommandHandler
Permission int
Arguments map[string]*CommandArgument
ArgumentCount int
RequiredArgumentCount int
}
type CommandArgument struct {
Index int
Name string
Required bool
Type int
}
func (c *CommandHolder) Call(ctx *CommandContext) {
ctx.holder = c
if c.ArgumentCount > 0 {
// Arguments are cached, construct usage
if err := c.Validate(ctx); err != nil {
if err == UsageError {
ctx.Reply("Usage: " + ctx.Prefix + c.Usage)
} else {
ctx.Reply(err.Error())
}
return
}
}
// Use a goroutine to avoid any blocking operations
c.Handler(ctx)
}
var (
UsageError = errors.New("usage")
)
func (c *CommandHolder) Validate(ctx *CommandContext) error {
if ctx.ArgumentCount < c.RequiredArgumentCount {
return UsageError
}
var argValue string
for _, arg := range c.Arguments {
if ctx.ArgumentCount < arg.Index+1 {
break
}
if !arg.Required {
continue
}
argValue = ctx.Arguments[arg.Index]
if argValue == "" {
return errors.New("The " + arg.Name + " argument is required.")
}
}
return nil
}

35
commands/dispatcher.go Normal file
View File

@ -0,0 +1,35 @@
package commands
import "strings"
type CommandHandler func(ctx *CommandContext)
var (
commandMap = make(map[string]*CommandHolder)
)
func Register(prefix string, f CommandHandler) {
h := parseCommandUsage(prefix)
h.Handler = f
commandMap[h.Name] = h
}
func Find(prefix, name, full string) *CommandHolder {
if strings.Index(name, prefix) == 0 {
name = name[len(prefix):]
if holder := Handler(name); holder != nil {
return holder
}
}
return nil
}
func Handler(name string) *CommandHolder {
if holder, ok := commandMap[name]; ok {
return holder
}
return nil
}

71
commands/parser.go Normal file
View File

@ -0,0 +1,71 @@
package commands
type SpaceTokenizer struct {
Input string
}
func (t *SpaceTokenizer) NextToken() string {
if len(t.Input) == 0 {
return ""
}
ch := t.Input[0]
if ch == '"' {
// Scan until closing quote or end of string
for i := 1; i < len(t.Input); i++ {
if t.Input[i] == '"' && t.Input[i-1] != '\\' {
ret := t.Input[1:i]
if i+2 < len(t.Input) {
t.Input = t.Input[i+2:]
} else {
t.Input = ""
}
return ret
}
}
} else {
for i := 0; i < len(t.Input); i++ {
if t.Input[i] == ' ' {
ret := t.Input[0:i]
if i+1 < len(t.Input) {
t.Input = t.Input[i+1:]
} else {
t.Input = ""
}
return ret
}
}
}
ret := t.Input
t.Input = ""
return ret
}
func (t *SpaceTokenizer) Empty() bool {
return t.Input == ""
}
func NewSpaceTokenizer(input string) *SpaceTokenizer {
return &SpaceTokenizer{Input: input}
}
func ParseCommandArguments(command string) []string {
tokenizer := NewSpaceTokenizer(command)
arguments := make([]string, 0)
for {
if tokenizer.Empty() {
break
}
arguments = append(arguments, tokenizer.NextToken())
}
return arguments
}

69
commands/parser_test.go Normal file
View File

@ -0,0 +1,69 @@
package commands
import (
"regexp"
"testing"
)
func TestParseCommandArguments(t *testing.T) {
args := ParseCommandArguments("normal \"testing quoted\" and normal \"end closed\"")
expected := []string{"normal", "testing quoted", "and", "normal", "end closed"}
for i := 0; i < len(expected); i++ {
if args[i] != expected[i] {
t.Errorf("Expected %s, got %s at index %d", expected[i], args[i], i)
}
}
}
func TestDispatching(t *testing.T) {
Register("testing", func(ctx *CommandContext) {
// Something
})
if handler := Find("!", "!testing", "testing"); handler == nil {
t.Error("Expected handler for testing")
}
}
func TestRegexpDispatching(t *testing.T) {
RegisterMatch(regexp.MustCompile("^compliment"), func(ctx *CommandContext) {
// Something
})
if handler := Find("", "compliment", "compliment me"); handler == nil {
t.Error("Expected handler for compliment")
}
}
func TestArgumentParsing(t *testing.T) {
holder := parseCommandUsage("test <arg1> <arg2> [arg3]")
if holder.Name != "test" {
t.Error("Unexpected command name")
}
if len(holder.Arguments) != 3 {
t.Error("Invalid number of command arguments:", len(holder.Arguments))
}
if holder.RequiredArgumentCount != 2 {
t.Error("Invalid required argument count")
}
}
func TestArgumentDispatching(t *testing.T) {
holder := parseCommandUsage("test <arg1> <arg2> [arg3]")
if holder.Name != "test" {
t.Error("Unexpected command name")
}
if len(holder.Arguments) != 3 {
t.Error("Invalid number of command arguments:", len(holder.Arguments))
}
if holder.RequiredArgumentCount != 2 {
t.Error("Invalid required argument count")
}
}

54
events/events.go Normal file
View File

@ -0,0 +1,54 @@
package events
import (
"sync"
)
type eventHandler func(args ...interface{})
var (
eventMap = make(map[string][]eventHandler)
eventMapLock sync.RWMutex
allListeners = make([]eventHandler, 0)
initialized = false
)
func On(name string, f eventHandler) {
eventMapLock.Lock()
defer eventMapLock.Unlock()
handlers, exists := eventMap[name]
if !exists {
handlers = make([]eventHandler, 0)
}
handlers = append(handlers, f)
eventMap[name] = handlers
}
func OnALl(f eventHandler) {
allListeners = append(allListeners, f)
}
func Call(name string, args ...interface{}) {
if !initialized && name != Init {
Call(Init)
initialized = true
}
eventMapLock.RLock()
handlers, exists := eventMap[name]
eventMapLock.RUnlock()
if !exists {
return
}
for _, handler := range handlers {
handler(args...)
}
}

29
events/names.go Normal file
View File

@ -0,0 +1,29 @@
package events
const (
Init = "init"
ServerStarted = "server_started"
ServerClosing = "server_closing"
// LoggedIn, Args: User, Address, X, Y, Z
LoggedIn = "logged_in"
// Authenticated, Args: User, UUID
Authenticated = "authenticated"
// Join, Args: User
Join = "join"
// Leave, Args: User
Leave = "leave"
// Op, Args: User, Source?
Op = "op"
// Deop, Args: User, Source?
Deop = "deop"
// Message, Args: User, Message
Message = "message"
)

9
go.mod
View File

@ -1,12 +1,17 @@
module sleepvote module meow.tf/residentsleeper
go 1.12 go 1.12
require ( require (
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/fsnotify/fsnotify v1.4.7 // indirect github.com/fsnotify/fsnotify v1.4.7 // indirect
github.com/hpcloud/tail v1.0.0 github.com/hpcloud/tail v1.0.0
github.com/tystuyfzand/mcgorcon v0.0.0-20190416171454-d0d528ef5548 github.com/ppacher/nbt v0.0.0-20181201174858-0cad976cf07c
github.com/tystuyfzand/mcgorcon v0.0.0-20190418232414-4bb1402707f4
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583
golang.org/x/sys v0.0.0-20190415145633-3fd5a3612ccd // indirect golang.org/x/sys v0.0.0-20190415145633-3fd5a3612ccd // indirect
gopkg.in/fsnotify.v1 v1.4.7 // indirect gopkg.in/fsnotify.v1 v1.4.7 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
layeh.com/gopher-json v0.0.0-20190114024228-97fed8db8427
layeh.com/gopher-luar v1.0.5
) )

18
go.sum
View File

@ -1,12 +1,26 @@
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 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/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 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 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/ppacher/nbt v0.0.0-20181201174858-0cad976cf07c h1:d9Pm+C0vGwMRxn04D6MjHbqkEvG+qlpHjlARadAAmpQ=
github.com/tystuyfzand/mcgorcon v0.0.0-20190416171454-d0d528ef5548/go.mod h1:MpDGxcw1VVpnQrSbEjy5ZRc+RVgO3j68N8RuI8snkF4= github.com/ppacher/nbt v0.0.0-20181201174858-0cad976cf07c/go.mod h1:vvnpyLNjExupwOjP8Dvqprqwtt3BxXKoDSvWkHvCODs=
github.com/tystuyfzand/mcgorcon v0.0.0-20190418232414-4bb1402707f4 h1:E2wndDHuZBEqCUijjvLhfFu8wInU9ciBZuXXkn/lGHk=
github.com/tystuyfzand/mcgorcon v0.0.0-20190418232414-4bb1402707f4/go.mod h1:MpDGxcw1VVpnQrSbEjy5ZRc+RVgO3j68N8RuI8snkF4=
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583 h1:SZPG5w7Qxq7bMcMVl6e3Ht2X7f+AAGQdzjkbyOnNNZ8=
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190415145633-3fd5a3612ccd h1:MNN7PRW7zYXd8upVO5qfKeOnQG74ivRNv7sz4k4cQMs= golang.org/x/sys v0.0.0-20190415145633-3fd5a3612ccd h1:MNN7PRW7zYXd8upVO5qfKeOnQG74ivRNv7sz4k4cQMs=
golang.org/x/sys v0.0.0-20190415145633-3fd5a3612ccd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 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 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
layeh.com/gopher-json v0.0.0-20190114024228-97fed8db8427 h1:RZkKxMR3jbQxdCEcglq3j7wY3PRJIopAwBlx1RE71X0=
layeh.com/gopher-json v0.0.0-20190114024228-97fed8db8427/go.mod h1:ivKkcY8Zxw5ba0jldhZCYYQfGdb2K6u9tbYK1AwMIBc=
layeh.com/gopher-luar v1.0.5 h1:fBuMh/xVN7bZxOsFzY6mxL2I+0ePJIWfyGc0dBdpqs4=
layeh.com/gopher-luar v1.0.5/go.mod h1:N3rev/ttQd8yVluXaYsa0M/eknzRYWe+pxZ35ZFmaaI=

292
main.go
View File

@ -2,43 +2,30 @@ package main
import ( import (
"flag" "flag"
"fmt"
"github.com/hpcloud/tail"
"github.com/tystuyfzand/mcgorcon"
"log" "log"
"math" "meow.tf/residentsleeper/commands"
"meow.tf/residentsleeper/events"
"meow.tf/residentsleeper/rcon"
"meow.tf/residentsleeper/scripting/config"
"meow.tf/residentsleeper/scripting/minecraft"
"os" "os"
"path" "path"
"regexp" "regexp"
"strconv" "strconv"
"sync" "strings"
"time"
)
const (
timeThreadRegexp = "^\\[.*?\\]\\s\\[.*?\\/INFO\\]:\\s"
tickDuration = time.Millisecond * 50
) )
var ( var (
serverPath string serverPath string
rconPort int rconPort int
rconPassword string rconPassword string
debug bool
client *mcgorcon.Client client *rcon.Session
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("^(.*?)=(.*)$") configRegexp = regexp.MustCompile("^(.*?)=(.*)$")
flagDebug = flag.Bool("debug", false, "debug messages")
) )
func init() { func init() {
@ -48,6 +35,8 @@ func init() {
func main() { func main() {
flag.Parse() flag.Parse()
debug = *flagDebug
// Load properties // Load properties
configPath := path.Join(serverPath, "server.properties") configPath := path.Join(serverPath, "server.properties")
@ -74,225 +63,86 @@ func main() {
log.Fatalln("RCON is not enabled: No password set") log.Fatalln("RCON is not enabled: No password set")
} }
go queryPlayersLoop() events.On(events.Message, handleCommands)
client = rcon.NewSession("localhost", rconPort, rconPassword)
client.Debug = debug
scriptPath := path.Join(serverPath, "scripts")
if _, err := os.Stat(scriptPath); !os.IsNotExist(err) {
config.BasePath = path.Join(scriptPath, "config")
minecraft.BasePath = serverPath
minecraft.WorldPath = path.Join(serverPath, cfg["level-name"])
if _, err = os.Stat(config.BasePath); os.IsNotExist(err) {
os.Mkdir(config.BasePath, 0755)
}
log.Println("Loading scripts from", scriptPath)
err = LoadScripts(scriptPath)
if err != nil {
log.Println("Warning: Unable to load scripts -", err)
}
}
logPath := path.Join(serverPath, "logs/latest.log") logPath := path.Join(serverPath, "logs/latest.log")
log.Println("Starting rcon connection")
ensureConnection()
logParser(logPath) logParser(logPath)
} }
func logParser(logPath string) { func handleCommands(eventArgs ...interface{}) {
log.Println("Watching log path ", logPath) user := eventArgs[0].(string)
str := eventArgs[1].(string)
stat, err := os.Stat(logPath) if debug {
log.Println("Message from " + user + ": " + str)
if err != nil {
log.Fatalln("Unable to open log file:", err)
} }
seek := &tail.SeekInfo{ args := commands.ParseCommandArguments(str)
Offset: stat.Size(),
}
// Start parsing file idx := strings.Index(str, " ")
t, err := tail.TailFile(logPath, tail.Config{Location: seek, Follow: true, ReOpen: true})
if err != nil { var argString string
log.Fatalln("Unable to open file:", err)
}
var m []string if idx == -1 {
argString = ""
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 { } else {
serverMessage(fmt.Sprintf("%s wants to sleep (%d of %d votes)", user, len(votes), requiredVotes)) argString = strings.TrimSpace(str[idx+1:])
}
}
func mimicSleeping(t int) {
var err error
if t == -1 {
t, err = queryTime()
if err != nil {
return
}
} }
voteLock.Lock() var command string
votes = make(map[string]bool)
voteLock.Unlock()
difference := 24000 - t if len(args) > 1 {
command, args = args[0], args[1:]
} else {
command = str
args = []string{}
}
log.Println("Adding time", difference) // Find the channel that the message came from. Override and use State if enabled.
t, err = addTime(difference) match := commands.Find("!", command, str)
if err != nil || t > 12000 { if match == nil {
serverMessage("Could not set the time, sorry")
return return
} }
serverMessage("Good morning everyone!") ctx := &commands.CommandContext{
User: user,
Prefix: "!",
Command: command,
ArgumentString: argString,
Arguments: args,
ArgumentCount: len(args),
// Force the vote to expire Reply: func(text string) {
expireTime = time.Now() client.SendMessage(user, text)
} },
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)) match.Call(ctx)
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
}

110
parser.go Normal file
View File

@ -0,0 +1,110 @@
package main
import (
"github.com/acarl005/stripansi"
"github.com/hpcloud/tail"
"log"
"meow.tf/residentsleeper/events"
"meow.tf/residentsleeper/rcon"
"os"
"regexp"
"strings"
)
const (
timeThreadRegexp = "^\\[.*?\\]\\s\\[.*?\\/INFO\\]:\\s"
)
var (
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")
loggedInRegexp = regexp.MustCompile(timeThreadRegexp + "(.*?)\\[(.*?)\\] logged in with entity id \\d+ at \\((.*?)\\)")
startedRegexp = regexp.MustCompile(timeThreadRegexp + "Done \\(.*?\\)! For help, type \"help\"")
authenticatedRegexp = regexp.MustCompile(timeThreadRegexp + "UUID of player (.*?) is (.*?)$")
opRegexp = regexp.MustCompile("Made (.*?) a server operator")
deopRegexp = regexp.MustCompile("Made (.*?) no longer a server operator")
sourceRegexp = regexp.MustCompile(timeThreadRegexp + "\\[(.*?): (.*?)\\]")
sleepRegexp = regexp.MustCompile("^z{3,}$")
timeRegexp = regexp.MustCompile("(\\d+)$")
)
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 {
line.Text = stripansi.Strip(strings.TrimSpace(line.Text))
if debug {
log.Println("Parsing line", line.Text)
log.Println("Bytes:", []byte(line.Text))
}
if m = messageRegexp.FindStringSubmatch(line.Text); m != nil {
events.Call(events.Message, m[1], m[2])
} else if m = loggedInRegexp.FindStringSubmatch(line.Text); m != nil {
position, err := rcon.SliceToFloats(strings.Split(m[3], ", "))
if err != nil {
position = []float64{0, 0, 0}
}
events.Call(events.LoggedIn, m[1], m[2], position[0], position[1], position[2])
} else if m = authenticatedRegexp.FindStringSubmatch(line.Text); m != nil {
events.Call(events.Authenticated, m[1], m[2])
} else if m = joinedRegexp.FindStringSubmatch(line.Text); m != nil {
events.Call(events.Join, m[1])
} else if m = leftRegexp.FindStringSubmatch(line.Text); m != nil {
events.Call(events.Leave, m[1])
} else if m = opRegexp.FindStringSubmatch(line.Text); m != nil {
source := "Server"
subM := sourceRegexp.FindStringSubmatch(line.Text)
if subM != nil {
source = subM[1]
}
events.Call(events.Op, m[1], source)
} else if m = deopRegexp.FindStringSubmatch(line.Text); m != nil {
source := "Server"
subM := sourceRegexp.FindStringSubmatch(line.Text)
if subM != nil {
source = subM[1]
}
events.Call(events.Deop, m[1], source)
} else if stoppingRegexp.MatchString(line.Text) {
events.Call(events.ServerClosing)
client.Disconnect()
} else if startedRegexp.MatchString(line.Text) {
events.Call(events.ServerStarted)
} else if rconRegexp.MatchString(line.Text) {
events.Call(events.Init)
}
}
}

48
parser_test.go Normal file
View File

@ -0,0 +1,48 @@
package main
import (
"bufio"
"os"
"regexp"
"testing"
)
type regexpTest struct {
input string
re *regexp.Regexp
}
func Test_Regexp(t *testing.T) {
inputs := map[string]regexpTest{
"join": {"[19:22:13] [Server thread/INFO]: notch joined the game", joinedRegexp},
"leave": {"[19:16:13] [Server thread/INFO]: notch left the game", leftRegexp},
"message": {"[19:37:13] [Server thread/INFO]: <notch> hello", messageRegexp},
"spigotMessage": {"[23:32:42] [Async Chat Thread - #2/INFO]: <ccatss> zzz", messageRegexp},
"rcon": {"[16:46:42] [RCON Listener #1/INFO]: RCON running on 0.0.0.0:25575", rconRegexp},
}
for key, re := range inputs {
if !re.re.MatchString(re.input) {
t.Fatal("Regexp for", key, "did not match")
}
}
}
func Test_EndCharacters(t *testing.T) {
f, err := os.Open("latest.log")
if err != nil {
t.Fatal("Log file not found")
}
defer f.Close()
scanner := bufio.NewScanner(f)
if !scanner.Scan() {
t.Fatal("Log empty")
}
t.Log(scanner.Text())
t.Log(scanner.Bytes())
}

81
rcon/players.go Normal file
View File

@ -0,0 +1,81 @@
package rcon
import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
)
var (
userRegexp = regexp.MustCompile("There are (\\d+) of a max of (\\d+) players online: (.*?)$")
teleportedRegexp = regexp.MustCompile("^Teleported (.*?) to (.*?)$")
)
func (r *Session) OnlinePlayers() (int, int, []string, error) {
res, err := r.connection().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 (r *Session) GetLocation(user string) []float32 {
res, err := r.connection().SendCommand(fmt.Sprintf("execute at %s run tp %s ~ ~ ~", user, user))
if err != nil {
return nil
}
m := teleportedRegexp.FindStringSubmatch(res)
pos, err := SliceToFloats(strings.Split(m[2], ","))
if err != nil {
return nil
}
ret := make([]float32, 3)
for i, v := range pos {
ret[i] = float32(v)
}
return ret
}
func (r *Session) Teleport(user, destination string) (string, error) {
return r.connection().SendCommand(fmt.Sprintf("tp %s %s", user, destination))
}
func SliceToFloats(str []string) ([]float64, error) {
ret := make([]float64, len(str))
var err error
for i, s := range str {
s = strings.TrimSpace(s)
ret[i], err = strconv.ParseFloat(s, 64)
if err != nil {
return nil, err
}
}
return ret, err
}

115
rcon/rcon.go Normal file
View File

@ -0,0 +1,115 @@
package rcon
import (
"encoding/json"
"fmt"
"github.com/tystuyfzand/mcgorcon"
"log"
"time"
)
const (
pingDuration = 30 * time.Second
)
type Session struct {
host string
port int
password string
Debug bool
client *mcgorcon.Client
lastUse time.Time
}
func NewSession(host string, port int, password string) *Session {
return &Session{host: host, port: port, password: password}
}
func (r *Session) Disconnect() {
if r.client != nil {
r.client.Close()
}
}
func (r *Session) connection() *mcgorcon.Client {
if r.client != nil {
if time.Since(r.lastUse) > pingDuration {
_, err := r.client.SendCommand("seed")
if err == nil {
r.lastUse = time.Now()
return r.client
}
} else {
return r.client
}
}
// Open new connection
r.client = r.openConnection()
r.lastUse = time.Now()
return r.client
}
func (r *Session) openConnection() *mcgorcon.Client {
var c *mcgorcon.Client
var err error
for c == nil || err != nil {
c, err = mcgorcon.Dial(r.host, r.port, r.password)
if err == nil {
break
}
<-time.After(10 * time.Second)
}
log.Println("Connection opened")
return c
}
func (r *Session) SendCommand(command string) (string, error) {
if r.Debug {
log.Println("Send command: " + command)
}
return r.connection().SendCommand(command)
}
func (r *Session) ServerMessage(message string) error {
_, err := r.SendCommand(fmt.Sprintf("say %s", message))
return err
}
func (r *Session) SendMessage(user, message string) error {
_, err := r.SendCommand(fmt.Sprintf("msg %s %s", user, message))
return err
}
type colorMessage struct {
Text string `json:"text"`
Color string `json:"color"`
}
func (r *Session) SendColorfulMessage(target, color, message string) error {
messages := []colorMessage{
{Text: message, Color: color},
}
b, err := json.Marshal(messages)
if err != nil {
return err
}
_, err = r.SendCommand(fmt.Sprintf("tellraw %s %s", target, string(b)))
return err
}

47
rcon/time.go Normal file
View File

@ -0,0 +1,47 @@
package rcon
import (
"fmt"
"regexp"
"strconv"
)
var (
timeRegexp = regexp.MustCompile("(\\d+)$")
)
func (r *Session) AddTime(delta int) int {
res, err := r.connection().SendCommand(fmt.Sprintf("time add %d", delta))
if err != nil {
return -1
}
m := timeRegexp.FindStringSubmatch(res)
if m == nil {
return -1
}
ret, _ := strconv.Atoi(m[1])
return ret
}
func (r *Session) GetTime() int {
res, err := r.connection().SendCommand("time query daytime")
if err != nil {
return -1
}
m := timeRegexp.FindStringSubmatch(res)
if m == nil {
return -1
}
v, _ := strconv.Atoi(m[1])
return v
}

55
scripting.go Normal file
View File

@ -0,0 +1,55 @@
package main
import (
"github.com/yuin/gopher-lua"
"io/ioutil"
luajson "layeh.com/gopher-json"
"layeh.com/gopher-luar"
"log"
"meow.tf/residentsleeper/scripting/commands"
"meow.tf/residentsleeper/scripting/config"
"meow.tf/residentsleeper/scripting/event"
"meow.tf/residentsleeper/scripting/minecraft"
"meow.tf/residentsleeper/scripting/regexp"
"path"
)
func LoadScripts(scriptPath string) error {
files, err := ioutil.ReadDir(scriptPath)
if err != nil {
return err
}
for _, file := range files {
if file.IsDir() {
continue
}
log.Println("Loading script", file.Name())
err = loadScript(path.Join(scriptPath, file.Name()))
if err != nil {
return err
}
}
return nil
}
func loadScript(scriptFile string) error {
L := lua.NewState()
luajson.Preload(L)
L.PreloadModule("config", config.Loader)
L.PreloadModule("event", event.Loader)
L.PreloadModule("regexp", regexp.Loader)
L.PreloadModule("minecraft", minecraft.Loader)
L.PreloadModule("commands", commands.Loader)
L.SetGlobal("rcon", luar.New(L, client))
return L.DoFile(scriptFile)
}

View File

@ -0,0 +1,37 @@
package commands
import (
lua "github.com/yuin/gopher-lua"
"meow.tf/residentsleeper/commands"
)
func Loader(L *lua.LState) int {
mod := L.SetFuncs(L.NewTable(), exports)
L.Push(mod)
return 1
}
var exports = map[string]lua.LGFunction{
"register": commandRegister,
}
func commandRegister(L *lua.LState) int {
name := L.CheckString(1)
handler := L.CheckFunction(2)
cb := func(ctx *commands.CommandContext) {
L.Push(handler)
L.Push(lua.LString(ctx.User))
for _, v := range ctx.Arguments {
L.Push(lua.LString(v))
}
L.PCall(1+ctx.ArgumentCount, 0, nil)
}
commands.Register(name, cb)
return 0
}

View File

@ -0,0 +1,96 @@
package config
import (
"github.com/yuin/gopher-lua"
"io/ioutil"
luajson "layeh.com/gopher-json"
"os"
"path"
)
var (
BasePath string
)
func Loader(L *lua.LState) int {
// register functions to the table
mod := L.SetFuncs(L.NewTable(), exports)
// returns the module
L.Push(mod)
return 1
}
var exports = map[string]lua.LGFunction{
"load": loadFunc,
"save": saveFunc,
}
func loadFunc(L *lua.LState) int {
name := L.CheckString(1)
f, err := os.Open(path.Join(BasePath, name+".json"))
if err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(err.Error()))
return 2
}
defer f.Close()
b, err := ioutil.ReadAll(f)
if err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(err.Error()))
return 2
}
v, err := luajson.Decode(L, b)
if err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(err.Error()))
return 2
}
L.Push(v)
L.Push(lua.LNil)
return 2
}
func saveFunc(L *lua.LState) int {
name := L.CheckString(1)
value := L.CheckAny(2)
f, err := os.Create(path.Join(BasePath, name+".json"))
if err != nil {
L.Push(lua.LString(err.Error()))
return 1
}
b, err := luajson.Encode(value)
if err != nil {
L.Push(lua.LString(err.Error()))
return 1
}
_, err = f.Write(b)
if err != nil {
L.Push(lua.LString(err.Error()))
return 1
}
err = f.Close()
if err != nil {
L.Push(lua.LString(err.Error()))
return 1
}
return 0
}

37
scripting/event/event.go Normal file
View File

@ -0,0 +1,37 @@
package event
import (
lua "github.com/yuin/gopher-lua"
luar "layeh.com/gopher-luar"
"meow.tf/residentsleeper/events"
)
func Loader(L *lua.LState) int {
// register functions to the table
mod := L.SetFuncs(L.NewTable(), exports)
// returns the module
L.Push(mod)
return 1
}
var exports = map[string]lua.LGFunction{
"on": onFunc,
}
func onFunc(L *lua.LState) int {
name := L.CheckString(1)
handler := L.CheckFunction(2)
events.On(name, func(args ...interface{}) {
L.Push(handler)
for _, arg := range args {
L.Push(luar.New(L, arg))
}
L.PCall(len(args), 0, handler)
})
return 0
}

View File

@ -0,0 +1,33 @@
package minecraft
import (
"github.com/ppacher/nbt"
lua "github.com/yuin/gopher-lua"
)
func TagToLuaValue(L *lua.LState, tag nbt.Tag) lua.LValue {
switch tag.TagID() {
case nbt.TagDouble:
return lua.LNumber(tag.(*nbt.DoubleTag).Value)
case nbt.TagFloat:
return lua.LNumber(tag.(*nbt.FloatTag).Value)
case nbt.TagInt:
return lua.LNumber(tag.(*nbt.IntTag).Value)
case nbt.TagLong:
return lua.LNumber(tag.(*nbt.LongTag).Value)
case nbt.TagString:
return lua.LString(tag.(*nbt.StringTag).Value)
case nbt.TagCompound:
t := L.NewTable()
c := tag.(*nbt.CompoundTag)
for key, v := range c.Tags {
t.RawSetString(key, TagToLuaValue(L, v))
}
return t
}
return lua.LNil
}

View File

@ -0,0 +1,69 @@
package minecraft
import (
"encoding/json"
"github.com/yuin/gopher-lua"
"meow.tf/residentsleeper/events"
"os"
"path"
)
var (
ops = make([]*minecraftOp, 0)
)
func init() {
events.On(events.Init, loadOps)
events.On(events.Op, oppedPlayer)
events.On(events.Deop, deoppedPlayer)
}
type minecraftOp struct {
UUID string `json:"uuid"`
Name string `json:"name"`
Level int `json:"level"`
BypassesPlayerLimit bool `json:"bypassesPlayerLimit"`
}
func loadOps(args ...interface{}) {
opPath := path.Join(BasePath, "ops.json")
f, err := os.Open(opPath)
if err != nil {
return
}
defer f.Close()
ops = make([]*minecraftOp, 0)
if err = json.NewDecoder(f).Decode(&ops); err != nil {
return
}
}
func oppedPlayer(args ...interface{}) {
loadOps()
}
func deoppedPlayer(args ...interface{}) {
loadOps()
}
func IsOp(name string) bool {
for _, op := range ops {
if op.Name == name {
return true
}
}
return false
}
func isOpFunc(L *lua.LState) int {
name := L.CheckString(1)
L.Push(lua.LBool(IsOp(name)))
return 1
}

View File

@ -0,0 +1,125 @@
package minecraft
import (
"compress/gzip"
"github.com/ppacher/nbt"
lua "github.com/yuin/gopher-lua"
"meow.tf/residentsleeper/events"
"os"
"path"
"sync"
)
var (
BasePath string
WorldPath string
uuids = make(map[string]string)
uuidLock sync.RWMutex
)
func init() {
events.On(events.Authenticated, onAuthenticated)
events.On(events.Leave, onLeave)
}
func onAuthenticated(args ...interface{}) {
name := args[0].(string)
uuid := args[1].(string)
uuidLock.Lock()
defer uuidLock.Unlock()
uuids[name] = uuid
}
func onLeave(args ...interface{}) {
uuidLock.Lock()
defer uuidLock.Unlock()
delete(uuids, args[0].(string))
}
func GetUUID(name string) (string, bool) {
uuidLock.RLock()
defer uuidLock.RUnlock()
uuid, exists := uuids[name]
return uuid, exists
}
func Loader(L *lua.LState) int {
// register functions to the table
mod := L.SetFuncs(L.NewTable(), exports)
// returns the module
L.Push(mod)
return 1
}
var exports = map[string]lua.LGFunction{
"getUUID": getUuidFunc,
"loadPlayer": loadPlayerFunc,
"isOp": isOpFunc,
}
func getUuidFunc(L *lua.LState) int {
name := L.CheckString(1)
uuid, exists := GetUUID(name)
if exists {
L.Push(lua.LString(uuid))
} else {
L.Push(lua.LNil)
}
return 1
}
func loadPlayerFunc(L *lua.LState) int {
name := L.CheckString(1)
uuid, exists := GetUUID(name)
if !exists {
L.Push(lua.LNil)
L.Push(lua.LString("User does not exist"))
return 2
}
playerPath := path.Join(WorldPath, "playerdata", uuid+".dat")
f, err := os.Open(playerPath)
if err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(err.Error()))
return 2
}
defer f.Close()
r, err := gzip.NewReader(f)
if err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(err.Error()))
return 2
}
defer r.Close()
tag, err := nbt.ReadNamedTag(r)
if err != nil {
L.Push(lua.LNil)
L.Push(lua.LString(err.Error()))
return 2
}
L.Push(TagToLuaValue(L, tag))
L.Push(lua.LNil)
return 2
}

View File

@ -0,0 +1,30 @@
package regexp
import (
lua "github.com/yuin/gopher-lua"
luar "layeh.com/gopher-luar"
"regexp"
)
func Loader(L *lua.LState) int {
// register functions to the table
mod := L.SetFuncs(L.NewTable(), exports)
// returns the module
L.Push(mod)
return 1
}
var exports = map[string]lua.LGFunction{
"MustCompile": compileFunc,
}
func compileFunc(L *lua.LState) int {
str := L.CheckString(1)
re := regexp.MustCompile(str)
L.Push(luar.New(L, re))
return 1
}

View File

@ -0,0 +1,98 @@
local event = require('event')
local regexp = require('regexp')
zzzRe = regexp.MustCompile('^z{3,}$')
ratio = 0.30
votes = {}
onlinePlayers = 0
expireTime = 0
event.on('init', function()
-- This is called when the server is started or the script is initialized (late load)
online, max, names, err = rcon:OnlinePlayers()
onlinePlayers = online
end)
event.on('message', function(user, message)
if not zzzRe:MatchString(message) then
return
end
t = rcon:GetTime()
if t < 12000 then
rcon:SendColorfulMessage(user, 'red', "It's not night time, go mine some more.")
return
end
-- Reset votes if we got this far and the previous vote expired
if os.time() > expireTime then
votes = {}
end
local difference = 24000 - t
-- 20 ticks per second (50ms per tick)
expireTime = os.time() + ((difference * 50) / 1000)
votes[user] = true
local currentVotes = tablelength(votes)
local requiredVotes = math.ceil(onlinePlayers * ratio)
if currentVotes >= requiredVotes then
mimicSleeping(t)
else
rcon:SendColorfulMessage('@a', 'blue', string.format('%s wants to sleep (%d of %d votes)', user, currentVotes, requiredVotes))
end
end)
event.on('logged_in', function(user)
onlinePlayers = onlinePlayers + 1
end)
event.on('leave', function(user)
onlinePlayers = onlinePlayers - 1
if votes[user] then
votes[user] = nil
if os.time() > expireTime then
votes = {}
return
end
end
local requiredVotes = math.ceil(onlinePlayers * ratio)
if tablelength(votes) > requiredVotes then
mimicSleeping(-1)
end
end)
function mimicSleeping(t)
if t == -1 then
t = rcon:GetTime()
end
local difference = 24000 - t
votes = {}
local newTime = rcon:AddTime(difference)
if newTime < 0 or newTime > 12000 then
rcon:ServerMessage('Unable to set time!')
return
end
rcon:SendColorfulMessage('@a', 'green', 'Good morning, rise and shine!')
end
function tablelength(T)
local count = 0
for _ in pairs(T) do count = count + 1 end
return count
end

124
scripts/timetracker.lua Normal file
View File

@ -0,0 +1,124 @@
local event = require('event')
local commands = require('commands')
config = require('config')
playtimes = {}
playerLogin = {}
initialized = false
function initTimeTracking()
if initialized then
return
end
initialized = true
print('Initializing time tracking')
local loadedPlaytimes, err = config.load('playtime')
if loadedPlaytimes and not err then
playtimes = loadedPlaytimes
end
online, max, names, err = rcon:OnlinePlayers()
if err ~= nil then
print('Unable to load online players: ' .. err:Error())
return
end
for index = 1, #names do
print('Setting login time for ' .. names[index])
playerLogin[names[index]] = os.time()
end
end
initTimeTracking()
event.on('init', initTimeTracking)
event.on('join', function(user)
playerLogin[user] = os.time()
end)
event.on('leave', function(user)
local timeOnline = os.time() - playerLogin[user]
if playtimes[user] ~= nil then
playtimes[user] = playtimes[user] + timeOnline
else
playtimes[user] = timeOnline
end
playerLogin[user] = nil
err = config.save('playtime', playtimes)
if err ~= nil then
print('Unable to save playtimes')
end
end)
commands.register('playtime [name]', function(user, name)
if name == nil then
name = user
end
print('User ' .. user .. ' is checking playtime of ' .. name)
if playtimes[name] == nil and playerLogin[name] == nil then
rcon:SendMessage(user, 'User has not logged any playtime.')
return
end
local playTime = playtimes[name] or 0
if playerLogin[name] ~= nil then
local timeOnline = os.time() - playerLogin[name]
playTime = playTime + timeOnline
end
print('Time played: ' .. playTime)
rcon:SendColorfulMessage('@a', 'green', name .. ' has played for ' .. formatElapsed(playTime))
end)
function formatElapsed(elapsedSeconds)
local weeks, days, hours, minutes, seconds = formatSeconds(elapsedSeconds)
local weeksTxt, daysTxt, hoursTxt, minutesTxt, secondsTxt = ""
if weeks == 1 then weeksTxt = 'week' else weeksTxt = 'weeks' end
if days == 1 then daysTxt = 'day' else daysTxt = 'days' end
if hours == 1 then hoursTxt = 'hour' else hoursTxt = 'hours' end
if minutes == 1 then minutesTxt = 'minute' else minutesTxt = 'minutes' end
if seconds == 1 then secondsTxt = 'second' else secondsTxt = 'seconds' end
if elapsedSeconds >= 604800 then
return weeks..' '..weeksTxt..', '..days..' '..daysTxt..', '..hours..' '..hoursTxt..', '..minutes..' '..minutesTxt..', '..seconds..' '..secondsTxt
elseif elapsedSeconds >= 86400 then
return days..' '..daysTxt..', '..hours..' '..hoursTxt..', '..minutes..' '..minutesTxt..', '..seconds..' '..secondsTxt
elseif elapsedSeconds >= 3600 then
return hours..' '..hoursTxt..', '..minutes..' '..minutesTxt..', '..seconds..' '..secondsTxt
elseif elapsedSeconds >= 60 then
return minutes..' '..minutesTxt..', '..seconds..' '..secondsTxt
else
return seconds..' '..secondsTxt
end
end
function formatSeconds(secondsArg)
local weeks = math.floor(secondsArg / 604800)
local remainder = secondsArg % 604800
local days = math.floor(remainder / 86400)
local remainder = remainder % 86400
local hours = math.floor(remainder / 3600)
local remainder = remainder % 3600
local minutes = math.floor(remainder / 60)
local seconds = remainder % 60
return weeks, days, hours, minutes, seconds
end

137
scripts/warps.lua Normal file
View File

@ -0,0 +1,137 @@
local event = require('event')
local commands = require('commands')
minecraft = require('minecraft')
config = require('config')
warpDelay = 60 * 5
homes = {}
warps = {}
lastHomeTime = {}
lastWarpTime = {}
event.on('init', function()
local loadedHomes, err = config.load('homes')
if loadedHomes and not err then
homes = loadedHomes
end
end)
commands.register('sethome', function(user)
local loc = rcon:GetLocation(user)
local c = {}
for i, v in loc() do
c[i] = v
end
homes[user] = c
err = config.save('homes', homes)
if err ~= nil then
print('Unable to save homes')
end
rcon:SendMessage(user, string.format("Home location set to %d, %d, %d", c[1], c[2], c[3]))
end)
commands.register('resethome', function(user)
homes[user] = nil
err = config.save('homes', homes)
if err ~= nil then
print('Unable to save homes')
end
rcon:SendMessage(user, 'Home location reset')
end)
commands.register('home', function(user)
local loc = nil
if homes[user] ~= nil then
loc = homes[user]
end
if loc == nil then
rcon:SendMessage(user, "You haven't set your spawn or home yet.")
return
end
local lastWarp = lastHomeTime[user]
if lastWarp ~= nil and lastWarp + warpDelay > os.time() and not minecraft.isOp(user) then
local delay = warpDelay - (os.time() - lastWarp)
rcon:SendMessage(user, "You need to wait " .. delay .. " seconds before warping again.")
return
end
lastHomeTime[user] = os.time()
rcon:Teleport(user, string.format("%f %d %f", loc[1], loc[2], loc[3]))
end)
commands.register('setwarp <place>', function(user, place)
if not minecraft.isOp(user) then
return
end
local loc = rcon:GetLocation(user)
local c = {}
for i, v in loc() do
c[i] = v
end
warps[place] = c
err = config.save('warps', warps)
if err ~= nil then
print('Unable to save warps')
end
rcon:SendMessage(user, string.format("Warp %s set to %d, %d, %d", place, c[1], c[2], c[3]))
end)
commands.register('warp <place>', function(user, place)
if warps[place] == nil then
rcon:SendMessage(user, "This warp point doesn't exist")
return
end
local loc = warps[place]
local lastWarp = lastWarpTime[user]
if lastWarp ~= nil and lastWarp + warpDelay > os.time() and not minecraft.isOp(user) then
local delay = warpDelay - (os.time() - lastWarp)
rcon:SendMessage(user, "You need to wait " .. delay .. " seconds before warping again.")
return
end
lastWarpTime[user] = os.time()
rcon:Teleport(user, string.format("%f %d %f", loc[1], loc[2], loc[3]))
end)
tprequests = {}
commands.register('tpa <target>', function(user, target)
end)
commands.register('tpaccept', function(user)
end)
commands.register('tpdeny', function(user)
end)

5
scripts/welcome.lua Normal file
View File

@ -0,0 +1,5 @@
local event = require('event')
event.on('join', function(user)
rcon:ServerMessage('Welcome to the server ' .. user .. '!!!')
end)