Files
mc/server/session/command.go
T

316 lines
9.4 KiB
Go
Raw Normal View History

2026-07-09 08:33:57 +08:00
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
}