From 2dac5bff5d26e46b0c97a8c14d436108794961fe Mon Sep 17 00:00:00 2001 From: robinbraemer Date: Sun, 9 Aug 2020 22:02:10 +0200 Subject: [PATCH] add command system & CommandExecuteEvent --- go.mod | 2 +- go.sum | 4 +- pkg/proxy/builtin_commands.go | 70 ++++++++++++++++ pkg/proxy/command.go | 132 +++++++++++++++++++++++++++++++ pkg/proxy/events.go | 35 ++++++++ pkg/proxy/proxy.go | 10 +++ pkg/proxy/session_client_play.go | 84 +++++++++----------- 7 files changed, 286 insertions(+), 51 deletions(-) create mode 100644 pkg/proxy/builtin_commands.go create mode 100644 pkg/proxy/command.go diff --git a/go.mod b/go.mod index 011f6dd5..09271d3e 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/spf13/viper v1.7.0 github.com/stretchr/testify v1.6.1 github.com/valyala/fasthttp v1.15.1 - go.minekube.com/common v0.0.0-20200804114822-9c4fad286696 + go.minekube.com/common v0.0.0-20200809185449-de163b8050bf go.uber.org/atomic v1.6.0 go.uber.org/zap v1.15.0 golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae // indirect diff --git a/go.sum b/go.sum index e2bcac67..979c842f 100644 --- a/go.sum +++ b/go.sum @@ -299,8 +299,8 @@ github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMI github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.minekube.com/common v0.0.0-20200804114822-9c4fad286696 h1:BIrnH7iDE48hMJmEL/eZf0+zZuDik4ZWQbbQMMCKt9w= -go.minekube.com/common v0.0.0-20200804114822-9c4fad286696/go.mod h1:PCdSdTInlQv6ggDIbVjLFs7ehSRP4i9KqYsLAeeNUYU= +go.minekube.com/common v0.0.0-20200809185449-de163b8050bf h1:HVTUSpJlMZwcRiwKZ0nMDvqMGnPZRBdPC5BklO26iDo= +go.minekube.com/common v0.0.0-20200809185449-de163b8050bf/go.mod h1:PCdSdTInlQv6ggDIbVjLFs7ehSRP4i9KqYsLAeeNUYU= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= diff --git a/pkg/proxy/builtin_commands.go b/pkg/proxy/builtin_commands.go new file mode 100644 index 00000000..70f10a84 --- /dev/null +++ b/pkg/proxy/builtin_commands.go @@ -0,0 +1,70 @@ +package proxy + +import ( + "context" + "fmt" + . "go.minekube.com/common/minecraft/color" + . "go.minekube.com/common/minecraft/component" + "time" +) + +type serverCmd struct{ proxy *Proxy } + +func (s *serverCmd) Invoke(c *Context) { + if len(c.Args) == 0 { + s.list(c) + return + } + s.connect(c) +} + +// switch server +func (s *serverCmd) connect(c *Context) { + player, ok := c.Source.(Player) + if !ok { + _ = c.Source.SendMessage(&Text{Content: "Only players can connect to a server!", S: Style{Color: Red}}) + return + } + + server := c.Args[0] + rs := s.proxy.Server(server) + if rs == nil { + _ = c.Source.SendMessage(&Text{Content: fmt.Sprintf("Server %q not registered", server), S: Style{Color: Red}}) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(s.proxy.config.ConnectionTimeout)) + defer cancel() + player.CreateConnectionRequest(rs).ConnectWithIndication(ctx) +} + +// list registered servers +func (s *serverCmd) list(c *Context) { + const maxEntries = 50 + var servers []Component + proxyServers := s.proxy.Servers() + for i, s := range proxyServers { + if i+1 == maxEntries { + servers = append(servers, &Text{ + Content: fmt.Sprintf("and %d more...", len(proxyServers)-i+1), + }) + break + } + servers = append(servers, &Text{ + Content: fmt.Sprintf(" %s - %s (%d players)\n", + s.ServerInfo().Name(), s.ServerInfo().Addr(), s.Players().Len()), + S: Style{ClickEvent: RunCommand(fmt.Sprintf("/server %s", s.ServerInfo().Name()))}, + }) + } + _ = c.Source.SendMessage(&Text{ + Content: fmt.Sprintf("\nServers (%d):\n", len(proxyServers)), + S: Style{Color: Green}, + Extra: []Component{&Text{ + S: Style{ + Color: Yellow, + HoverEvent: ShowText(&Text{Content: "Click to connect!", S: Style{Color: Green}}), + }, + Extra: servers, + }}, + }) +} diff --git a/pkg/proxy/command.go b/pkg/proxy/command.go new file mode 100644 index 00000000..57547b02 --- /dev/null +++ b/pkg/proxy/command.go @@ -0,0 +1,132 @@ +package proxy + +import ( + "context" + "errors" + "fmt" + "regexp" + "strings" + "sync" +) + +type CommandManager struct { + mu sync.RWMutex + commands map[string]*registration +} + +// newCommandManager returns a new CommandManager. +func newCommandManager() *CommandManager { + return &CommandManager{commands: map[string]*registration{}} +} + +type registration struct { + cmd Command + aliases []string +} + +// Register registers (and overrides) a command with the root literal name and optional aliases. +func (m *CommandManager) Register(cmd Command, name string, aliases ...string) { + if cmd == nil { + return + } + r := ®istration{ + cmd: cmd, + aliases: append(aliases, name), + } + m.mu.Lock() + defer m.mu.Unlock() + m.commands[name] = r + for _, name := range aliases { + m.commands[name] = r + } +} + +// Unregister unregisters a command with its aliases. +func (m *CommandManager) Unregister(name string) { + m.mu.Lock() + r, ok := m.commands[name] + if ok { + for _, name := range r.aliases { + delete(m.commands, name) + } + } + delete(m.commands, name) + m.mu.Unlock() +} + +// Has return true if the command is registered. +func (m *CommandManager) Has(command string) bool { + m.mu.RLock() + _, ok := m.commands[command] + m.mu.RUnlock() + return ok +} + +// Invoke invokes a registered command. +func (m *CommandManager) Invoke(ctx *Context, command string) (found bool, err error) { + if len(command) == 0 { + return false, errors.New("command must not be empty") + } + if ctx == nil { + return false, errors.New("ctx must not be nil") + } + if ctx.Source == nil { + return false, errors.New("ctx source must not be nil") + } + if ctx.Context == nil { + ctx.Context = context.Background() + } + m.mu.RLock() + r, ok := m.commands[command] + m.mu.RUnlock() + if !ok { + return false, nil + } + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic while invoking command: %v", r) + } + }() + r.cmd.Invoke(ctx) + return true, err +} + +// Command is an invokable command. +type Command interface { + Invoke(*Context) +} + +// Func is a shorthand type that implements the Command interface. +type Func func(*Context) + +// Invoke implements Command. +func (f Func) Invoke(c *Context) { + f(c) +} + +// Context is a command invocation context. +type Context struct { + context.Context + Source CommandSource + Args []string +} + +var spaceRegex = regexp.MustCompile(`\s+`) + +// trimSpaces removes all spaces that are to much. +func trimSpaces(s string) string { + s = strings.TrimSpace(s) + return spaceRegex.ReplaceAllString(s, " ") // remove to much spaces in between +} + +func extract(commandline string) (command string, args []string, ok bool) { + split := strings.Split(commandline, " ") + if len(split) != 0 { + command = split[0] + ok = true + } + if len(split) > 1 { + args = split[1:] + } + return +} diff --git a/pkg/proxy/events.go b/pkg/proxy/events.go index ea68cbc5..3ef42d8d 100644 --- a/pkg/proxy/events.go +++ b/pkg/proxy/events.go @@ -652,6 +652,7 @@ func (s *PlayerSettingsChangedEvent) Settings() player.Settings { // // PlayerChatEvent is fired when a player sends a chat message. +// Note that messages with a leading "/" do not trigger this event, but instead CommandExecuteEvent. type PlayerChatEvent struct { player Player message string @@ -678,3 +679,37 @@ func (c *PlayerChatEvent) SetAllowed(allowed bool) { func (c *PlayerChatEvent) Allowed() bool { return !c.denied } + +// +// +// +// +// + +// CommandExecuteEvent is fired when someone wants to execute a command. +type CommandExecuteEvent struct { + source CommandSource + commandline string + + denied bool +} + +// Source returns the command source that wants to run the command. +func (c *CommandExecuteEvent) Source() CommandSource { + return c.source +} + +// Command returns the whole commandline without the leading "/". +func (c *CommandExecuteEvent) Command() string { + return c.commandline +} + +// SetAllowed sets whether the command is allowed to be executed. +func (c *CommandExecuteEvent) SetAllowed(allowed bool) { + c.denied = !allowed +} + +// Allowed returns true when the command is allowed to be executed. +func (c *CommandExecuteEvent) Allowed() bool { + return !c.denied +} diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go index 25cf90d3..a740ab52 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -31,6 +31,7 @@ type Proxy struct { *connect config *config.Config event *event.Manager + command *CommandManager channelRegistrar *ChannelRegistrar authenticator *auth.Authenticator @@ -54,6 +55,7 @@ func New(config config.Config) (s *Proxy) { closed: make(chan struct{}), config: &config, event: event.NewManager(), + command: newCommandManager(), channelRegistrar: NewChannelRegistrar(), servers: map[string]RegisteredServer{}, authenticator: auth.NewAuthenticator(), @@ -130,6 +132,9 @@ func (p *Proxy) preInit() (err error) { if len(c.Servers) != 0 { zap.S().Infof("Pre-registered %d servers", len(c.Servers)) } + + // Register builtin commands + p.command.Register(&serverCmd{proxy: p}, "server") return } @@ -174,6 +179,11 @@ func (p *Proxy) Event() *event.Manager { return p.event } +// Command returns the Proxy's command manager. +func (p *Proxy) Command() *CommandManager { + return p.command +} + // Config returns the config used by the Proxy. func (p *Proxy) Config() config.Config { return *p.config diff --git a/pkg/proxy/session_client_play.go b/pkg/proxy/session_client_play.go index a5180a4d..38eb6dc4 100644 --- a/pkg/proxy/session_client_play.go +++ b/pkg/proxy/session_client_play.go @@ -2,10 +2,7 @@ package proxy import ( "context" - "fmt" "github.com/gammazero/deque" - "go.minekube.com/common/minecraft/color" - "go.minekube.com/common/minecraft/component" "go.minekube.com/gate/pkg/event" "go.minekube.com/gate/pkg/proto" "go.minekube.com/gate/pkg/proto/packet" @@ -324,25 +321,46 @@ func (c *clientPlaySessionHandler) handleChat(p *packet.Chat) { return } - zap.S().Debugf("ChatPacket> %s: %s", c.player.Username(), p.Message) - - // TODO add a proper proxy commands system here + // Is it a command? if strings.HasPrefix(p.Message, "/") { - args := strings.Split(p.Message, " ") - if len(args) != 0 && strings.HasPrefix(args[0], "/server") { - c.serverCmd(args) + commandline := trimSpaces(strings.TrimPrefix(p.Message, "/")) + + e := &CommandExecuteEvent{ + source: c.player, + commandline: commandline, + } + c.proxy().event.Fire(e) + if !e.Allowed() || !c.player.Active() { + return } - return - } - e := &PlayerChatEvent{ - player: c.player, - message: p.Message, - } - c.proxy().Event().Fire(e) - if !e.Allowed() { - return + cmd, args, _ := extract(commandline) + if c.proxy().command.Has(cmd) { + zap.S().Infof("%s executing command /%s", c.player, commandline) + // Invoke registered command + _, err := c.proxy().command.Invoke(&Context{ + Context: context.Background(), + Source: c.player, + Args: args, + }, cmd) + if err != nil { + zap.S().Errorf("Error invoking command %q: %v", commandline, err) + } + return + } + // Else, proxy command not registered, forward to server. + } else { + e := &PlayerChatEvent{ + player: c.player, + message: p.Message, + } + c.proxy().Event().Fire(e) + if !e.Allowed() || !c.player.Active() { + return + } + zap.S().Debugf("Chat> %s: %s", c.player, p.Message) } + // Forward to server _ = serverMc.WritePacket(&packet.Chat{ Message: p.Message, @@ -351,36 +369,6 @@ func (c *clientPlaySessionHandler) handleChat(p *packet.Chat) { }) } -// TODO use proper command system -func (c *clientPlaySessionHandler) serverCmd(args []string) { - if len(args) > 1 { - // switch server - successful := c.player.CreateConnectionRequest(c.proxy().Server(args[1])).ConnectWithIndication(context.Background()) - if successful { - _ = c.player.SendMessage(&component.Text{ - Content: "Connected to server " + args[1], - S: component.Style{Color: color.Green}, - }) - } - } else { - // list registered servers - var servers []component.Component - for _, s := range c.proxy().Servers() { - servers = append(servers, &component.Text{ - Content: fmt.Sprintf(" %s - %s\n", s.ServerInfo().Name(), s.ServerInfo().Addr()), - }) - } - _ = c.player.SendMessage(&component.Text{ - Content: fmt.Sprintf("\nServers (%d):\n", len(servers)), - S: component.Style{Color: color.Green}, - Extra: []component.Component{&component.Text{ - S: component.Style{Color: color.Yellow}, - Extra: servers, - }}, - }) - } -} - func (c *clientPlaySessionHandler) player_() *connectedPlayer { return c.player }