From b52b38179a2d5a759d5683679b2df2eb3851ea2d Mon Sep 17 00:00:00 2001 From: Tyler Date: Thu, 9 Jul 2020 21:01:29 -0400 Subject: [PATCH] Major updates/patches, functionality, api --- .drone.yml | 16 ++ .gitignore | 3 +- Dockerfile | 9 + commands.go | 76 --------- commands/arguments.go | 88 ++++++++++ commands/context.go | 15 ++ commands/definition.go | 69 ++++++++ commands/dispatcher.go | 35 ++++ commands/parser.go | 71 ++++++++ commands/parser_test.go | 69 ++++++++ events/events.go | 54 ++++++ events/names.go | 29 ++++ go.mod | 9 +- go.sum | 18 +- main.go | 292 ++++++++------------------------- parser.go | 110 +++++++++++++ parser_test.go | 48 ++++++ rcon/players.go | 81 +++++++++ rcon/rcon.go | 115 +++++++++++++ rcon/time.go | 47 ++++++ scripting.go | 55 +++++++ scripting/commands/commands.go | 37 +++++ scripting/config/config.go | 96 +++++++++++ scripting/event/event.go | 37 +++++ scripting/minecraft/nbt.go | 33 ++++ scripting/minecraft/ops.go | 69 ++++++++ scripting/minecraft/players.go | 125 ++++++++++++++ scripting/regexp/regexp.go | 30 ++++ scripts/residentsleeper.lua | 98 +++++++++++ scripts/timetracker.lua | 124 ++++++++++++++ scripts/warps.lua | 137 ++++++++++++++++ scripts/welcome.lua | 5 + 32 files changed, 1798 insertions(+), 302 deletions(-) create mode 100644 .drone.yml create mode 100644 Dockerfile delete mode 100644 commands.go create mode 100644 commands/arguments.go create mode 100644 commands/context.go create mode 100644 commands/definition.go create mode 100644 commands/dispatcher.go create mode 100644 commands/parser.go create mode 100644 commands/parser_test.go create mode 100644 events/events.go create mode 100644 events/names.go create mode 100644 parser.go create mode 100644 parser_test.go create mode 100644 rcon/players.go create mode 100644 rcon/rcon.go create mode 100644 rcon/time.go create mode 100644 scripting.go create mode 100644 scripting/commands/commands.go create mode 100644 scripting/config/config.go create mode 100644 scripting/event/event.go create mode 100644 scripting/minecraft/nbt.go create mode 100644 scripting/minecraft/ops.go create mode 100644 scripting/minecraft/players.go create mode 100644 scripting/regexp/regexp.go create mode 100644 scripts/residentsleeper.lua create mode 100644 scripts/timetracker.lua create mode 100644 scripts/warps.lua create mode 100644 scripts/welcome.lua diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..3e55476 --- /dev/null +++ b/.drone.yml @@ -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 \ No newline at end of file diff --git a/.gitignore b/.gitignore index caa32e6..a167da4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea/ -*.iml \ No newline at end of file +*.iml +*.log \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..afc8974 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM golang:alpine AS builder + +RUN go build -o sleeper + +FROM alpine + +COPY --from=builder sleeper /sleeper + +CMD ["/sleeper"] \ No newline at end of file diff --git a/commands.go b/commands.go deleted file mode 100644 index 98cd613..0000000 --- a/commands.go +++ /dev/null @@ -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]) -} \ No newline at end of file diff --git a/commands/arguments.go b/commands/arguments.go new file mode 100644 index 0000000..d967332 --- /dev/null +++ b/commands/arguments.go @@ -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 [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 +} diff --git a/commands/context.go b/commands/context.go new file mode 100644 index 0000000..79557d6 --- /dev/null +++ b/commands/context.go @@ -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) +} diff --git a/commands/definition.go b/commands/definition.go new file mode 100644 index 0000000..aac8f13 --- /dev/null +++ b/commands/definition.go @@ -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 +} diff --git a/commands/dispatcher.go b/commands/dispatcher.go new file mode 100644 index 0000000..60de801 --- /dev/null +++ b/commands/dispatcher.go @@ -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 +} diff --git a/commands/parser.go b/commands/parser.go new file mode 100644 index 0000000..5cbbe8e --- /dev/null +++ b/commands/parser.go @@ -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 +} diff --git a/commands/parser_test.go b/commands/parser_test.go new file mode 100644 index 0000000..2813038 --- /dev/null +++ b/commands/parser_test.go @@ -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 [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 [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") + } +} diff --git a/events/events.go b/events/events.go new file mode 100644 index 0000000..da5b009 --- /dev/null +++ b/events/events.go @@ -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...) + } +} diff --git a/events/names.go b/events/names.go new file mode 100644 index 0000000..c0d015f --- /dev/null +++ b/events/names.go @@ -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" +) diff --git a/go.mod b/go.mod index 8fef5d1..365fb24 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,17 @@ -module sleepvote +module meow.tf/residentsleeper go 1.12 require ( + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d github.com/fsnotify/fsnotify v1.4.7 // indirect 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 gopkg.in/fsnotify.v1 v1.4.7 // 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 ) diff --git a/go.sum b/go.sum index 5e8dfe6..031c083 100644 --- a/go.sum +++ b/go.sum @@ -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/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= +github.com/ppacher/nbt v0.0.0-20181201174858-0cad976cf07c h1:d9Pm+C0vGwMRxn04D6MjHbqkEvG+qlpHjlARadAAmpQ= +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/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= +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= diff --git a/main.go b/main.go index da2850e..afb2ed9 100644 --- a/main.go +++ b/main.go @@ -2,43 +2,30 @@ package main import ( "flag" - "fmt" - "github.com/hpcloud/tail" - "github.com/tystuyfzand/mcgorcon" "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" "path" "regexp" "strconv" - "sync" - "time" -) - -const ( - timeThreadRegexp = "^\\[.*?\\]\\s\\[.*?\\/INFO\\]:\\s" - - tickDuration = time.Millisecond * 50 + "strings" ) var ( - serverPath string - rconPort int + serverPath string + rconPort int rconPassword string + debug bool - 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: (.*?)$") + client *rcon.Session configRegexp = regexp.MustCompile("^(.*?)=(.*)$") + + flagDebug = flag.Bool("debug", false, "debug messages") ) func init() { @@ -48,6 +35,8 @@ func init() { func main() { flag.Parse() + debug = *flagDebug + // Load properties configPath := path.Join(serverPath, "server.properties") @@ -74,225 +63,86 @@ func main() { 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") - log.Println("Starting rcon connection") - - ensureConnection() - logParser(logPath) } -func logParser(logPath string) { - log.Println("Watching log path ", logPath) +func handleCommands(eventArgs ...interface{}) { + user := eventArgs[0].(string) + str := eventArgs[1].(string) - stat, err := os.Stat(logPath) - - if err != nil { - log.Fatalln("Unable to open log file:", err) + if debug { + log.Println("Message from " + user + ": " + str) } - seek := &tail.SeekInfo{ - Offset: stat.Size(), - } + args := commands.ParseCommandArguments(str) - // Start parsing file - t, err := tail.TailFile(logPath, tail.Config{Location: seek, Follow: true, ReOpen: true}) + idx := strings.Index(str, " ") - if err != nil { - log.Fatalln("Unable to open file:", err) - } + var argString string - 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) + if idx == -1 { + argString = "" } 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 - } + argString = strings.TrimSpace(str[idx+1:]) } - voteLock.Lock() - votes = make(map[string]bool) - voteLock.Unlock() + var command string - 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 { - serverMessage("Could not set the time, sorry") + if match == nil { 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 - 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) + Reply: func(text string) { + client.SendMessage(user, text) + }, } - requiredVotes := int(math.Ceil(float64(onlinePlayers) * 0.30)) - - if len(votes) >= requiredVotes { - mimicSleeping(-1) - } + match.Call(ctx) } - -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 -} \ No newline at end of file diff --git a/parser.go b/parser.go new file mode 100644 index 0000000..dcacbc4 --- /dev/null +++ b/parser.go @@ -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) + } + } +} diff --git a/parser_test.go b/parser_test.go new file mode 100644 index 0000000..a653d08 --- /dev/null +++ b/parser_test.go @@ -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]: hello", messageRegexp}, + "spigotMessage": {"[23:32:42] [Async Chat Thread - #2/INFO]: 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()) +} diff --git a/rcon/players.go b/rcon/players.go new file mode 100644 index 0000000..d46518e --- /dev/null +++ b/rcon/players.go @@ -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 +} diff --git a/rcon/rcon.go b/rcon/rcon.go new file mode 100644 index 0000000..7b4b741 --- /dev/null +++ b/rcon/rcon.go @@ -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 +} diff --git a/rcon/time.go b/rcon/time.go new file mode 100644 index 0000000..0999600 --- /dev/null +++ b/rcon/time.go @@ -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 +} diff --git a/scripting.go b/scripting.go new file mode 100644 index 0000000..37a9462 --- /dev/null +++ b/scripting.go @@ -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) +} diff --git a/scripting/commands/commands.go b/scripting/commands/commands.go new file mode 100644 index 0000000..fcda2e6 --- /dev/null +++ b/scripting/commands/commands.go @@ -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 +} diff --git a/scripting/config/config.go b/scripting/config/config.go new file mode 100644 index 0000000..ebda5b5 --- /dev/null +++ b/scripting/config/config.go @@ -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 +} diff --git a/scripting/event/event.go b/scripting/event/event.go new file mode 100644 index 0000000..2009cff --- /dev/null +++ b/scripting/event/event.go @@ -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 +} diff --git a/scripting/minecraft/nbt.go b/scripting/minecraft/nbt.go new file mode 100644 index 0000000..dcf77f4 --- /dev/null +++ b/scripting/minecraft/nbt.go @@ -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 +} diff --git a/scripting/minecraft/ops.go b/scripting/minecraft/ops.go new file mode 100644 index 0000000..43e2a16 --- /dev/null +++ b/scripting/minecraft/ops.go @@ -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 +} diff --git a/scripting/minecraft/players.go b/scripting/minecraft/players.go new file mode 100644 index 0000000..63316fa --- /dev/null +++ b/scripting/minecraft/players.go @@ -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 +} diff --git a/scripting/regexp/regexp.go b/scripting/regexp/regexp.go new file mode 100644 index 0000000..81f68e5 --- /dev/null +++ b/scripting/regexp/regexp.go @@ -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 +} diff --git a/scripts/residentsleeper.lua b/scripts/residentsleeper.lua new file mode 100644 index 0000000..c0f9320 --- /dev/null +++ b/scripts/residentsleeper.lua @@ -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 \ No newline at end of file diff --git a/scripts/timetracker.lua b/scripts/timetracker.lua new file mode 100644 index 0000000..5a78e58 --- /dev/null +++ b/scripts/timetracker.lua @@ -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 \ No newline at end of file diff --git a/scripts/warps.lua b/scripts/warps.lua new file mode 100644 index 0000000..9fa9171 --- /dev/null +++ b/scripts/warps.lua @@ -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 ', 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 ', 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 ', function(user, target) + +end) + +commands.register('tpaccept', function(user) +end) + +commands.register('tpdeny', function(user) +end) \ No newline at end of file diff --git a/scripts/welcome.lua b/scripts/welcome.lua new file mode 100644 index 0000000..5d2c6c2 --- /dev/null +++ b/scripts/welcome.lua @@ -0,0 +1,5 @@ +local event = require('event') + +event.on('join', function(user) + rcon:ServerMessage('Welcome to the server ' .. user .. '!!!') +end) \ No newline at end of file