package session import ( "math" "github.com/df-mc/dragonfly/server/cmd" "github.com/go-gl/mathgl/mgl64" "github.com/sandertv/gophertunnel/minecraft/protocol" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" "golang.org/x/text/language" ) // SendCommandOutput sends the output of a command to the player. It will be shown to the caller of the // command, which might be the player or a websocket server. func (s *Session) SendCommandOutput(output *cmd.Output, l language.Tag) { if s == Nop { return } messages := make([]protocol.CommandOutputMessage, 0, output.MessageCount()+output.ErrorCount()) for _, message := range output.Messages() { om := protocol.CommandOutputMessage{Success: true, Message: message.String()} if t, ok := message.(translation); ok { om.Message, om.Parameters = t.Resolve(l), t.Params(l) } messages = append(messages, om) } for _, err := range output.Errors() { om := protocol.CommandOutputMessage{Message: err.Error()} if t, ok := err.(translation); ok { om.Message, om.Parameters = t.Resolve(l), t.Params(l) } messages = append(messages, om) } s.writePacket(&packet.CommandOutput{ CommandOrigin: s.handlers[packet.IDCommandRequest].(*CommandRequestHandler).origin, OutputType: packet.CommandOutputTypeAllOutput, SuccessCount: uint32(output.MessageCount()), OutputMessages: messages, }) } type translation interface { Resolve(l language.Tag) string Params(l language.Tag) []string } // BuildAvailableCommands builds an AvailableCommands packet and the runnable command map for the Source // passed. The input map may contain aliases. It returns the AvailableCommands packet and the runnable command map // for the commands that the Source can execute. func BuildAvailableCommands( commands map[string]cmd.Command, src cmd.Source, softEnums map[string]struct{}, ) (*packet.AvailableCommands, map[string]map[int]cmd.Runnable) { m := make(map[string]map[int]cmd.Runnable, len(commands)) pk := &packet.AvailableCommands{} var enums []commandEnum enumIndices := map[string]uint32{} var dynamicEnums []commandEnum dynamicEnumIndices := map[string]uint32{} suffixIndices := map[string]uint32{} for alias, c := range commands { if c.Name() != alias { // Don't add duplicate entries for aliases. continue } if run := c.Runnables(src); len(run) > 0 { m[alias] = run } else { continue } params := c.Params(src) overloads := make([]protocol.CommandOverload, len(params)) aliasesIndex := uint32(math.MaxUint32) if len(c.Aliases()) > 0 { aliasesIndex = uint32(len(enumIndices)) enumIndices[c.Name()+"Aliases"] = aliasesIndex enums = append(enums, commandEnum{Type: c.Name() + "Aliases", Options: c.Aliases()}) } for i, params := range params { for _, paramInfo := range params { t, enum := valueToParamType(paramInfo, src) suffix := paramInfo.Suffix opt := byte(0) if suffix != "" { index, ok := suffixIndices[suffix] if !ok { index = uint32(len(pk.Suffixes)) suffixIndices[suffix] = index pk.Suffixes = append(pk.Suffixes, suffix) } t = protocol.CommandArgSuffixed | index } else { t |= protocol.CommandArgValid if len(enum.Options) > 0 || enum.Type != "" { _, dynamic := softEnums[enum.Type] if !dynamic { index, ok := enumIndices[enum.Type] if !ok { index = uint32(len(enums)) enumIndices[enum.Type] = index enums = append(enums, enum) } t |= protocol.CommandArgEnum | index } else { index, ok := dynamicEnumIndices[enum.Type] if !ok { index = uint32(len(dynamicEnums)) dynamicEnumIndices[enum.Type] = index dynamicEnums = append(dynamicEnums, enum) } t |= protocol.CommandArgSoftEnum | index } } } overloads[i].Parameters = append(overloads[i].Parameters, protocol.CommandParameter{ Name: paramInfo.Name, Type: t, Optional: paramInfo.Optional, Options: opt, }) } } pk.Commands = append(pk.Commands, protocol.Command{ Name: c.Name(), Description: c.Description(), AliasesOffset: aliasesIndex, PermissionLevel: protocol.CommandPermissionLevelAny, Overloads: overloads, }) } pk.DynamicEnums = make([]protocol.DynamicEnum, 0, len(dynamicEnums)) for _, e := range dynamicEnums { pk.DynamicEnums = append(pk.DynamicEnums, protocol.DynamicEnum{Type: e.Type, Values: e.Options}) } enumValueIndices := make(map[string]uint32, len(enums)*3) pk.EnumValues = make([]string, 0, len(enumValueIndices)) pk.Enums = make([]protocol.CommandEnum, 0, len(enums)) for _, enum := range enums { protoEnum := protocol.CommandEnum{Type: enum.Type} for _, opt := range enum.Options { index, ok := enumValueIndices[opt] if !ok { index = uint32(len(pk.EnumValues)) enumValueIndices[opt] = index pk.EnumValues = append(pk.EnumValues, opt) } protoEnum.ValueIndices = append(protoEnum.ValueIndices, index) } pk.Enums = append(pk.Enums, protoEnum) } return pk, m } // sendAvailableCommands sends all available commands of the server. Once sent, they will be visible in the // /help list and will be auto-completed. func (s *Session) sendAvailableCommands(co Controllable, softEnums map[string]struct{}) map[string]map[int]cmd.Runnable { commands := cmd.Commands() pk, m := BuildAvailableCommands(commands, co, softEnums) s.writePacket(pk) return m } type commandEnum struct { Type string Options []string } // valueToParamType finds the command argument type of the value passed and returns it, in addition to creating // an enum if applicable. func valueToParamType(i cmd.ParamInfo, source cmd.Source) (t uint32, enum commandEnum) { switch i.Value.(type) { case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: return protocol.CommandArgTypeInt, enum case float32, float64: return protocol.CommandArgTypeFloat, enum case string: return protocol.CommandArgTypeString, enum case cmd.Varargs: return protocol.CommandArgTypeRawText, enum case cmd.Target, []cmd.Target: return protocol.CommandArgTypeTarget, enum case bool: return 0, commandEnum{ Type: "Boolean", Options: []string{"true", "false"}, } case mgl64.Vec3: return protocol.CommandArgTypePosition, enum case cmd.SubCommand: return 0, commandEnum{ Type: "SubCommand" + i.Name, Options: []string{i.Name}, } } if enum, ok := i.Value.(cmd.Enum); ok { return 0, commandEnum{ Type: enum.Type(), Options: enum.Options(source), } } return protocol.CommandArgTypeValue, enum } // resendCommands resends all commands that a Session has access to if the map of runnable commands passed does not // match with the commands that the Session is currently allowed to execute. // True is returned if the commands were resent. func (s *Session) resendCommands( before map[string]map[int]cmd.Runnable, co Controllable, softEnums map[string]struct{}, ) (map[string]map[int]cmd.Runnable, bool) { commands := cmd.Commands() m := make(map[string]map[int]cmd.Runnable, len(commands)) for alias, c := range commands { if c.Name() == alias { if run := c.Runnables(co); len(run) > 0 { m[alias] = run } } } if len(before) != len(m) { return s.sendAvailableCommands(co, softEnums), true } // First check for commands that were newly added. for name, r := range m { for k := range r { if _, ok := before[name][k]; !ok { return s.sendAvailableCommands(co, softEnums), true } } } // Then check for commands that a player could execute before, but no longer can. for name, r := range before { for k := range r { if _, ok := m[name][k]; !ok { return s.sendAvailableCommands(co, softEnums), true } } } return m, false } // enums returns a map of all enums exposed to the Session and records the values those enums currently hold. func (s *Session) enums(co Controllable) (map[string]cmd.Enum, map[string][]string) { enums, enumValues := make(map[string]cmd.Enum), make(map[string][]string) for alias, c := range cmd.Commands() { if c.Name() == alias { for _, params := range c.Params(co) { for _, paramInfo := range params { if enum, ok := paramInfo.Value.(cmd.Enum); ok { enums[enum.Type()] = enum enumValues[enum.Type()] = enum.Options(co) } } } } } return enums, enumValues } // resendEnums checks the options of the enums passed against the values that were previously recorded. If they do not // match, and the enum is in softEnums, the enum is resent via UpdateSoftEnum. If the enum is not yet in softEnums, // it is added and the full AvailableCommands packet is resent. func (s *Session) resendEnums( enums map[string]cmd.Enum, before map[string][]string, softEnums map[string]struct{}, r map[string]map[int]cmd.Runnable, c Controllable, ) map[string]map[int]cmd.Runnable { for name, enum := range enums { valuesBefore := before[name] values := enum.Options(c) before[name] = values changed := false if len(valuesBefore) != len(values) { changed = true } else { for k, v := range values { if valuesBefore[k] != v { changed = true break } } } if !changed { continue } if _, dynamic := softEnums[name]; dynamic { s.writePacket(&packet.UpdateSoftEnum{EnumType: name, Options: values, ActionType: packet.SoftEnumActionSet}) } else { softEnums[name] = struct{}{} r = s.sendAvailableCommands(c, softEnums) } } return r }