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,61 @@
|
||||
package bossbar
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BossBar represents a boss bar that may be sent to a player. It is shown as a purple bar with text above
|
||||
// it. The health shown by the bar may be changed.
|
||||
type BossBar struct {
|
||||
text string
|
||||
health float64
|
||||
c Colour
|
||||
}
|
||||
|
||||
// New creates a new boss bar with the text passed. The text is formatted according to the rules of
|
||||
// fmt.Sprintln.
|
||||
// By default, the boss bar will have a full health bar. To change this, use BossBar.WithHealthPercentage().
|
||||
// The default colour of the BossBar is Purple. This can be changed using BossBar.WithColour.
|
||||
func New(text ...any) BossBar {
|
||||
return BossBar{text: format(text), health: 1, c: Purple()}
|
||||
}
|
||||
|
||||
// Text returns the text of the boss bar: The text passed when creating the bar using New().
|
||||
func (bar BossBar) Text() string {
|
||||
return bar.text
|
||||
}
|
||||
|
||||
// WithHealthPercentage sets the health percentage of the boss bar. The value passed must be between 0 and 1.
|
||||
// If a value out of that range is passed, WithHealthPercentage panics.
|
||||
// The new BossBar with the changed health percentage is returned.
|
||||
func (bar BossBar) WithHealthPercentage(v float64) BossBar {
|
||||
if v < 0 || v > 1 {
|
||||
panic("boss bar: value out of range: health percentage must be between 0.0 and 1.0")
|
||||
}
|
||||
bar.health = v
|
||||
return bar
|
||||
}
|
||||
|
||||
// WithColour returns a copy of the BossBar with the Colour passed.
|
||||
func (bar BossBar) WithColour(c Colour) BossBar {
|
||||
bar.c = c
|
||||
return bar
|
||||
}
|
||||
|
||||
// HealthPercentage returns the health percentage of the boss bar. The number returned is a value between 0
|
||||
// and 1, with 0 being an empty boss bar and 1 being a full one.
|
||||
func (bar BossBar) HealthPercentage() float64 {
|
||||
return bar.health
|
||||
}
|
||||
|
||||
// Colour returns the colour of the BossBar.
|
||||
func (bar BossBar) Colour() Colour {
|
||||
return bar.c
|
||||
}
|
||||
|
||||
// format is a utility function to format a list of values to have spaces between them, but no newline at the
|
||||
// end, which is typically used for sending messages, popups and tips.
|
||||
func format(a []any) string {
|
||||
return strings.TrimSuffix(fmt.Sprintln(a...), "\n")
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package bossbar
|
||||
|
||||
// Colour is the colour of a BossBar.
|
||||
type Colour struct{ colour }
|
||||
|
||||
// Pink is the colour for a pink boss bar.
|
||||
func Pink() Colour {
|
||||
return Colour{colour(0)}
|
||||
}
|
||||
|
||||
// Blue is the colour for a blue boss bar.
|
||||
func Blue() Colour {
|
||||
return Colour{colour(1)}
|
||||
}
|
||||
|
||||
// Red is the colour for a red boss bar.
|
||||
func Red() Colour {
|
||||
return Colour{colour(2)}
|
||||
}
|
||||
|
||||
// Green is the colour for a green boss bar.
|
||||
func Green() Colour {
|
||||
return Colour{colour(3)}
|
||||
}
|
||||
|
||||
// Yellow is the colour for a yellow boss bar.
|
||||
func Yellow() Colour {
|
||||
return Colour{colour(4)}
|
||||
}
|
||||
|
||||
// Purple is the colour for a purple boss bar.
|
||||
func Purple() Colour {
|
||||
return Colour{colour(5)}
|
||||
}
|
||||
|
||||
// RebeccaPurple is the colour for a rebecca purple boss bar.
|
||||
func RebeccaPurple() Colour {
|
||||
return Colour{colour(6)}
|
||||
}
|
||||
|
||||
// White is the colour for a white boss bar.
|
||||
func White() Colour {
|
||||
return Colour{colour(7)}
|
||||
}
|
||||
|
||||
type colour uint8
|
||||
|
||||
func (c colour) Uint8() uint8 {
|
||||
return uint8(c)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Global represents a global chat. Players will write in this chat by default
|
||||
// when they send any message in the chat.
|
||||
var Global = New()
|
||||
|
||||
// Chat represents the in-game chat. Messages may be written to it to send a
|
||||
// message to all subscribers.
|
||||
// Methods on Chat may be called from multiple goroutines concurrently. Chat
|
||||
// implements the io.Writer and io.StringWriter interfaces. fmt.Fprintf and
|
||||
// fmt.Fprint may be used to write formatted messages to the chat.
|
||||
type Chat struct {
|
||||
m sync.Mutex
|
||||
subscribers map[uuid.UUID]Subscriber
|
||||
}
|
||||
|
||||
// New returns a new chat.
|
||||
func New() *Chat {
|
||||
return &Chat{subscribers: map[uuid.UUID]Subscriber{}}
|
||||
}
|
||||
|
||||
// Write writes the byte slice p as a string to the chat. It is equivalent to
|
||||
// calling Chat.WriteString(string(p)).
|
||||
func (chat *Chat) Write(p []byte) (n int, err error) {
|
||||
return chat.WriteString(string(p))
|
||||
}
|
||||
|
||||
// WriteString writes a string s to the chat.
|
||||
func (chat *Chat) WriteString(s string) (n int, err error) {
|
||||
chat.m.Lock()
|
||||
defer chat.m.Unlock()
|
||||
for _, subscriber := range chat.subscribers {
|
||||
subscriber.Message(s)
|
||||
}
|
||||
return len(s), nil
|
||||
}
|
||||
|
||||
// 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 Translation, a ...any) {
|
||||
chat.m.Lock()
|
||||
defer chat.m.Unlock()
|
||||
for _, subscriber := range chat.subscribers {
|
||||
if translator, ok := subscriber.(Translator); ok {
|
||||
translator.Messaget(t, a...)
|
||||
continue
|
||||
}
|
||||
subscriber.Message(t.F(a...).String())
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe adds a subscriber to the chat, sending it every message written to
|
||||
// the chat. In order to remove it again, use Chat.Unsubscribe().
|
||||
func (chat *Chat) Subscribe(s Subscriber) {
|
||||
chat.m.Lock()
|
||||
defer chat.m.Unlock()
|
||||
chat.subscribers[s.UUID()] = s
|
||||
}
|
||||
|
||||
// Subscribed checks if a subscriber is currently subscribed to the chat.
|
||||
func (chat *Chat) Subscribed(s Subscriber) bool {
|
||||
chat.m.Lock()
|
||||
defer chat.m.Unlock()
|
||||
_, ok := chat.subscribers[s.UUID()]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Unsubscribe removes a subscriber from the chat, so that messages written to
|
||||
// the chat will no longer be sent to it.
|
||||
func (chat *Chat) Unsubscribe(s Subscriber) {
|
||||
chat.m.Lock()
|
||||
defer chat.m.Unlock()
|
||||
delete(chat.subscribers, s.UUID())
|
||||
}
|
||||
|
||||
// Close closes the chat, removing all subscribers from it.
|
||||
func (chat *Chat) Close() error {
|
||||
chat.m.Lock()
|
||||
chat.subscribers = nil
|
||||
chat.m.Unlock()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/sandertv/gophertunnel/minecraft/text"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Subscriber represents an entity that may subscribe to a Chat. In order to do
|
||||
// so, the Subscriber must implement methods to send messages to it.
|
||||
type Subscriber interface {
|
||||
// UUID returns a unique ID for the Subscriber.
|
||||
UUID() uuid.UUID
|
||||
// Message sends a formatted message to the subscriber. The message is
|
||||
// formatted as it would when using fmt.Println.
|
||||
Message(a ...any)
|
||||
}
|
||||
|
||||
// Translator is a Subscriber that is able to translate messages to their own
|
||||
// locale.
|
||||
type Translator interface {
|
||||
// Messaget sends a Translation message to the Translator, using the
|
||||
// arguments passed to fill out any translation parameters.
|
||||
Messaget(t Translation, a ...any)
|
||||
}
|
||||
|
||||
// StdoutSubscriber is an implementation of Subscriber that forwards messages
|
||||
// sent to the chat to the stdout.
|
||||
type StdoutSubscriber struct{}
|
||||
|
||||
var id = uuid.New()
|
||||
|
||||
// UUID ...
|
||||
func (c StdoutSubscriber) UUID() uuid.UUID {
|
||||
return id
|
||||
}
|
||||
|
||||
// Message ...
|
||||
func (c StdoutSubscriber) Message(a ...any) {
|
||||
s := make([]string, len(a))
|
||||
for i, b := range a {
|
||||
s[i] = fmt.Sprint(b)
|
||||
}
|
||||
t := text.ANSI(strings.Join(s, " "))
|
||||
if !strings.HasSuffix(t, "\n") {
|
||||
fmt.Println(t)
|
||||
return
|
||||
}
|
||||
fmt.Print(t)
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/sandertv/gophertunnel/minecraft/text"
|
||||
"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>")
|
||||
|
||||
var MessageBedTooFar = Translate(str("%tile.bed.tooFar"), 0, `Bed is too far away`).Enc("<grey>%v</grey>")
|
||||
var MessageBedObstructed = Translate(str("%tile.bed.obstructed"), 0, `Bed is obstructed`).Enc("<grey>%v</grey>")
|
||||
var MessageRespawnPointSet = Translate(str("%tile.bed.respawnSet"), 0, `Respawn point set`).Enc("<grey>%v</grey>")
|
||||
var MessageNoSleep = Translate(str("%tile.bed.noSleep"), 0, `You can only sleep at night and during thunderstorms`).Enc("<grey>%v</grey>")
|
||||
var MessageBedIsOccupied = Translate(str("%tile.bed.occupied"), 0, `This bed is occupied`).Enc("<grey>%v</grey>")
|
||||
var MessageSleeping = Translate(str("%chat.type.sleeping"), 2, `%v is sleeping in a bed. To skip to dawn, %v more users need to sleep in beds at the same time.`)
|
||||
var MessageBedNotValid = Translate(str("%tile.bed.notValid"), 0, `Your home bed was missing or obstructed`)
|
||||
|
||||
type str string
|
||||
|
||||
// Resolve returns the translation identifier as a string.
|
||||
func (s str) Resolve(language.Tag) string { return string(s) }
|
||||
|
||||
// TranslationString is a value that can resolve a translated version of itself
|
||||
// for a language.Tag passed.
|
||||
type TranslationString interface {
|
||||
// Resolve finds a suitable translated version for a translation string for
|
||||
// a specific language.Tag.
|
||||
Resolve(l language.Tag) string
|
||||
}
|
||||
|
||||
// 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 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) Translation {
|
||||
return Translation{str: str, params: params, fallback: fallback, format: "%v"}
|
||||
}
|
||||
|
||||
// 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 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'.
|
||||
// Enc accepts colouring formats parsed by text.Colourf.
|
||||
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
|
||||
// be exactly equal to the number specified in Translate. If not, F will panic.
|
||||
// Arguments passed are converted to strings using fmt.Sprint(). Exceptions are
|
||||
// made for argument values of the type TranslationString, Translation and
|
||||
// translation, which are resolved based on the Translator's language.
|
||||
// Translations used as arguments should not require any parameters.
|
||||
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)))
|
||||
}
|
||||
return translation{t: t, params: a}
|
||||
}
|
||||
|
||||
// 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 Translation.F.
|
||||
// translation implements the fmt.Stringer and error interfaces.
|
||||
type translation struct {
|
||||
t Translation
|
||||
params []any
|
||||
}
|
||||
|
||||
// Resolve translates the TranslationString of the translation to the language
|
||||
// passed and returns it.
|
||||
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 Resolve.
|
||||
func (t translation) Params(l language.Tag) []string {
|
||||
params := make([]string, len(t.params))
|
||||
for i, arg := range t.params {
|
||||
if str, ok := arg.(TranslationString); ok {
|
||||
params[i] = str.Resolve(l)
|
||||
continue
|
||||
}
|
||||
params[i] = fmt.Sprint(arg)
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
// 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.params...)
|
||||
}
|
||||
|
||||
// Error formats and returns the fallback value of the translation.
|
||||
func (t translation) Error() string {
|
||||
return t.String()
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
"github.com/df-mc/dragonfly/server/entity"
|
||||
"github.com/df-mc/dragonfly/server/entity/effect"
|
||||
"github.com/df-mc/dragonfly/server/item/inventory"
|
||||
"github.com/df-mc/dragonfly/server/player/skin"
|
||||
"github.com/df-mc/dragonfly/server/session"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/text/language"
|
||||
"math/rand/v2"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Config holds options that a Player can be created with.
|
||||
type Config struct {
|
||||
Session *session.Session
|
||||
Skin skin.Skin
|
||||
XUID string
|
||||
UUID uuid.UUID
|
||||
Name string
|
||||
Locale language.Tag
|
||||
GameMode world.GameMode
|
||||
|
||||
Position mgl64.Vec3
|
||||
Rotation cube.Rotation
|
||||
Velocity mgl64.Vec3
|
||||
Health float64
|
||||
MaxHealth float64
|
||||
FoodTick int
|
||||
Food int
|
||||
Exhaustion, Saturation float64
|
||||
AirSupply int
|
||||
MaxAirSupply int
|
||||
EnchantmentSeed int64
|
||||
Experience int
|
||||
HeldSlot int
|
||||
Inventory *inventory.Inventory
|
||||
OffHand *inventory.Inventory
|
||||
Armour *inventory.Armour
|
||||
EnderChestInventory *inventory.Inventory
|
||||
FireTicks int64
|
||||
FallDistance float64
|
||||
Effects []effect.Effect
|
||||
}
|
||||
|
||||
// Apply applies fields from a Config to a world.EntityData, filling out empty
|
||||
// fields with reasonable defaults.
|
||||
func (cfg Config) Apply(data *world.EntityData) {
|
||||
conf := fillDefaults(cfg)
|
||||
|
||||
data.Name, data.Pos, data.Rot = conf.Name, conf.Position, conf.Rotation
|
||||
slot := uint32(conf.HeldSlot)
|
||||
pdata := &playerData{
|
||||
xuid: conf.XUID,
|
||||
ui: inventory.New(54, nil),
|
||||
inv: conf.Inventory,
|
||||
enderChest: conf.EnderChestInventory,
|
||||
offHand: conf.OffHand,
|
||||
armour: conf.Armour,
|
||||
hunger: newHungerManager(),
|
||||
health: entity.NewHealthManager(conf.Health, conf.MaxHealth), // 20, 20
|
||||
experience: entity.NewExperienceManager(),
|
||||
effects: entity.NewEffectManager(conf.Effects...),
|
||||
locale: conf.Locale,
|
||||
cooldowns: make(map[string]time.Time),
|
||||
mc: &entity.MovementComputer{Gravity: 0.08, Drag: 0.02, DragBeforeGravity: true},
|
||||
heldSlot: &slot,
|
||||
gameMode: conf.GameMode,
|
||||
skin: conf.Skin,
|
||||
enchantSeed: conf.EnchantmentSeed,
|
||||
s: conf.Session,
|
||||
h: NopHandler{},
|
||||
speed: 0.1,
|
||||
flightSpeed: 0.05,
|
||||
verticalFlightSpeed: 1.0,
|
||||
scale: 1.0,
|
||||
airSupplyTicks: conf.AirSupply,
|
||||
maxAirSupplyTicks: conf.MaxAirSupply,
|
||||
breathing: true,
|
||||
nameTag: conf.Name,
|
||||
fireTicks: conf.FireTicks,
|
||||
fallDistance: conf.FallDistance,
|
||||
}
|
||||
pdata.hunger.foodLevel, pdata.hunger.foodTick, pdata.hunger.exhaustionLevel, pdata.hunger.saturationLevel = conf.Food, conf.FoodTick, conf.Exhaustion, conf.Saturation
|
||||
pdata.experience.Add(conf.Experience)
|
||||
data.Data = pdata
|
||||
}
|
||||
|
||||
// fillDefaults fills empty fields in a Config with reasonable default values.
|
||||
func fillDefaults(conf Config) Config {
|
||||
if (conf.Locale == language.Tag{}) {
|
||||
conf.Locale = language.BritishEnglish
|
||||
}
|
||||
if conf.Inventory == nil {
|
||||
conf.Inventory = inventory.New(36, nil)
|
||||
}
|
||||
if conf.EnderChestInventory == nil {
|
||||
conf.EnderChestInventory = inventory.New(27, nil)
|
||||
}
|
||||
if conf.OffHand == nil {
|
||||
conf.OffHand = inventory.New(1, nil)
|
||||
}
|
||||
if conf.Armour == nil {
|
||||
conf.Armour = inventory.NewArmour(nil)
|
||||
}
|
||||
if conf.Food == 0 && conf.FoodTick == 0 && conf.Exhaustion == 0 && conf.Saturation == 0 {
|
||||
conf.Food, conf.Saturation = 20, 5
|
||||
}
|
||||
if conf.EnchantmentSeed == 0 {
|
||||
conf.EnchantmentSeed = rand.Int64()
|
||||
}
|
||||
if conf.MaxAirSupply == 0 {
|
||||
conf.AirSupply, conf.MaxAirSupply = 300, 300
|
||||
}
|
||||
if conf.MaxHealth == 0 {
|
||||
conf.MaxHealth, conf.Health = 20, 20
|
||||
}
|
||||
if conf.GameMode == nil {
|
||||
conf.GameMode = world.GameModeSurvival
|
||||
}
|
||||
return conf
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package debug
|
||||
|
||||
// Renderer represents an interface for a renderer that can manage debug shapes.
|
||||
type Renderer interface {
|
||||
// AddDebugShape adds a debug shape to the renderer, which should be rendered to the player. If the shape
|
||||
// already exists, it will be updated with the new information.
|
||||
AddDebugShape(shape Shape)
|
||||
// RemoveDebugShape removes a debug shape from the renderer by its unique identifier.
|
||||
RemoveDebugShape(shape Shape)
|
||||
// VisibleDebugShapes returns a slice of all debug shapes that are currently being shown by the renderer.
|
||||
VisibleDebugShapes() []Shape
|
||||
// RemoveAllDebugShapes clears all debug shapes from the renderer, removing them from the view of the player.
|
||||
RemoveAllDebugShapes()
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
)
|
||||
|
||||
var nextShapeID atomic.Int32
|
||||
|
||||
// Shape represents a shape that can be drawn to a player from any point in the world.
|
||||
type Shape interface {
|
||||
// ShapeID returns the unique identifier of the shape. This is used to either update or remove the shape
|
||||
// after it has been sent to the player.
|
||||
ShapeID() int
|
||||
}
|
||||
|
||||
// shape is a base type for all shapes that implements the Shape interface. It contains a unique identifier
|
||||
// that is lazily initialised when the ShapeID method is called for the first time.
|
||||
type shape struct {
|
||||
id atomic.Int32
|
||||
}
|
||||
|
||||
// ShapeID ...
|
||||
func (s *shape) ShapeID() int {
|
||||
if id := s.id.Load(); id != 0 {
|
||||
return int(id)
|
||||
}
|
||||
s.id.CompareAndSwap(0, nextShapeID.Add(1))
|
||||
return int(s.id.Load())
|
||||
}
|
||||
|
||||
// Arrow represents an arrow shape that can be drawn at any point in the world. It has a head which can also
|
||||
// be positioned anywhere, and the length, radius and number of segments can be changed.
|
||||
type Arrow struct {
|
||||
shape
|
||||
|
||||
// Colour is the colour that will be used for the line and head. If empty, it will default to white.
|
||||
Colour color.RGBA
|
||||
// Position is the origin position of the shape in the world.
|
||||
Position mgl64.Vec3
|
||||
// EndPosition is the end position of the arrow in the world. The arrow will be drawn from Position to
|
||||
// EndPosition, with the head being drawn at EndPosition.
|
||||
EndPosition mgl64.Vec3
|
||||
// HeadLength is the length of the head to be drawn at the end of the arrow. If zero, it will default
|
||||
// to 1.0.
|
||||
HeadLength float64
|
||||
// HeadRadius is the radius of the head to be drawn at the end of the arrow. If zero, it will default
|
||||
// to 0.5.
|
||||
HeadRadius float64
|
||||
// HeadSegments is the number of segments that the head of the arrow will be drawn with. The more
|
||||
// segments, the smoother the head will look. If zero, it will default to 4.
|
||||
HeadSegments int
|
||||
// Entity is an optional entity handle to attach the shape to.
|
||||
Entity *world.EntityHandle
|
||||
}
|
||||
|
||||
// Box represents a hollow box that can be drawn at any point in the world, with a bounds that can be set.
|
||||
type Box struct {
|
||||
shape
|
||||
|
||||
// Colour is the colour that will be used for the outline. If empty, it will default to white.
|
||||
Colour color.RGBA
|
||||
// Position is the origin position of the shape in the world.
|
||||
Position mgl64.Vec3
|
||||
// Scale is the rate to scale the shape from its origin point. If zero, it will default to 1.0.
|
||||
Scale float64
|
||||
// Bounds is the size of the box in the world, acting as an offset from the Position. If empty,
|
||||
// it will default to a 1x1x1 box.
|
||||
Bounds mgl64.Vec3
|
||||
// Entity is an optional entity handle to attach the shape to.
|
||||
Entity *world.EntityHandle
|
||||
}
|
||||
|
||||
// Circle represents a hollow circle that can be drawn at any point in the world, with the scale being used
|
||||
// to control the radius.
|
||||
type Circle struct {
|
||||
shape
|
||||
|
||||
// Colour is the colour that will be used for the outline. If empty, it will default to white.
|
||||
Colour color.RGBA
|
||||
// Position is the origin position of the shape in the world.
|
||||
Position mgl64.Vec3
|
||||
// Scale is the radius of the circle. If zero, it will default to 1.0.
|
||||
Scale float64
|
||||
// Segments is the number of segments that the circle will be drawn with. The more segments, the smoother
|
||||
// the circle will look. If empty, it will default to 20.
|
||||
Segments int
|
||||
// Entity is an optional entity handle to attach the shape to.
|
||||
Entity *world.EntityHandle
|
||||
}
|
||||
|
||||
// Line represents a line that can be drawn at any point in the world, with a start and end position.
|
||||
type Line struct {
|
||||
shape
|
||||
|
||||
// Colour is the colour that will be used for the line. If empty, it will default to white.
|
||||
Colour color.RGBA
|
||||
// Position is the origin position of the shape in the world.
|
||||
Position mgl64.Vec3
|
||||
// EndPosition is the end position of the line in the world. The line will be drawn from Position to
|
||||
// EndPosition.
|
||||
EndPosition mgl64.Vec3
|
||||
// Entity is an optional entity handle to attach the shape to.
|
||||
Entity *world.EntityHandle
|
||||
}
|
||||
|
||||
// Sphere represents a hollow sphere that can be drawn at any point in the world, with one line in each axis.
|
||||
// The scale is used to control the radius of the sphere.
|
||||
type Sphere struct {
|
||||
shape
|
||||
|
||||
// Colour is the colour that will be used for the outline. If empty, it will default to white.
|
||||
Colour color.RGBA
|
||||
// Position is the origin position of the shape in the world.
|
||||
Position mgl64.Vec3
|
||||
// Scale is the radius of the sphere. If zero, it will default to 1.0.
|
||||
Scale float64
|
||||
// Segments is the number of segments that the circle will be drawn with. The more segments, the smoother
|
||||
// the circle will look. If empty, it will default to 20.
|
||||
Segments int
|
||||
// Entity is an optional entity handle to attach the shape to.
|
||||
Entity *world.EntityHandle
|
||||
}
|
||||
|
||||
// Text represents text that can be drawn at any point in the world, looking like a normal entity nametag
|
||||
// without actually being attached to an entity.
|
||||
type Text struct {
|
||||
shape
|
||||
|
||||
// Colour is the colour that will be used for the actual text. If empty, the text will default to white.
|
||||
Colour color.RGBA
|
||||
// BackgroundColour is the colour used for the text background. If empty, it will default to a
|
||||
// translucent black.
|
||||
BackgroundColour color.RGBA
|
||||
// HideBackground specifies whether the text background should be hidden entirely. Takes precedence
|
||||
// over BackgroundColour when set.
|
||||
HideBackground bool
|
||||
// Position is the origin position of the shape in the world.
|
||||
Position mgl64.Vec3
|
||||
// Rotation is the rotation of the shape, applied only when LockRotation is set.
|
||||
Rotation mgl64.Vec3
|
||||
// Scale is the size of the text. If zero, it will default to 1.0.
|
||||
Scale float64
|
||||
// Text is the text to be displayed on the shape. The background automatically scales to fit the text.
|
||||
Text string
|
||||
// LockRotation specifies whether the text should be locked to the orientation set by Rotation.
|
||||
// If false, the text will rotate to always face the camera.
|
||||
LockRotation bool
|
||||
// DisableDepthTest specifies whether the text should show through walls. If false, the text
|
||||
// will be occluded by geometry in front of it.
|
||||
DisableDepthTest bool
|
||||
// HideBackface specifies whether the background should be hidden on the back side of the shape.
|
||||
// Has no visible effect unless LockRotation is also set.
|
||||
HideBackface bool
|
||||
// HideBackfaceText specifies whether the text should be hidden on the back side of the shape.
|
||||
// Has no visible effect unless LockRotation is also set.
|
||||
HideBackfaceText bool
|
||||
// Entity is an optional entity handle to attach the shape to.
|
||||
Entity *world.EntityHandle
|
||||
}
|
||||
|
||||
// Cylinder represents a hollow cylinder, or frustum, that can be drawn at any point in the world, with a
|
||||
// height running up the Y axis. The base and top each have their own radius on the X and Z axes, allowing
|
||||
// for tapered and elliptical cylinders. A Cone is the special case of a Cylinder with a zero top radius.
|
||||
type Cylinder struct {
|
||||
shape
|
||||
|
||||
// Colour is the colour that will be used for the outline. If empty, it will default to white.
|
||||
Colour color.RGBA
|
||||
// Position is the origin position of the shape in the world.
|
||||
Position mgl64.Vec3
|
||||
// Scale is the rate to scale the shape from its origin point. If zero, it will default to 1.0.
|
||||
Scale float64
|
||||
// BaseRadius is the radius of the cylinder's base along the X and Z axes. If empty, it will default to a
|
||||
// radius of 1.0 on each axis. Differing X and Z radii produce an elliptical cylinder.
|
||||
BaseRadius mgl64.Vec2
|
||||
// TopRadius is the radius of the cylinder's top along the X and Z axes. If empty, it will default to
|
||||
// BaseRadius, producing a straight cylinder. A smaller TopRadius tapers the cylinder into a frustum.
|
||||
TopRadius mgl64.Vec2
|
||||
// Height is the height of the cylinder. If zero, it will default to 1.0.
|
||||
Height float64
|
||||
// Segments is the number of segments that the cylinder will be drawn with. The more segments, the
|
||||
// smoother the cylinder will look. If zero, it will default to 20.
|
||||
Segments int
|
||||
// Entity is an optional entity handle to attach the shape to.
|
||||
Entity *world.EntityHandle
|
||||
}
|
||||
|
||||
// Pyramid represents a pyramid that can be drawn at any point in the world, with a base on the X and Z axes
|
||||
// and a height running up the Y axis to a single apex.
|
||||
type Pyramid struct {
|
||||
shape
|
||||
|
||||
// Colour is the colour that will be used for the outline. If empty, it will default to white.
|
||||
Colour color.RGBA
|
||||
// Position is the origin position of the shape in the world.
|
||||
Position mgl64.Vec3
|
||||
// Scale is the rate to scale the shape from its origin point. If zero, it will default to 1.0.
|
||||
Scale float64
|
||||
// Width is the width along the X axis of the pyramid base. If zero, it will default to 1.0.
|
||||
Width float64
|
||||
// Depth is the depth along the Z axis of the pyramid base. If zero, it will default to Width.
|
||||
Depth float64
|
||||
// Height is the height of the pyramid. If zero, it will default to 1.0.
|
||||
Height float64
|
||||
// Entity is an optional entity handle to attach the shape to.
|
||||
Entity *world.EntityHandle
|
||||
}
|
||||
|
||||
// Ellipsoid represents a hollow ellipsoid that can be drawn at any point in the world, with a radius along
|
||||
// each of the X, Y and Z axes.
|
||||
type Ellipsoid struct {
|
||||
shape
|
||||
|
||||
// Colour is the colour that will be used for the outline. If empty, it will default to white.
|
||||
Colour color.RGBA
|
||||
// Position is the origin position of the shape in the world.
|
||||
Position mgl64.Vec3
|
||||
// Scale is the rate to scale the shape from its origin point. If zero, it will default to 1.0.
|
||||
Scale float64
|
||||
// Radii are the radii of the ellipsoid along the X, Y and Z axes. If empty, it will default to a radius
|
||||
// of 1.0 on each axis.
|
||||
Radii mgl64.Vec3
|
||||
// SegmentsPerAxis is the number of segments that the ellipsoid will be drawn with per axis. The more
|
||||
// segments, the smoother the ellipsoid will look. If zero, it will default to 20.
|
||||
SegmentsPerAxis int
|
||||
// Entity is an optional entity handle to attach the shape to.
|
||||
Entity *world.EntityHandle
|
||||
}
|
||||
|
||||
// Cone represents a cone that can be drawn at any point in the world, with a base on the X and Z axes and a
|
||||
// height running up the Y axis to a single apex.
|
||||
type Cone struct {
|
||||
shape
|
||||
|
||||
// Colour is the colour that will be used for the outline. If empty, it will default to white.
|
||||
Colour color.RGBA
|
||||
// Position is the origin position of the shape in the world.
|
||||
Position mgl64.Vec3
|
||||
// Scale is the rate to scale the shape from its origin point. If zero, it will default to 1.0.
|
||||
Scale float64
|
||||
// Radii are the radii along the X and Z axes of the cone base. If empty, it will default to a radius of
|
||||
// 1.0 on each axis.
|
||||
Radii mgl64.Vec2
|
||||
// Height is the height of the cone. If zero, it will default to 1.0.
|
||||
Height float64
|
||||
// Segments is the number of segments that the cone will be drawn with. The more segments, the smoother
|
||||
// the cone will look. If zero, it will default to 20.
|
||||
Segments int
|
||||
// Entity is an optional entity handle to attach the shape to.
|
||||
Entity *world.EntityHandle
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package dialogue
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// Button represents a button added to a dialogue menu and consists of just
|
||||
// text.
|
||||
type Button struct {
|
||||
// Text holds the text displayed on the button. It may use Minecraft
|
||||
// formatting codes.
|
||||
Text string
|
||||
}
|
||||
|
||||
// MarshalJSON ...
|
||||
func (b Button) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]any{
|
||||
"button_name": b.Text,
|
||||
"text": "",
|
||||
"mode": 0, // "Click" activation
|
||||
"type": 1, // "Command" type
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package dialogue
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Dialogue represents a dialogue menu. This menu can consist of a title, a
|
||||
// body and up to 6 different buttons. The menu also shows a 3D render of the
|
||||
// entity that is sending the dialogue.
|
||||
type Dialogue struct {
|
||||
title, body string
|
||||
submittable Submittable
|
||||
buttons []Button
|
||||
display DisplaySettings
|
||||
}
|
||||
|
||||
// New creates a new Dialogue menu using the Submittable passed to handle the
|
||||
// dialogue interactions. The title passed is formatted following the rules of
|
||||
// fmt.Sprintln.
|
||||
func New(submittable Submittable, title ...any) Dialogue {
|
||||
t := reflect.TypeOf(submittable)
|
||||
if t.Kind() != reflect.Struct {
|
||||
panic("submittable must be struct")
|
||||
}
|
||||
m := Dialogue{title: format(title), submittable: submittable}
|
||||
m.verify()
|
||||
return m
|
||||
}
|
||||
|
||||
// MarshalJSON ...
|
||||
func (m Dialogue) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(m.Buttons())
|
||||
}
|
||||
|
||||
// WithBody creates a copy of the Dialogue and changes its body to the body
|
||||
// passed, after which the new Dialogue is returned. The text is formatted
|
||||
// following the rules of fmt.Sprintln.
|
||||
func (m Dialogue) WithBody(body ...any) Dialogue {
|
||||
m.body = format(body)
|
||||
return m
|
||||
}
|
||||
|
||||
// WithDisplay returns a new Dialogue with the DisplaySettings passed.
|
||||
func (m Dialogue) WithDisplay(display DisplaySettings) Dialogue {
|
||||
m.display = display
|
||||
return m
|
||||
}
|
||||
|
||||
// WithButtons creates a copy of the Dialogue and appends the buttons passed to
|
||||
// the existing buttons, after which the new Dialogue is returned.
|
||||
func (m Dialogue) WithButtons(buttons ...Button) Dialogue {
|
||||
m.buttons = append(m.buttons, buttons...)
|
||||
m.verify()
|
||||
return m
|
||||
}
|
||||
|
||||
// Title returns the formatted title passed to the dialogue upon construction
|
||||
// using New().
|
||||
func (m Dialogue) Title() string {
|
||||
return m.title
|
||||
}
|
||||
|
||||
// Body returns the formatted text in the body passed to the menu using
|
||||
// WithBody().
|
||||
func (m Dialogue) Body() string {
|
||||
return m.body
|
||||
}
|
||||
|
||||
// Display returns the DisplaySettings of the Dialogue as specified using
|
||||
// WithDisplay().
|
||||
func (m Dialogue) Display() DisplaySettings {
|
||||
return m.display
|
||||
}
|
||||
|
||||
// Buttons returns a slice of buttons of the Submittable. It parses them from
|
||||
// the fields using reflection and returns them.
|
||||
func (m Dialogue) Buttons() []Button {
|
||||
v := reflect.New(reflect.TypeOf(m.submittable)).Elem()
|
||||
v.Set(reflect.ValueOf(m.submittable))
|
||||
|
||||
buttons := make([]Button, 0)
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
field := v.Field(i)
|
||||
if !field.CanSet() {
|
||||
continue
|
||||
}
|
||||
// Each exported field is guaranteed to be of type Button.
|
||||
buttons = append(buttons, field.Interface().(Button))
|
||||
}
|
||||
buttons = append(buttons, m.buttons...)
|
||||
return buttons
|
||||
}
|
||||
|
||||
// Submit submits an index of the pressed button to the Submittable. If the
|
||||
// index is invalid, an error is returned.
|
||||
func (m Dialogue) Submit(index uint, submitter Submitter, tx *world.Tx) error {
|
||||
buttons := m.Buttons()
|
||||
if index >= uint(len(buttons)) {
|
||||
return fmt.Errorf("button index points to inexistent button: %v (only %v buttons present)", index, len(buttons))
|
||||
}
|
||||
m.submittable.Submit(submitter, buttons[index], tx)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the dialogue, calling the Close method on the Submittable if it
|
||||
// implements the Closer interface.
|
||||
func (m Dialogue) Close(submitter Submitter, tx *world.Tx) {
|
||||
if closer, ok := m.submittable.(Closer); ok {
|
||||
closer.Close(submitter, tx)
|
||||
}
|
||||
}
|
||||
|
||||
// verify verifies if the dialogue is valid, checking all fields are of the
|
||||
// type Button and there are no more than 6 buttons in total. It panics if the
|
||||
// dialogue is invalid.
|
||||
func (m Dialogue) verify() {
|
||||
v := reflect.New(reflect.TypeOf(m.submittable)).Elem()
|
||||
v.Set(reflect.ValueOf(m.submittable))
|
||||
var buttons int
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
if !v.Field(i).CanSet() {
|
||||
continue
|
||||
}
|
||||
if _, ok := v.Field(i).Interface().(Button); !ok {
|
||||
panic("all exported fields must be of the type dialogue.Button")
|
||||
}
|
||||
buttons++
|
||||
}
|
||||
if buttons+len(m.buttons) > 6 {
|
||||
panic("maximum of 6 buttons allowed")
|
||||
}
|
||||
}
|
||||
|
||||
// format is a utility function to format a list of values to have spaces
|
||||
// between them, but no newline at the end.
|
||||
func format(a []any) string {
|
||||
return strings.TrimSuffix(strings.TrimSuffix(fmt.Sprintln(a...), "\n"), "\n")
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package dialogue
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
)
|
||||
|
||||
// DisplaySettings holds optional fields that change the way the dialogue,
|
||||
// particularly the entity shown in it, is displayed.
|
||||
type DisplaySettings struct {
|
||||
// EntityScale specifies the scale of the entity displayed in the dialogue.
|
||||
EntityScale mgl64.Vec3
|
||||
// EntityOffset specifies the offset of the entity shown in the dialogue.
|
||||
EntityOffset mgl64.Vec3
|
||||
// EntityRotation is the rotation of the entity shown in the dialogue. This
|
||||
// rotation functions a bit differently to the normal entity rotation in
|
||||
// Minecraft: The values are still degrees, but pitch (rot[1]) values are
|
||||
// whole-body pitch instead of head-specific, and rot[2] is whole-body roll.
|
||||
EntityRotation mgl64.Vec3
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the DisplaySettings to JSON.
|
||||
func (d DisplaySettings) MarshalJSON() ([]byte, error) {
|
||||
// Yaw and pitch are swapped in this display.
|
||||
d.EntityRotation[0], d.EntityRotation[1] = d.EntityRotation[1], d.EntityRotation[0]-32
|
||||
m := map[string]any{
|
||||
// Translate needs to be multiplied by -32 to get a rough block
|
||||
// equivalent.
|
||||
"translate": d.EntityOffset.Mul(-32),
|
||||
// Entity is rotated by 32 degrees by default.
|
||||
"rotate": d.EntityRotation,
|
||||
"scale": [3]float64{1, 1, 1},
|
||||
}
|
||||
if (d.EntityScale != mgl64.Vec3{}) {
|
||||
m["scale"] = d.EntityScale
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package dialogue
|
||||
|
||||
import "github.com/df-mc/dragonfly/server/world"
|
||||
|
||||
// Submittable is a structure which may be submitted by sending it as a dialogue
|
||||
// using dialogue.New(). The struct will have its Submit method called with the
|
||||
// button pressed. A struct that implements the Submittable interface must only
|
||||
// have exported fields with the type dialogue.Button.
|
||||
type Submittable interface {
|
||||
// Submit is called when the Submitter submits the dialogue sent to it. The
|
||||
// method is called with the button that was pressed. It may be compared
|
||||
// with buttons in the Submittable struct to check which button was pressed.
|
||||
// Additionally, the world.Tx of the Submitter is passed.
|
||||
Submit(submitter Submitter, pressed Button, tx *world.Tx)
|
||||
}
|
||||
|
||||
// Submitter is an entity that is able to submit a dialogue sent to it. It is
|
||||
// able to interact with the buttons in the dialogue. The Submitter is also
|
||||
// able to close the dialogue.
|
||||
type Submitter interface {
|
||||
SendDialogue(d Dialogue, e world.Entity)
|
||||
CloseDialogue()
|
||||
}
|
||||
|
||||
// Closer represents a dialogue which has special logic when being closed by a
|
||||
// Submitter.
|
||||
type Closer interface {
|
||||
// Close is called when the Submitter closes a dialogue.
|
||||
Close(submitter Submitter, tx *world.Tx)
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
package form
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Element represents an element that may be added to a Form. Any of the types in this package that implement
|
||||
// the element interface may be used as struct fields when passing the form structure to form.New().
|
||||
type Element interface {
|
||||
json.Marshaler
|
||||
elem()
|
||||
}
|
||||
|
||||
// MenuElement represents an element that may be added to a Menu form. This includes buttons, dividers,
|
||||
// headers, and labels.
|
||||
type MenuElement interface {
|
||||
json.Marshaler
|
||||
menuElem()
|
||||
}
|
||||
|
||||
// Divider represents a visual separator element on a form. It displays a horizontal line.
|
||||
type Divider struct{}
|
||||
|
||||
// MarshalJSON ...
|
||||
func (d Divider) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]any{
|
||||
"type": "divider",
|
||||
"text": "",
|
||||
})
|
||||
}
|
||||
|
||||
// Header represents a header element on a form. It displays larger, emphasised text for section titles.
|
||||
type Header struct {
|
||||
// Text is the text held by the header. The text may contain Minecraft formatting codes.
|
||||
Text string
|
||||
}
|
||||
|
||||
// NewHeader creates and returns a new Header with the text passed.
|
||||
func NewHeader(text string) Header {
|
||||
return Header{Text: text}
|
||||
}
|
||||
|
||||
// MarshalJSON ...
|
||||
func (h Header) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]any{
|
||||
"type": "header",
|
||||
"text": h.Text,
|
||||
})
|
||||
}
|
||||
|
||||
// Label represents a static label on a form. It serves only to display a box of text, and users cannot
|
||||
// submit values to it.
|
||||
type Label struct {
|
||||
// Text is the text held by the label. The text may contain Minecraft formatting codes.
|
||||
Text string
|
||||
}
|
||||
|
||||
// NewLabel creates and returns a new Label with the values passed.
|
||||
func NewLabel(text string) Label {
|
||||
return Label{Text: text}
|
||||
}
|
||||
|
||||
// MarshalJSON ...
|
||||
func (l Label) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]any{
|
||||
"type": "label",
|
||||
"text": l.Text,
|
||||
})
|
||||
}
|
||||
|
||||
// Input represents a text input box element. Submitters may write any text in these boxes with no specific
|
||||
// length.
|
||||
type Input struct {
|
||||
// Text is the text displayed over the input element. The text may contain Minecraft formatting codes.
|
||||
Text string
|
||||
// Default is the default value filled out in the input. The user may remove this value and fill out its
|
||||
// own text. The text may contain Minecraft formatting codes.
|
||||
Default string
|
||||
// Placeholder is the text displayed in the input box if it does not contain any text filled out by the
|
||||
// user. The text may contain Minecraft formatting codes.
|
||||
Placeholder string
|
||||
// Tooltip is an optional text displayed when hovering over the element's info icon. The icon only
|
||||
// appears when a tooltip is set.
|
||||
Tooltip string
|
||||
|
||||
value string
|
||||
}
|
||||
|
||||
// NewInput creates and returns a new Input with the values passed.
|
||||
func NewInput(text, defaultValue, placeholder string) Input {
|
||||
return Input{Text: text, Default: defaultValue, Placeholder: placeholder}
|
||||
}
|
||||
|
||||
// WithTooltip returns a copy of the Input with the tooltip set.
|
||||
func (i Input) WithTooltip(tooltip string) Input {
|
||||
i.Tooltip = tooltip
|
||||
return i
|
||||
}
|
||||
|
||||
// MarshalJSON ...
|
||||
func (i Input) MarshalJSON() ([]byte, error) {
|
||||
m := map[string]any{
|
||||
"type": "input",
|
||||
"text": i.Text,
|
||||
"default": i.Default,
|
||||
"placeholder": i.Placeholder,
|
||||
}
|
||||
if i.Tooltip != "" {
|
||||
m["tooltip"] = i.Tooltip
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
// Value returns the value filled out by the user.
|
||||
func (i Input) Value() string {
|
||||
return i.value
|
||||
}
|
||||
|
||||
// Toggle represents an on-off button element. Submitters may either toggle this on or off, which will then
|
||||
// hold a value of true or false respectively.
|
||||
type Toggle struct {
|
||||
// Text is the text displayed over the toggle element. The text may contain Minecraft formatting codes.
|
||||
Text string
|
||||
// Default determines if the toggle should be on/off by default.
|
||||
Default bool
|
||||
// Tooltip is an optional text displayed when hovering over the element's info icon. The icon only
|
||||
// appears when a tooltip is set.
|
||||
Tooltip string
|
||||
|
||||
value bool
|
||||
}
|
||||
|
||||
// NewToggle creates and returns a new Toggle with the values passed.
|
||||
func NewToggle(text string, defaultValue bool) Toggle {
|
||||
return Toggle{Text: text, Default: defaultValue}
|
||||
}
|
||||
|
||||
// WithTooltip returns a copy of the Toggle with the tooltip set.
|
||||
func (t Toggle) WithTooltip(tooltip string) Toggle {
|
||||
t.Tooltip = tooltip
|
||||
return t
|
||||
}
|
||||
|
||||
// MarshalJSON ...
|
||||
func (t Toggle) MarshalJSON() ([]byte, error) {
|
||||
m := map[string]any{
|
||||
"type": "toggle",
|
||||
"text": t.Text,
|
||||
"default": t.Default,
|
||||
}
|
||||
if t.Tooltip != "" {
|
||||
m["tooltip"] = t.Tooltip
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
// Value returns the value filled out by the user.
|
||||
func (t Toggle) Value() bool {
|
||||
return t.value
|
||||
}
|
||||
|
||||
// Slider represents a slider element. Submitters may move the slider to values within the range of the slider
|
||||
// to select a value.
|
||||
type Slider struct {
|
||||
// Text is the text displayed over the slider element. The text may contain Minecraft formatting codes.
|
||||
Text string
|
||||
// Min and Max are used to specify the minimum and maximum range of the slider. A value lower or higher
|
||||
// than these values cannot be selected.
|
||||
Min, Max float64
|
||||
// StepSize is the size that one step of the slider takes up. When set to 1.0 for example, a submitter
|
||||
// will be able to select only whole values.
|
||||
StepSize float64
|
||||
// Default is the default value filled out for the slider.
|
||||
Default float64
|
||||
// Tooltip is an optional text displayed when hovering over the element's info icon. The icon only
|
||||
// appears when a tooltip is set.
|
||||
Tooltip string
|
||||
|
||||
value float64
|
||||
}
|
||||
|
||||
// NewSlider creates and returns a new Slider using the values passed.
|
||||
func NewSlider(text string, min, max, stepSize, defaultValue float64) Slider {
|
||||
return Slider{Text: text, Min: min, Max: max, StepSize: stepSize, Default: defaultValue}
|
||||
}
|
||||
|
||||
// WithTooltip returns a copy of the Slider with the tooltip set.
|
||||
func (s Slider) WithTooltip(tooltip string) Slider {
|
||||
s.Tooltip = tooltip
|
||||
return s
|
||||
}
|
||||
|
||||
// MarshalJSON ...
|
||||
func (s Slider) MarshalJSON() ([]byte, error) {
|
||||
m := map[string]any{
|
||||
"type": "slider",
|
||||
"text": s.Text,
|
||||
"min": s.Min,
|
||||
"max": s.Max,
|
||||
"step": s.StepSize,
|
||||
"default": s.Default,
|
||||
}
|
||||
if s.Tooltip != "" {
|
||||
m["tooltip"] = s.Tooltip
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
// Value returns the value filled out by the user.
|
||||
func (s Slider) Value() float64 {
|
||||
return s.value
|
||||
}
|
||||
|
||||
// Dropdown represents a dropdown which, when clicked, opens a window with the options set in the Options
|
||||
// field. Submitters may select one of the options.
|
||||
type Dropdown struct {
|
||||
// Text is the text displayed over the dropdown element. The text may contain Minecraft formatting codes.
|
||||
Text string
|
||||
// Options holds a list of options that a Submitter may select. The order of these options is retained
|
||||
// when shown to the submitter of the form.
|
||||
Options []string
|
||||
// DefaultIndex is the index in the Options slice that is used as default. When sent to a Submitter, the
|
||||
// value at this index in the Options slice will be selected.
|
||||
DefaultIndex int
|
||||
// Tooltip is an optional text displayed when hovering over the element's info icon. The icon only
|
||||
// appears when a tooltip is set.
|
||||
Tooltip string
|
||||
|
||||
value int
|
||||
}
|
||||
|
||||
// NewDropdown creates and returns new Dropdown using the values passed.
|
||||
func NewDropdown(text string, options []string, defaultIndex int) Dropdown {
|
||||
return Dropdown{Text: text, Options: options, DefaultIndex: defaultIndex}
|
||||
}
|
||||
|
||||
// WithTooltip returns a copy of the Dropdown with the tooltip set.
|
||||
func (d Dropdown) WithTooltip(tooltip string) Dropdown {
|
||||
d.Tooltip = tooltip
|
||||
return d
|
||||
}
|
||||
|
||||
// MarshalJSON ...
|
||||
func (d Dropdown) MarshalJSON() ([]byte, error) {
|
||||
m := map[string]any{
|
||||
"type": "dropdown",
|
||||
"text": d.Text,
|
||||
"default": d.DefaultIndex,
|
||||
"options": d.Options,
|
||||
}
|
||||
if d.Tooltip != "" {
|
||||
m["tooltip"] = d.Tooltip
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
// Value returns the value that the Submitter submitted. The value is an index pointing to the selected option
|
||||
// in the Options slice.
|
||||
func (d Dropdown) Value() int {
|
||||
return d.value
|
||||
}
|
||||
|
||||
// StepSlider represents a slider that has a number of options that may be selected. It is essentially a
|
||||
// combination of a Dropdown and a Slider, looking like a slider but having properties like a dropdown.
|
||||
type StepSlider Dropdown
|
||||
|
||||
// NewStepSlider creates and returns new StepSlider using the values passed.
|
||||
func NewStepSlider(text string, options []string, defaultIndex int) StepSlider {
|
||||
return StepSlider{Text: text, Options: options, DefaultIndex: defaultIndex}
|
||||
}
|
||||
|
||||
// WithTooltip returns a copy of the StepSlider with the tooltip set.
|
||||
func (s StepSlider) WithTooltip(tooltip string) StepSlider {
|
||||
s.Tooltip = tooltip
|
||||
return s
|
||||
}
|
||||
|
||||
// MarshalJSON ...
|
||||
func (s StepSlider) MarshalJSON() ([]byte, error) {
|
||||
m := map[string]any{
|
||||
"type": "step_slider",
|
||||
"text": s.Text,
|
||||
"default": s.DefaultIndex,
|
||||
"steps": s.Options,
|
||||
}
|
||||
if s.Tooltip != "" {
|
||||
m["tooltip"] = s.Tooltip
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
// Value returns the value that the Submitter submitted. The value is an index pointing to the selected option
|
||||
// in the Options slice.
|
||||
func (s StepSlider) Value() int {
|
||||
return s.value
|
||||
}
|
||||
|
||||
// Button represents a button added to a Menu or Modal form. The button has text on it and an optional image,
|
||||
// which may be either retrieved from a website or the local assets of the game.
|
||||
type Button struct {
|
||||
// Text holds the text displayed on the button. It may use Minecraft formatting codes and may have
|
||||
// newlines.
|
||||
Text string
|
||||
// Image holds a path to an image for the button. The Image may either be a URL pointing to an image,
|
||||
// such as 'https://someimagewebsite.com/someimage.png', or a path pointing to a local asset, such as
|
||||
// 'textures/blocks/grass_carried'.
|
||||
Image string
|
||||
}
|
||||
|
||||
// NewButton creates and returns a new Button using the text and image passed.
|
||||
func NewButton(text, image string) Button {
|
||||
return Button{Text: text, Image: image}
|
||||
}
|
||||
|
||||
// MarshalJSON ...
|
||||
func (b Button) MarshalJSON() ([]byte, error) {
|
||||
m := map[string]any{
|
||||
"type": "button",
|
||||
"text": b.Text,
|
||||
}
|
||||
if b.Image != "" {
|
||||
buttonType := "path"
|
||||
if strings.HasPrefix(b.Image, "http:") || strings.HasPrefix(b.Image, "https:") {
|
||||
buttonType = "url"
|
||||
}
|
||||
m["image"] = map[string]any{"type": buttonType, "data": b.Image}
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
func (Divider) elem() {}
|
||||
func (Header) elem() {}
|
||||
func (Label) elem() {}
|
||||
func (Input) elem() {}
|
||||
func (Toggle) elem() {}
|
||||
func (Slider) elem() {}
|
||||
func (Dropdown) elem() {}
|
||||
func (StepSlider) elem() {}
|
||||
|
||||
func (Divider) menuElem() {}
|
||||
func (Header) menuElem() {}
|
||||
func (Label) menuElem() {}
|
||||
func (Button) menuElem() {}
|
||||
@@ -0,0 +1,217 @@
|
||||
package form
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
)
|
||||
|
||||
// Form represents a form that may be sent to a Submitter. The three types of forms, custom forms, menu forms
|
||||
// and modal forms implement this interface.
|
||||
type Form interface {
|
||||
json.Marshaler
|
||||
SubmitJSON(b []byte, submitter Submitter, tx *world.Tx) error
|
||||
}
|
||||
|
||||
// Custom represents a form that may be sent to a player and has fields that should be filled out by the
|
||||
// player that the form is sent to.
|
||||
type Custom struct {
|
||||
title string
|
||||
submittable Submittable
|
||||
}
|
||||
|
||||
// MarshalJSON ...
|
||||
func (f Custom) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]any{
|
||||
"type": "custom_form",
|
||||
"title": f.title,
|
||||
"content": f.Elements(),
|
||||
})
|
||||
}
|
||||
|
||||
// New creates a new (custom) form with the title passed and returns it. The title is formatted according to
|
||||
// the rules of fmt.Sprintln.
|
||||
// The submittable passed is used to create the structure of the form. The values of the Submittable's form
|
||||
// fields are used to set text, defaults and placeholders. If the Submittable passed is not a struct, New
|
||||
// panics. New also panics if one of the exported field types of the Submittable is not one that implements
|
||||
// the Element interface.
|
||||
func New(submittable Submittable, title ...any) Custom {
|
||||
t := reflect.TypeOf(submittable)
|
||||
if t.Kind() != reflect.Struct {
|
||||
panic("submittable must be struct")
|
||||
}
|
||||
f := Custom{title: format(title), submittable: submittable}
|
||||
f.verify()
|
||||
return f
|
||||
}
|
||||
|
||||
// Title returns the formatted title passed when the form was created using New().
|
||||
func (f Custom) Title() string {
|
||||
return f.title
|
||||
}
|
||||
|
||||
// Elements returns a list of all elements as set in the Submittable passed to form.New().
|
||||
func (f Custom) Elements() []Element {
|
||||
v := reflect.New(reflect.TypeOf(f.submittable)).Elem()
|
||||
v.Set(reflect.ValueOf(f.submittable))
|
||||
n := v.NumField()
|
||||
|
||||
elements := make([]Element, 0, n)
|
||||
for i := 0; i < n; i++ {
|
||||
field := v.Field(i)
|
||||
if !field.CanSet() {
|
||||
continue
|
||||
}
|
||||
// Each exported field is guaranteed to implement the Element interface.
|
||||
elements = append(elements, field.Interface().(Element))
|
||||
}
|
||||
return elements
|
||||
}
|
||||
|
||||
// SubmitJSON submits a JSON data slice to the form. The form will check all values in the JSON array passed,
|
||||
// making sure their values are valid for the form's elements.
|
||||
// If the values are valid and can be parsed properly, the Submittable.Submit() method of the form's Submittable is
|
||||
// called and the fields of the Submittable will be filled out.
|
||||
func (f Custom) SubmitJSON(b []byte, submitter Submitter, tx *world.Tx) error {
|
||||
if b == nil {
|
||||
if closer, ok := f.submittable.(Closer); ok {
|
||||
closer.Close(submitter, tx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
dec := json.NewDecoder(bytes.NewBuffer(b))
|
||||
dec.UseNumber()
|
||||
|
||||
var data []any
|
||||
if err := dec.Decode(&data); err != nil {
|
||||
return fmt.Errorf("error decoding JSON data to slice: %w", err)
|
||||
}
|
||||
|
||||
v := reflect.New(reflect.TypeOf(f.submittable)).Elem()
|
||||
v.Set(reflect.ValueOf(f.submittable))
|
||||
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
fieldV := v.Field(i)
|
||||
if !fieldV.CanSet() {
|
||||
continue
|
||||
}
|
||||
e := fieldV.Interface().(Element)
|
||||
if len(data) == 0 {
|
||||
return fmt.Errorf("form JSON data array does not have enough values")
|
||||
}
|
||||
if elementReadonly(e) {
|
||||
data = data[1:]
|
||||
continue
|
||||
}
|
||||
elem, err := f.parseValue(e, data[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing form response value: %w", err)
|
||||
}
|
||||
fieldV.Set(elem)
|
||||
data = data[1:]
|
||||
}
|
||||
|
||||
v.Interface().(Submittable).Submit(submitter, tx)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// elementReadonly returns true if the element is read only.
|
||||
func elementReadonly(e Element) bool {
|
||||
switch e.(type) {
|
||||
case Divider, Header, Label:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// parseValue parses a value into the Element passed and returns it as a reflection Value. If the value is not
|
||||
// valid for the element, an error is returned.
|
||||
func (f Custom) parseValue(elem Element, s any) (reflect.Value, error) {
|
||||
var ok bool
|
||||
var value reflect.Value
|
||||
|
||||
switch element := elem.(type) {
|
||||
case Input:
|
||||
element.value, ok = s.(string)
|
||||
if !ok {
|
||||
return value, fmt.Errorf("value %v is not allowed for input element", s)
|
||||
}
|
||||
if !utf8.ValidString(element.value) {
|
||||
return value, fmt.Errorf("value %v is not valid UTF8", s)
|
||||
}
|
||||
value = reflect.ValueOf(element)
|
||||
case Toggle:
|
||||
element.value, ok = s.(bool)
|
||||
if !ok {
|
||||
return value, fmt.Errorf("value %v is not allowed for toggle element", s)
|
||||
}
|
||||
value = reflect.ValueOf(element)
|
||||
case Slider:
|
||||
v, ok := s.(json.Number)
|
||||
f, err := v.Float64()
|
||||
if !ok || err != nil {
|
||||
return value, fmt.Errorf("value %v is not allowed for slider element", s)
|
||||
}
|
||||
if f > element.Max || f < element.Min {
|
||||
return value, fmt.Errorf("slider value %v is out of range %v-%v", f, element.Min, element.Max)
|
||||
}
|
||||
element.value = f
|
||||
value = reflect.ValueOf(element)
|
||||
case Dropdown:
|
||||
v, ok := s.(json.Number)
|
||||
f, err := v.Int64()
|
||||
if !ok || err != nil {
|
||||
return value, fmt.Errorf("value %v is not allowed for dropdown element", s)
|
||||
}
|
||||
if f < 0 || int(f) >= len(element.Options) {
|
||||
return value, fmt.Errorf("dropdown value %v is out of range %v-%v", f, 0, len(element.Options)-1)
|
||||
}
|
||||
element.value = int(f)
|
||||
value = reflect.ValueOf(element)
|
||||
case StepSlider:
|
||||
v, ok := s.(json.Number)
|
||||
f, err := v.Int64()
|
||||
if !ok || err != nil {
|
||||
return value, fmt.Errorf("value %v is not allowed for dropdown element", s)
|
||||
}
|
||||
if f < 0 || int(f) >= len(element.Options) {
|
||||
return value, fmt.Errorf("dropdown value %v is out of range %v-%v", f, 0, len(element.Options)-1)
|
||||
}
|
||||
element.value = int(f)
|
||||
value = reflect.ValueOf(element)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// verify verifies if the form is valid, checking if the fields all implement the Element interface. It panics
|
||||
// if the form is not valid.
|
||||
func (f Custom) verify() {
|
||||
el := reflect.TypeOf((*Element)(nil)).Elem()
|
||||
|
||||
v := reflect.New(reflect.TypeOf(f.submittable)).Elem()
|
||||
v.Set(reflect.ValueOf(f.submittable))
|
||||
|
||||
t := reflect.TypeOf(f.submittable)
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
if !v.Field(i).CanSet() {
|
||||
continue
|
||||
}
|
||||
if !t.Field(i).Type.Implements(el) {
|
||||
panic("all exported fields must implement form.Element interface")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// format is a utility function to format a list of values to have spaces between them, but no newline at the
|
||||
// end.
|
||||
func format(a []any) string {
|
||||
return strings.TrimSuffix(strings.TrimSuffix(fmt.Sprintln(a...), "\n"), "\n")
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package form
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
)
|
||||
|
||||
// Menu represents a menu form. These menus are made up of a title and a body, with a number of elements which
|
||||
// come below the body. These elements can include buttons, dividers, headers, and labels.
|
||||
type Menu struct {
|
||||
title, body string
|
||||
submittable MenuSubmittable
|
||||
elements []MenuElement
|
||||
}
|
||||
|
||||
// NewMenu creates a new Menu form using the MenuSubmittable passed to handle the output of the form. The
|
||||
// title passed is formatted following the rules of fmt.Sprintln.
|
||||
func NewMenu(submittable MenuSubmittable, title ...any) Menu {
|
||||
t := reflect.TypeOf(submittable)
|
||||
if t.Kind() != reflect.Struct {
|
||||
panic("submittable must be struct")
|
||||
}
|
||||
m := Menu{title: format(title), submittable: submittable}
|
||||
m.verify()
|
||||
return m
|
||||
}
|
||||
|
||||
// MarshalJSON ...
|
||||
func (m Menu) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]any{
|
||||
"type": "form",
|
||||
"title": m.title,
|
||||
"content": m.body,
|
||||
"elements": m.Elements(),
|
||||
})
|
||||
}
|
||||
|
||||
// WithBody creates a copy of the Menu form and changes its body to the body passed, after which the new Menu
|
||||
// form is returned. The text is formatted following the rules of fmt.Sprintln.
|
||||
func (m Menu) WithBody(body ...any) Menu {
|
||||
m.body = format(body)
|
||||
return m
|
||||
}
|
||||
|
||||
// AddButton appends a button to the menu's element list and returns the updated Menu.
|
||||
func (m Menu) AddButton(button Button) Menu {
|
||||
m.elements = append(m.elements, button)
|
||||
return m
|
||||
}
|
||||
|
||||
// AddDivider appends a divider to the menu's element list and returns the updated Menu.
|
||||
func (m Menu) AddDivider(divider Divider) Menu {
|
||||
m.elements = append(m.elements, divider)
|
||||
return m
|
||||
}
|
||||
|
||||
// AddHeader appends a header to the menu's element list and returns the updated Menu.
|
||||
func (m Menu) AddHeader(header Header) Menu {
|
||||
m.elements = append(m.elements, header)
|
||||
return m
|
||||
}
|
||||
|
||||
// AddLabel appends a label to the menu's element list and returns the updated Menu.
|
||||
func (m Menu) AddLabel(label Label) Menu {
|
||||
m.elements = append(m.elements, label)
|
||||
return m
|
||||
}
|
||||
|
||||
// WithButtons creates a copy of the Menu form and appends the buttons passed to the existing elements, after
|
||||
// which the new Menu form is returned.
|
||||
func (m Menu) WithButtons(buttons ...Button) Menu {
|
||||
for _, b := range buttons {
|
||||
m.elements = append(m.elements, b)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// WithElements creates a copy of the Menu form and appends the elements passed to the existing elements, after
|
||||
// which the new Menu form is returned. This allows adding any MenuElement type.
|
||||
func (m Menu) WithElements(elements ...MenuElement) Menu {
|
||||
m.elements = append(m.elements, elements...)
|
||||
return m
|
||||
}
|
||||
|
||||
// Title returns the formatted title passed to the menu upon construction using NewMenu().
|
||||
func (m Menu) Title() string {
|
||||
return m.title
|
||||
}
|
||||
|
||||
// Body returns the formatted text in the body passed to the menu using WithBody().
|
||||
func (m Menu) Body() string {
|
||||
return m.body
|
||||
}
|
||||
|
||||
// Buttons returns a list of all buttons of the MenuSubmittable. It collects buttons from the MenuSubmittable
|
||||
// fields and any buttons added via WithButtons(), AddButton().
|
||||
func (m Menu) Buttons() []Button {
|
||||
v := reflect.New(reflect.TypeOf(m.submittable)).Elem()
|
||||
v.Set(reflect.ValueOf(m.submittable))
|
||||
|
||||
buttons := make([]Button, 0)
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
field := v.Field(i)
|
||||
if !field.CanSet() {
|
||||
continue
|
||||
}
|
||||
if b, ok := field.Interface().(Button); ok {
|
||||
buttons = append(buttons, b)
|
||||
}
|
||||
}
|
||||
for _, elem := range m.elements {
|
||||
if b, ok := elem.(Button); ok {
|
||||
buttons = append(buttons, b)
|
||||
}
|
||||
}
|
||||
return buttons
|
||||
}
|
||||
|
||||
// Elements returns all elements of this menu form. It collects elements from the MenuSubmittable
|
||||
// fields and any elements added via WithElements().
|
||||
func (m Menu) Elements() []MenuElement {
|
||||
v := reflect.New(reflect.TypeOf(m.submittable)).Elem()
|
||||
v.Set(reflect.ValueOf(m.submittable))
|
||||
|
||||
elements := make([]MenuElement, 0, v.NumField()+len(m.elements))
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
field := v.Field(i)
|
||||
if !field.CanSet() {
|
||||
continue
|
||||
}
|
||||
elements = append(elements, field.Interface().(MenuElement))
|
||||
}
|
||||
elements = append(elements, m.elements...)
|
||||
return elements
|
||||
}
|
||||
|
||||
// SubmitJSON submits a JSON value to the menu, containing the index of the button clicked.
|
||||
func (m Menu) SubmitJSON(b []byte, submitter Submitter, tx *world.Tx) error {
|
||||
if b == nil {
|
||||
if closer, ok := m.submittable.(Closer); ok {
|
||||
closer.Close(submitter, tx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var index uint
|
||||
err := json.Unmarshal(b, &index)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot parse button index as int: %w", err)
|
||||
}
|
||||
buttons := m.Buttons()
|
||||
if index >= uint(len(buttons)) {
|
||||
return fmt.Errorf("button index points to inexistent button: %v (only %v buttons present)", index, len(buttons))
|
||||
}
|
||||
m.submittable.Submit(submitter, buttons[index], tx)
|
||||
return nil
|
||||
}
|
||||
|
||||
// verify verifies if the form is valid, checking all exported fields implement MenuElement.
|
||||
// It panics if the form is not valid.
|
||||
func (m Menu) verify() {
|
||||
v := reflect.New(reflect.TypeOf(m.submittable)).Elem()
|
||||
v.Set(reflect.ValueOf(m.submittable))
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
if !v.Field(i).CanSet() {
|
||||
continue
|
||||
}
|
||||
if _, ok := v.Field(i).Interface().(MenuElement); !ok {
|
||||
panic("all exported fields must implement form.MenuElement")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package form
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Modal represents a modal form. These forms have a body with text and two buttons at the end, typically one
|
||||
// for Yes and one for No. These buttons may have custom text, but can, unlike with a Menu form, not have
|
||||
// images next to them.
|
||||
type Modal struct {
|
||||
title, body string
|
||||
submittable ModalSubmittable
|
||||
}
|
||||
|
||||
// NewModal creates a new Modal form using the ModalSubmittable passed to handle the output of the form. The
|
||||
// title passed is formatted following the fmt.Sprintln rules.
|
||||
// Default 'yes' and 'no' buttons may be passed by setting the two exported struct fields of the submittable
|
||||
// to YesButton() and NoButton() respectively.
|
||||
func NewModal(submittable ModalSubmittable, title ...any) Modal {
|
||||
t := reflect.TypeOf(submittable)
|
||||
if t.Kind() != reflect.Struct {
|
||||
panic("submittable must be struct")
|
||||
}
|
||||
m := Modal{title: format(title), submittable: submittable}
|
||||
m.verify()
|
||||
return m
|
||||
}
|
||||
|
||||
// YesButton returns a Button which may be used as a default 'yes' button for a modal form.
|
||||
func YesButton() Button {
|
||||
return Button{Text: "gui.yes"}
|
||||
}
|
||||
|
||||
// NoButton returns a Button which may be used as a default 'no' button for a modal form.
|
||||
func NoButton() Button {
|
||||
return Button{Text: "gui.no"}
|
||||
}
|
||||
|
||||
// MarshalJSON ...
|
||||
func (m Modal) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]any{
|
||||
"type": "modal",
|
||||
"title": m.title,
|
||||
"content": m.body,
|
||||
"button1": m.Buttons()[0].Text,
|
||||
"button2": m.Buttons()[1].Text,
|
||||
})
|
||||
}
|
||||
|
||||
// WithBody creates a copy of the Modal form and changes its body to the body passed, after which the new Modal
|
||||
// form is returned. The text is formatted following the rules of fmt.Sprintln.
|
||||
func (m Modal) WithBody(body ...any) Modal {
|
||||
m.body = format(body)
|
||||
return m
|
||||
}
|
||||
|
||||
// Title returns the formatted title passed to the menu upon construction using NewModal().
|
||||
func (m Modal) Title() string {
|
||||
return m.title
|
||||
}
|
||||
|
||||
// Body returns the formatted text in the body passed to the menu using WithBody().
|
||||
func (m Modal) Body() string {
|
||||
return m.body
|
||||
}
|
||||
|
||||
// SubmitJSON submits a JSON byte slice to the modal form. This byte slice contains a JSON encoded bool in it,
|
||||
// which is used to determine which button was clicked.
|
||||
func (m Modal) SubmitJSON(b []byte, submitter Submitter, tx *world.Tx) error {
|
||||
if b == nil {
|
||||
if closer, ok := m.submittable.(Closer); ok {
|
||||
closer.Close(submitter, tx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var value bool
|
||||
if err := json.Unmarshal(b, &value); err != nil {
|
||||
return fmt.Errorf("error parsing JSON as bool: %w", err)
|
||||
}
|
||||
if value {
|
||||
m.submittable.Submit(submitter, m.Buttons()[0], tx)
|
||||
return nil
|
||||
}
|
||||
m.submittable.Submit(submitter, m.Buttons()[1], tx)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Buttons returns a list of all buttons of the Modal form, which will always be a total of two buttons.
|
||||
func (m Modal) Buttons() []Button {
|
||||
v := reflect.New(reflect.TypeOf(m.submittable)).Elem()
|
||||
v.Set(reflect.ValueOf(m.submittable))
|
||||
|
||||
buttons := make([]Button, 0, v.NumField())
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
field := v.Field(i)
|
||||
if !field.CanSet() {
|
||||
continue
|
||||
}
|
||||
// Each exported field is guaranteed to be of type Button.
|
||||
buttons = append(buttons, field.Interface().(Button))
|
||||
}
|
||||
return buttons
|
||||
}
|
||||
|
||||
// verify verifies that the Modal form is valid. It checks if exactly two exported fields are present and
|
||||
// ensures that both have the Button type.
|
||||
func (m Modal) verify() {
|
||||
var count int
|
||||
|
||||
v := reflect.New(reflect.TypeOf(m.submittable)).Elem()
|
||||
v.Set(reflect.ValueOf(m.submittable))
|
||||
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
if !v.Field(i).CanSet() {
|
||||
continue
|
||||
}
|
||||
if _, ok := v.Field(i).Interface().(Button); !ok {
|
||||
panic("both exported fields must be of the type form.Button")
|
||||
}
|
||||
count++
|
||||
}
|
||||
if count != 2 {
|
||||
panic("modal form must have exactly two exported fields of the type form.Button")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package form
|
||||
|
||||
import "github.com/df-mc/dragonfly/server/world"
|
||||
|
||||
// Submittable is a structure which may be submitted by sending it as a form using form.New(). When filled out
|
||||
// and submitted, the struct will have its Submit method called and its fields will have the values that the
|
||||
// Submitter passed filled out.
|
||||
// The fields of a Submittable struct must be either unexported or have a type of one of those that implement
|
||||
// the form.Element interface.
|
||||
type Submittable interface {
|
||||
// Submit is called when the Submitter submits the form sent to it. Once this method is called, all fields
|
||||
// in the struct will have their values filled out as filled out by the Submitter.
|
||||
Submit(submitter Submitter, tx *world.Tx)
|
||||
}
|
||||
|
||||
// MenuSubmittable is a structure which may be submitted by sending it as a form using form.NewMenu(), much
|
||||
// like a Submittable. The struct will have its Submit method called with the button pressed.
|
||||
// A struct that implements the MenuSubmittable interface must only have exported fields with the type
|
||||
// form.Button.
|
||||
type MenuSubmittable interface {
|
||||
// Submit is called when the Submitter submits the menu form sent to it. The method is called with the
|
||||
// button that was pressed. It may be compared with buttons in the MenuSubmittable struct to check which
|
||||
// button was pressed.
|
||||
Submit(submitter Submitter, pressed Button, tx *world.Tx)
|
||||
}
|
||||
|
||||
// ModalSubmittable is a structure which may be submitted by sending it as a form using form.NewModal(), much
|
||||
// like a Submittable and a MenuSubmittable. The struct will have its Submit method called with the button
|
||||
// pressed.
|
||||
// A struct that implements the ModalSubmittable interface must have exactly two exported fields with the type
|
||||
// form.Button, which may be used to specify the text of the Modal form's buttons. Unlike with a Menu form,
|
||||
// buttons on a Modal form will not have images.
|
||||
type ModalSubmittable MenuSubmittable
|
||||
|
||||
// Closer represents a form which has special logic when being closed by a Submitter.
|
||||
type Closer interface {
|
||||
// Close is called when the Submitter closes a form.
|
||||
Close(submitter Submitter, tx *world.Tx)
|
||||
}
|
||||
|
||||
// Submitter is an entity that is able to submit a form sent to it. It is able to fill out fields in the form
|
||||
// which will then be present when handled. The Submitter is also able to close the form.
|
||||
type Submitter interface {
|
||||
SendForm(form Form)
|
||||
CloseForm()
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
"github.com/df-mc/dragonfly/server/cmd"
|
||||
"github.com/df-mc/dragonfly/server/event"
|
||||
"github.com/df-mc/dragonfly/server/item"
|
||||
"github.com/df-mc/dragonfly/server/player/skin"
|
||||
"github.com/df-mc/dragonfly/server/session"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
)
|
||||
|
||||
type Context = event.Context[*Player]
|
||||
|
||||
// Handler handles events that are called by a player. Implementations of Handler may be used to listen to
|
||||
// specific events such as when a player chats or moves.
|
||||
type Handler interface {
|
||||
// HandleMove handles the movement of a player. ctx.Cancel() may be called to cancel the movement event.
|
||||
// The new position, yaw and pitch are passed.
|
||||
HandleMove(ctx *Context, newPos mgl64.Vec3, newRot cube.Rotation)
|
||||
// HandleJump handles the player jumping.
|
||||
HandleJump(p *Player)
|
||||
// HandleTeleport handles the teleportation of a player. ctx.Cancel() may be called to cancel it.
|
||||
HandleTeleport(ctx *Context, pos mgl64.Vec3)
|
||||
// HandleChangeWorld handles when the player is added to a new world. before may be nil.
|
||||
HandleChangeWorld(p *Player, before, after *world.World)
|
||||
// HandleToggleSprint handles when the player starts or stops sprinting.
|
||||
// After is true if the player is sprinting after toggling (changing their sprinting state).
|
||||
HandleToggleSprint(ctx *Context, after bool)
|
||||
// HandleToggleSneak handles when the player starts or stops sneaking.
|
||||
// After is true if the player is sneaking after toggling (changing their sneaking state).
|
||||
HandleToggleSneak(ctx *Context, after bool)
|
||||
// HandleChat handles a message sent in the chat by a player. ctx.Cancel() may be called to cancel the
|
||||
// message being sent in chat.
|
||||
// The message may be changed by assigning to *message.
|
||||
HandleChat(ctx *Context, message *string)
|
||||
// HandleFoodLoss handles the food bar of a player depleting naturally, for example because the player was
|
||||
// sprinting and jumping. ctx.Cancel() may be called to cancel the food points being lost.
|
||||
HandleFoodLoss(ctx *Context, from int, to *int)
|
||||
// HandleHeal handles the player being healed by a healing source. ctx.Cancel() may be called to cancel
|
||||
// the healing.
|
||||
// The health added may be changed by assigning to *health.
|
||||
HandleHeal(ctx *Context, health *float64, src world.HealingSource)
|
||||
// HandleHurt handles the player being hurt by any damage source. ctx.Cancel() may be called to cancel the
|
||||
// damage being dealt to the player.
|
||||
// The damage dealt to the player may be changed by assigning to *damage.
|
||||
// *damage is the final damage dealt to the player. Immune is set to true
|
||||
// if the player was hurt during an immunity frame with higher damage than
|
||||
// the original cause of the immunity frame. In this case, the damage is
|
||||
// reduced but the player is still knocked back.
|
||||
HandleHurt(ctx *Context, damage *float64, immune bool, attackImmunity *time.Duration, src world.DamageSource)
|
||||
// HandleDeath handles the player dying to a particular damage cause.
|
||||
HandleDeath(p *Player, src world.DamageSource, keepInv *bool)
|
||||
// HandleRespawn handles the respawning of the player in the world. The spawn position passed may be
|
||||
// changed by assigning to *pos. The world.World in which the Player is respawned may be modifying by assigning to
|
||||
// *w. This world may be the world the Player died in, but it might also point to a different world (the overworld)
|
||||
// if the Player died in the nether or end.
|
||||
HandleRespawn(p *Player, pos *mgl64.Vec3, w **world.World)
|
||||
// HandleSkinChange handles the player changing their skin. ctx.Cancel() may be called to cancel the skin
|
||||
// change.
|
||||
HandleSkinChange(ctx *Context, skin *skin.Skin)
|
||||
// HandleFireExtinguish handles the player extinguishing a fire at a specific position. ctx.Cancel() may
|
||||
// be called to cancel the fire being extinguished.
|
||||
// cube.Pos can be used to see where was the fire extinguished, may be used to cancel this on specific positions.
|
||||
HandleFireExtinguish(ctx *Context, pos cube.Pos)
|
||||
// HandleStartBreak handles the player starting to break a block at the position passed. ctx.Cancel() may
|
||||
// be called to stop the player from breaking the block completely.
|
||||
HandleStartBreak(ctx *Context, pos cube.Pos)
|
||||
// HandleBlockBreak handles a block that is being broken by a player. ctx.Cancel() may be called to cancel
|
||||
// the block being broken. A pointer to a slice of the block's drops is passed, and may be altered
|
||||
// to change what items will actually be dropped.
|
||||
HandleBlockBreak(ctx *Context, pos cube.Pos, drops *[]item.Stack, xp *int)
|
||||
// HandleBlockPlace handles the player placing a specific block at a position in its world. ctx.Cancel()
|
||||
// may be called to cancel the block being placed.
|
||||
HandleBlockPlace(ctx *Context, pos cube.Pos, b world.Block)
|
||||
// HandleBlockPick handles the player picking a specific block at a position in its world. ctx.Cancel()
|
||||
// may be called to cancel the block being picked.
|
||||
HandleBlockPick(ctx *Context, pos cube.Pos, b world.Block)
|
||||
// HandleItemUse handles the player using an item in the air. It is called for each item, although most
|
||||
// will not actually do anything. Items such as snowballs may be thrown if HandleItemUse does not cancel
|
||||
// the context using ctx.Cancel(). It is not called if the player is holding no item.
|
||||
HandleItemUse(ctx *Context)
|
||||
// HandleItemUseOnBlock handles the player using the item held in its main hand on a block at the block
|
||||
// position passed. The face of the block clicked is also passed, along with the relative click position.
|
||||
// The click position has X, Y and Z values which are all in the range 0.0-1.0. It is also called if the
|
||||
// player is holding no item.
|
||||
HandleItemUseOnBlock(ctx *Context, pos cube.Pos, face cube.Face, clickPos mgl64.Vec3)
|
||||
// HandleItemUseOnEntity handles the player using the item held in its main hand on an entity passed to
|
||||
// the method.
|
||||
// HandleItemUseOnEntity is always called when a player uses an item on an entity, regardless of whether
|
||||
// the item actually does anything when used on an entity. It is also called if the player is holding no
|
||||
// item.
|
||||
HandleItemUseOnEntity(ctx *Context, e world.Entity)
|
||||
// HandleItemRelease handles the player releasing an item after using it for
|
||||
// a particular duration. These include items such as bows.
|
||||
HandleItemRelease(ctx *Context, item item.Stack, dur time.Duration)
|
||||
// HandleItemConsume handles the player consuming an item. This is called whenever a consumable such as
|
||||
// food is consumed.
|
||||
HandleItemConsume(ctx *Context, item item.Stack)
|
||||
// HandleAttackEntity handles the player attacking an entity using the item held in its hand. ctx.Cancel()
|
||||
// may be called to cancel the attack, which will cancel damage dealt to the target and will stop the
|
||||
// entity from being knocked back.
|
||||
// The entity attacked may not be alive (implements entity.Living), in which case no damage will be dealt
|
||||
// and the target won't be knocked back.
|
||||
// The entity attacked may also be immune when this method is called, in which case no damage and knock-
|
||||
// back will be dealt.
|
||||
// The knock back force and height is also provided which can be modified.
|
||||
// The attack can be a critical attack, which would increase damage by a factor of 1.5 and
|
||||
// spawn critical hit particles around the target entity. These particles will not be displayed
|
||||
// if no damage is dealt.
|
||||
HandleAttackEntity(ctx *Context, e world.Entity, force, height *float64, critical *bool)
|
||||
// HandleExperienceGain handles the player gaining experience. ctx.Cancel() may be called to cancel
|
||||
// the gain.
|
||||
// The amount is also provided which can be modified.
|
||||
HandleExperienceGain(ctx *Context, amount *int)
|
||||
// HandlePunchAir handles the player punching air.
|
||||
HandlePunchAir(ctx *Context)
|
||||
// HandleSignEdit handles the player editing a sign. It is called for every keystroke while editing a sign and
|
||||
// has both the old text passed and the text after the edit. This typically only has a change of one character.
|
||||
HandleSignEdit(ctx *Context, pos cube.Pos, frontSide bool, oldText, newText string)
|
||||
// HandleSleep handles the player beginning the sleep action. ctx.Cancel() may be called to cancel the action.
|
||||
HandleSleep(ctx *Context, sendReminder *bool)
|
||||
// HandleLecternPageTurn handles the player turning a page in a lectern. ctx.Cancel() may be called to cancel the
|
||||
// page turn. The page number may be changed by assigning to *page.
|
||||
HandleLecternPageTurn(ctx *Context, pos cube.Pos, oldPage int, newPage *int)
|
||||
// HandleItemDamage handles the event wherein the item either held by the player or as armour takes
|
||||
// damage through usage.
|
||||
// The type of the item may be checked to determine whether it was armour or a tool used. The damage to
|
||||
// the item is passed.
|
||||
HandleItemDamage(ctx *Context, i item.Stack, damage *int)
|
||||
// HandleItemPickup handles the player picking up an item from the ground. The item stack laying on the
|
||||
// ground is passed. ctx.Cancel() may be called to prevent the player from picking up the item.
|
||||
HandleItemPickup(ctx *Context, i *item.Stack)
|
||||
// HandleHeldSlotChange handles the player changing the slot they are currently holding.
|
||||
HandleHeldSlotChange(ctx *Context, from, to int)
|
||||
// HandleItemDrop handles the player dropping an item on the ground.
|
||||
// ctx.Cancel() may be called to prevent the player from dropping the item.Stack passed on the ground.
|
||||
HandleItemDrop(ctx *Context, s item.Stack)
|
||||
// HandleTransfer handles a player being transferred to another server. ctx.Cancel() may be called to
|
||||
// cancel the transfer.
|
||||
HandleTransfer(ctx *Context, addr *net.UDPAddr)
|
||||
// HandleCommandExecution handles the command execution of a player, who wrote a command in the chat.
|
||||
// ctx.Cancel() may be called to cancel the command execution.
|
||||
HandleCommandExecution(ctx *Context, command cmd.Command, args []string)
|
||||
// HandleQuit handles the closing of a player. It is always called when the player is disconnected,
|
||||
// regardless of the reason.
|
||||
HandleQuit(p *Player)
|
||||
// HandleDiagnostics handles the latest diagnostics data that the player has sent to the server. This is
|
||||
// not sent by every client however, only those with the "Creator > Enable Client Diagnostics" setting
|
||||
// enabled.
|
||||
HandleDiagnostics(p *Player, d session.Diagnostics)
|
||||
}
|
||||
|
||||
// NopHandler implements the Handler interface but does not execute any code when an event is called. The
|
||||
// default Handler of players is set to NopHandler.
|
||||
// Users may embed NopHandler to avoid having to implement each method.
|
||||
type NopHandler struct{}
|
||||
|
||||
// Compile time check to make sure NopHandler implements Handler.
|
||||
var _ Handler = NopHandler{}
|
||||
|
||||
func (NopHandler) HandleItemDrop(*Context, item.Stack) {}
|
||||
func (NopHandler) HandleHeldSlotChange(*Context, int, int) {}
|
||||
func (NopHandler) HandleMove(*Context, mgl64.Vec3, cube.Rotation) {}
|
||||
func (NopHandler) HandleJump(*Player) {}
|
||||
func (NopHandler) HandleTeleport(*Context, mgl64.Vec3) {}
|
||||
func (NopHandler) HandleChangeWorld(*Player, *world.World, *world.World) {}
|
||||
func (NopHandler) HandleToggleSprint(*Context, bool) {}
|
||||
func (NopHandler) HandleToggleSneak(*Context, bool) {}
|
||||
func (NopHandler) HandleCommandExecution(*Context, cmd.Command, []string) {}
|
||||
func (NopHandler) HandleTransfer(*Context, *net.UDPAddr) {}
|
||||
func (NopHandler) HandleChat(*Context, *string) {}
|
||||
func (NopHandler) HandleSkinChange(*Context, *skin.Skin) {}
|
||||
func (NopHandler) HandleFireExtinguish(*Context, cube.Pos) {}
|
||||
func (NopHandler) HandleStartBreak(*Context, cube.Pos) {}
|
||||
func (NopHandler) HandleBlockBreak(*Context, cube.Pos, *[]item.Stack, *int) {}
|
||||
func (NopHandler) HandleBlockPlace(*Context, cube.Pos, world.Block) {}
|
||||
func (NopHandler) HandleBlockPick(*Context, cube.Pos, world.Block) {}
|
||||
func (NopHandler) HandleSignEdit(*Context, cube.Pos, bool, string, string) {}
|
||||
func (NopHandler) HandleSleep(*Context, *bool) {}
|
||||
func (NopHandler) HandleLecternPageTurn(*Context, cube.Pos, int, *int) {}
|
||||
func (NopHandler) HandleItemPickup(*Context, *item.Stack) {}
|
||||
func (NopHandler) HandleItemUse(*Context) {}
|
||||
func (NopHandler) HandleItemUseOnBlock(*Context, cube.Pos, cube.Face, mgl64.Vec3) {}
|
||||
func (NopHandler) HandleItemUseOnEntity(*Context, world.Entity) {}
|
||||
func (NopHandler) HandleItemRelease(ctx *Context, item item.Stack, dur time.Duration) {}
|
||||
func (NopHandler) HandleItemConsume(*Context, item.Stack) {}
|
||||
func (NopHandler) HandleItemDamage(*Context, item.Stack, *int) {}
|
||||
func (NopHandler) HandleAttackEntity(*Context, world.Entity, *float64, *float64, *bool) {}
|
||||
func (NopHandler) HandleExperienceGain(*Context, *int) {}
|
||||
func (NopHandler) HandlePunchAir(*Context) {}
|
||||
func (NopHandler) HandleHurt(*Context, *float64, bool, *time.Duration, world.DamageSource) {}
|
||||
func (NopHandler) HandleHeal(*Context, *float64, world.HealingSource) {}
|
||||
func (NopHandler) HandleFoodLoss(*Context, int, *int) {}
|
||||
func (NopHandler) HandleDeath(*Player, world.DamageSource, *bool) {}
|
||||
func (NopHandler) HandleRespawn(*Player, *mgl64.Vec3, **world.World) {}
|
||||
func (NopHandler) HandleQuit(*Player) {}
|
||||
func (NopHandler) HandleDiagnostics(*Player, session.Diagnostics) {}
|
||||
@@ -0,0 +1,101 @@
|
||||
package hud
|
||||
|
||||
// Element represents a HUD element in the game that can either be hidden or shown.
|
||||
type Element struct {
|
||||
element
|
||||
}
|
||||
|
||||
type element uint8
|
||||
|
||||
// PaperDoll is the element that shows the player's paper doll, which is a visual representation of the
|
||||
// player's character model and equipment, as well as any currently played animations. It is located in the
|
||||
// top left corner of the screen.
|
||||
func PaperDoll() Element {
|
||||
return Element{0}
|
||||
}
|
||||
|
||||
// Armour is the element that shows the player's armour level, sitting either above the hotbar or at the top
|
||||
// of the screen on in non-classic views.
|
||||
func Armour() Element {
|
||||
return Element{1}
|
||||
}
|
||||
|
||||
// ToolTips is the element that shows useful hints and tips to the player, such as how to use items or
|
||||
// how to perform certain actions in the game. These tips are displayed at the top right of the screen.
|
||||
func ToolTips() Element {
|
||||
return Element{2}
|
||||
}
|
||||
|
||||
// TouchControls is the element that shows the touch controls on the screen, which is used for touch-based
|
||||
// devices.
|
||||
func TouchControls() Element {
|
||||
return Element{3}
|
||||
}
|
||||
|
||||
// Crosshair is the element that shows the crosshair in the middle of the screen, which is used for aiming
|
||||
// and targeting entities or blocks.
|
||||
func Crosshair() Element {
|
||||
return Element{4}
|
||||
}
|
||||
|
||||
// HotBar is the element that shows all the items in the player's hotbar, located at the bottom of the screen.
|
||||
func HotBar() Element {
|
||||
return Element{5}
|
||||
}
|
||||
|
||||
// Health is the element that shows the player's health bar, sitting either above the hotbar or at the top
|
||||
// of the screen on in non-classic views.
|
||||
func Health() Element {
|
||||
return Element{6}
|
||||
}
|
||||
|
||||
// ProgressBar is the element that shows the player's experience bar. It is always located just above the
|
||||
// hotbar.
|
||||
func ProgressBar() Element {
|
||||
return Element{7}
|
||||
}
|
||||
|
||||
// Hunger is the element that shows the player's hunger bar, which indicates how hungry the player is and
|
||||
// how much food they need to consume to restore their hunger. It is located either above the hotbar or at the
|
||||
// top of the screen on in non-classic views.
|
||||
func Hunger() Element {
|
||||
return Element{8}
|
||||
}
|
||||
|
||||
// AirBubbles is the element that shows the player's air bubbles, which indicate how much air the player has
|
||||
// left when underwater. It is located either above the hotbar or at the top of the screen on in non-classic
|
||||
// views. It is only visible when the player is underwater or they are regenerating air after being underwater.
|
||||
func AirBubbles() Element {
|
||||
return Element{9}
|
||||
}
|
||||
|
||||
// HorseHealth is the element that shows the health of the player's horse, which replaces the player's own
|
||||
// health bar when riding a horse/other entity with health.
|
||||
func HorseHealth() Element {
|
||||
return Element{10}
|
||||
}
|
||||
|
||||
// StatusEffects is the element that shows the icons of the currently active status effects, located on the
|
||||
// right side of the screen.
|
||||
func StatusEffects() Element {
|
||||
return Element{11}
|
||||
}
|
||||
|
||||
// ItemText is the element that shows the text of the item currently held in the player's hand, which is
|
||||
// displayed just above the hotbar when switching to a new item.
|
||||
func ItemText() Element {
|
||||
return Element{12}
|
||||
}
|
||||
|
||||
// Uint8 returns the element type as a uint8.
|
||||
func (s element) Uint8() uint8 {
|
||||
return uint8(s)
|
||||
}
|
||||
|
||||
// All returns all the HUD elements that are available to be shown or hidden in the game.
|
||||
func All() []Element {
|
||||
return []Element{
|
||||
PaperDoll(), Armour(), ToolTips(), TouchControls(), Crosshair(), HotBar(), Health(),
|
||||
ProgressBar(), Hunger(), AirBubbles(), HorseHealth(), StatusEffects(), ItemText(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package hud
|
||||
|
||||
// Renderer represents an interface that can manage HUD elements for a player.
|
||||
type Renderer interface {
|
||||
// ShowHudElement shows a HUD element to the renderer if it is not already shown.
|
||||
ShowHudElement(e Element)
|
||||
// HideHudElement hides a HUD element from the renderer if it is not already hidden.
|
||||
HideHudElement(e Element)
|
||||
// HudElementHidden checks if a HUD element is currently hidden from the renderer.
|
||||
HudElementHidden(e Element) bool
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// hungerManager handles the changes in hunger, exhaustion and saturation of a player.
|
||||
type hungerManager struct {
|
||||
mu sync.RWMutex
|
||||
foodLevel int
|
||||
saturationLevel float64
|
||||
exhaustionLevel float64
|
||||
foodTick int
|
||||
}
|
||||
|
||||
// newHungerManager returns a new hunger manager with the default values for food level, saturation level and
|
||||
// exhaustion level.
|
||||
func newHungerManager() *hungerManager {
|
||||
return &hungerManager{foodLevel: 20, saturationLevel: 5, foodTick: 1}
|
||||
}
|
||||
|
||||
// Food returns the current food level of a player. The level returned is guaranteed to always be between 0
|
||||
// and 20.
|
||||
func (m *hungerManager) Food() int {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.foodLevel
|
||||
}
|
||||
|
||||
// SetFood sets the food level of a player. The level passed must be in a range of 0-20. If the level passed
|
||||
// is negative, the food level will be set to 0. If the level exceeds 20, the food level will be set to 20.
|
||||
func (m *hungerManager) SetFood(level int) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.foodLevel = max(min(level, 20), 0)
|
||||
}
|
||||
|
||||
// AddFood adds a number of food points to the current food level of a player.
|
||||
func (m *hungerManager) AddFood(points int) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.foodLevel = max(min(m.foodLevel+points, 20), 0)
|
||||
}
|
||||
|
||||
// Reset resets the hunger manager to its default values, identical to those set when creating a new manager
|
||||
// using newHungerManager.
|
||||
func (m *hungerManager) Reset() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.foodLevel = 20
|
||||
m.saturationLevel = 5
|
||||
m.exhaustionLevel = 0
|
||||
m.foodTick = 1
|
||||
}
|
||||
|
||||
// ResetExhaustion resets the player's exhaustion level to 0. It prevents the
|
||||
// player's food level from decreasing immediately after cancelling food loss.
|
||||
func (m *hungerManager) resetExhaustion() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.exhaustionLevel = 0
|
||||
m.saturationLevel = 0
|
||||
m.foodTick = 1
|
||||
}
|
||||
|
||||
// exhaust exhausts the player by the amount of points passed. If the total exhaustion level exceeds 4, a
|
||||
// saturation point, or food point, if saturation is 0, will be subtracted.
|
||||
func (m *hungerManager) exhaust(points float64) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.exhaustionLevel += points
|
||||
for m.exhaustionLevel >= 4 {
|
||||
// Maximum exhaustion value is 4, so keep removing one saturation point until the exhaustion level
|
||||
// is below 4.
|
||||
m.exhaustionLevel -= 4
|
||||
m.desaturate()
|
||||
}
|
||||
}
|
||||
|
||||
// saturate saturates the player's food and saturation by the amount of points passed. Note that the total
|
||||
// saturation will never exceed the total food value.
|
||||
func (m *hungerManager) saturate(food int, saturation float64) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.foodLevel = max(min(m.foodLevel+food, 20), 0)
|
||||
m.saturationLevel = max(min(m.saturationLevel+saturation, float64(m.foodLevel)), 0)
|
||||
}
|
||||
|
||||
// desaturate removes one saturation point from the player. If the saturation level of the player is already
|
||||
// 0, a point will be subtracted from the food level instead. If that level, too, is already 0, nothing will
|
||||
// happen.
|
||||
func (m *hungerManager) desaturate() {
|
||||
if m.saturationLevel <= 0 && m.foodLevel != 0 {
|
||||
m.foodLevel--
|
||||
} else if m.saturationLevel > 0 {
|
||||
m.saturationLevel = max(m.saturationLevel-1, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// canQuicklyRegenerate checks if the player can quickly regenerate. The function returns true if Food() returns 20
|
||||
// and the player still has saturation left.
|
||||
// The rate of regeneration is 1/0.5 seconds.
|
||||
func (m *hungerManager) canQuicklyRegenerate() bool {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
return m.foodLevel == 20 && m.saturationLevel > 0
|
||||
}
|
||||
|
||||
// canRegenerate checks if the player with the amount of food levels in the hunger manager can regenerate.
|
||||
// The function returns true if Food() returns either 18-20.
|
||||
// The rate of regeneration is 1/4 seconds.
|
||||
func (m *hungerManager) canRegenerate() bool {
|
||||
return m.Food() >= 18
|
||||
}
|
||||
|
||||
// canSprint returns true if the food level of the player is 7 or higher.
|
||||
func (m *hungerManager) canSprint() bool {
|
||||
return m.Food() > 6
|
||||
}
|
||||
|
||||
// starving checks if the player is currently considered to be starving. True is returned if Food() returns 0.
|
||||
func (m *hungerManager) starving() bool {
|
||||
return m.Food() == 0
|
||||
}
|
||||
|
||||
// StarvationDamageSource is the world.DamageSource passed when a player is
|
||||
// dealt damage from an empty food bar.
|
||||
type StarvationDamageSource struct{}
|
||||
|
||||
func (StarvationDamageSource) ReducedByArmour() bool { return false }
|
||||
func (StarvationDamageSource) ReducedByResistance() bool { return false }
|
||||
func (StarvationDamageSource) Fire() bool { return false }
|
||||
func (StarvationDamageSource) IgnoreTotem() bool { return false }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
package playerdb
|
||||
|
||||
import "github.com/df-mc/dragonfly/server/entity/effect"
|
||||
|
||||
func effectsToData(effects []effect.Effect) []jsonEffect {
|
||||
data := make([]jsonEffect, len(effects))
|
||||
for key, eff := range effects {
|
||||
id, ok := effect.ID(eff.Type())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
data[key] = jsonEffect{
|
||||
ID: id,
|
||||
Duration: eff.Duration(),
|
||||
Level: eff.Level(),
|
||||
Ambient: eff.Ambient(),
|
||||
ParticlesHidden: eff.ParticlesHidden(),
|
||||
Infinite: eff.Infinite(),
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func dataToEffects(data []jsonEffect) []effect.Effect {
|
||||
effects := make([]effect.Effect, len(data))
|
||||
for i, d := range data {
|
||||
e, ok := effect.ByID(d.ID)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch eff := e.(type) {
|
||||
case effect.LastingType:
|
||||
switch {
|
||||
case d.Ambient:
|
||||
effects[i] = effect.NewAmbient(eff, d.Level, d.Duration)
|
||||
case d.Infinite:
|
||||
effects[i] = effect.NewInfinite(eff, d.Level)
|
||||
default:
|
||||
effects[i] = effect.New(eff, d.Level, d.Duration)
|
||||
}
|
||||
|
||||
if d.ParticlesHidden {
|
||||
effects[i] = effects[i].WithoutParticles()
|
||||
}
|
||||
default:
|
||||
effects[i] = effect.NewInstant(eff, d.Level)
|
||||
}
|
||||
}
|
||||
return effects
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package playerdb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/df-mc/dragonfly/server/internal/nbtconv"
|
||||
"github.com/df-mc/dragonfly/server/item"
|
||||
"github.com/sandertv/gophertunnel/minecraft/nbt"
|
||||
)
|
||||
|
||||
// InventoryData is a struct that contains all data of the player inventories.
|
||||
type InventoryData struct {
|
||||
// Items contains all the items in the player's main inventory.
|
||||
// This excludes armour and offhand.
|
||||
Items []item.Stack
|
||||
// Boots, Leggings, Chestplate, Helmet are armour pieces that belong to the slot corresponding to the name.
|
||||
Boots item.Stack
|
||||
Leggings item.Stack
|
||||
Chestplate item.Stack
|
||||
Helmet item.Stack
|
||||
// OffHand is what the player is carrying in their non-main hand, like a shield or arrows.
|
||||
OffHand item.Stack
|
||||
// MainHandSlot saves the slot in the hotbar that the player is currently switched to.
|
||||
// Should be between 0-8.
|
||||
MainHandSlot uint32
|
||||
}
|
||||
|
||||
func invToData(data InventoryData) jsonInventoryData {
|
||||
d := jsonInventoryData{
|
||||
MainHandSlot: data.MainHandSlot,
|
||||
OffHand: encodeItem(data.OffHand),
|
||||
}
|
||||
d.Items = encodeItems(data.Items)
|
||||
d.Boots = encodeItem(data.Boots)
|
||||
d.Leggings = encodeItem(data.Leggings)
|
||||
d.Chestplate = encodeItem(data.Chestplate)
|
||||
d.Helmet = encodeItem(data.Helmet)
|
||||
return d
|
||||
}
|
||||
|
||||
func dataToInv(data jsonInventoryData) InventoryData {
|
||||
d := InventoryData{
|
||||
MainHandSlot: data.MainHandSlot,
|
||||
OffHand: decodeItem(data.OffHand),
|
||||
Items: make([]item.Stack, 36),
|
||||
}
|
||||
decodeItems(data.Items, d.Items)
|
||||
d.Boots = decodeItem(data.Boots)
|
||||
d.Leggings = decodeItem(data.Leggings)
|
||||
d.Chestplate = decodeItem(data.Chestplate)
|
||||
d.Helmet = decodeItem(data.Helmet)
|
||||
return d
|
||||
}
|
||||
|
||||
func encodeItems(items []item.Stack) (encoded []jsonSlot) {
|
||||
encoded = make([]jsonSlot, 0, len(items))
|
||||
for slot, i := range items {
|
||||
data := encodeItem(i)
|
||||
if data == nil {
|
||||
continue
|
||||
}
|
||||
encoded = append(encoded, jsonSlot{Slot: slot, Item: data})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func decodeItems(encoded []jsonSlot, items []item.Stack) {
|
||||
for _, i := range encoded {
|
||||
items[i.Slot] = decodeItem(i.Item)
|
||||
}
|
||||
}
|
||||
|
||||
func encodeItem(item item.Stack) []byte {
|
||||
if item.Empty() {
|
||||
return nil
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
itemNBT := nbtconv.WriteItem(item, true)
|
||||
encoder := nbt.NewEncoderWithEncoding(&b, nbt.LittleEndian)
|
||||
err := encoder.Encode(itemNBT)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
func decodeItem(data []byte) item.Stack {
|
||||
var itemNBT map[string]any
|
||||
decoder := nbt.NewDecoderWithEncoding(bytes.NewBuffer(data), nbt.LittleEndian)
|
||||
err := decoder.Decode(&itemNBT)
|
||||
if err != nil {
|
||||
return item.Stack{}
|
||||
}
|
||||
return nbtconv.Item(itemNBT, nil)
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package playerdb
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
"github.com/df-mc/dragonfly/server/item"
|
||||
"github.com/df-mc/dragonfly/server/item/inventory"
|
||||
"github.com/df-mc/dragonfly/server/player"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (p *Provider) fromJson(d jsonData, lookupWorld func(world.Dimension) *world.World) (player.Config, *world.World) {
|
||||
dim, _ := world.DimensionByID(int(d.Dimension))
|
||||
mode, _ := world.GameModeByID(int(d.GameMode))
|
||||
conf := player.Config{
|
||||
UUID: uuid.MustParse(d.UUID),
|
||||
XUID: d.XUID,
|
||||
Name: d.Username,
|
||||
Position: d.Position,
|
||||
Rotation: cube.Rotation{d.Yaw, d.Pitch},
|
||||
Velocity: d.Velocity,
|
||||
Health: d.Health,
|
||||
MaxHealth: d.MaxHealth,
|
||||
Food: d.Hunger,
|
||||
FoodTick: d.FoodTick,
|
||||
Exhaustion: d.ExhaustionLevel,
|
||||
Saturation: d.SaturationLevel,
|
||||
Experience: d.Experience,
|
||||
AirSupply: d.AirSupply,
|
||||
MaxAirSupply: d.MaxAirSupply,
|
||||
EnchantmentSeed: d.EnchantmentSeed,
|
||||
GameMode: mode,
|
||||
Effects: dataToEffects(d.Effects),
|
||||
FireTicks: d.FireTicks,
|
||||
FallDistance: d.FallDistance,
|
||||
Inventory: inventory.New(36, nil),
|
||||
EnderChestInventory: inventory.New(27, nil),
|
||||
OffHand: inventory.New(1, nil),
|
||||
Armour: inventory.NewArmour(nil),
|
||||
}
|
||||
echest := make([]item.Stack, 27)
|
||||
decodeItems(d.EnderChestInventory, echest)
|
||||
invData := dataToInv(d.Inventory)
|
||||
|
||||
for slot, stack := range invData.Items {
|
||||
_ = conf.Inventory.SetItem(slot, stack)
|
||||
}
|
||||
_ = conf.OffHand.SetItem(0, invData.OffHand)
|
||||
conf.Armour.Set(invData.Helmet, invData.Chestplate, invData.Leggings, invData.Boots)
|
||||
conf.HeldSlot = int(invData.MainHandSlot)
|
||||
|
||||
for slot, stack := range echest {
|
||||
_ = conf.EnderChestInventory.SetItem(slot, stack)
|
||||
}
|
||||
return conf, lookupWorld(dim)
|
||||
}
|
||||
|
||||
func (p *Provider) toJson(d player.Config, w *world.World) jsonData {
|
||||
dim, _ := world.DimensionID(w.Dimension())
|
||||
mode, _ := world.GameModeID(d.GameMode)
|
||||
offHand, _ := d.OffHand.Item(0)
|
||||
return jsonData{
|
||||
UUID: d.UUID.String(),
|
||||
Username: d.Name,
|
||||
Position: d.Position,
|
||||
Velocity: d.Velocity,
|
||||
Yaw: d.Rotation.Yaw(),
|
||||
Pitch: d.Rotation.Pitch(),
|
||||
Health: d.Health,
|
||||
MaxHealth: d.MaxHealth,
|
||||
Hunger: d.Food,
|
||||
FoodTick: d.FoodTick,
|
||||
ExhaustionLevel: d.Exhaustion,
|
||||
SaturationLevel: d.Saturation,
|
||||
Experience: d.Experience,
|
||||
AirSupply: d.AirSupply,
|
||||
MaxAirSupply: d.MaxAirSupply,
|
||||
EnchantmentSeed: d.EnchantmentSeed,
|
||||
GameMode: uint8(mode),
|
||||
Effects: effectsToData(d.Effects),
|
||||
FireTicks: d.FireTicks,
|
||||
FallDistance: d.FallDistance,
|
||||
Inventory: invToData(InventoryData{
|
||||
Items: d.Inventory.Slots(),
|
||||
Boots: d.Armour.Boots(),
|
||||
Leggings: d.Armour.Leggings(),
|
||||
Chestplate: d.Armour.Chestplate(),
|
||||
Helmet: d.Armour.Helmet(),
|
||||
OffHand: offHand,
|
||||
MainHandSlot: uint32(d.HeldSlot),
|
||||
}),
|
||||
EnderChestInventory: encodeItems(d.EnderChestInventory.Slots()),
|
||||
Dimension: uint8(dim),
|
||||
}
|
||||
}
|
||||
|
||||
type jsonData struct {
|
||||
UUID string
|
||||
XUID string
|
||||
Username string
|
||||
Position, Velocity mgl64.Vec3
|
||||
Yaw, Pitch float64
|
||||
Health, MaxHealth float64
|
||||
Hunger int
|
||||
FoodTick int
|
||||
ExhaustionLevel, SaturationLevel float64
|
||||
EnchantmentSeed int64
|
||||
Experience int
|
||||
AirSupply, MaxAirSupply int
|
||||
GameMode uint8
|
||||
Inventory jsonInventoryData
|
||||
EnderChestInventory []jsonSlot
|
||||
Effects []jsonEffect
|
||||
FireTicks int64
|
||||
FallDistance float64
|
||||
Dimension uint8
|
||||
}
|
||||
|
||||
type jsonInventoryData struct {
|
||||
Items []jsonSlot
|
||||
Boots []byte
|
||||
Leggings []byte
|
||||
Chestplate []byte
|
||||
Helmet []byte
|
||||
OffHand []byte
|
||||
MainHandSlot uint32
|
||||
}
|
||||
|
||||
type jsonSlot struct {
|
||||
Item []byte
|
||||
Slot int
|
||||
}
|
||||
|
||||
type jsonEffect struct {
|
||||
ID int
|
||||
Level int
|
||||
Duration time.Duration
|
||||
Ambient bool
|
||||
ParticlesHidden bool
|
||||
Infinite bool
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package playerdb
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/df-mc/dragonfly/server/player"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/df-mc/goleveldb/leveldb"
|
||||
"github.com/df-mc/goleveldb/leveldb/opt"
|
||||
"github.com/google/uuid"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Provider is a player data provider that uses a LevelDB database to store data. The data passed on
|
||||
// will first be converted to make sure it can be marshaled into JSON. This JSON (in bytes) will then
|
||||
// be stored in the database under a key that is the byte representation of the player's UUID.
|
||||
type Provider struct {
|
||||
db *leveldb.DB
|
||||
}
|
||||
|
||||
// NewProvider creates a new player data provider that saves and loads data using
|
||||
// a LevelDB database.
|
||||
func NewProvider(path string) (*Provider, error) {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
_ = os.Mkdir(path, 0777)
|
||||
}
|
||||
db, err := leveldb.OpenFile(path, &opt.Options{Compression: opt.SnappyCompression})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Provider{db: db}, nil
|
||||
}
|
||||
|
||||
// Save ...
|
||||
func (p *Provider) Save(id uuid.UUID, d player.Config, w *world.World) error {
|
||||
b, err := json.Marshal(p.toJson(d, w))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.db.Put(id[:], b, nil)
|
||||
}
|
||||
|
||||
// Load ...
|
||||
func (p *Provider) Load(id uuid.UUID, world func(world.Dimension) *world.World) (player.Config, *world.World, error) {
|
||||
b, err := p.db.Get(id[:], nil)
|
||||
if err != nil {
|
||||
return player.Config{}, nil, err
|
||||
}
|
||||
var d jsonData
|
||||
err = json.Unmarshal(b, &d)
|
||||
if err != nil {
|
||||
return player.Config{}, nil, err
|
||||
}
|
||||
conf, w := p.fromJson(d, world)
|
||||
|
||||
return conf, w, nil
|
||||
}
|
||||
|
||||
// Close ...
|
||||
func (p *Provider) Close() error {
|
||||
return p.db.Close()
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/google/uuid"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Provider represents a value that may provide data to a Player value. It usually does the reading and
|
||||
// writing of the player data so that the Player may use it.
|
||||
type Provider interface {
|
||||
// Save is called when the player leaves the server. The Data of the player is passed.
|
||||
Save(uuid uuid.UUID, data Config, w *world.World) error
|
||||
// Load is called when the player joins and passes the UUID of the player.
|
||||
// It expects to the player data, and an error that is nil if the player data could be found. If non-nil, the player
|
||||
// will use default values, and you can use an empty Data struct.
|
||||
Load(uuid uuid.UUID, world func(world.Dimension) *world.World) (Config, *world.World, error)
|
||||
// Closer is used on server close when the server calls Provider.Close() and is used to safely close the Provider.
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// Compile time check to make sure NopProvider implements Provider.
|
||||
var _ Provider = (*NopProvider)(nil)
|
||||
|
||||
// NopProvider is a player data provider that won't store any data and instead always return default values
|
||||
type NopProvider struct{}
|
||||
|
||||
func (NopProvider) Save(uuid.UUID, Config, *world.World) error { return nil }
|
||||
func (NopProvider) Load(uuid.UUID, func(world.Dimension) *world.World) (Config, *world.World, error) {
|
||||
return Config{}, nil, errors.New("")
|
||||
}
|
||||
func (NopProvider) Close() error { return nil }
|
||||
@@ -0,0 +1,105 @@
|
||||
package scoreboard
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Scoreboard represents a scoreboard that may be sent to a player. The scoreboard is shown on the right side
|
||||
// of the player's screen.
|
||||
// Scoreboard implements the io.Writer and io.StringWriter interfaces. fmt.Fprintf and fmt.Fprint may be used
|
||||
// to write formatted text to the scoreboard.
|
||||
type Scoreboard struct {
|
||||
name string
|
||||
lines []string
|
||||
padding bool
|
||||
descending bool
|
||||
}
|
||||
|
||||
// New returns a new scoreboard with the display name passed. Once returned, lines may be added to the
|
||||
// scoreboard to add text to it. The name is formatted according to the rules of fmt.Sprintln.
|
||||
// Changing the scoreboard after sending it to a player will not update the scoreboard of the player
|
||||
// automatically: Player.SendScoreboard() must be called again to update it.
|
||||
func New(name ...any) *Scoreboard {
|
||||
return &Scoreboard{name: strings.TrimSuffix(fmt.Sprintln(name...), "\n"), padding: true}
|
||||
}
|
||||
|
||||
// Name returns the display name of the scoreboard, as passed during the construction of the scoreboard.
|
||||
func (board *Scoreboard) Name() string {
|
||||
return board.name
|
||||
}
|
||||
|
||||
// Write writes a slice of data as text to the scoreboard. Newlines may be written to create a new line on
|
||||
// the scoreboard.
|
||||
func (board *Scoreboard) Write(p []byte) (n int, err error) {
|
||||
return board.WriteString(string(p))
|
||||
}
|
||||
|
||||
// WriteString writes a string of text to the scoreboard. Newlines may be written to create a new line on
|
||||
// the scoreboard.
|
||||
func (board *Scoreboard) WriteString(s string) (n int, err error) {
|
||||
lines := strings.Split(s, "\n")
|
||||
board.lines = append(board.lines, lines...)
|
||||
|
||||
// Scoreboards can have up to 15 lines. (16 including the title.)
|
||||
if len(board.lines) >= 15 {
|
||||
return len(lines), fmt.Errorf("write scoreboard: maximum of 15 lines of text exceeded")
|
||||
}
|
||||
return len(lines), nil
|
||||
}
|
||||
|
||||
// Set changes a specific line in the scoreboard and adds empty lines until this index is reached. Set panics if the
|
||||
// index passed is negative or 15+.
|
||||
func (board *Scoreboard) Set(index int, s string) {
|
||||
if index < 0 || index >= 15 {
|
||||
panic(fmt.Sprintf("index out of range %v", index))
|
||||
}
|
||||
if diff := index - (len(board.lines) - 1); diff > 0 {
|
||||
board.lines = append(board.lines, make([]string, diff)...)
|
||||
}
|
||||
// Remove new lines from the string
|
||||
board.lines[index] = strings.TrimSuffix(strings.TrimSuffix(s, "\n"), "\n")
|
||||
}
|
||||
|
||||
// Remove removes a specific line from the scoreboard. Remove panics if the index passed is negative or 15+.
|
||||
func (board *Scoreboard) Remove(index int) {
|
||||
if index < 0 || index >= 15 {
|
||||
panic(fmt.Sprintf("index out of range %v", index))
|
||||
}
|
||||
board.lines = append(board.lines[:index], board.lines[index+1:]...)
|
||||
}
|
||||
|
||||
// RemovePadding removes the padding of one space that is added to the start of every line.
|
||||
func (board *Scoreboard) RemovePadding() {
|
||||
board.padding = false
|
||||
}
|
||||
|
||||
// Lines returns the data of the Scoreboard as a slice of strings.
|
||||
func (board *Scoreboard) Lines() []string {
|
||||
lines := slices.Clone(board.lines)
|
||||
if board.padding {
|
||||
for i, line := range lines {
|
||||
if len(board.name)-len(line)-2 <= 0 {
|
||||
lines[i] = " " + line + " "
|
||||
continue
|
||||
}
|
||||
lines[i] = " " + line + strings.Repeat(" ", len(board.name)-len(line)-2)
|
||||
}
|
||||
}
|
||||
if board.descending {
|
||||
slices.Reverse(lines)
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
// Descending returns whether the scoreboard is sorted in descending order.
|
||||
func (board *Scoreboard) Descending() bool {
|
||||
return board.descending
|
||||
}
|
||||
|
||||
// SetDescending sets the scoreboard sort order to descending.
|
||||
// When sending the scoreboard to the player, it will reverse the order of the lines to match when it's ascending.
|
||||
func (board *Scoreboard) SetDescending() {
|
||||
board.descending = true
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package skin
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
)
|
||||
|
||||
const (
|
||||
// AnimationHead is an animation that is played over the head part of the skin.
|
||||
AnimationHead AnimationType = iota
|
||||
// AnimationBody32x32 is an animation that is played over the body of a skin with a 32x32(/64) size. This
|
||||
// is the usual animation type for body animations.
|
||||
AnimationBody32x32
|
||||
// AnimationBody128x128 is an animation that is played over a body of a skin with a 128x128 size. This is
|
||||
// the animation type for body animations with high resolution.
|
||||
AnimationBody128x128
|
||||
)
|
||||
|
||||
// AnimationType represents a type of the animation. It is one of the constants above, and specifies to what
|
||||
// part of the body it is assigned.
|
||||
type AnimationType int
|
||||
|
||||
// Animation represents an animation that plays over the skin every so often. It is assigned to a particular
|
||||
// part of the skin, which is represented by one of the constants above.
|
||||
type Animation struct {
|
||||
w, h int
|
||||
aType AnimationType
|
||||
|
||||
// Pix holds skin data for every frame of the animation. This is an RGBA byte slice, meaning that every
|
||||
// first byte is a Red value, the second a Green value, the third a Blue value and the fourth an Alpha
|
||||
// value.
|
||||
Pix []uint8
|
||||
|
||||
// FrameCount is the amount of frames that the animation plays for. Exactly this amount of frames should
|
||||
// be present in the Pix animation data.
|
||||
FrameCount int
|
||||
|
||||
// AnimationExpression is the player's animation expression.
|
||||
AnimationExpression int
|
||||
}
|
||||
|
||||
// NewAnimation returns a new animation using the width and height passed, with the type specifying what part
|
||||
// of the body to display it on.
|
||||
// NewAnimation fills out the Pix field adequately and sets FrameCount to 1 by default.
|
||||
func NewAnimation(width, height int, expression int, animationType AnimationType) Animation {
|
||||
return Animation{
|
||||
w: width,
|
||||
h: height,
|
||||
aType: animationType,
|
||||
Pix: make([]uint8, width*height*4),
|
||||
FrameCount: 1,
|
||||
AnimationExpression: expression,
|
||||
}
|
||||
}
|
||||
|
||||
// Type returns the type of the animation, which is one of the constants above.
|
||||
func (a Animation) Type() AnimationType {
|
||||
return a.aType
|
||||
}
|
||||
|
||||
// ColorModel ...
|
||||
func (a Animation) ColorModel() color.Model {
|
||||
return color.RGBAModel
|
||||
}
|
||||
|
||||
// Bounds ...
|
||||
func (a Animation) Bounds() image.Rectangle {
|
||||
return image.Rectangle{
|
||||
Max: image.Point{X: a.w, Y: a.h},
|
||||
}
|
||||
}
|
||||
|
||||
// At returns the colour at a given position in the animation data, provided the X and Y are within the bounds
|
||||
// of the animation passed during construction.
|
||||
// The concrete type returned by At is a color.RGBA value.
|
||||
func (a Animation) At(x, y int) color.Color {
|
||||
if x < 0 || y < 0 || x >= a.w || y >= a.h {
|
||||
panic("pixel coordinates out of bounds")
|
||||
}
|
||||
offset := x*4 + a.w*y*4
|
||||
return color.RGBA{
|
||||
R: a.Pix[offset],
|
||||
G: a.Pix[offset+1],
|
||||
B: a.Pix[offset+2],
|
||||
A: a.Pix[offset+3],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package skin
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// Cape represents the cape that a skin may additionally have. A skin is of a fixed size (always 32x64 bytes)
|
||||
// and may be either empty or of that size.
|
||||
type Cape struct {
|
||||
w, h int
|
||||
|
||||
// Pix holds the colour data of the cape in an RGBA byte array, similarly to the way that the pixels of
|
||||
// a Skin are stored.
|
||||
// The size of Pix is always 32 * 64 * 4 bytes.
|
||||
Pix []uint8
|
||||
}
|
||||
|
||||
// NewCape initialises a new Cape using the width and height passed. The pixels are pre-allocated so that the
|
||||
// Cape may be used immediately.
|
||||
func NewCape(width, height int) Cape {
|
||||
return Cape{w: width, h: height, Pix: make([]uint8, width*height*4)}
|
||||
}
|
||||
|
||||
// ColorModel ...
|
||||
func (c Cape) ColorModel() color.Model {
|
||||
return color.RGBAModel
|
||||
}
|
||||
|
||||
// Bounds returns the bounds of the cape, which is always 32x64 or 0x0, depending on if the cape has any data
|
||||
// in it.
|
||||
func (c Cape) Bounds() image.Rectangle {
|
||||
return image.Rectangle{
|
||||
Max: image.Point{X: c.w, Y: c.h},
|
||||
}
|
||||
}
|
||||
|
||||
// At returns the colour at a given position in the cape, provided the X and Y are within the bounds of the
|
||||
// cape passed during construction.
|
||||
// The concrete type returned by At is a color.RGBA value.
|
||||
func (c Cape) At(x, y int) color.Color {
|
||||
if x < 0 || y < 0 || x >= c.w || y >= c.h {
|
||||
panic("pixel coordinates out of bounds")
|
||||
}
|
||||
offset := x*4 + c.w*y*4
|
||||
return color.RGBA{
|
||||
R: c.Pix[offset],
|
||||
G: c.Pix[offset+1],
|
||||
B: c.Pix[offset+2],
|
||||
A: c.Pix[offset+3],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package skin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// ModelConfig specifies the way that the model (geometry data) is used to form the complete skin. It does
|
||||
// this by setting model names for specific keys found in the struct.
|
||||
type ModelConfig struct {
|
||||
// Default is the 'default' model to use. This model is essentially the model of the skin that will be
|
||||
// used at all times, when nothing special is being done. (For example, an animation)
|
||||
// The field holds the name of one of the models present in the JSON of the skin's model.
|
||||
// This field should always be filled out.
|
||||
Default string `json:"default"`
|
||||
// AnimatedFace is the model of an animation played over the face. This field should be set if the model
|
||||
// contains the model of an animation, in which case this field should hold the name of that model.
|
||||
AnimatedFace string `json:"animated_face,omitempty"`
|
||||
}
|
||||
|
||||
// modelConfigContainer is a container of the model config data when encoded.
|
||||
type modelConfigContainer struct {
|
||||
Geometry ModelConfig `json:"geometry"`
|
||||
}
|
||||
|
||||
// Encode encodes a ModelConfig into its JSON representation.
|
||||
func (cfg ModelConfig) Encode() []byte {
|
||||
b, _ := json.Marshal(modelConfigContainer{Geometry: cfg})
|
||||
return b
|
||||
}
|
||||
|
||||
// DecodeModelConfig attempts to decode a ModelConfig from the JSON data passed. If not successful, an error
|
||||
// is returned.
|
||||
func DecodeModelConfig(b []byte) (ModelConfig, error) {
|
||||
var m modelConfigContainer
|
||||
err := json.Unmarshal(b, &m)
|
||||
return m.Geometry, err
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package skin
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// Skin holds the data of a skin that a player has equipped. It includes geometry data, the texture and the
|
||||
// cape, if one is present.
|
||||
// Skin implements the image.Image interface to ease working with the value as an image.
|
||||
type Skin struct {
|
||||
w, h int
|
||||
// Persona specifies if the skin uses the persona skin system.
|
||||
Persona bool
|
||||
PlayFabID string
|
||||
FullID string
|
||||
|
||||
// Pix holds the raw pixel data of the skin. This is an RGBA byte slice, meaning that every first byte is
|
||||
// a Red value, the second a Green value, the third a Blue value and the fourth an Alpha value.
|
||||
Pix []uint8
|
||||
|
||||
// ModelConfig specifies how the Model field below should be used to form the total skin.
|
||||
ModelConfig ModelConfig
|
||||
// Model holds the raw JSON data that represents the model of the skin. If empty, it means the skin holds
|
||||
// the standard skin data (geometry.humanoid).
|
||||
// TODO: Write a full API for this. The model should be able to be easily modified or created runtime.
|
||||
Model []byte
|
||||
|
||||
// Cape holds the cape of the skin. By default, an empty cape is set in the skin. Cape.Exists() may be
|
||||
// called to check if the cape actually has any data.
|
||||
Cape Cape
|
||||
|
||||
// Animations holds a list of all animations that the skin has. These animations must be pointed to in the
|
||||
// ModelConfig, in order to display them on the skin.
|
||||
Animations []Animation
|
||||
}
|
||||
|
||||
// New creates a new skin using the width and height passed. The dimensions passed must be either 64x32,
|
||||
// 64x64 or 128x128. An error is returned if other dimensions are used.
|
||||
// The skin pixels are initialised for the skin, and a random skin ID is picked. The model name and model is
|
||||
// left empty.
|
||||
func New(width, height int) Skin {
|
||||
return Skin{
|
||||
w: width,
|
||||
h: height,
|
||||
Pix: make([]uint8, width*height*4),
|
||||
}
|
||||
}
|
||||
|
||||
// Bounds returns the bounds of the skin. These are either 64x32, 64x64 or 128, depending on the bounds of the
|
||||
// skin of the player.
|
||||
func (s Skin) Bounds() image.Rectangle {
|
||||
return image.Rectangle{
|
||||
Max: image.Point{X: s.w, Y: s.h},
|
||||
}
|
||||
}
|
||||
|
||||
// ColorModel returns color.RGBAModel.
|
||||
func (s Skin) ColorModel() color.Model {
|
||||
return color.RGBAModel
|
||||
}
|
||||
|
||||
// At returns the colour at a given position in the skin. The concrete value of the colour returned is a color.RGBA
|
||||
// value.
|
||||
// If the x or y values exceed the bounds of the skin, At will panic.
|
||||
func (s Skin) At(x, y int) color.Color {
|
||||
if x < 0 || y < 0 || x >= s.w || y >= s.h {
|
||||
panic("pixel coordinates out of bounds")
|
||||
}
|
||||
offset := x*4 + s.w*y*4
|
||||
return color.RGBA{
|
||||
R: s.Pix[offset],
|
||||
G: s.Pix[offset+1],
|
||||
B: s.Pix[offset+2],
|
||||
A: s.Pix[offset+3],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package title
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Title represents a title that may be sent to the player. The title will show up as large text in the middle
|
||||
// of the screen, with optional subtitle and action text.
|
||||
type Title struct {
|
||||
text, subtitle, actionText string
|
||||
fadeInDuration, fadeOutDuration, duration time.Duration
|
||||
}
|
||||
|
||||
// New returns a new title using the text passed. The text is formatted according to the formatting rules of
|
||||
// fmt.Sprintln, but with no newline at the end.
|
||||
// The title has default durations set, which will generally suffice.
|
||||
func New(text ...any) Title {
|
||||
return Title{
|
||||
text: format(text),
|
||||
fadeInDuration: time.Second / 20,
|
||||
fadeOutDuration: time.Second / 20,
|
||||
duration: time.Second * 2,
|
||||
}
|
||||
}
|
||||
|
||||
// Text returns the text of the title, as passed to New when created.
|
||||
func (title Title) Text() string {
|
||||
return title.text
|
||||
}
|
||||
|
||||
// WithSubtitle sets the subtitle of the title. The text passed will be formatted according to the formatting
|
||||
// rules of fmt.Sprintln, but without the newline.
|
||||
// The subtitle is shown under the title in a somewhat smaller font.
|
||||
// The new Title with the subtitle is returned.
|
||||
func (title Title) WithSubtitle(text ...any) Title {
|
||||
title.subtitle = format(text)
|
||||
return title
|
||||
}
|
||||
|
||||
// Subtitle returns the subtitle of the title, as passed to SetSubtitle. Subtitle returns an empty string if
|
||||
// no subtitle was previously set.
|
||||
func (title Title) Subtitle() string {
|
||||
return title.subtitle
|
||||
}
|
||||
|
||||
// WithActionText sets the action text of the title. This text is roughly the same as sending a tip/popup, but
|
||||
// will synchronise with the title.
|
||||
// SetActionText will format the text passed using the formatting rules of fmt.Sprintln, but without newline.
|
||||
// The new Title with the action text is returned.
|
||||
func (title Title) WithActionText(text ...any) Title {
|
||||
title.actionText = format(text)
|
||||
return title
|
||||
}
|
||||
|
||||
// ActionText returns the action text added to the title. This text is roughly the same as sending a tip, but
|
||||
// will synchronise with the title. By default, the action text is empty.
|
||||
func (title Title) ActionText() string {
|
||||
return title.actionText
|
||||
}
|
||||
|
||||
// Duration returns the duration that the title will be visible for, without fading in or out. By default,
|
||||
// this is two seconds.
|
||||
func (title Title) Duration() time.Duration {
|
||||
return title.duration
|
||||
}
|
||||
|
||||
// WithDuration sets the duration that the title will be visible for without fading in or fading out.
|
||||
// The new Title with the duration is returned.
|
||||
func (title Title) WithDuration(d time.Duration) Title {
|
||||
title.duration = d
|
||||
return title
|
||||
}
|
||||
|
||||
// WithFadeInDuration sets the duration that the title takes to fade in on the screen.
|
||||
// The new Title with the fade-in duration is returned.
|
||||
func (title Title) WithFadeInDuration(d time.Duration) Title {
|
||||
title.fadeInDuration = d
|
||||
return title
|
||||
}
|
||||
|
||||
// FadeInDuration returns the duration that the fade-in of the title takes. By default, this is a quarter of
|
||||
// a second.
|
||||
func (title Title) FadeInDuration() time.Duration {
|
||||
return title.fadeInDuration
|
||||
}
|
||||
|
||||
// WithFadeOutDuration sets the duration that the title takes to fade out of the screen.
|
||||
// The new Title with the fade-out duration is returned.
|
||||
func (title Title) WithFadeOutDuration(d time.Duration) Title {
|
||||
title.fadeOutDuration = d
|
||||
return title
|
||||
}
|
||||
|
||||
// FadeOutDuration returns the duration that the fade-out of the title takes.By default, this is a quarter of
|
||||
// a second.
|
||||
func (title Title) FadeOutDuration() time.Duration {
|
||||
return title.fadeOutDuration
|
||||
}
|
||||
|
||||
// format is a utility function to format a list of values to have spaces between them, but no newline at the
|
||||
// end.
|
||||
func format(a []any) string {
|
||||
return strings.TrimSuffix(strings.TrimSuffix(fmt.Sprintln(a...), "\n"), "\n")
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
"github.com/df-mc/dragonfly/server/item"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
)
|
||||
|
||||
// Type is a world.EntityType implementation for Player.
|
||||
var Type ptype
|
||||
|
||||
type ptype struct{}
|
||||
|
||||
func (t ptype) Open(tx *world.Tx, handle *world.EntityHandle, data *world.EntityData) world.Entity {
|
||||
pd := data.Data.(*playerData)
|
||||
p := &Player{
|
||||
tx: tx,
|
||||
handle: handle,
|
||||
data: data,
|
||||
playerData: pd,
|
||||
}
|
||||
|
||||
pd.offHand.SlotValidatorFunc(func(s item.Stack, _ int) bool {
|
||||
if s.Empty() {
|
||||
return true
|
||||
}
|
||||
it, allowedInOffhand := s.Item().(item.OffHand)
|
||||
return allowedInOffhand && it.OffHand()
|
||||
})
|
||||
|
||||
if pd.s != nil {
|
||||
pd.s.HandleInventories(tx, p, pd.inv, pd.offHand, pd.enderChest, pd.ui, pd.armour, pd.heldSlot)
|
||||
} else {
|
||||
pd.inv.SlotFunc(func(slot int, before, after item.Stack) {
|
||||
if slot == int(*p.heldSlot) {
|
||||
p.broadcastItems(slot, before, after)
|
||||
}
|
||||
})
|
||||
pd.offHand.SlotFunc(p.broadcastItems)
|
||||
pd.armour.Inventory().SlotFunc(p.broadcastArmour)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (ptype) EncodeEntity() string { return "minecraft:player" }
|
||||
func (ptype) NetworkOffset() float64 { return 1.621 }
|
||||
func (ptype) BBox(e world.Entity) cube.BBox {
|
||||
p := e.(*Player)
|
||||
s := p.Scale()
|
||||
_, sleeping := p.Sleeping()
|
||||
switch {
|
||||
case sleeping:
|
||||
return cube.Box(-0.1*s, 0, -0.1*s, 0.1*s, 0.2*s, 0.1*s)
|
||||
case p.Gliding(), p.Swimming(), p.Crawling():
|
||||
return cube.Box(-0.3*s, 0, -0.3*s, 0.3*s, 0.6*s, 0.3*s)
|
||||
case p.Sneaking():
|
||||
return cube.Box(-0.3*s, 0, -0.3*s, 0.3*s, 1.49*s, 0.3*s)
|
||||
default:
|
||||
return cube.Box(-0.3*s, 0, -0.3*s, 0.3*s, 1.8*s, 0.3*s)
|
||||
}
|
||||
}
|
||||
func (t ptype) DecodeNBT(map[string]any, *world.EntityData) {}
|
||||
func (t ptype) EncodeNBT(*world.EntityData) map[string]any { return nil }
|
||||
Reference in New Issue
Block a user