up3
Build and deploy / Build (push) Has been cancelled
Build and deploy / Update Contributors (push) Has been cancelled
Build and deploy / Deploy (push) Has been cancelled
Build and deploy / Build (push) Has been cancelled
Build and deploy / Update Contributors (push) Has been cancelled
Build and deploy / Deploy (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,320 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/df-mc/dragonfly/server/internal/sliceutil"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
"math/rand/v2"
|
||||
"reflect"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Line represents a command line holding command arguments that were passed upon the execution of the
|
||||
// command. It is a convenience wrapper around a string slice.
|
||||
type Line struct {
|
||||
args []string
|
||||
seen []string
|
||||
src Source
|
||||
cmd Command
|
||||
}
|
||||
|
||||
// SyntaxError returns a translated syntax error.
|
||||
func (line *Line) SyntaxError() error {
|
||||
if len(line.args) == 0 {
|
||||
return MessageSyntax.F(strings.Join(line.seen, " "), "", "")
|
||||
}
|
||||
next := strings.Join(line.args[1:], " ")
|
||||
if next != "" {
|
||||
next = " " + next
|
||||
}
|
||||
return MessageSyntax.F(strings.Join(line.seen, " ")+" ", line.args[0], next)
|
||||
}
|
||||
|
||||
// UsageError returns a translated usage error.
|
||||
func (line *Line) UsageError() error {
|
||||
return MessageUsage.F(line.cmd.Usage())
|
||||
}
|
||||
|
||||
// Next reads the next argument from the command line and returns it. If there were no more arguments to
|
||||
// consume, false is returned.
|
||||
func (line *Line) Next() (string, bool) {
|
||||
v, ok := line.NextN(1)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
return v[0], true
|
||||
}
|
||||
|
||||
// NextN reads the next N arguments from the command line and returns them. If there were not enough arguments
|
||||
// (n arguments), false is returned.
|
||||
func (line *Line) NextN(n int) ([]string, bool) {
|
||||
if len(line.args) < n {
|
||||
return nil, false
|
||||
}
|
||||
v := line.args[:n]
|
||||
return v, true
|
||||
}
|
||||
|
||||
// RemoveNext consumes the next argument from the command line.
|
||||
func (line *Line) RemoveNext() {
|
||||
line.RemoveN(1)
|
||||
}
|
||||
|
||||
// RemoveN consumes the next N arguments from the command line.
|
||||
func (line *Line) RemoveN(n int) {
|
||||
if len(line.args) < n {
|
||||
line.args = nil
|
||||
return
|
||||
}
|
||||
line.seen = append(line.seen, line.args[:n]...)
|
||||
line.args = line.args[n:]
|
||||
}
|
||||
|
||||
// Leftover takes the leftover arguments from the command line.
|
||||
func (line *Line) Leftover() []string {
|
||||
v := line.args
|
||||
line.args = nil
|
||||
return v
|
||||
}
|
||||
|
||||
// Len returns the leftover length of the arguments in the command line.
|
||||
func (line *Line) Len() int {
|
||||
return len(line.args)
|
||||
}
|
||||
|
||||
// parser manages the parsing of a Line, turning the raw arguments into values which are then stored in the
|
||||
// struct fields.
|
||||
type parser struct {
|
||||
currentField string
|
||||
}
|
||||
|
||||
// parseArgument parses the next argument from the command line passed and sets it to value v passed. If
|
||||
// parsing was not successful, an error is returned.
|
||||
func (p parser) parseArgument(line *Line, v reflect.Value, optional bool, name string, source Source, tx *world.Tx) (error, bool) {
|
||||
var err error
|
||||
i := v.Interface()
|
||||
if line.Len() == 0 && optional {
|
||||
// The command run didn't have enough arguments for this parameter, but
|
||||
// it was optional, so it does not matter. Make sure to clear the value
|
||||
// though.
|
||||
v.Set(reflect.Zero(v.Type()))
|
||||
return nil, false
|
||||
}
|
||||
switch i.(type) {
|
||||
case int, int8, int16, int32, int64:
|
||||
err = p.int(line, v)
|
||||
case uint, uint8, uint16, uint32, uint64:
|
||||
err = p.uint(line, v)
|
||||
case float32, float64:
|
||||
err = p.float(line, v)
|
||||
case string:
|
||||
err = p.string(line, v)
|
||||
case bool:
|
||||
err = p.bool(line, v)
|
||||
case mgl64.Vec3:
|
||||
err = p.vec3(line, v)
|
||||
case Varargs:
|
||||
err = p.varargs(line, v)
|
||||
case []Target:
|
||||
err = p.targets(line, v, tx)
|
||||
case SubCommand:
|
||||
err = p.sub(line, name)
|
||||
default:
|
||||
if param, ok := i.(Parameter); ok {
|
||||
err = param.Parse(line, v)
|
||||
break
|
||||
}
|
||||
if enum, ok := i.(Enum); ok {
|
||||
err = p.enum(line, v, enum, source)
|
||||
break
|
||||
}
|
||||
panic(fmt.Sprintf("non-command parameter type %T in command structure", i))
|
||||
}
|
||||
if err == nil {
|
||||
// The argument was parsed successfully, so it needs to be removed from the command line.
|
||||
line.RemoveNext()
|
||||
}
|
||||
return err, err == nil
|
||||
}
|
||||
|
||||
// int ...
|
||||
func (p parser) int(line *Line, v reflect.Value) error {
|
||||
arg, ok := line.Next()
|
||||
if !ok {
|
||||
return line.UsageError()
|
||||
}
|
||||
value, err := strconv.ParseInt(arg, 10, v.Type().Bits())
|
||||
if err != nil {
|
||||
return MessageNumberInvalid.F(arg)
|
||||
}
|
||||
v.SetInt(value)
|
||||
return nil
|
||||
}
|
||||
|
||||
// uint ...
|
||||
func (p parser) uint(line *Line, v reflect.Value) error {
|
||||
arg, ok := line.Next()
|
||||
if !ok {
|
||||
return line.UsageError()
|
||||
}
|
||||
value, err := strconv.ParseUint(arg, 10, v.Type().Bits())
|
||||
if err != nil {
|
||||
return MessageNumberInvalid.F(arg)
|
||||
}
|
||||
v.SetUint(value)
|
||||
return nil
|
||||
}
|
||||
|
||||
// float ...
|
||||
func (p parser) float(line *Line, v reflect.Value) error {
|
||||
arg, ok := line.Next()
|
||||
if !ok {
|
||||
return line.UsageError()
|
||||
}
|
||||
value, err := strconv.ParseFloat(arg, v.Type().Bits())
|
||||
if err != nil {
|
||||
return MessageNumberInvalid.F(arg)
|
||||
}
|
||||
v.SetFloat(value)
|
||||
return nil
|
||||
}
|
||||
|
||||
// string ...
|
||||
func (p parser) string(line *Line, v reflect.Value) error {
|
||||
arg, ok := line.Next()
|
||||
if !ok {
|
||||
return line.UsageError()
|
||||
}
|
||||
v.SetString(arg)
|
||||
return nil
|
||||
}
|
||||
|
||||
// bool ...
|
||||
func (p parser) bool(line *Line, v reflect.Value) error {
|
||||
arg, ok := line.Next()
|
||||
if !ok {
|
||||
return line.UsageError()
|
||||
}
|
||||
value, err := strconv.ParseBool(arg)
|
||||
if err != nil {
|
||||
return MessageBooleanInvalid.F(arg)
|
||||
}
|
||||
v.SetBool(value)
|
||||
return nil
|
||||
}
|
||||
|
||||
// enum ...
|
||||
func (p parser) enum(line *Line, val reflect.Value, v Enum, source Source) error {
|
||||
arg, ok := line.Next()
|
||||
if !ok {
|
||||
return line.UsageError()
|
||||
}
|
||||
opts := v.Options(source)
|
||||
ind := slices.IndexFunc(opts, func(s string) bool {
|
||||
return strings.EqualFold(s, arg)
|
||||
})
|
||||
if ind < 0 {
|
||||
return MessageParameterInvalid.F(arg)
|
||||
}
|
||||
val.SetString(opts[ind])
|
||||
return nil
|
||||
}
|
||||
|
||||
// sub reads verifies a SubCommand against the next argument.
|
||||
func (p parser) sub(line *Line, name string) error {
|
||||
arg, ok := line.Next()
|
||||
if !ok {
|
||||
return line.UsageError()
|
||||
}
|
||||
if strings.EqualFold(name, arg) {
|
||||
return nil
|
||||
}
|
||||
return MessageParameterInvalid.F(arg)
|
||||
}
|
||||
|
||||
// vec3 ...
|
||||
func (p parser) vec3(line *Line, v reflect.Value) error {
|
||||
if err := p.float(line, v.Index(0)); err != nil {
|
||||
return err
|
||||
}
|
||||
line.RemoveNext()
|
||||
if err := p.float(line, v.Index(1)); err != nil {
|
||||
return err
|
||||
}
|
||||
line.RemoveNext()
|
||||
return p.float(line, v.Index(2))
|
||||
}
|
||||
|
||||
// varargs ...
|
||||
func (p parser) varargs(line *Line, v reflect.Value) error {
|
||||
v.SetString(strings.Join(line.Leftover(), " "))
|
||||
return nil
|
||||
}
|
||||
|
||||
// targets ...
|
||||
func (p parser) targets(line *Line, v reflect.Value, tx *world.Tx) error {
|
||||
targets, err := p.parseTargets(line, tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(targets) == 0 {
|
||||
return MessageNoTargets.F()
|
||||
}
|
||||
v.Set(reflect.ValueOf(targets))
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseTargets parses one or more Targets from the Line passed.
|
||||
func (p parser) parseTargets(line *Line, tx *world.Tx) ([]Target, error) {
|
||||
entities, players := targets(tx)
|
||||
first, ok := line.Next()
|
||||
if !ok {
|
||||
return nil, line.UsageError()
|
||||
}
|
||||
switch first[:min(len(first), 2)] {
|
||||
case "@p":
|
||||
pos := line.src.Position()
|
||||
playerDistances := make([]float64, len(players))
|
||||
for i, p := range players {
|
||||
playerDistances[i] = p.Position().Sub(pos).Len()
|
||||
}
|
||||
sort.Slice(players, func(i, j int) bool {
|
||||
return playerDistances[i] < playerDistances[j]
|
||||
})
|
||||
if len(players) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return sliceutil.Convert[Target](players[0:1]), nil
|
||||
case "@e":
|
||||
return entities, nil
|
||||
case "@a":
|
||||
return sliceutil.Convert[Target](players), nil
|
||||
case "@s":
|
||||
return []Target{line.src}, nil
|
||||
case "@r":
|
||||
if len(players) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return []Target{players[rand.IntN(len(players))]}, nil
|
||||
default:
|
||||
target, err := p.parsePlayer(first, players)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []Target{target}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// parsePlayer attempts to find a target whose name matches the name passed.
|
||||
func (p parser) parsePlayer(name string, players []NamedTarget) (Target, error) {
|
||||
if ind := slices.IndexFunc(players, func(target NamedTarget) bool {
|
||||
return strings.EqualFold(target.Name(), name)
|
||||
}); ind != -1 {
|
||||
return players[ind], nil
|
||||
}
|
||||
return nil, MessagePlayerNotFound.F()
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Package cmd implements a Minecraft specific command system, which may be used simply by 'plugging' it in
|
||||
// and sending commands registered in an AvailableCommandsPacket.
|
||||
//
|
||||
// The cmd package handles commands in a specific way: It requires a struct to be passed to the cmd.New()
|
||||
// function, which implements the Runnable interface. For every exported field in the struct, executing the
|
||||
// command will result in the parsing of the arguments using the types of the fields of the struct, in the
|
||||
// order that they appear in. 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.
|
||||
//
|
||||
// Commands may be registered using the cmd.Register() method. By itself, this method will not ensure that the
|
||||
// client will be able to use the command: The user of the cmd package must handle commands itself and run the
|
||||
// appropriate one using the cmd.ByAlias function.
|
||||
package cmd
|
||||
@@ -0,0 +1,95 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/df-mc/dragonfly/server/player/chat"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// Output holds the output of a command execution. It holds success messages
|
||||
// and error messages, which the source of a command execution gets sent.
|
||||
type Output struct {
|
||||
errors []error
|
||||
messages []fmt.Stringer
|
||||
}
|
||||
|
||||
// Errorf formats an error message and adds it to the command output.
|
||||
func (o *Output) Errorf(format string, a ...any) {
|
||||
o.errors = append(o.errors, fmt.Errorf(format, a...))
|
||||
}
|
||||
|
||||
// Error formats an error message and adds it to the command output.
|
||||
func (o *Output) Error(a ...any) {
|
||||
if len(a) == 1 {
|
||||
if err, ok := a[0].(error); ok {
|
||||
o.errors = append(o.errors, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
o.errors = append(o.errors, errors.New(fmt.Sprint(a...)))
|
||||
}
|
||||
|
||||
// Errort adds a translation as an error message and parameterises it using the
|
||||
// arguments passed. Errort panics if the number of arguments is incorrect.
|
||||
func (o *Output) Errort(t chat.Translation, a ...any) {
|
||||
o.errors = append(o.errors, t.F(a...))
|
||||
}
|
||||
|
||||
// Printf formats a (success) message and adds it to the command output.
|
||||
func (o *Output) Printf(format string, a ...any) {
|
||||
o.messages = append(o.messages, stringer(fmt.Sprintf(format, a...)))
|
||||
}
|
||||
|
||||
// Print formats a (success) message and adds it to the command output.
|
||||
func (o *Output) Print(a ...any) {
|
||||
o.messages = append(o.messages, stringer(fmt.Sprint(a...)))
|
||||
}
|
||||
|
||||
// Printt adds a translation as a (success) message and parameterises it using
|
||||
// the arguments passed. Printt panics if the number of arguments is incorrect.
|
||||
func (o *Output) Printt(t chat.Translation, a ...any) {
|
||||
o.messages = append(o.messages, t.F(a...))
|
||||
}
|
||||
|
||||
// Errors returns a list of all errors added to the command output. Usually
|
||||
// only one error message is set: After one error message, execution of a
|
||||
// command typically terminates.
|
||||
func (o *Output) Errors() []error {
|
||||
return o.errors
|
||||
}
|
||||
|
||||
// ErrorCount returns the count of errors that the command output has.
|
||||
func (o *Output) ErrorCount() int {
|
||||
return len(o.errors)
|
||||
}
|
||||
|
||||
// Messages returns a list of all messages added to the command output. The
|
||||
// amount of messages present depends on the command called.
|
||||
func (o *Output) Messages() []fmt.Stringer {
|
||||
return o.messages
|
||||
}
|
||||
|
||||
// MessageCount returns the count of (success) messages that the command output
|
||||
// has.
|
||||
func (o *Output) MessageCount() int {
|
||||
return len(o.messages)
|
||||
}
|
||||
|
||||
type stringer string
|
||||
|
||||
func (s stringer) String() string { return string(s) }
|
||||
|
||||
var MessageSyntax = chat.Translate(str("%commands.generic.syntax"), 3, `Syntax error: unexpected value: at "%v>>%v<<%v"`).Enc("<red>%v</red>")
|
||||
var MessageUsage = chat.Translate(str("%commands.generic.usage"), 1, `Usage: %v`).Enc("<red>%v</red>")
|
||||
var MessageUnknown = chat.Translate(str("%commands.generic.unknown"), 1, `Unknown command: "%v": Please check that the command exists and that you have permission to use it.`).Enc("<red>%v</red>")
|
||||
var MessageNoTargets = chat.Translate(str("%commands.generic.noTargetMatch"), 0, `No targets matched selector`).Enc("<red>%v</red>")
|
||||
var MessageNumberInvalid = chat.Translate(str("%commands.generic.num.invalid"), 1, `'%v' is not a valid number`).Enc("<red>> %v</red>")
|
||||
var MessageBooleanInvalid = chat.Translate(str("%commands.generic.boolean.invalid"), 1, `'%v' is not true or false`).Enc("<red>> %v</red>")
|
||||
var MessagePlayerNotFound = chat.Translate(str("%commands.generic.player.notFound"), 0, `That player cannot be found`).Enc("<red>> %v</red>")
|
||||
var MessageParameterInvalid = chat.Translate(str("%commands.generic.parameter.invalid"), 1, `'%v' is not a valid parameter`).Enc("<red>> %v</red>")
|
||||
|
||||
type str string
|
||||
|
||||
// Resolve returns the translation identifier as a string.
|
||||
func (s str) Resolve(language.Tag) string { return string(s) }
|
||||
@@ -0,0 +1,157 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
)
|
||||
|
||||
// Parameter is an interface for a generic parameters. Users may have types as command parameters that
|
||||
// implement this parameter.
|
||||
type Parameter interface {
|
||||
// Parse takes an arbitrary amount of arguments from the command Line passed and parses it, so that it can
|
||||
// store it to value v. If the arguments cannot be parsed from the Line, an error should be returned.
|
||||
Parse(line *Line, v reflect.Value) error
|
||||
// Type returns the type of the parameter. It will show up in the usage of the command, and, if one of the
|
||||
// known type names, will also show up client-side.
|
||||
Type() string
|
||||
}
|
||||
|
||||
// Enum is an interface for enum-type parameters. Users may have types as command parameters that implement
|
||||
// this parameter in order to allow a specific set of options only.
|
||||
// Enum implementations must be of the type string, for example:
|
||||
//
|
||||
// type GameMode string
|
||||
// func (GameMode) Type() string { return "GameMode" }
|
||||
// func (GameMode) Options(Source) []string { return []string{"survival", "creative"} }
|
||||
//
|
||||
// Their values will then automatically be set to whichever option returned in Enum.Options is selected by
|
||||
// the user.
|
||||
type Enum interface {
|
||||
// Type returns the type of the enum. This type shows up client-side in the command usage, in the spot
|
||||
// where parameter types otherwise are.
|
||||
// Type names returned are used as an identifier for this enum type. Different Enum implementations must
|
||||
// return a different string in the Type method.
|
||||
Type() string
|
||||
// Options should return a list of options that show up on the client side. The command will ensure that
|
||||
// the argument passed to the enum parameter will be equal to one of these options. The provided Source
|
||||
// can also be used to change the enums for each player.
|
||||
Options(source Source) []string
|
||||
}
|
||||
|
||||
// ParamDescriber may be implemented by a Runnable to programmatically describe its parameters
|
||||
// without relying on struct field reflection.
|
||||
type ParamDescriber interface {
|
||||
DescribeParams(src Source) []ParamInfo
|
||||
}
|
||||
|
||||
// SubCommand represents a subcommand that may be added as a static value that must be written. Adding
|
||||
// multiple Runnable implementations to the command in New with different SubCommand fields as the
|
||||
// first parameter allows for commands with subcommands.
|
||||
type SubCommand struct{}
|
||||
|
||||
// Varargs is an argument type that may be used to capture all arguments that follow. This is useful for,
|
||||
// for example, messages and names.
|
||||
type Varargs string
|
||||
|
||||
// Optional is an argument type that may be used to make any of the available parameter types optional. Optional command
|
||||
// parameters may only occur at the end of the Runnable struct. No non-optional parameter is allowed after an optional
|
||||
// parameter.
|
||||
type Optional[T any] struct {
|
||||
val T
|
||||
set bool
|
||||
}
|
||||
|
||||
// Load returns the value specified upon executing the command and a bool that is true if the parameter was filled out
|
||||
// by the Source.
|
||||
func (o Optional[T]) Load() (T, bool) {
|
||||
return o.val, o.set
|
||||
}
|
||||
|
||||
// LoadOr returns the value specified upon executing the command, or a value 'or' if the parameter was not filled out
|
||||
// by the Source.
|
||||
func (o Optional[T]) LoadOr(or T) T {
|
||||
if o.set {
|
||||
return o.val
|
||||
}
|
||||
return or
|
||||
}
|
||||
|
||||
// with returns an Optional[T] with the value passed. It also sets the 'set' field to true.
|
||||
func (o Optional[T]) with(val any) any {
|
||||
return Optional[T]{val: val.(T), set: true}
|
||||
}
|
||||
|
||||
// optionalT is used to identify a parameter of the Optional type.
|
||||
type optionalT interface {
|
||||
with(val any) any
|
||||
}
|
||||
|
||||
// typeNameOf returns a readable type name for the interface value passed. If none could be found, 'value'
|
||||
// is returned.
|
||||
func typeNameOf(i any, name string) string {
|
||||
switch i.(type) {
|
||||
case int, int8, int16, int32, int64:
|
||||
return "int"
|
||||
case uint, uint8, uint16, uint32, uint64:
|
||||
return "uint"
|
||||
case float32, float64:
|
||||
return "float"
|
||||
case string:
|
||||
return "string"
|
||||
case Varargs:
|
||||
return "text"
|
||||
case bool:
|
||||
return "bool"
|
||||
case mgl64.Vec3:
|
||||
return "x y z"
|
||||
case []Target:
|
||||
return "target"
|
||||
case SubCommand:
|
||||
return name
|
||||
}
|
||||
if param, ok := i.(Parameter); ok {
|
||||
return param.Type()
|
||||
}
|
||||
if enum, ok := i.(Enum); ok {
|
||||
return enum.Type()
|
||||
}
|
||||
return "value"
|
||||
}
|
||||
|
||||
// unwrap returns the underlying reflect.Value of a reflect.Value, assuming it is of the Optional[T] type.
|
||||
func unwrap(v reflect.Value) reflect.Value {
|
||||
if _, ok := v.Interface().(optionalT); ok {
|
||||
return reflect.New(v.Field(0).Type()).Elem()
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// optional checks if the reflect.Value passed implements the optionalT interface.
|
||||
func optional(v reflect.Value) bool {
|
||||
_, ok := v.Interface().(optionalT)
|
||||
return ok
|
||||
}
|
||||
|
||||
// suffix returns the suffix of the parameter as set in the struct field, if any.
|
||||
func suffix(v reflect.StructField) string {
|
||||
_, str := tag(v)
|
||||
return str
|
||||
}
|
||||
|
||||
// name returns the name of the parameter as set in the struct tag if it exists, or the field's name if not.
|
||||
func name(v reflect.StructField) string {
|
||||
str, _ := tag(v)
|
||||
if str == "" {
|
||||
return v.Name
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// tag returns the name and suffix as specified in the 'cmd' tag, or empty strings if not present.
|
||||
func tag(v reflect.StructField) (name string, suffix string) {
|
||||
t, _ := v.Tag.Lookup("cmd")
|
||||
a, b, _ := strings.Cut(t, ",")
|
||||
return a, b
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package cmd
|
||||
|
||||
import "sync"
|
||||
|
||||
// commands holds a list of registered commands indexed by their name.
|
||||
var commands sync.Map
|
||||
|
||||
// Register registers a command with its name and all aliases that it has. Any command with the same name or
|
||||
// aliases will be overwritten.
|
||||
func Register(command Command) {
|
||||
commands.Store(command.name, command)
|
||||
for _, alias := range command.aliases {
|
||||
commands.Store(alias, command)
|
||||
}
|
||||
}
|
||||
|
||||
// ByAlias looks up a command by an alias. If found, the command and true are returned. If not, the returned
|
||||
// command is nil and the bool is false.
|
||||
func ByAlias(alias string) (Command, bool) {
|
||||
command, ok := commands.Load(alias)
|
||||
if !ok {
|
||||
return Command{}, false
|
||||
}
|
||||
return command.(Command), ok
|
||||
}
|
||||
|
||||
// Commands returns a map of all registered commands indexed by the alias they were registered with.
|
||||
func Commands() map[string]Command {
|
||||
cmd := make(map[string]Command)
|
||||
commands.Range(func(key, value any) bool {
|
||||
cmd[key.(string)] = value.(Command)
|
||||
return true
|
||||
})
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package cmd
|
||||
|
||||
// Source represents a source of a command execution. Commands may limit the sources that can run them by
|
||||
// implementing the Allower interface.
|
||||
// Source implements Target. A Source must always be able to target itself.
|
||||
type Source interface {
|
||||
Target
|
||||
// SendCommandOutput sends a command output to the source. The way the output is applied, depends on what
|
||||
// kind of source it is.
|
||||
// SendCommandOutput is called by a Command automatically after being run.
|
||||
SendCommandOutput(o *Output)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/internal/sliceutil"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// Target represents the target of a command. A []Target may be used as command parameter
|
||||
// types to allow passing targets to the command.
|
||||
type Target interface {
|
||||
// Position returns the position of the Target as an mgl64.Vec3.
|
||||
Position() mgl64.Vec3
|
||||
}
|
||||
|
||||
// NamedTarget is a Target that has a name.
|
||||
type NamedTarget interface {
|
||||
Target
|
||||
// Name returns a name of the Target. Note that this name needs not to be and is not unique for a Target.
|
||||
Name() string
|
||||
}
|
||||
|
||||
// targets returns all Targets selectable by the Source passed.
|
||||
func targets(tx *world.Tx) (entities []Target, players []NamedTarget) {
|
||||
ent := sliceutil.Convert[Target](slices.Collect(tx.Entities()))
|
||||
pl := sliceutil.Convert[NamedTarget](slices.Collect(tx.Players()))
|
||||
return ent, pl
|
||||
}
|
||||
Reference in New Issue
Block a user