Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(Client-side) translation system #961

Merged
merged 11 commits into from
Dec 22, 2024
19 changes: 6 additions & 13 deletions config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,16 @@
# The name as it shows up in the server list. Minecraft colour codes may be used in this name to format the
# name of the server.
Name = "Dragonfly Server"
# The message shown to players when the server is shutting down. The message may be left empty to direct
# players to the server list directly.
ShutdownMessage = "Server closed."
# AuthEnabled controls whether or not players must be connected to Xbox Live in order to join the server.
# AuthEnabled controls whether players must be connected to Xbox Live in order to join the server.
AuthEnabled = true
# JoinMessage is the message that appears when a player joins the server. Leave this empty to disable it.
# %v is the placeholder for the username of the player. Set this to "" to disable.
JoinMessage = "%v has joined the game"
# QuitMessage is the message that appears when a player leaves the server. Leave this empty to disable it.
# %v is the placeholder for the username of the player. Set this to "" to disable.
QuitMessage = "%v has left the game"
# DisableJoinQuitMessages specifies if join/quit messages should be broadcast when players join the server.
DisableJoinQuitMessages = false

[World]
# The folder that the world files (will) reside in, relative to the working directory. If not currently
# present, the folder will be made.
Folder = "world"
# Whether or not the worlds' data will be saved and loaded. If true, the server will use the
# Whether the worlds' data will be saved and loaded. If true, the server will use the
# default LevelDB data provider and if false, an empty provider will be used. To use your
# own provider, turn this value to false, as you will still be able to pass your own provider.
SaveData = true
Expand All @@ -36,7 +29,7 @@
# The maximum chunk radius that players may set in their settings. If they try to set it above this number,
# it will be capped and set to the max.
MaximumChunkRadius = 32
# Whether or not a player's data will be saved and loaded. If true, the server will use the
# Whether a player's data will be saved and loaded. If true, the server will use the
# default LevelDB data provider and if false, an empty provider will be used. To use your
# own provider, turn this value to false, as you will still be able to pass your own provider.
SaveData = true
Expand All @@ -49,5 +42,5 @@
AutoBuildPack = true
# Folder configures the directory used by the server to load resource packs.
Folder = "resources"
# Required configures whether or not the server will require players to have a resource pack to join.
# Required configures whether the server will require players to have a resource pack to join.
Required = true
102 changes: 62 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,22 @@ import (
// command. It is a convenience wrapper around a string slice.
type Line struct {
args []string
seen []string
src Source
}

// SyntaxError returns a translated syntax error.
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 +66,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 +93,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 +133,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 +145,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 +159,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 +173,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 +187,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 +197,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 +211,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 +262,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 +273,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 +301,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
Loading
Loading