From f9512b2485e2aca270f9b9edf5c92ce355ea42b3 Mon Sep 17 00:00:00 2001 From: Tyler Date: Sat, 12 Jun 2021 01:28:34 -0400 Subject: [PATCH] Move to event loop, prettify warps, add delay to warp with damage counter --- docker/entrypoint.sh | 12 +- events/names.go | 14 +- go.mod | 2 +- go.sum | 12 +- main.go | 29 ++- parser.go | 33 +-- rcon/rcon.go | 11 +- scripting.go | 33 ++- scripting/commands/commands.go | 17 +- scripting/event/event.go | 15 +- scripting/eventloop/eventloop.go | 372 +++++++++++++++++++++++++++++++ scripts/warps.lua | 64 ++++-- 12 files changed, 528 insertions(+), 86 deletions(-) create mode 100644 scripting/eventloop/eventloop.go diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index bc9b58b..8802dd0 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -1,2 +1,12 @@ #!/bin/sh -/usr/bin/sleeper -dir ${SERVER_PATH:-/data} \ No newline at end of file +if [ -z "$SERVER_PATH" ]; then + SERVER_PATH="/data" +fi + +SCRIPT_PATH="$SERVER_PATH/scripts" + +if [ -d "/scripts" ]; then + SCRIPT_PATH="/scripts" +fi + +/usr/bin/sleeper -dir $SERVER_PATH -scriptPath $SCRIPT_PATH \ No newline at end of file diff --git a/events/names.go b/events/names.go index c0d015f..5519e03 100644 --- a/events/names.go +++ b/events/names.go @@ -6,24 +6,24 @@ const ( ServerStarted = "server_started" ServerClosing = "server_closing" - // LoggedIn, Args: User, Address, X, Y, Z + // LoggedIn Args: User, Address, X, Y, Z LoggedIn = "logged_in" - // Authenticated, Args: User, UUID + // Authenticated Args: User, UUID Authenticated = "authenticated" - // Join, Args: User + // Join Args: User Join = "join" - // Leave, Args: User + // Leave Args: User Leave = "leave" - // Op, Args: User, Source? + // Op Args: User, Source? Op = "op" - // Deop, Args: User, Source? + // Deop Args: User, Source? Deop = "deop" - // Message, Args: User, Message + // Message Args: User, Message Message = "message" ) diff --git a/go.mod b/go.mod index 365fb24..11284d5 100644 --- a/go.mod +++ b/go.mod @@ -7,9 +7,9 @@ require ( github.com/fsnotify/fsnotify v1.4.7 // indirect github.com/hpcloud/tail v1.0.0 github.com/ppacher/nbt v0.0.0-20181201174858-0cad976cf07c + github.com/sirupsen/logrus v1.8.1 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 diff --git a/go.sum b/go.sum index 031c083..8c58429 100644 --- a/go.sum +++ b/go.sum @@ -3,19 +3,27 @@ github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat6 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/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 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= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/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= diff --git a/main.go b/main.go index 69a78c3..660308e 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,7 @@ package main import ( "flag" - "log" + log "github.com/sirupsen/logrus" "meow.tf/residentsleeper/commands" "meow.tf/residentsleeper/events" "meow.tf/residentsleeper/rcon" @@ -17,6 +17,7 @@ import ( var ( serverPath string + scriptPath string rconPort int rconPassword string debug bool @@ -30,6 +31,7 @@ var ( func init() { flag.StringVar(&serverPath, "dir", "", "server directory") + flag.StringVar(&scriptPath, "scriptPath", "", "script path override") } func main() { @@ -37,13 +39,17 @@ func main() { debug = *flagDebug + if debug { + log.SetLevel(log.DebugLevel) + } + // Load properties configPath := path.Join(serverPath, "server.properties") cfg, err := loadServerConfig(configPath) if err != nil { - log.Fatalln("Unable to load config:", err) + log.WithError(err).Fatalln("Unable to load config") } if rconEnabled, ok := cfg["enable-rcon"]; !ok || rconEnabled != "true" { @@ -75,7 +81,9 @@ func main() { client.Debug = debug - scriptPath := path.Join(serverPath, "scripts") + if scriptPath == "" { + scriptPath = path.Join(serverPath, "scripts") + } if _, err := os.Stat(scriptPath); !os.IsNotExist(err) { config.BasePath = path.Join(scriptPath, "config") @@ -87,11 +95,11 @@ func main() { os.Mkdir(config.BasePath, 0755) } - log.Println("Loading scripts from", scriptPath) - err = LoadScripts(scriptPath) + log.WithField("path", scriptPath).Info("Loading scripts") + err = loadScripts(scriptPath) if err != nil { - log.Println("Warning: Unable to load scripts -", err) + log.WithError(err).Warn("Unable to load scripts") } } @@ -104,9 +112,10 @@ func handleCommands(eventArgs ...interface{}) { user := eventArgs[0].(string) str := eventArgs[1].(string) - if debug { - log.Println("Message from " + user + ": " + str) - } + log.WithFields(log.Fields{ + "user": user, + "message": str, + }).Debug("Message received") args := commands.ParseCommandArguments(str) @@ -150,5 +159,5 @@ func handleCommands(eventArgs ...interface{}) { }, } - match.Call(ctx) + go match.Call(ctx) } diff --git a/parser.go b/parser.go index dcacbc4..17fb41a 100644 --- a/parser.go +++ b/parser.go @@ -3,7 +3,7 @@ package main import ( "github.com/acarl005/stripansi" "github.com/hpcloud/tail" - "log" + log "github.com/sirupsen/logrus" "meow.tf/residentsleeper/events" "meow.tf/residentsleeper/rcon" "os" @@ -33,12 +33,12 @@ var ( ) func logParser(logPath string) { - log.Println("Watching log path", logPath) + log.WithField("path", logPath).Info("Watching log path") stat, err := os.Stat(logPath) if err != nil { - log.Fatalln("Unable to open log file:", err) + log.WithError(err).Fatalln("Unable to stat log file") } seek := &tail.SeekInfo{ @@ -49,7 +49,7 @@ func logParser(logPath string) { t, err := tail.TailFile(logPath, tail.Config{Location: seek, Follow: true, ReOpen: true}) if err != nil { - log.Fatalln("Unable to open file:", err) + log.WithError(err).Fatalln("Unable to open log file") } var m []string @@ -63,7 +63,7 @@ func logParser(logPath string) { } if m = messageRegexp.FindStringSubmatch(line.Text); m != nil { - events.Call(events.Message, m[1], m[2]) + go 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], ", ")) @@ -71,13 +71,13 @@ func logParser(logPath string) { position = []float64{0, 0, 0} } - events.Call(events.LoggedIn, m[1], m[2], position[0], position[1], position[2]) + go 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]) + go events.Call(events.Authenticated, m[1], m[2]) } else if m = joinedRegexp.FindStringSubmatch(line.Text); m != nil { - events.Call(events.Join, m[1]) + go events.Call(events.Join, m[1]) } else if m = leftRegexp.FindStringSubmatch(line.Text); m != nil { - events.Call(events.Leave, m[1]) + go events.Call(events.Leave, m[1]) } else if m = opRegexp.FindStringSubmatch(line.Text); m != nil { source := "Server" @@ -87,7 +87,7 @@ func logParser(logPath string) { source = subM[1] } - events.Call(events.Op, m[1], source) + go events.Call(events.Op, m[1], source) } else if m = deopRegexp.FindStringSubmatch(line.Text); m != nil { source := "Server" @@ -97,14 +97,17 @@ func logParser(logPath string) { source = subM[1] } - events.Call(events.Deop, m[1], source) + go events.Call(events.Deop, m[1], source) } else if stoppingRegexp.MatchString(line.Text) { - events.Call(events.ServerClosing) - client.Disconnect() + go func() { + events.Call(events.ServerClosing) + + client.Disconnect() + }() } else if startedRegexp.MatchString(line.Text) { - events.Call(events.ServerStarted) + go events.Call(events.ServerStarted) } else if rconRegexp.MatchString(line.Text) { - events.Call(events.Init) + go events.Call(events.Init) } } } diff --git a/rcon/rcon.go b/rcon/rcon.go index 7b4b741..744f11d 100644 --- a/rcon/rcon.go +++ b/rcon/rcon.go @@ -3,8 +3,8 @@ package rcon import ( "encoding/json" "fmt" + log "github.com/sirupsen/logrus" "github.com/tystuyfzand/mcgorcon" - "log" "time" ) @@ -68,15 +68,16 @@ func (r *Session) openConnection() *mcgorcon.Client { <-time.After(10 * time.Second) } - log.Println("Connection opened") + log.WithFields(log.Fields{ + "host": r.host, + "port": r.port, + }).Info("Connection opened") return c } func (r *Session) SendCommand(command string) (string, error) { - if r.Debug { - log.Println("Send command: " + command) - } + log.WithField("command", command).Debug("Sending command") return r.connection().SendCommand(command) } diff --git a/scripting.go b/scripting.go index 37a9462..c832f01 100644 --- a/scripting.go +++ b/scripting.go @@ -1,20 +1,21 @@ package main import ( + log "github.com/sirupsen/logrus" "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/eventloop" "meow.tf/residentsleeper/scripting/minecraft" "meow.tf/residentsleeper/scripting/regexp" "path" ) -func LoadScripts(scriptPath string) error { +func loadScripts(scriptPath string) error { files, err := ioutil.ReadDir(scriptPath) if err != nil { @@ -26,7 +27,7 @@ func LoadScripts(scriptPath string) error { continue } - log.Println("Loading script", file.Name()) + log.WithField("file", file.Name()).Info("Loading script") err = loadScript(path.Join(scriptPath, file.Name())) @@ -39,17 +40,25 @@ func LoadScripts(scriptPath string) error { } func loadScript(scriptFile string) error { - L := lua.NewState() + loop := eventloop.NewEventLoop() - luajson.Preload(L) + var err error - 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) + loop.Run(func(L *lua.LState) { + luajson.Preload(L) - L.SetGlobal("rcon", luar.New(L, client)) + 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) - return L.DoFile(scriptFile) + L.SetGlobal("rcon", luar.New(L, client)) + + err = L.DoFile(scriptFile) + }) + + loop.Start() + + return err } diff --git a/scripting/commands/commands.go b/scripting/commands/commands.go index fcda2e6..506c823 100644 --- a/scripting/commands/commands.go +++ b/scripting/commands/commands.go @@ -3,6 +3,7 @@ package commands import ( lua "github.com/yuin/gopher-lua" "meow.tf/residentsleeper/commands" + "meow.tf/residentsleeper/scripting/eventloop" ) func Loader(L *lua.LState) int { @@ -20,15 +21,19 @@ func commandRegister(L *lua.LState) int { handler := L.CheckFunction(2) + loop := eventloop.FromState(L) + cb := func(ctx *commands.CommandContext) { - L.Push(handler) - L.Push(lua.LString(ctx.User)) + loop.RunOnLoop(func(L *lua.LState) { + L.Push(handler) + L.Push(lua.LString(ctx.User)) - for _, v := range ctx.Arguments { - L.Push(lua.LString(v)) - } + for _, v := range ctx.Arguments { + L.Push(lua.LString(v)) + } - L.PCall(1+ctx.ArgumentCount, 0, nil) + L.PCall(1+ctx.ArgumentCount, 0, nil) + }) } commands.Register(name, cb) diff --git a/scripting/event/event.go b/scripting/event/event.go index 2009cff..d14d9de 100644 --- a/scripting/event/event.go +++ b/scripting/event/event.go @@ -4,6 +4,7 @@ import ( lua "github.com/yuin/gopher-lua" luar "layeh.com/gopher-luar" "meow.tf/residentsleeper/events" + "meow.tf/residentsleeper/scripting/eventloop" ) func Loader(L *lua.LState) int { @@ -23,14 +24,18 @@ func onFunc(L *lua.LState) int { name := L.CheckString(1) handler := L.CheckFunction(2) + loop := eventloop.FromState(L) + events.On(name, func(args ...interface{}) { - L.Push(handler) + loop.RunOnLoop(func(L *lua.LState) { + L.Push(handler) - for _, arg := range args { - L.Push(luar.New(L, arg)) - } + for _, arg := range args { + L.Push(luar.New(L, arg)) + } - L.PCall(len(args), 0, handler) + L.PCall(len(args), 0, handler) + }) }) return 0 diff --git a/scripting/eventloop/eventloop.go b/scripting/eventloop/eventloop.go new file mode 100644 index 0000000..59bfa7d --- /dev/null +++ b/scripting/eventloop/eventloop.go @@ -0,0 +1,372 @@ +package eventloop + +import ( + lua "github.com/yuin/gopher-lua" + luar "layeh.com/gopher-luar" + "log" + "sync" + "time" +) + +type job struct { + cancelled bool + fn func() +} + +type Timer struct { + job + timer *time.Timer +} + +type Interval struct { + job + ticker *time.Ticker + stopChan chan struct{} +} + +type EventLoop struct { + vm *lua.LState + jobChan chan func() + jobCount int32 + canRun bool + + auxJobs []func() + auxJobsLock sync.Mutex + wakeup chan struct{} + + stopCond *sync.Cond + running bool + + enableConsole bool +} + +func NewEventLoop(opts ...Option) *EventLoop { + vm := lua.NewState() + + loop := &EventLoop{ + vm: vm, + jobChan: make(chan func()), + wakeup: make(chan struct{}, 1), + stopCond: sync.NewCond(&sync.Mutex{}), + enableConsole: true, + } + + for _, opt := range opts { + opt(loop) + } + + vm.SetGlobal("__loop", luar.New(vm, loop)) + + vm.PreloadModule("timers", loop.timerLoader) + + return loop +} + +type Option func(*EventLoop) + +// EnableConsole controls whether the "console" module is loaded into +// the runtime used by the loop. By default, loops are created with +// the "console" module loaded, pass EnableConsole(false) to +// NewEventLoop to disable this behavior. +func EnableConsole(enableConsole bool) Option { + return func(loop *EventLoop) { + loop.enableConsole = enableConsole + } +} + +func (loop *EventLoop) timerLoader(L *lua.LState) int { + // register functions to the table + mod := L.SetFuncs(L.NewTable(), map[string]lua.LGFunction{ + "setInterval": loop.setInterval, + "setTimeout": loop.setTimeout, + "clearInterval": loop.clearIntervalBinding, + }) + + // returns the module + L.Push(mod) + return 1 +} + +func (loop *EventLoop) schedule(repeating bool) int { + if fn, ok := loop.vm.Get(1).(*lua.LFunction); ok { + delay := loop.vm.CheckInt(2) + + log.Println("Delay:", delay) + var args []lua.LValue + if loop.vm.GetTop() > 2 { + args = make([]lua.LValue, loop.vm.GetTop()-2) + + for i := 3; i < loop.vm.GetTop(); i++ { + args[i-3] = loop.vm.Get(i) + } + } + f := func() { + loop.vm.Push(fn) + + for _, arg := range args { + loop.vm.Push(arg) + } + + err := loop.vm.PCall(len(args), 0, fn) + + if err != nil { + log.Fatalln("Error calling function:", err) + } + } + loop.jobCount++ + + var val interface{} + + if repeating { + val = loop.addInterval(f, time.Duration(delay)*time.Millisecond) + } else { + val = loop.addTimeout(f, time.Duration(delay)*time.Millisecond) + } + + loop.vm.Push(luar.New(loop.vm, val)) + return 1 + } else { + log.Println("Unable to get function to call") + } + return 0 +} + +func (loop *EventLoop) setTimeout(_ *lua.LState) int { + return loop.schedule(false) +} + +func (loop *EventLoop) setInterval(_ *lua.LState) int { + return loop.schedule(true) +} + +func (loop *EventLoop) clearIntervalBinding(L *lua.LState) int { + ud := L.CheckUserData(1) + + if interval, ok := ud.Value.(*Interval); ok { + loop.clearInterval(interval) + } + + return 0 +} + +// SetTimeout schedules to run the specified function in the context +// of the loop as soon as possible after the specified timeout period. +// SetTimeout returns a Timer which can be passed to ClearTimeout. +// The instance of goja.Runtime that is passed to the function and any Values derived +// from it must not be used outside of the function. SetTimeout is +// safe to call inside or outside of the loop. +func (loop *EventLoop) SetTimeout(fn func(*lua.LState), timeout time.Duration) *Timer { + t := loop.addTimeout(func() { fn(loop.vm) }, timeout) + loop.addAuxJob(func() { + loop.jobCount++ + }) + return t +} + +// ClearTimeout cancels a Timer returned by SetTimeout if it has not run yet. +// ClearTimeout is safe to call inside or outside of the loop. +func (loop *EventLoop) ClearTimeout(t *Timer) { + loop.addAuxJob(func() { + loop.clearTimeout(t) + }) +} + +// SetInterval schedules to repeatedly run the specified function in +// the context of the loop as soon as possible after every specified +// timeout period. SetInterval returns an Interval which can be +// passed to ClearInterval. The instance of goja.Runtime that is passed to the +// function and any Values derived from it must not be used outside of +// the function. SetInterval is safe to call inside or outside of the +// loop. +func (loop *EventLoop) SetInterval(fn func(*lua.LState), timeout time.Duration) *Interval { + i := loop.addInterval(func() { fn(loop.vm) }, timeout) + loop.addAuxJob(func() { + loop.jobCount++ + }) + return i +} + +// ClearInterval cancels an Interval returned by SetInterval. +// ClearInterval is safe to call inside or outside of the loop. +func (loop *EventLoop) ClearInterval(i *Interval) { + loop.addAuxJob(func() { + loop.clearInterval(i) + }) +} + +func (loop *EventLoop) setRunning() { + loop.stopCond.L.Lock() + if loop.running { + panic("Loop is already started") + } + loop.running = true + loop.stopCond.L.Unlock() +} + +// Run calls the specified function, starts the event loop and waits until there are no more delayed jobs to run +// after which it stops the loop and returns. +// The instance of goja.Runtime that is passed to the function and any Values derived from it must not be used outside +// of the function. +// Do NOT use this function while the loop is already running. Use RunOnLoop() instead. +// If the loop is already started it will panic. +func (loop *EventLoop) Run(fn func(*lua.LState)) { + loop.setRunning() + fn(loop.vm) + loop.run(false) +} + +// Start the event loop in the background. The loop continues to run until Stop() is called. +// If the loop is already started it will panic. +func (loop *EventLoop) Start() { + loop.setRunning() + go loop.run(true) +} + +// Stop the loop that was started with Start(). After this function returns there will be no more jobs executed +// by the loop. It is possible to call Start() or Run() again after this to resume the execution. +// Note, it does not cancel active timeouts. +// It is not allowed to run Start() and Stop() concurrently. +// Calling Stop() on an already stopped loop or inside the loop will hang. +func (loop *EventLoop) Stop() { + loop.jobChan <- func() { + loop.canRun = false + } + + loop.stopCond.L.Lock() + for loop.running { + loop.stopCond.Wait() + } + loop.stopCond.L.Unlock() +} + +// RunOnLoop schedules to run the specified function in the context of the loop as soon as possible. +// The order of the runs is preserved (i.e. the functions will be called in the same order as calls to RunOnLoop()) +// The instance of goja.Runtime that is passed to the function and any Values derived from it must not be used outside +// of the function. It is safe to call inside or outside of the loop. +func (loop *EventLoop) RunOnLoop(fn func(*lua.LState)) { + loop.addAuxJob(func() { fn(loop.vm) }) +} + +func (loop *EventLoop) runAux() { + loop.auxJobsLock.Lock() + jobs := loop.auxJobs + loop.auxJobs = nil + loop.auxJobsLock.Unlock() + for _, job := range jobs { + job() + } +} + +func (loop *EventLoop) run(inBackground bool) { + loop.canRun = true + loop.runAux() + + for loop.canRun && (inBackground || loop.jobCount > 0) { + select { + case job := <-loop.jobChan: + job() + if loop.canRun { + select { + case <-loop.wakeup: + loop.runAux() + default: + } + } + case <-loop.wakeup: + loop.runAux() + } + } + loop.stopCond.L.Lock() + loop.running = false + loop.stopCond.L.Unlock() + loop.stopCond.Broadcast() +} + +func (loop *EventLoop) addAuxJob(fn func()) { + loop.auxJobsLock.Lock() + loop.auxJobs = append(loop.auxJobs, fn) + loop.auxJobsLock.Unlock() + select { + case loop.wakeup <- struct{}{}: + default: + } +} + +func (loop *EventLoop) addTimeout(f func(), timeout time.Duration) *Timer { + t := &Timer{ + job: job{fn: f}, + } + t.timer = time.AfterFunc(timeout, func() { + loop.jobChan <- func() { + loop.doTimeout(t) + } + }) + + return t +} + +func (loop *EventLoop) addInterval(f func(), timeout time.Duration) *Interval { + i := &Interval{ + job: job{fn: f}, + ticker: time.NewTicker(timeout), + stopChan: make(chan struct{}), + } + + go i.run(loop) + return i +} + +func (loop *EventLoop) doTimeout(t *Timer) { + if !t.cancelled { + t.fn() + t.cancelled = true + loop.jobCount-- + } +} + +func (loop *EventLoop) doInterval(i *Interval) { + if !i.cancelled { + i.fn() + } +} + +func (loop *EventLoop) clearTimeout(t *Timer) { + if t != nil && !t.cancelled { + t.timer.Stop() + t.cancelled = true + loop.jobCount-- + } +} + +func (loop *EventLoop) clearInterval(i *Interval) { + if i != nil && !i.cancelled { + i.cancelled = true + close(i.stopChan) + loop.jobCount-- + } +} + +func (i *Interval) run(loop *EventLoop) { +L: + for { + select { + case <-i.stopChan: + i.ticker.Stop() + break L + case <-i.ticker.C: + loop.jobChan <- func() { + loop.doInterval(i) + } + } + } +} + +func FromState(L *lua.LState) *EventLoop { + loopGlobal := L.GetGlobal("__loop") + + if ud, ok := loopGlobal.(*lua.LUserData); ok { + return ud.Value.(*EventLoop) + } + + return nil +} diff --git a/scripts/warps.lua b/scripts/warps.lua index 9fa9171..4f62d8d 100644 --- a/scripts/warps.lua +++ b/scripts/warps.lua @@ -1,5 +1,6 @@ local event = require('event') local commands = require('commands') +timers = require('timers') minecraft = require('minecraft') config = require('config') @@ -11,12 +12,22 @@ warps = {} lastHomeTime = {} lastWarpTime = {} +objectiveName = "damageTaken" + event.on('init', function() local loadedHomes, err = config.load('homes') if loadedHomes and not err then homes = loadedHomes end + + local loadedWarps, err = config.load('warps') + + if loadedWarps and not err then + warps = loadedWarps + end + + rcon:SendCommand('scoreboard objectives add ' .. objectiveName .. ' minecraft.custom:minecraft.damage_taken') end) commands.register('sethome', function(user) @@ -36,7 +47,7 @@ commands.register('sethome', function(user) print('Unable to save homes') end - rcon:SendMessage(user, string.format("Home location set to %d, %d, %d", c[1], c[2], c[3])) + rcon:SendColorfulMessage(user, 'green', string.format("Home location set to %d, %d, %d", c[1], c[2], c[3])) end) commands.register('resethome', function(user) @@ -48,7 +59,7 @@ commands.register('resethome', function(user) print('Unable to save homes') end - rcon:SendMessage(user, 'Home location reset') + rcon:SendColorfulMessage(user, 'green', 'Home location reset') end) commands.register('home', function(user) @@ -59,7 +70,7 @@ commands.register('home', function(user) end if loc == nil then - rcon:SendMessage(user, "You haven't set your spawn or home yet.") + rcon:SendColorfulMessage(user, 'red', "You haven't set your spawn or home yet.") return end @@ -68,13 +79,35 @@ commands.register('home', function(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.") + rcon:SendColorfulMessage(user, 'red', "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])) + local delayLeft = 5 + + local originalDamage, err = rcon:SendCommand("scoreboard players get " .. user .. " " .. objectiveName) + + local interval = nil + + interval = timers.setInterval(function() + if delayLeft == 0 then + local newDamage, err = rcon:SendCommand("scoreboard players get " .. user .. " " .. objectiveName) + + timers.clearInterval(interval) + + if newDamage ~= originalDamage then + rcon:SendColorfulMessage(user, 'red', "Teleport was interrupted by combat.") + return + end + + rcon:SendCommand('execute in minecraft:overworld run tp ' .. user .. ' ' .. string.format("%f %d %f", loc[1], loc[2], loc[3])) + else + rcon:SendColorfulMessage(user, 'gray', "Teleporting in " .. delayLeft .. " seconds") + delayLeft = delayLeft - 1 + end + end, 1000) end) commands.register('setwarp ', function(user, place) @@ -82,7 +115,6 @@ commands.register('setwarp ', function(user, place) return end - local loc = rcon:GetLocation(user) local c = {} @@ -99,12 +131,12 @@ commands.register('setwarp ', function(user, place) 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])) + rcon:SendColorfulMessage(user, 'green', 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") + rcon:SendColorfulMessage(user, 'red', "This warp point doesn't exist") return end @@ -115,23 +147,11 @@ commands.register('warp ', function(user, place) 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.") + rcon:SendColorfulMessage(user, 'red', "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])) + rcon:SendCommand('execute in minecraft:overworld run tp ' .. 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