Major updates/patches, functionality, api
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
parent
afb79eaceb
commit
b52b38179a
16
.drone.yml
Normal file
16
.drone.yml
Normal 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
3
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
.idea/
|
||||
*.iml
|
||||
*.iml
|
||||
*.log
|
9
Dockerfile
Normal file
9
Dockerfile
Normal file
@ -0,0 +1,9 @@
|
||||
FROM golang:alpine AS builder
|
||||
|
||||
RUN go build -o sleeper
|
||||
|
||||
FROM alpine
|
||||
|
||||
COPY --from=builder sleeper /sleeper
|
||||
|
||||
CMD ["/sleeper"]
|
76
commands.go
76
commands.go
@ -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
88
commands/arguments.go
Normal 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
15
commands/context.go
Normal 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
69
commands/definition.go
Normal 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
35
commands/dispatcher.go
Normal 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
71
commands/parser.go
Normal 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
69
commands/parser_test.go
Normal 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
54
events/events.go
Normal 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
29
events/names.go
Normal 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
9
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
|
||||
)
|
||||
|
18
go.sum
18
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=
|
||||
|
292
main.go
292
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
|
||||
}
|
110
parser.go
Normal file
110
parser.go
Normal 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
48
parser_test.go
Normal 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
81
rcon/players.go
Normal 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
115
rcon/rcon.go
Normal 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
47
rcon/time.go
Normal 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
55
scripting.go
Normal 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)
|
||||
}
|
37
scripting/commands/commands.go
Normal file
37
scripting/commands/commands.go
Normal 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
|
||||
}
|
96
scripting/config/config.go
Normal file
96
scripting/config/config.go
Normal 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
37
scripting/event/event.go
Normal 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
|
||||
}
|
33
scripting/minecraft/nbt.go
Normal file
33
scripting/minecraft/nbt.go
Normal 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
|
||||
}
|
69
scripting/minecraft/ops.go
Normal file
69
scripting/minecraft/ops.go
Normal 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
|
||||
}
|
125
scripting/minecraft/players.go
Normal file
125
scripting/minecraft/players.go
Normal 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
|
||||
}
|
30
scripting/regexp/regexp.go
Normal file
30
scripting/regexp/regexp.go
Normal 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
|
||||
}
|
98
scripts/residentsleeper.lua
Normal file
98
scripts/residentsleeper.lua
Normal 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
124
scripts/timetracker.lua
Normal 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
137
scripts/warps.lua
Normal 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
5
scripts/welcome.lua
Normal file
@ -0,0 +1,5 @@
|
||||
local event = require('event')
|
||||
|
||||
event.on('join', function(user)
|
||||
rcon:ServerMessage('Welcome to the server ' .. user .. '!!!')
|
||||
end)
|
Loading…
Reference in New Issue
Block a user