355 lines
12 KiB
Go
355 lines
12 KiB
Go
|
|
package cmd
|
||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/csv"
|
||
|
|
"fmt"
|
||
|
|
"go/ast"
|
||
|
|
"reflect"
|
||
|
|
"slices"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"github.com/df-mc/dragonfly/server/world"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Runnable represents a Command that may be run by a Command source. The Command must be a struct type and
|
||
|
|
// its fields represent the parameters of the Command. When the Run method is called, these fields are set
|
||
|
|
// and may be used for behaviour in the Command. Fields unexported or ignored using the `cmd:"-"` struct tag (see
|
||
|
|
// below) have their values copied but retained.
|
||
|
|
// A Runnable may have exported fields only of the following types:
|
||
|
|
// int8, int16, int32, int64, int, uint8, uint16, uint32, uint64, uint,
|
||
|
|
// float32, float64, string, bool, mgl64.Vec3, Varargs, []Target, cmd.SubCommand, Optional[T] (to make a parameter
|
||
|
|
// optional), or a type that implements the cmd.Parameter or cmd.Enum interface. cmd.Enum implementations must be of the
|
||
|
|
// type string.
|
||
|
|
// Fields in the Runnable struct may have `cmd:` struct tag to specify the name and suffix of a parameter as such:
|
||
|
|
//
|
||
|
|
// type T struct {
|
||
|
|
// Param int `cmd:"name,suffix"`
|
||
|
|
// }
|
||
|
|
//
|
||
|
|
// If no name is set, the field name is used. Additionally, the name as specified in the struct tag may be '-' to make
|
||
|
|
// the parser ignore the field. In this case, the field does not have to be of one of the types above.
|
||
|
|
type Runnable interface {
|
||
|
|
// Run runs the Command, using the arguments passed to the Command. The source is passed to the method,
|
||
|
|
// which is the source of the Command execution, and the output is passed, to which messages may be
|
||
|
|
// added which get sent to the source.
|
||
|
|
Run(src Source, o *Output, tx *world.Tx)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Allower may be implemented by a type also implementing Runnable to limit the sources that may run the
|
||
|
|
// command.
|
||
|
|
type Allower interface {
|
||
|
|
// Allow checks if the Source passed is allowed to execute the command. True is returned if the Source is
|
||
|
|
// allowed to execute the command.
|
||
|
|
Allow(src Source) bool
|
||
|
|
}
|
||
|
|
|
||
|
|
// Command is a wrapper around a Runnable. It provides additional identity and utility methods for the actual
|
||
|
|
// runnable command so that it may be identified more easily.
|
||
|
|
type Command struct {
|
||
|
|
v []reflect.Value
|
||
|
|
name string
|
||
|
|
description string
|
||
|
|
usage string
|
||
|
|
aliases []string
|
||
|
|
}
|
||
|
|
|
||
|
|
// New returns a new Command using the name and description passed. Command
|
||
|
|
// names and aliases are all converted to lowercase. The Runnable passed must
|
||
|
|
// be a (pointer to a) struct, with its fields representing the parameters of
|
||
|
|
// the command. When the command is run, the Run method of the Runnable will be
|
||
|
|
// called after all fields have their values from the parsed command set. If r
|
||
|
|
// is not a struct or a pointer to a struct, New panics.
|
||
|
|
func New(name, description string, aliases []string, r ...Runnable) Command {
|
||
|
|
name = strings.ToLower(name)
|
||
|
|
for i, alias := range aliases {
|
||
|
|
aliases[i] = strings.ToLower(alias)
|
||
|
|
}
|
||
|
|
|
||
|
|
usages := make([]string, len(r))
|
||
|
|
runnableValues := make([]reflect.Value, len(r))
|
||
|
|
|
||
|
|
if len(aliases) > 0 && slices.Index(aliases, name) == -1 {
|
||
|
|
aliases = append(aliases, name)
|
||
|
|
}
|
||
|
|
|
||
|
|
for i, runnable := range r {
|
||
|
|
t := reflect.TypeOf(runnable)
|
||
|
|
if t.Kind() != reflect.Struct && (t.Kind() != reflect.Ptr || t.Elem().Kind() != reflect.Struct) {
|
||
|
|
panic(fmt.Sprintf("Runnable r must be struct or pointer to struct, but got %v", t.Kind()))
|
||
|
|
}
|
||
|
|
original := reflect.ValueOf(runnable)
|
||
|
|
if t.Kind() == reflect.Ptr {
|
||
|
|
original = original.Elem()
|
||
|
|
}
|
||
|
|
|
||
|
|
cp := reflect.New(original.Type()).Elem()
|
||
|
|
if err := verifySignature(cp); err != nil {
|
||
|
|
panic(err.Error())
|
||
|
|
}
|
||
|
|
runnableValues[i], usages[i] = original, parseUsage(name, cp)
|
||
|
|
}
|
||
|
|
|
||
|
|
return Command{name: name, description: description, aliases: aliases, v: runnableValues, usage: strings.Join(usages, "\n")}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Name returns the name of the command. The name is guaranteed to be lowercase and will never have spaces in
|
||
|
|
// it. This name is used to call the command, and is shown in the /help list.
|
||
|
|
func (cmd Command) Name() string {
|
||
|
|
return cmd.name
|
||
|
|
}
|
||
|
|
|
||
|
|
// Description returns the description of the command. The description is shown in the /help list, and
|
||
|
|
// provides information on the functionality of a command.
|
||
|
|
func (cmd Command) Description() string {
|
||
|
|
return cmd.description
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usage returns the usage of the command. The usage will be roughly equal to the one showed by the client
|
||
|
|
// in-game.
|
||
|
|
func (cmd Command) Usage() string {
|
||
|
|
return cmd.usage
|
||
|
|
}
|
||
|
|
|
||
|
|
// Aliases returns a list of aliases for the command. In addition to the name of the command, the command may
|
||
|
|
// be called using one of these aliases.
|
||
|
|
func (cmd Command) Aliases() []string {
|
||
|
|
return cmd.aliases
|
||
|
|
}
|
||
|
|
|
||
|
|
// Execute executes the Command as a source with the args passed. The args are parsed assuming they do not
|
||
|
|
// start with the command name. Execute will attempt to parse and execute one Runnable at a time. If one of
|
||
|
|
// the Runnable was able to parse args correctly, it will be executed and no more Runnables will be attempted
|
||
|
|
// to be run.
|
||
|
|
// If parsing of all Runnables was unsuccessful, a command output with an error message is sent to the Source
|
||
|
|
// passed, and the Run method of the Runnables are not called.
|
||
|
|
// The Source passed must not be nil. The method will panic if a nil Source is passed.
|
||
|
|
func (cmd Command) Execute(args string, source Source, tx *world.Tx) {
|
||
|
|
if source == nil {
|
||
|
|
panic("execute: invalid command source: source must not be nil")
|
||
|
|
}
|
||
|
|
output := &Output{}
|
||
|
|
defer source.SendCommandOutput(output)
|
||
|
|
|
||
|
|
var leastErroneous error
|
||
|
|
var leastArgsLeft *Line
|
||
|
|
|
||
|
|
for _, v := range cmd.v {
|
||
|
|
cp := reflect.New(v.Type())
|
||
|
|
cp.Elem().Set(v)
|
||
|
|
line, err := cmd.executeRunnable(cp, args, source, output, tx)
|
||
|
|
if err == nil {
|
||
|
|
// Command was executed successfully: We won't execute any of the other Runnable values passed, as
|
||
|
|
// we've already found an overload that works.
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if line == nil {
|
||
|
|
// This Runnable was not runnable by the source passed. Only if no error was yet set, we set an
|
||
|
|
// error for the wrong source.
|
||
|
|
if leastErroneous == nil {
|
||
|
|
leastErroneous = err
|
||
|
|
}
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if leastArgsLeft == nil || line.Len() <= leastArgsLeft.Len() {
|
||
|
|
// If the line had less (or equal) arguments left than the previous lowest, we update the error,
|
||
|
|
// so that we can return an error that applies for the most successful Runnable.
|
||
|
|
leastErroneous = err
|
||
|
|
leastArgsLeft = line
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// No working Runnable found for the arguments passed. We add the most
|
||
|
|
// applicable error to the output and stop there.
|
||
|
|
if leastArgsLeft != nil {
|
||
|
|
output.Error(leastArgsLeft.SyntaxError())
|
||
|
|
}
|
||
|
|
output.Error(leastErroneous)
|
||
|
|
}
|
||
|
|
|
||
|
|
// ParamInfo holds the information of a parameter in a Runnable. Information of a parameter may be obtained
|
||
|
|
// by calling Command.Params().
|
||
|
|
type ParamInfo struct {
|
||
|
|
Name string
|
||
|
|
Value any
|
||
|
|
Optional bool
|
||
|
|
Suffix string
|
||
|
|
}
|
||
|
|
|
||
|
|
// Params returns a list of all parameters of the runnables. No assumptions should be done on the values that
|
||
|
|
// they hold: Only the types are guaranteed to be consistent.
|
||
|
|
func (cmd Command) Params(src Source) [][]ParamInfo {
|
||
|
|
params := make([][]ParamInfo, 0, len(cmd.v))
|
||
|
|
for _, runnable := range cmd.v {
|
||
|
|
if allower, ok := runnable.Interface().(Allower); ok && !allower.Allow(src) {
|
||
|
|
// This source cannot execute this runnable.
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
// If the runnable can describe its own parameters, prefer that over reflection.
|
||
|
|
if d, ok := runnable.Interface().(ParamDescriber); ok {
|
||
|
|
params = append(params, d.DescribeParams(src))
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
elem := reflect.New(runnable.Type()).Elem()
|
||
|
|
elem.Set(runnable)
|
||
|
|
|
||
|
|
var fields []ParamInfo
|
||
|
|
for _, t := range exportedFields(elem) {
|
||
|
|
field := elem.FieldByName(t.Name)
|
||
|
|
fields = append(fields, ParamInfo{
|
||
|
|
Name: name(t),
|
||
|
|
Value: unwrap(field).Interface(),
|
||
|
|
Optional: optional(field),
|
||
|
|
Suffix: suffix(t),
|
||
|
|
})
|
||
|
|
}
|
||
|
|
params = append(params, fields)
|
||
|
|
}
|
||
|
|
return params
|
||
|
|
}
|
||
|
|
|
||
|
|
// Runnables returns a map of all Runnable implementations of the Command that a Source can execute.
|
||
|
|
func (cmd Command) Runnables(src Source) map[int]Runnable {
|
||
|
|
m := make(map[int]Runnable, len(cmd.v))
|
||
|
|
for i, runnable := range cmd.v {
|
||
|
|
v := runnable.Interface().(Runnable)
|
||
|
|
if allower, ok := v.(Allower); !ok || allower.Allow(src) {
|
||
|
|
m[i] = v
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return m
|
||
|
|
}
|
||
|
|
|
||
|
|
// String returns the usage of the command. The usage will be roughly equal to the one showed by the client
|
||
|
|
// in-game.
|
||
|
|
func (cmd Command) String() string {
|
||
|
|
return cmd.usage
|
||
|
|
}
|
||
|
|
|
||
|
|
// executeRunnable executes a Runnable v, by parsing the args passed using the source and output obtained. If
|
||
|
|
// parsing was not successful or the Runnable could not be run by this source, an error is returned, and the
|
||
|
|
// leftover command line.
|
||
|
|
func (cmd Command) executeRunnable(v reflect.Value, args string, source Source, output *Output, tx *world.Tx) (*Line, error) {
|
||
|
|
if a, ok := v.Interface().(Allower); ok && !a.Allow(source) {
|
||
|
|
return nil, MessageUnknown.F(cmd.name)
|
||
|
|
}
|
||
|
|
|
||
|
|
var argFrags []string
|
||
|
|
if args != "" {
|
||
|
|
r := csv.NewReader(strings.NewReader(args))
|
||
|
|
r.Comma, r.LazyQuotes = ' ', true
|
||
|
|
record, err := r.Read()
|
||
|
|
if err != nil {
|
||
|
|
// When LazyQuotes is enabled, this really never appears to return
|
||
|
|
// an error when we read only one line. Just in case it does though,
|
||
|
|
// we return the command usage.
|
||
|
|
return nil, MessageUsage.F(cmd.Usage())
|
||
|
|
}
|
||
|
|
argFrags = record
|
||
|
|
}
|
||
|
|
parser := parser{}
|
||
|
|
arguments := &Line{args: argFrags, src: source, seen: []string{"/" + cmd.name}, cmd: cmd}
|
||
|
|
|
||
|
|
// We iterate over all the fields of the struct: Each of the fields will have an argument parsed to
|
||
|
|
// produce its value.
|
||
|
|
signature := v.Elem()
|
||
|
|
for _, t := range exportedFields(signature) {
|
||
|
|
field := signature.FieldByName(t.Name)
|
||
|
|
parser.currentField = t.Name
|
||
|
|
opt := optional(field)
|
||
|
|
|
||
|
|
val := field
|
||
|
|
if opt {
|
||
|
|
val = reflect.New(field.Field(0).Type()).Elem()
|
||
|
|
}
|
||
|
|
|
||
|
|
err, success := parser.parseArgument(arguments, val, opt, name(t), source, tx)
|
||
|
|
if err != nil {
|
||
|
|
// Parsing was not successful, we return immediately as we don't
|
||
|
|
// need to call the Runnable.
|
||
|
|
return arguments, err
|
||
|
|
}
|
||
|
|
if success && opt {
|
||
|
|
field.Set(reflect.ValueOf(field.Interface().(optionalT).with(val.Interface())))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if arguments.Len() != 0 {
|
||
|
|
return arguments, arguments.UsageError()
|
||
|
|
}
|
||
|
|
|
||
|
|
v.Interface().(Runnable).Run(source, output, tx)
|
||
|
|
return arguments, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// parseUsage parses the usage of a command found in value v using the name passed. It accounts for optional
|
||
|
|
// parameters and converts types to a more friendly representation.
|
||
|
|
func parseUsage(commandName string, command reflect.Value) string {
|
||
|
|
parts := make([]string, 0, command.NumField()+1)
|
||
|
|
parts = append(parts, "/"+commandName)
|
||
|
|
|
||
|
|
for _, t := range exportedFields(command) {
|
||
|
|
field := command.FieldByName(t.Name)
|
||
|
|
|
||
|
|
typeName := typeNameOf(field.Interface(), name(t))
|
||
|
|
if _, ok := field.Interface().(optionalT); ok {
|
||
|
|
typeName = typeNameOf(reflect.New(field.Field(0).Type()).Elem().Interface(), name(t))
|
||
|
|
}
|
||
|
|
if _, ok := field.Interface().(SubCommand); ok {
|
||
|
|
parts = append(parts, typeName)
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if optional(field) {
|
||
|
|
parts = append(parts, "["+name(t)+": "+typeName+"]"+suffix(t))
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
parts = append(parts, "<"+name(t)+": "+typeName+">"+suffix(t))
|
||
|
|
}
|
||
|
|
return strings.Join(parts, " ")
|
||
|
|
}
|
||
|
|
|
||
|
|
// verifySignature verifies the passed struct pointer value signature to ensure it is a valid command,
|
||
|
|
// checking things such as the validity of the optional struct tags.
|
||
|
|
// If not valid, an error is returned.
|
||
|
|
func verifySignature(command reflect.Value) error {
|
||
|
|
optionalField := false
|
||
|
|
for _, t := range exportedFields(command) {
|
||
|
|
field := command.FieldByName(t.Name)
|
||
|
|
|
||
|
|
// If the field is not optional, while the last field WAS optional, we return an error, as this is
|
||
|
|
// not parsable in an expected way.
|
||
|
|
opt := optional(field)
|
||
|
|
if !opt && optionalField {
|
||
|
|
return fmt.Errorf("command must only have optional parameters at the end")
|
||
|
|
}
|
||
|
|
val := field
|
||
|
|
if opt {
|
||
|
|
val = reflect.New(field.Field(0).Type()).Elem()
|
||
|
|
}
|
||
|
|
if _, ok := val.Interface().(Enum); ok && val.Kind() != reflect.String {
|
||
|
|
return fmt.Errorf("parameters implementing Enum must be of the type string")
|
||
|
|
}
|
||
|
|
optionalField = opt
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// exportedFields returns all exported struct fields of the reflect.Value passed. It returns the fields as returned by
|
||
|
|
// reflect.VisibleFields, but filters out unexported fields, anonymous fields and fields that have a name value in the
|
||
|
|
// 'cmd' tag of '-'.
|
||
|
|
func exportedFields(command reflect.Value) []reflect.StructField {
|
||
|
|
visible := reflect.VisibleFields(command.Type())
|
||
|
|
fields := make([]reflect.StructField, 0, len(visible))
|
||
|
|
|
||
|
|
for _, t := range visible {
|
||
|
|
if !ast.IsExported(t.Name) || name(t) == "-" || t.Anonymous {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
field := command.FieldByName(t.Name)
|
||
|
|
if !field.CanSet() {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
fields = append(fields, t)
|
||
|
|
}
|
||
|
|
return fields
|
||
|
|
}
|