Major updates/patches, functionality, api
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
This commit is contained in:
parent
afb79eaceb
commit
b52b38179a
|
@ -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
|
|
@ -1,2 +1,3 @@
|
||||||
.idea/
|
.idea/
|
||||||
*.iml
|
*.iml
|
||||||
|
*.log
|
|
@ -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])
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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...)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
go 1.12
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
|
||||||
github.com/fsnotify/fsnotify v1.4.7 // indirect
|
github.com/fsnotify/fsnotify v1.4.7 // indirect
|
||||||
github.com/hpcloud/tail v1.0.0
|
github.com/hpcloud/tail v1.0.0
|
||||||
github.com/tystuyfzand/mcgorcon v0.0.0-20190416171454-d0d528ef5548
|
github.com/ppacher/nbt v0.0.0-20181201174858-0cad976cf07c
|
||||||
|
github.com/tystuyfzand/mcgorcon v0.0.0-20190418232414-4bb1402707f4
|
||||||
|
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583
|
||||||
golang.org/x/sys v0.0.0-20190415145633-3fd5a3612ccd // indirect
|
golang.org/x/sys v0.0.0-20190415145633-3fd5a3612ccd // indirect
|
||||||
gopkg.in/fsnotify.v1 v1.4.7 // indirect
|
gopkg.in/fsnotify.v1 v1.4.7 // indirect
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||||
|
layeh.com/gopher-json v0.0.0-20190114024228-97fed8db8427
|
||||||
|
layeh.com/gopher-luar v1.0.5
|
||||||
)
|
)
|
||||||
|
|
18
go.sum
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 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/tystuyfzand/mcgorcon v0.0.0-20190416171454-d0d528ef5548 h1:ho5QHzfPALI0+v7sPzJFtSHDH/5amyuG88+3ycp70Rk=
|
github.com/ppacher/nbt v0.0.0-20181201174858-0cad976cf07c h1:d9Pm+C0vGwMRxn04D6MjHbqkEvG+qlpHjlARadAAmpQ=
|
||||||
github.com/tystuyfzand/mcgorcon v0.0.0-20190416171454-d0d528ef5548/go.mod h1:MpDGxcw1VVpnQrSbEjy5ZRc+RVgO3j68N8RuI8snkF4=
|
github.com/ppacher/nbt v0.0.0-20181201174858-0cad976cf07c/go.mod h1:vvnpyLNjExupwOjP8Dvqprqwtt3BxXKoDSvWkHvCODs=
|
||||||
|
github.com/tystuyfzand/mcgorcon v0.0.0-20190418232414-4bb1402707f4 h1:E2wndDHuZBEqCUijjvLhfFu8wInU9ciBZuXXkn/lGHk=
|
||||||
|
github.com/tystuyfzand/mcgorcon v0.0.0-20190418232414-4bb1402707f4/go.mod h1:MpDGxcw1VVpnQrSbEjy5ZRc+RVgO3j68N8RuI8snkF4=
|
||||||
|
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583 h1:SZPG5w7Qxq7bMcMVl6e3Ht2X7f+AAGQdzjkbyOnNNZ8=
|
||||||
|
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
|
||||||
|
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190415145633-3fd5a3612ccd h1:MNN7PRW7zYXd8upVO5qfKeOnQG74ivRNv7sz4k4cQMs=
|
golang.org/x/sys v0.0.0-20190415145633-3fd5a3612ccd h1:MNN7PRW7zYXd8upVO5qfKeOnQG74ivRNv7sz4k4cQMs=
|
||||||
golang.org/x/sys v0.0.0-20190415145633-3fd5a3612ccd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190415145633-3fd5a3612ccd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
layeh.com/gopher-json v0.0.0-20190114024228-97fed8db8427 h1:RZkKxMR3jbQxdCEcglq3j7wY3PRJIopAwBlx1RE71X0=
|
||||||
|
layeh.com/gopher-json v0.0.0-20190114024228-97fed8db8427/go.mod h1:ivKkcY8Zxw5ba0jldhZCYYQfGdb2K6u9tbYK1AwMIBc=
|
||||||
|
layeh.com/gopher-luar v1.0.5 h1:fBuMh/xVN7bZxOsFzY6mxL2I+0ePJIWfyGc0dBdpqs4=
|
||||||
|
layeh.com/gopher-luar v1.0.5/go.mod h1:N3rev/ttQd8yVluXaYsa0M/eknzRYWe+pxZ35ZFmaaI=
|
||||||
|
|
286
main.go
286
main.go
|
@ -2,43 +2,30 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
|
||||||
"github.com/hpcloud/tail"
|
|
||||||
"github.com/tystuyfzand/mcgorcon"
|
|
||||||
"log"
|
"log"
|
||||||
"math"
|
"meow.tf/residentsleeper/commands"
|
||||||
|
"meow.tf/residentsleeper/events"
|
||||||
|
"meow.tf/residentsleeper/rcon"
|
||||||
|
"meow.tf/residentsleeper/scripting/config"
|
||||||
|
"meow.tf/residentsleeper/scripting/minecraft"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"strings"
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
timeThreadRegexp = "^\\[.*?\\]\\s\\[.*?\\/INFO\\]:\\s"
|
|
||||||
|
|
||||||
tickDuration = time.Millisecond * 50
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
serverPath string
|
serverPath string
|
||||||
rconPort int
|
rconPort int
|
||||||
rconPassword string
|
rconPassword string
|
||||||
|
debug bool
|
||||||
|
|
||||||
client *mcgorcon.Client
|
client *rcon.Session
|
||||||
|
|
||||||
messageRegexp = regexp.MustCompile(timeThreadRegexp + "<(.*?)>\\s(.*)")
|
|
||||||
joinedRegexp = regexp.MustCompile(timeThreadRegexp + "(.*?) joined the game$")
|
|
||||||
leftRegexp = regexp.MustCompile(timeThreadRegexp + "(.*?) left the game$")
|
|
||||||
stoppingRegexp = regexp.MustCompile(timeThreadRegexp + "Stopping server")
|
|
||||||
rconRegexp = regexp.MustCompile(timeThreadRegexp + "RCON running on")
|
|
||||||
|
|
||||||
sleepRegexp = regexp.MustCompile("^z{3,}$")
|
|
||||||
timeRegexp = regexp.MustCompile("(\\d+)$")
|
|
||||||
userRegexp = regexp.MustCompile("There are (\\d+) of a max (\\d+) players online: (.*?)$")
|
|
||||||
|
|
||||||
configRegexp = regexp.MustCompile("^(.*?)=(.*)$")
|
configRegexp = regexp.MustCompile("^(.*?)=(.*)$")
|
||||||
|
|
||||||
|
flagDebug = flag.Bool("debug", false, "debug messages")
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -48,6 +35,8 @@ func init() {
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
debug = *flagDebug
|
||||||
|
|
||||||
// Load properties
|
// Load properties
|
||||||
configPath := path.Join(serverPath, "server.properties")
|
configPath := path.Join(serverPath, "server.properties")
|
||||||
|
|
||||||
|
@ -74,225 +63,86 @@ func main() {
|
||||||
log.Fatalln("RCON is not enabled: No password set")
|
log.Fatalln("RCON is not enabled: No password set")
|
||||||
}
|
}
|
||||||
|
|
||||||
go queryPlayersLoop()
|
events.On(events.Message, handleCommands)
|
||||||
|
|
||||||
|
client = rcon.NewSession("localhost", rconPort, rconPassword)
|
||||||
|
|
||||||
|
client.Debug = debug
|
||||||
|
|
||||||
|
scriptPath := path.Join(serverPath, "scripts")
|
||||||
|
|
||||||
|
if _, err := os.Stat(scriptPath); !os.IsNotExist(err) {
|
||||||
|
config.BasePath = path.Join(scriptPath, "config")
|
||||||
|
|
||||||
|
minecraft.BasePath = serverPath
|
||||||
|
minecraft.WorldPath = path.Join(serverPath, cfg["level-name"])
|
||||||
|
|
||||||
|
if _, err = os.Stat(config.BasePath); os.IsNotExist(err) {
|
||||||
|
os.Mkdir(config.BasePath, 0755)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Loading scripts from", scriptPath)
|
||||||
|
err = LoadScripts(scriptPath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Warning: Unable to load scripts -", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logPath := path.Join(serverPath, "logs/latest.log")
|
logPath := path.Join(serverPath, "logs/latest.log")
|
||||||
|
|
||||||
log.Println("Starting rcon connection")
|
|
||||||
|
|
||||||
ensureConnection()
|
|
||||||
|
|
||||||
logParser(logPath)
|
logParser(logPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func logParser(logPath string) {
|
func handleCommands(eventArgs ...interface{}) {
|
||||||
log.Println("Watching log path ", logPath)
|
user := eventArgs[0].(string)
|
||||||
|
str := eventArgs[1].(string)
|
||||||
|
|
||||||
stat, err := os.Stat(logPath)
|
if debug {
|
||||||
|
log.Println("Message from " + user + ": " + str)
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("Unable to open log file:", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
seek := &tail.SeekInfo{
|
args := commands.ParseCommandArguments(str)
|
||||||
Offset: stat.Size(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start parsing file
|
idx := strings.Index(str, " ")
|
||||||
t, err := tail.TailFile(logPath, tail.Config{Location: seek, Follow: true, ReOpen: true})
|
|
||||||
|
|
||||||
if err != nil {
|
var argString string
|
||||||
log.Fatalln("Unable to open file:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var m []string
|
if idx == -1 {
|
||||||
|
argString = ""
|
||||||
for line := range t.Lines {
|
|
||||||
|
|
||||||
if m = messageRegexp.FindStringSubmatch(line.Text); m != nil {
|
|
||||||
handleMessage(m[1], m[2])
|
|
||||||
} else if m = joinedRegexp.FindStringSubmatch(line.Text); m != nil {
|
|
||||||
userJoined(m[1])
|
|
||||||
} else if m = leftRegexp.FindStringSubmatch(line.Text); m != nil {
|
|
||||||
userLeft(m[1])
|
|
||||||
} else if stoppingRegexp.MatchString(line.Text) {
|
|
||||||
log.Println("Server closing")
|
|
||||||
|
|
||||||
if client != nil {
|
|
||||||
// Close the server connection to allow it to exit normally
|
|
||||||
client.Close()
|
|
||||||
}
|
|
||||||
} else if rconRegexp.MatchString(line.Text) {
|
|
||||||
log.Println("Rcon started, connecting")
|
|
||||||
|
|
||||||
if client != nil {
|
|
||||||
client.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reconnect, as we got a new rcon start line
|
|
||||||
ensureConnection()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
votes = make(map[string]bool)
|
|
||||||
voteLock sync.RWMutex
|
|
||||||
|
|
||||||
onlinePlayers int
|
|
||||||
expireTime time.Time
|
|
||||||
)
|
|
||||||
|
|
||||||
func queryPlayersLoop() {
|
|
||||||
t := time.NewTicker(30 * time.Second)
|
|
||||||
|
|
||||||
for {
|
|
||||||
if client == nil {
|
|
||||||
<- t.C
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
online, _, _, err := onlineUsers()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
onlinePlayers = online
|
|
||||||
|
|
||||||
<- t.C
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleMessage(user, message string) {
|
|
||||||
if !sleepRegexp.MatchString(message) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query time from server
|
|
||||||
// Add seconds
|
|
||||||
t, err := queryTime()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
sendMessage(user, "Something went wrong and the time couldn't be retrieved.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if t < 12000 {
|
|
||||||
sendMessage(user, "It's not night time, go mine some more.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
difference := 24000 - t
|
|
||||||
|
|
||||||
if expireTime.IsZero() || time.Now().After(expireTime) {
|
|
||||||
expDuration := time.Duration(difference) * tickDuration
|
|
||||||
|
|
||||||
expireTime = time.Now().Add(expDuration)
|
|
||||||
|
|
||||||
// Reset the time after
|
|
||||||
time.AfterFunc(expDuration, func() {
|
|
||||||
voteLock.Lock()
|
|
||||||
votes = make(map[string]bool)
|
|
||||||
voteLock.Unlock()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
voteLock.RLock()
|
|
||||||
if _, exists := votes[user]; exists {
|
|
||||||
voteLock.RUnlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
voteLock.RUnlock()
|
|
||||||
|
|
||||||
voteLock.Lock()
|
|
||||||
votes[user] = true
|
|
||||||
voteLock.Unlock()
|
|
||||||
|
|
||||||
requiredVotes := int(math.Ceil(float64(onlinePlayers) * 0.30))
|
|
||||||
|
|
||||||
if len(votes) >= requiredVotes {
|
|
||||||
mimicSleeping(t)
|
|
||||||
} else {
|
} else {
|
||||||
serverMessage(fmt.Sprintf("%s wants to sleep (%d of %d votes)", user, len(votes), requiredVotes))
|
argString = strings.TrimSpace(str[idx+1:])
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func mimicSleeping(t int) {
|
var command string
|
||||||
var err error
|
|
||||||
|
|
||||||
if t == -1 {
|
if len(args) > 1 {
|
||||||
t, err = queryTime()
|
command, args = args[0], args[1:]
|
||||||
|
} else {
|
||||||
if err != nil {
|
command = str
|
||||||
return
|
args = []string{}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
voteLock.Lock()
|
// Find the channel that the message came from. Override and use State if enabled.
|
||||||
votes = make(map[string]bool)
|
|
||||||
voteLock.Unlock()
|
|
||||||
|
|
||||||
difference := 24000 - t
|
match := commands.Find("!", command, str)
|
||||||
|
|
||||||
log.Println("Adding time", difference)
|
if match == nil {
|
||||||
|
|
||||||
t, err = addTime(difference)
|
|
||||||
|
|
||||||
if err != nil || t > 12000 {
|
|
||||||
serverMessage("Could not set the time, sorry")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
serverMessage("Good morning everyone!")
|
ctx := &commands.CommandContext{
|
||||||
|
User: user,
|
||||||
|
Prefix: "!",
|
||||||
|
Command: command,
|
||||||
|
ArgumentString: argString,
|
||||||
|
Arguments: args,
|
||||||
|
ArgumentCount: len(args),
|
||||||
|
|
||||||
// Force the vote to expire
|
Reply: func(text string) {
|
||||||
expireTime = time.Now()
|
client.SendMessage(user, text)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func userJoined(user string) {
|
match.Call(ctx)
|
||||||
onlinePlayers++
|
|
||||||
|
|
||||||
log.Println("Player joined:", user)
|
|
||||||
}
|
|
||||||
|
|
||||||
func userLeft(user string) {
|
|
||||||
onlinePlayers--
|
|
||||||
|
|
||||||
log.Println("Player left:", user)
|
|
||||||
|
|
||||||
voteLock.Lock()
|
|
||||||
defer voteLock.Unlock()
|
|
||||||
|
|
||||||
if _, exists := votes[user]; exists {
|
|
||||||
delete(votes, user)
|
|
||||||
}
|
|
||||||
|
|
||||||
requiredVotes := int(math.Ceil(float64(onlinePlayers) * 0.30))
|
|
||||||
|
|
||||||
if len(votes) >= requiredVotes {
|
|
||||||
mimicSleeping(-1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensureConnection() *mcgorcon.Client {
|
|
||||||
if client == nil {
|
|
||||||
c, err := mcgorcon.Dial("localhost", rconPort, rconPassword)
|
|
||||||
|
|
||||||
tries := 0
|
|
||||||
|
|
||||||
for err != nil {
|
|
||||||
c, err = mcgorcon.Dial("localhost", rconPort, rconPassword)
|
|
||||||
|
|
||||||
tries++
|
|
||||||
|
|
||||||
if tries >= 10 {
|
|
||||||
log.Fatalln("Unable to connect to rcon, giving up.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
client = &c
|
|
||||||
}
|
|
||||||
|
|
||||||
return client
|
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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