Skip to content

Commit

Permalink
cmd: Use translated strings for command errors.
Browse files Browse the repository at this point in the history
  • Loading branch information
Sandertv committed Dec 22, 2024
1 parent 6452ff8 commit 53b0738
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 53 deletions.
101 changes: 61 additions & 40 deletions server/cmd/argument.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import (
"errors"
"fmt"
"github.com/df-mc/dragonfly/server/internal/sliceutil"
"github.com/df-mc/dragonfly/server/player/chat"
"github.com/df-mc/dragonfly/server/world"
"github.com/go-gl/mathgl/mgl64"
"math/rand"
"reflect"
"slices"
"sort"
"strconv"
"strings"
Expand All @@ -17,9 +19,21 @@ import (
// command. It is a convenience wrapper around a string slice.
type Line struct {
args []string
seen []string
src Source
}

func (line *Line) SyntaxError() error {
if len(line.args) == 0 {
return chat.MessageCommandSyntax.F(line.seen, "", "")
}
next := strings.Join(line.args[1:], " ")
if next != "" {
next = " " + next
}
return chat.MessageCommandSyntax.F(strings.Join(line.seen, " ")+" ", line.args[0], next)
}

// 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) {
Expand Down Expand Up @@ -51,6 +65,7 @@ func (line *Line) RemoveN(n int) {
line.args = nil
return
}
line.seen = append(line.seen, line.args[:n]...)
line.args = line.args[n:]
}

Expand All @@ -77,6 +92,13 @@ type parser struct {
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)
Expand Down Expand Up @@ -110,11 +132,6 @@ func (p parser) parseArgument(line *Line, v reflect.Value, optional bool, name s
if err == nil {
// The argument was parsed successfully, so it needs to be removed from the command line.
line.RemoveNext()
} else if errors.Is(err, ErrInsufficientArgs) && optional {
// The command ran 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
}
return err, err == nil
}
Expand All @@ -127,11 +144,11 @@ var ErrInsufficientArgs = errors.New("not enough arguments for command")
func (p parser) int(line *Line, v reflect.Value) error {
arg, ok := line.Next()
if !ok {
return ErrInsufficientArgs
return line.SyntaxError()
}
value, err := strconv.ParseInt(arg, 10, v.Type().Bits())
if err != nil {
return fmt.Errorf(`cannot parse argument "%v" as type %v for argument "%v"`, arg, v.Kind(), p.currentField)
return line.SyntaxError()
}
v.SetInt(value)
return nil
Expand All @@ -141,11 +158,11 @@ func (p parser) int(line *Line, v reflect.Value) error {
func (p parser) uint(line *Line, v reflect.Value) error {
arg, ok := line.Next()
if !ok {
return ErrInsufficientArgs
return line.SyntaxError()
}
value, err := strconv.ParseUint(arg, 10, v.Type().Bits())
if err != nil {
return fmt.Errorf(`cannot parse argument "%v" as type %v for argument "%v"`, arg, v.Kind(), p.currentField)
return line.SyntaxError()
}
v.SetUint(value)
return nil
Expand All @@ -155,11 +172,11 @@ func (p parser) uint(line *Line, v reflect.Value) error {
func (p parser) float(line *Line, v reflect.Value) error {
arg, ok := line.Next()
if !ok {
return ErrInsufficientArgs
return line.SyntaxError()
}
value, err := strconv.ParseFloat(arg, v.Type().Bits())
if err != nil {
return fmt.Errorf(`cannot parse argument "%v" as type %v for argument "%v"`, arg, v.Kind(), p.currentField)
return line.SyntaxError()
}
v.SetFloat(value)
return nil
Expand All @@ -169,7 +186,7 @@ func (p parser) float(line *Line, v reflect.Value) error {
func (p parser) string(line *Line, v reflect.Value) error {
arg, ok := line.Next()
if !ok {
return ErrInsufficientArgs
return line.SyntaxError()
}
v.SetString(arg)
return nil
Expand All @@ -179,11 +196,11 @@ func (p parser) string(line *Line, v reflect.Value) error {
func (p parser) bool(line *Line, v reflect.Value) error {
arg, ok := line.Next()
if !ok {
return ErrInsufficientArgs
return line.SyntaxError()
}
value, err := strconv.ParseBool(arg)
if err != nil {
return fmt.Errorf(`cannot parse argument "%v" as type bool for argument "%v"`, arg, p.currentField)
return line.SyntaxError()
}
v.SetBool(value)
return nil
Expand All @@ -193,31 +210,29 @@ func (p parser) bool(line *Line, v reflect.Value) error {
func (p parser) enum(line *Line, val reflect.Value, v Enum, source Source) error {
arg, ok := line.Next()
if !ok {
return ErrInsufficientArgs
}
found := ""
for _, option := range v.Options(source) {
if strings.EqualFold(option, arg) {
found = option
}
return line.SyntaxError()
}
if found == "" {
return fmt.Errorf(`invalid argument "%v" for enum parameter "%v"`, arg, v.Type())
opts := v.Options(source)
ind := slices.IndexFunc(opts, func(s string) bool {
return strings.EqualFold(s, arg)
})
if ind < 0 {
return line.SyntaxError()
}
val.SetString(found)
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 ErrInsufficientArgs
return line.SyntaxError()
}
if strings.EqualFold(name, arg) {
return nil
}
return fmt.Errorf(`invalid argument "%v" for sub command "%v"`, arg, name)
return line.SyntaxError()
}

// vec3 ...
Expand Down Expand Up @@ -246,7 +261,7 @@ func (p parser) targets(line *Line, v reflect.Value, tx *world.Tx) error {
return err
}
if len(targets) == 0 {
return fmt.Errorf("no targets found")
return chat.MessageCommandNoTargets.F()
}
v.Set(reflect.ValueOf(targets))
return nil
Expand All @@ -257,7 +272,7 @@ func (p parser) parseTargets(line *Line, tx *world.Tx) ([]Target, error) {
entities, players := targets(tx)
first, ok := line.Next()
if !ok {
return nil, ErrInsufficientArgs
return nil, line.SyntaxError()
}
switch first {
case "@p":
Expand Down Expand Up @@ -285,21 +300,27 @@ func (p parser) parseTargets(line *Line, tx *world.Tx) ([]Target, error) {
}
return []Target{players[rand.Intn(len(players))]}, nil
default:
target, err := p.parsePlayer(players, first)
return []Target{target}, err
target, ok := p.parsePlayer(line, players)
if ok {
return []Target{target}, nil
}
return nil, nil
}
}

// parsePlayer parses one Player from the Line, reading more arguments if necessary to find a valid player
// from the players Target list.
func (p parser) parsePlayer(players []NamedTarget, name string) (Target, error) {
for _, p := range players {
if strings.EqualFold(p.Name(), name) {
// We found a match for this amount of arguments. Following arguments may still be a better
// match though (subset in the name, such as 'Hello' vs 'Hello World' as name), so keep going
// until we saturate the command line or pass 15 characters.
return p, nil
// parsePlayer parses one Player from the Line, consuming multiple arguments
// from Line if necessary.
func (p parser) parsePlayer(line *Line, players []NamedTarget) (Target, bool) {
name := ""
for i := 0; i < line.Len(); i++ {
name += line.args[0]
if ind := slices.IndexFunc(players, func(target NamedTarget) bool {
return strings.EqualFold(target.Name(), name)
}); ind != -1 {
return players[ind], true
}
name += " "
line.RemoveNext()
}
return nil, fmt.Errorf("player with name '%v' not found", name)
return nil, false
}
20 changes: 11 additions & 9 deletions server/cmd/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cmd
import (
"encoding/csv"
"fmt"
"github.com/df-mc/dragonfly/server/player/chat"
"github.com/df-mc/dragonfly/server/world"
"go/ast"
"reflect"
Expand Down Expand Up @@ -221,24 +222,24 @@ func (cmd Command) String() string {
// 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) {
//lint:ignore ST1005 Error string is capitalised because it is shown to the player.
//goland:noinspection GoErrorStringFormat
return nil, fmt.Errorf("You cannot execute this command.")
return nil, chat.MessageCommandUnknown.F(cmd.name)
}

var argFrags []string
if args != "" {
r := csv.NewReader(strings.NewReader(args))
r.Comma = ' '
r.LazyQuotes = true
r.Comma, r.LazyQuotes = ' ', true
record, err := r.Read()
if err != nil {
return nil, fmt.Errorf("error parsing command string: %w", err)
// 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, chat.MessageCommandUsage.F(cmd.Usage())
}
argFrags = record
}
parser := parser{}
arguments := &Line{args: argFrags, src: source}
arguments := &Line{args: argFrags, src: source, seen: []string{"/" + cmd.name}}

// We iterate over all the fields of the struct: Each of the fields will have an argument parsed to
// produce its value.
Expand All @@ -255,15 +256,16 @@ func (cmd Command) executeRunnable(v reflect.Value, args string, source Source,

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.
// 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, fmt.Errorf("unexpected '%v'", strings.Join(arguments.args, " "))
return arguments, arguments.SyntaxError()
}

v.Interface().(Runnable).Run(source, output, tx)
Expand Down
6 changes: 6 additions & 0 deletions server/cmd/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ func (o *Output) Errorf(format string, a ...any) {

// 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...)))
}

Expand Down
11 changes: 8 additions & 3 deletions server/player/chat/translate.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ import (

// https://github.com/Mojang/bedrock-samples/blob/main/resource_pack/texts/en_GB.lang

var MessageJoin = Translate(str("%multiplayer.player.joined"), 1, "%v joined the game").Enc("<yellow>%v</yellow>")
var MessageQuit = Translate(str("%multiplayer.player.left"), 1, "%v left the game").Enc("<yellow>%v</yellow>")
var MessageServerDisconnect = Translate(str("%disconnect.disconnected"), 0, "Disconnected by Server").Enc("<yellow>%v</yellow>")
var MessageJoin = Translate(str("%multiplayer.player.joined"), 1, `%v joined the game`).Enc("<yellow>%v</yellow>")
var MessageQuit = Translate(str("%multiplayer.player.left"), 1, `%v left the game`).Enc("<yellow>%v</yellow>")
var MessageServerDisconnect = Translate(str("%disconnect.disconnected"), 0, `Disconnected by Server`).Enc("<yellow>%v</yellow>")

var MessageCommandSyntax = Translate(str("%commands.generic.syntax"), 3, `Syntax error: unexpected value: at "%v>>%v<<%v"`)
var MessageCommandUsage = Translate(str("%commands.generic.usage"), 1, `Usage: %v`)
var MessageCommandUnknown = Translate(str("%commands.generic.unknown"), 1, `Unknown command: "%v": Please check that the command exists and that you have permission to use it.`)
var MessageCommandNoTargets = Translate(str("%commands.generic.noTargetMatch"), 0, `No targets matched selector`)

type str string

Expand Down
2 changes: 1 addition & 1 deletion server/player/player.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ func (p *Player) ExecuteCommand(commandLine string) {
command, ok := cmd.ByAlias(args[0][1:])
if !ok {
o := &cmd.Output{}
o.Errorf("Unknown command: %v. Please check that the command exists and that you have permission to use it.", args[0])
o.Errort(chat.MessageCommandUnknown, args[0])
p.SendCommandOutput(o)
return
}
Expand Down

0 comments on commit 53b0738

Please sign in to comment.