Skip to content

Commit

Permalink
cmd/output.go: Accept translations in Printt and Errort methods.
Browse files Browse the repository at this point in the history
  • Loading branch information
Sandertv committed Dec 22, 2024
1 parent 16cdb23 commit 760af6a
Show file tree
Hide file tree
Showing 11 changed files with 140 additions and 76 deletions.
8 changes: 6 additions & 2 deletions config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@
ShutdownMessage = "Server closed."
# AuthEnabled controls whether players must be connected to Xbox Live in order to join the server.
AuthEnabled = true
# DisableJoinQuitMessages specifies if join/quit messages should be broadcast when players join the server.
DisableJoinQuitMessages = false
# 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 = "%multiplayer.player.joined"
# 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 = "%multiplayer.player.left"

[World]
# The folder that the world files (will) reside in, relative to the working directory. If not currently
Expand Down
41 changes: 30 additions & 11 deletions server/cmd/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ package cmd
import (
"errors"
"fmt"
"github.com/df-mc/dragonfly/server/player/chat"
)

// Output holds the output of a command execution. It holds success messages and error messages, which the
// source of a command execution gets sent.
// 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 []string
messages []fmt.Stringer
}

// Errorf formats an error message and adds it to the command output.
Expand All @@ -22,18 +23,31 @@ func (o *Output) Error(a ...any) {
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, fmt.Sprintf(format, a...))
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, fmt.Sprint(a...))
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.
// 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
}
Expand All @@ -43,13 +57,18 @@ 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() []string {
// 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.
// 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) }
29 changes: 19 additions & 10 deletions server/conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/df-mc/dragonfly/server/entity"
"github.com/df-mc/dragonfly/server/internal/packbuilder"
"github.com/df-mc/dragonfly/server/player"
"github.com/df-mc/dragonfly/server/player/chat"
"github.com/df-mc/dragonfly/server/player/playerdb"
"github.com/df-mc/dragonfly/server/world"
"github.com/df-mc/dragonfly/server/world/biome"
Expand Down Expand Up @@ -62,12 +63,12 @@ type Config struct {
// MaxChunkRadius is the maximum view distance that each player may have,
// measured in chunks. A chunk radius generally leads to more memory usage.
MaxChunkRadius int
// DisableJoinQuitMessages specifies if join/quit messages should be
// broadcast when players join the server.
DisableJoinQuitMessages bool
// ShutdownMessage is the message sent when the server shuts down, kicking
// all online players.
ShutdownMessage string
// JoinMessage, QuitMessage and ShutdownMessage are the messages to send for
// when a player joins or quits the server and when the server shuts down,
// kicking all online players. If set, JoinMessage and QuitMessage must have
// exactly 1 argument, which will be replaced with the name of the player
// joining or quitting.
JoinMessage, QuitMessage, ShutdownMessage chat.Translation
// StatusProvider provides the server status shown to players in the server
// list. By default, StatusProvider will show the server name from the Name
// field and the current player count and maximum players.
Expand Down Expand Up @@ -191,9 +192,14 @@ type UserConfig struct {
// AuthEnabled controls whether players must be connected to Xbox Live
// in order to join the server.
AuthEnabled bool
// DisableJoinQuitMessages specifies if the default join/quit messages
// should be broadcast when players join/quit the server.
DisableJoinQuitMessages bool
// 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
JoinMessage string
// 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
QuitMessage string
}
World struct {
// SaveData controls whether a world's data will be saved and loaded.
Expand Down Expand Up @@ -249,10 +255,13 @@ func (uc UserConfig) Config(log *slog.Logger) (Config, error) {
AuthDisabled: !uc.Server.AuthEnabled,
MaxPlayers: uc.Players.MaxCount,
MaxChunkRadius: uc.Players.MaximumChunkRadius,
DisableJoinQuitMessages: uc.Server.DisableJoinQuitMessages,
ShutdownMessage: uc.Server.ShutdownMessage,

Check failure on line 258 in server/conf.go

View workflow job for this annotation

GitHub Actions / Build

cannot use uc.Server.ShutdownMessage (variable of type string) as chat.Translation value in struct literal

Check failure on line 258 in server/conf.go

View workflow job for this annotation

GitHub Actions / Build

cannot use uc.Server.ShutdownMessage (variable of type string) as chat.Translation value in struct literal
DisableResourceBuilding: !uc.Resources.AutoBuildPack,
}
if uc.Server.JoinMessage != "" {
conf.JoinMessage = chat.Translate()

Check failure on line 262 in server/conf.go

View workflow job for this annotation

GitHub Actions / Build

not enough arguments in call to chat.Translate

Check failure on line 262 in server/conf.go

View workflow job for this annotation

GitHub Actions / Build

not enough arguments in call to chat.Translate
conf.JoinMessage, conf.QuitMessage = chat.MessageJoin, chat.MessageQuit
}
if uc.World.SaveData {
conf.WorldProvider, err = mcdb.Config{Log: log}.Open(uc.World.Folder)
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions server/player/chat/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ func (chat *Chat) WriteString(s string) (n int, err error) {
return len(s), nil
}

// Writet writes a Translatable message to a Chat, parameterising the message
// Writet writes a Translation message to a Chat, parameterising the message
// using the arguments passed. Messages are translated according to the locale
// of subscribers if they implement Translator. Subscribers that do not
// implement Translator have the fallback message sent.
func (chat *Chat) Writet(t Translatable, a ...any) {
func (chat *Chat) Writet(t Translation, a ...any) {
chat.m.Lock()
defer chat.m.Unlock()
for _, subscriber := range chat.subscribers {
Expand Down
4 changes: 2 additions & 2 deletions server/player/chat/subscriber.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ type Subscriber interface {
// Translator is a Subscriber that is able to translate messages to their own
// locale.
type Translator interface {
// Messaget sends a Translatable message to the Translator, using the
// Messaget sends a Translation message to the Translator, using the
// arguments passed to fill out any translation parameters.
Messaget(t Translatable, a ...any)
Messaget(t Translation, a ...any)
}

// StdoutSubscriber is an implementation of Subscriber that forwards messages
Expand Down
68 changes: 45 additions & 23 deletions server/player/chat/translate.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import (
"golang.org/x/text/language"
)

// 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>")

type str string

Expand All @@ -22,71 +25,90 @@ type TranslationString interface {
Resolve(l language.Tag) string
}

// Translate returns a Translatable for a TranslationString. The required number
// of parameters specifies how many arguments may be passed to Translatable.F.
// Translate returns a Translation for a TranslationString. The required number
// of parameters specifies how many arguments may be passed to Translation.F.
// The fallback string should be a 'standard' translation of the string, which
// is used when Translation.String is called on the Translation that results
// from a call to Translatable.F. This fallback string should have as many
// is used when translation.String is called on the translation that results
// from a call to Translation.F. This fallback string should have as many
// formatting identifiers (like in fmt.Sprintf) as the number of params.
func Translate(str TranslationString, params int, fallback string) Translatable {
return Translatable{str: str, params: params, fallback: fallback, format: "%v"}
func Translate(str TranslationString, params int, fallback string) Translation {
return Translation{str: str, params: params, fallback: fallback, format: "%v"}
}

// Translatable represents a TranslationString with additional formatting, that
// Translation represents a TranslationString with additional formatting, that
// may be filled out by calling F on it with a list of arguments for the
// translation.
type Translatable struct {
type Translation struct {
str TranslationString
format string
params int
fallback string
}

// Zero returns false if a Translation was not created using Translate or
// Untranslated.
func (t Translation) Zero() bool {
return t.format == ""
}

// Enc encapsulates the translation string into the format passed. This format
// should have exactly one formatting identifier, %v, to specify where the
// translation string should go, such as 'Translation: %v'.
// translation string should go, such as 'translation: %v'.
// Enc accepts colouring formats parsed by text.Colourf.
func (t Translatable) Enc(format string) Translatable {
func (t Translation) Enc(format string) Translation {
t.format = format
return t
}

// Resolve passes 0 arguments to the translation and resolves the translation
// string for the language passed. It is equal to calling t.F().Resolve(l).
// Resolve panics if the Translation requires at least 1 argument.
func (t Translation) Resolve(l language.Tag) string {
return t.F().Resolve(l)
}

// F takes arguments for a translation string passed and returns a filled out
// Translation that may be sent to players. The number of arguments passed must
// translation that may be sent to players. The number of arguments passed must
// be exactly equal to the number specified in Translate. If not, F will panic.
func (t Translatable) F(a ...any) Translation {
func (t Translation) F(a ...any) translation {
if len(a) != t.params {
panic(fmt.Sprintf("translation '%v' requires exactly %v parameters, got %v", t.format, t.params, len(a)))
}
params := make([]string, len(a))
for i, arg := range a {
params[i] = fmt.Sprint(arg)
}
return Translation{t: t, params: params, fallbackParams: a}
return translation{t: t, params: params, fallbackParams: a}
}

// Translation is a translation string with its arguments filled out. Format may
// translation is a translation string with its arguments filled out. Resolve may
// be called to obtain the translated version of the translation string and
// Params may be called to obtain the parameters passed in Translatable.F.
type Translation struct {
t Translatable
// Params may be called to obtain the parameters passed in Translation.F.
// translation implements the fmt.Stringer and error interfaces.
type translation struct {
t Translation
params []string
fallbackParams []any
}

// Format translates the TranslationString of the Translation to the language
// Resolve translates the TranslationString of the translation to the language
// passed and returns it.
func (t Translation) Format(l language.Tag) string {
func (t translation) Resolve(l language.Tag) string {
return text.Colourf(t.t.format, t.t.str.Resolve(l))
}

// Params returns a slice of values that are used to parameterise the
// translation returned by Format.
func (t Translation) Params() []string {
// translation returned by Resolve.
func (t translation) Params() []string {
return t.params
}

// String formats and returns the fallback value of the Translation.
func (t Translation) String() string {
// String formats and returns the fallback value of the translation.
func (t translation) String() string {
return fmt.Sprintf(text.Colourf(t.t.format, t.t.fallback), t.fallbackParams...)
}

// Error formats and returns the fallback value of the translation.
func (t translation) Error() string {
return t.String()
}
6 changes: 3 additions & 3 deletions server/player/player.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,8 +226,8 @@ func (p *Player) Messagef(f string, a ...any) {
// Messaget sends a translatable message to a player and parameterises it using
// the arguments passed. Messaget panics if an incorrect amount of arguments
// is passed.
func (p *Player) Messaget(t chat.Translatable, a ...any) {
p.session().SendTranslation(t.F(a...), p.locale)
func (p *Player) Messaget(t chat.Translation, a ...any) {
p.session().SendTranslation(t, p.locale, a)
}

// SendPopup sends a formatted popup to the player. The popup is shown above the hotbar of the player and
Expand Down Expand Up @@ -359,7 +359,7 @@ func (p *Player) Transfer(address string) error {

// SendCommandOutput sends the output of a command to the player.
func (p *Player) SendCommandOutput(output *cmd.Output) {
p.session().SendCommandOutput(output)
p.session().SendCommandOutput(output, p.locale)
}

// SendDialogue sends an NPC dialogue to the player, using the entity passed as the entity that the dialogue
Expand Down
13 changes: 7 additions & 6 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/df-mc/dragonfly/server/internal/sliceutil"
_ "github.com/df-mc/dragonfly/server/item" // Imported for maintaining correct initialisation order.
"github.com/df-mc/dragonfly/server/player"
"github.com/df-mc/dragonfly/server/player/chat"
"github.com/df-mc/dragonfly/server/player/skin"
"github.com/df-mc/dragonfly/server/session"
"github.com/df-mc/dragonfly/server/world"
Expand All @@ -22,7 +23,6 @@ import (
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/login"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sandertv/gophertunnel/minecraft/text"
"golang.org/x/exp/maps"
"golang.org/x/text/language"
"iter"
Expand Down Expand Up @@ -293,7 +293,7 @@ func (srv *Server) close() {

srv.conf.Log.Debug("Disconnecting players...")
for p := range srv.Players(nil) {
p.Disconnect(text.Colourf("<yellow>%v</yellow>", srv.conf.ShutdownMessage))
p.Disconnect(chat.MessageServerDisconnect.Resolve(p.Locale()))
}
srv.pwg.Wait()

Expand Down Expand Up @@ -523,10 +523,11 @@ func (srv *Server) createPlayer(id uuid.UUID, conn session.Conn, conf player.Con
srv.pwg.Add(1)

s := session.Config{
Log: srv.conf.Log,
MaxChunkRadius: srv.conf.MaxChunkRadius,
DisableJoinQuitMessages: srv.conf.DisableJoinQuitMessages,
HandleStop: srv.handleSessionClose,
Log: srv.conf.Log,
MaxChunkRadius: srv.conf.MaxChunkRadius,
JoinMessage: srv.conf.JoinMessage,
QuitMessage: srv.conf.QuitMessage,
HandleStop: srv.handleSessionClose,
}.New(conn)

conf.Name = conn.IdentityData().DisplayName
Expand Down
Loading

0 comments on commit 760af6a

Please sign in to comment.