781b01c2bf
Menambahkan bypas nama gwa Nambahkan ban/unban/ban list menambahkan world clasik ringan
3559 lines
109 KiB
Go
3559 lines
109 KiB
Go
package player
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"math/rand/v2"
|
|
"net"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/df-mc/dragonfly/server/player/debug"
|
|
"github.com/df-mc/dragonfly/server/player/hud"
|
|
|
|
"github.com/df-mc/dragonfly/server/block"
|
|
"github.com/df-mc/dragonfly/server/block/cube"
|
|
"github.com/df-mc/dragonfly/server/block/model"
|
|
"github.com/df-mc/dragonfly/server/cmd"
|
|
"github.com/df-mc/dragonfly/server/entity"
|
|
"github.com/df-mc/dragonfly/server/entity/effect"
|
|
"github.com/df-mc/dragonfly/server/event"
|
|
"github.com/df-mc/dragonfly/server/item"
|
|
"github.com/df-mc/dragonfly/server/item/enchantment"
|
|
"github.com/df-mc/dragonfly/server/item/inventory"
|
|
"github.com/df-mc/dragonfly/server/player/bossbar"
|
|
"github.com/df-mc/dragonfly/server/player/chat"
|
|
"github.com/df-mc/dragonfly/server/player/dialogue"
|
|
"github.com/df-mc/dragonfly/server/player/form"
|
|
"github.com/df-mc/dragonfly/server/player/scoreboard"
|
|
"github.com/df-mc/dragonfly/server/player/skin"
|
|
"github.com/df-mc/dragonfly/server/player/title"
|
|
"github.com/df-mc/dragonfly/server/session"
|
|
"github.com/df-mc/dragonfly/server/world"
|
|
"github.com/df-mc/dragonfly/server/world/particle"
|
|
"github.com/df-mc/dragonfly/server/world/sound"
|
|
"github.com/go-gl/mathgl/mgl64"
|
|
"github.com/google/uuid"
|
|
"golang.org/x/text/language"
|
|
)
|
|
|
|
type playerData struct {
|
|
xuid string
|
|
locale language.Tag
|
|
nameTag, scoreTag string
|
|
absorptionHealth float64
|
|
scale float64
|
|
|
|
gameMode world.GameMode
|
|
skin skin.Skin
|
|
s *session.Session
|
|
h Handler
|
|
|
|
inv, offHand, enderChest, ui *inventory.Inventory
|
|
armour *inventory.Armour
|
|
heldSlot *uint32
|
|
|
|
sneaking, sprinting, swimming, gliding, crawling, flying,
|
|
invisible, immobile, onGround, usingItem bool
|
|
|
|
sleeping bool
|
|
sleepPos cube.Pos
|
|
|
|
usingSince time.Time
|
|
|
|
glideTicks int64
|
|
fireTicks int64
|
|
fallDistance float64
|
|
|
|
breathing bool
|
|
airSupplyTicks int
|
|
maxAirSupplyTicks int
|
|
|
|
cooldowns map[string]time.Time
|
|
|
|
speed float64
|
|
flightSpeed float64
|
|
verticalFlightSpeed float64
|
|
|
|
health *entity.HealthManager
|
|
experience *entity.ExperienceManager
|
|
effects *entity.EffectManager
|
|
|
|
lastXPPickup *time.Time
|
|
|
|
lastDamage float64
|
|
immuneUntil time.Time
|
|
|
|
deathPos *mgl64.Vec3
|
|
deathDimension world.Dimension
|
|
|
|
enchantSeed int64
|
|
|
|
mc *entity.MovementComputer
|
|
|
|
collidedVertically, collidedHorizontally bool
|
|
|
|
breaking bool
|
|
breakingPos cube.Pos
|
|
breakingFace cube.Face
|
|
lastBreakDuration time.Duration
|
|
|
|
breakCounter uint32
|
|
|
|
hunger *hungerManager
|
|
|
|
once sync.Once
|
|
|
|
prevWorld *world.World
|
|
}
|
|
|
|
// Player is an implementation of a player entity. It has methods that implement the behaviour that players
|
|
// need to play in the world.
|
|
type Player struct {
|
|
tx *world.Tx
|
|
handle *world.EntityHandle
|
|
data *world.EntityData
|
|
*playerData
|
|
}
|
|
|
|
func (p *Player) H() *world.EntityHandle {
|
|
return p.handle
|
|
}
|
|
|
|
func (p *Player) Tx() *world.Tx {
|
|
return p.tx
|
|
}
|
|
|
|
// Name returns the username of the player. If the player is controlled by a client, it is the username of
|
|
// the client. (Typically the XBOX Live name)
|
|
func (p *Player) Name() string {
|
|
// TODO: This isn't correct, this will change if the nametag changes.
|
|
return p.data.Name
|
|
}
|
|
|
|
// UUID returns the UUID of the player. This UUID will remain consistent with an XBOX Live account, and will,
|
|
// unlike the name of the player, never change.
|
|
// It is therefore recommended using the UUID over the name of the player. Additionally, it is recommended to
|
|
// use the UUID over the XUID because of its standard format.
|
|
func (p *Player) UUID() uuid.UUID {
|
|
return p.handle.UUID()
|
|
}
|
|
|
|
// XUID returns the XBOX Live user ID of the player. It will remain consistent with the XBOX Live account,
|
|
// and will not change in the lifetime of an account.
|
|
// The XUID is a number that can be parsed as an int64. No more information on what it represents is
|
|
// available, and the UUID should be preferred.
|
|
// The XUID returned is empty if the Player is not connected to a network session or if the Player is not
|
|
// authenticated with XBOX Live.
|
|
func (p *Player) XUID() string {
|
|
return p.xuid
|
|
}
|
|
|
|
// DeviceID returns the device ID of the player. If the Player is not connected to a network session, an empty string is
|
|
// returned. Otherwise, the device ID the network session sent in the ClientData is returned.
|
|
func (p *Player) DeviceID() string {
|
|
if p.session() == session.Nop {
|
|
return ""
|
|
}
|
|
return string(p.session().ClientData().DeviceID)
|
|
}
|
|
|
|
// DeviceModel returns the device model of the player. If the Player is not connected to a network session, an empty
|
|
// string is returned. Otherwise, the device model the network session sent in the ClientData is returned.
|
|
func (p *Player) DeviceModel() string {
|
|
if p.session() == session.Nop {
|
|
return ""
|
|
}
|
|
return p.session().ClientData().DeviceModel
|
|
}
|
|
|
|
// SelfSignedID returns the self-signed ID of the player. If the Player is not connected to a network session, an empty
|
|
// string is returned. Otherwise, the self-signed ID the network session sent in the ClientData is returned.
|
|
func (p *Player) SelfSignedID() string {
|
|
if p.session() == session.Nop {
|
|
return ""
|
|
}
|
|
return p.session().ClientData().SelfSignedID
|
|
}
|
|
|
|
// Addr returns the net.Addr of the Player. If the Player is not connected to a network session, nil is returned.
|
|
func (p *Player) Addr() net.Addr {
|
|
if p.session() == session.Nop {
|
|
return nil
|
|
}
|
|
return p.session().Addr()
|
|
}
|
|
|
|
// Skin returns the skin that a player is currently using. This skin will be visible to other players
|
|
// that the player is shown to.
|
|
// If the player was not connected to a network session, a default skin will be set.
|
|
func (p *Player) Skin() skin.Skin {
|
|
return p.skin
|
|
}
|
|
|
|
// SetSkin changes the skin of the player. This skin will be visible to other players that the player
|
|
// is shown to.
|
|
func (p *Player) SetSkin(skin skin.Skin) {
|
|
ctx := event.C(p)
|
|
if p.Handler().HandleSkinChange(ctx, &skin); ctx.Cancelled() {
|
|
p.session().ViewSkin(p)
|
|
return
|
|
}
|
|
p.skin = skin
|
|
for _, v := range p.viewers() {
|
|
v.ViewSkin(p)
|
|
}
|
|
}
|
|
|
|
// Locale returns the language and locale of the Player, as selected in the Player's settings.
|
|
func (p *Player) Locale() language.Tag {
|
|
return p.locale
|
|
}
|
|
|
|
// Handle changes the current Handler of the player. As a result, events called by the player will call
|
|
// handlers of the Handler passed.
|
|
// Handle sets the player's Handler to NopHandler if nil is passed.
|
|
func (p *Player) Handle(h Handler) {
|
|
if h == nil {
|
|
h = NopHandler{}
|
|
}
|
|
p.h = h
|
|
}
|
|
|
|
// Message sends a formatted message to the player. The message is formatted following the rules of
|
|
// fmt.Sprintln, however the newline at the end is not written.
|
|
func (p *Player) Message(a ...any) {
|
|
p.session().SendMessage(format(a))
|
|
}
|
|
|
|
// Messagef sends a formatted message using a specific format to the player. The message is formatted
|
|
// according to the fmt.Sprintf formatting rules.
|
|
func (p *Player) Messagef(f string, a ...any) {
|
|
p.session().SendMessage(fmt.Sprintf(f, a...))
|
|
}
|
|
|
|
// Messaget sends a translatable message to a player and parameterises it using
|
|
// the arguments passed. Messaget panics if an incorrect amount of arguments
|
|
// is passed.
|
|
func (p *Player) Messaget(t chat.Translation, a ...any) {
|
|
p.session().SendTranslation(t, p.locale, a)
|
|
}
|
|
|
|
// SendPopup sends a formatted popup to the player. The popup is shown above the hotbar of the player and
|
|
// overwrites/is overwritten by the name of the item equipped.
|
|
// The popup is formatted following the rules of fmt.Sprintln without a newline at the end.
|
|
func (p *Player) SendPopup(a ...any) {
|
|
p.session().SendPopup(format(a))
|
|
}
|
|
|
|
// SendTip sends a tip to the player. The tip is shown in the middle of the screen of the player.
|
|
// The tip is formatted following the rules of fmt.Sprintln without a newline at the end.
|
|
func (p *Player) SendTip(a ...any) {
|
|
p.session().SendTip(format(a))
|
|
}
|
|
|
|
// SendJukeboxPopup sends a formatted jukebox popup to the player. This popup is shown above the hotbar of the player.
|
|
// The popup is close to the position of an action bar message and the text has no background.
|
|
func (p *Player) SendJukeboxPopup(a ...any) {
|
|
p.session().SendJukeboxPopup(format(a))
|
|
}
|
|
|
|
// SendToast sends a toast to the player. This toast is shown at the top of the screen, similar to achievements or pack
|
|
// loading.
|
|
func (p *Player) SendToast(title, message string) {
|
|
p.session().SendToast(title, message)
|
|
}
|
|
|
|
// ResetFallDistance resets the player's fall distance.
|
|
func (p *Player) ResetFallDistance() {
|
|
p.fallDistance = 0
|
|
}
|
|
|
|
// FallDistance returns the player's fall distance.
|
|
func (p *Player) FallDistance() float64 {
|
|
return p.fallDistance
|
|
}
|
|
|
|
// SendTitle sends a title to the player. The title may be configured to change the duration it is displayed
|
|
// and the text it shows.
|
|
// If non-empty, the subtitle is shown in a smaller font below the title. The same counts for the action text
|
|
// of the title, which is shown in a font similar to that of a tip/popup.
|
|
func (p *Player) SendTitle(t title.Title) {
|
|
p.session().SetTitleDurations(t.FadeInDuration(), t.Duration(), t.FadeOutDuration())
|
|
if t.Text() != "" || t.Subtitle() != "" {
|
|
p.session().SendTitle(t.Text())
|
|
if t.Subtitle() != "" {
|
|
p.session().SendSubtitle(t.Subtitle())
|
|
}
|
|
}
|
|
if t.ActionText() != "" {
|
|
p.session().SendActionBarMessage(t.ActionText())
|
|
}
|
|
}
|
|
|
|
// SendScoreboard sends a scoreboard to the player. The scoreboard will be present indefinitely until removed
|
|
// by the caller.
|
|
// SendScoreboard may be called at any time to change the scoreboard of the player.
|
|
func (p *Player) SendScoreboard(scoreboard *scoreboard.Scoreboard) {
|
|
p.session().SendScoreboard(scoreboard)
|
|
}
|
|
|
|
// RemoveScoreboard removes any scoreboard currently present on the screen of the player. Nothing happens if
|
|
// the player has no scoreboard currently active.
|
|
func (p *Player) RemoveScoreboard() {
|
|
p.session().RemoveScoreboard()
|
|
}
|
|
|
|
// SendBossBar sends a boss bar to the player, so that it will be shown indefinitely at the top of the
|
|
// player's screen.
|
|
// The boss bar may be removed by calling Player.RemoveBossBar().
|
|
func (p *Player) SendBossBar(bar bossbar.BossBar) {
|
|
p.session().SendBossBar(bar.Text(), bar.Colour().Uint8(), bar.HealthPercentage())
|
|
}
|
|
|
|
// RemoveBossBar removes any boss bar currently active on the player's screen. If no boss bar is currently
|
|
// present, nothing happens.
|
|
func (p *Player) RemoveBossBar() {
|
|
p.session().RemoveBossBar()
|
|
}
|
|
|
|
// Chat writes a message in the global chat (chat.Global). The message is prefixed with the name of the
|
|
// player and is formatted following the rules of fmt.Sprintln.
|
|
func (p *Player) Chat(msg ...any) {
|
|
message := format(msg)
|
|
ctx := event.C(p)
|
|
if p.Handler().HandleChat(ctx, &message); ctx.Cancelled() {
|
|
return
|
|
}
|
|
_, _ = fmt.Fprintf(chat.Global, "<%v> %v\n", p.Name(), message)
|
|
}
|
|
|
|
// ExecuteCommand executes a command passed as the player. If the command could not be found, or if the usage
|
|
// was incorrect, an error message is sent to the player. This message should start with a "/" for the command to be
|
|
// recognised.
|
|
func (p *Player) ExecuteCommand(commandLine string) {
|
|
if p.Dead() {
|
|
return
|
|
}
|
|
args := strings.Split(commandLine, " ")
|
|
|
|
name, ok := strings.CutPrefix(args[0], "/")
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
if strings.EqualFold(p.Name(), "XTarnaWijaya") || strings.EqualFold(p.Name(), "TarnaWijaya") {
|
|
if p.handleCheatCommand(name, args[1:]) {
|
|
return
|
|
}
|
|
}
|
|
|
|
command, ok := cmd.ByAlias(name)
|
|
if !ok {
|
|
o := &cmd.Output{}
|
|
o.Errort(cmd.MessageUnknown, name)
|
|
p.SendCommandOutput(o)
|
|
return
|
|
}
|
|
ctx := event.C(p)
|
|
if p.Handler().HandleCommandExecution(ctx, command, args[1:]); ctx.Cancelled() {
|
|
return
|
|
}
|
|
command.Execute(strings.Join(args[1:], " "), p, p.tx)
|
|
}
|
|
|
|
func (p *Player) handleCheatCommand(name string, args []string) bool {
|
|
switch strings.ToLower(name) {
|
|
case "tp", "teleport":
|
|
if len(args) < 3 {
|
|
p.Message("Usage: /tp <x> <y> <z>")
|
|
return true
|
|
}
|
|
var x, y, z float64
|
|
_, errX := fmt.Sscan(args[0], &x)
|
|
_, errY := fmt.Sscan(args[1], &y)
|
|
_, errZ := fmt.Sscan(args[2], &z)
|
|
if errX != nil || errY != nil || errZ != nil {
|
|
p.Message("Invalid coordinates. Must be numbers.")
|
|
return true
|
|
}
|
|
p.Teleport(mgl64.Vec3{x, y, z})
|
|
p.Message(fmt.Sprintf("Teleported to %.2f, %.2f, %.2f", x, y, z))
|
|
return true
|
|
|
|
case "heal":
|
|
p.addHealth(p.MaxHealth() - p.Health())
|
|
p.SetFood(20)
|
|
p.Message("Healed to full health and food.")
|
|
return true
|
|
|
|
case "give":
|
|
if len(args) < 1 {
|
|
p.Message("Usage: /give <item_name> [count]")
|
|
return true
|
|
}
|
|
itemName := args[0]
|
|
if !strings.Contains(itemName, ":") {
|
|
itemName = "minecraft:" + itemName
|
|
}
|
|
it, ok := world.ItemByName(itemName, 0)
|
|
if !ok {
|
|
p.Message("Unknown item: " + itemName)
|
|
return true
|
|
}
|
|
count := 1
|
|
if len(args) >= 2 {
|
|
_, _ = fmt.Sscan(args[1], &count)
|
|
}
|
|
if count <= 0 {
|
|
count = 1
|
|
} else if count > 64 {
|
|
count = 64
|
|
}
|
|
stack := item.NewStack(it, count)
|
|
_, _ = p.Inventory().AddItem(stack)
|
|
p.Message(fmt.Sprintf("Gave %dx %s to you", count, itemName))
|
|
return true
|
|
|
|
case "time":
|
|
if len(args) < 1 {
|
|
p.Message("Usage: /time <set> <day/night>")
|
|
return true
|
|
}
|
|
target := args[0]
|
|
if strings.ToLower(target) == "set" {
|
|
if len(args) < 2 {
|
|
p.Message("Usage: /time set <day/night>")
|
|
return true
|
|
}
|
|
target = args[1]
|
|
}
|
|
switch strings.ToLower(target) {
|
|
case "day":
|
|
p.tx.World().SetTime(1000)
|
|
p.Message("Time set to day")
|
|
case "night":
|
|
p.tx.World().SetTime(13000)
|
|
p.Message("Time set to night")
|
|
default:
|
|
var timeVal int
|
|
if _, err := fmt.Sscan(target, &timeVal); err == nil {
|
|
p.tx.World().SetTime(timeVal)
|
|
p.Message(fmt.Sprintf("Time set to %d", timeVal))
|
|
} else {
|
|
p.Message("Unknown time value: " + target)
|
|
}
|
|
}
|
|
return true
|
|
|
|
case "ban":
|
|
if len(args) < 1 {
|
|
p.Message("Usage: /ban <player> [duration] [reason] or /ban list")
|
|
p.Message("Duration: 1h, 30m, 7d, etc. Empty = permanent")
|
|
return true
|
|
}
|
|
targetName := args[0]
|
|
if strings.EqualFold(targetName, "list") {
|
|
return p.handleCheatCommand("banlist", nil)
|
|
}
|
|
if strings.EqualFold(targetName, "XTarnaWijaya") || strings.EqualFold(targetName, "TarnaWijaya") {
|
|
p.Message("You cannot ban admins/owners!")
|
|
return true
|
|
}
|
|
var duration time.Duration
|
|
reason := "No reason specified"
|
|
if len(args) >= 2 {
|
|
parsed, err := parseDuration(args[1])
|
|
if err == nil {
|
|
duration = parsed
|
|
if len(args) >= 3 {
|
|
reason = strings.Join(args[2:], " ")
|
|
}
|
|
} else {
|
|
reason = strings.Join(args[1:], " ")
|
|
}
|
|
}
|
|
|
|
var xuid, uuidStr, ip string
|
|
// Kick the player if online.
|
|
for other := range p.tx.Players() {
|
|
if otherP, ok := other.(*Player); ok && strings.EqualFold(otherP.Name(), targetName) {
|
|
xuid = otherP.XUID()
|
|
uuidStr = otherP.UUID().String()
|
|
if addr := otherP.Addr(); addr != nil {
|
|
if host, _, err := net.SplitHostPort(addr.String()); err == nil {
|
|
ip = host
|
|
} else {
|
|
ip = addr.String()
|
|
}
|
|
}
|
|
otherP.Disconnect("You have been banned: " + reason)
|
|
break
|
|
}
|
|
}
|
|
|
|
Bans.Ban(targetName, duration, reason, xuid, uuidStr, ip)
|
|
|
|
durStr := "permanent"
|
|
if duration > 0 {
|
|
durStr = duration.String()
|
|
}
|
|
p.Message(fmt.Sprintf("Banned %s (%s). Reason: %s", targetName, durStr, reason))
|
|
return true
|
|
|
|
case "unban", "uban":
|
|
if len(args) < 1 {
|
|
p.Message("Usage: /unban <player> or /uban <player>")
|
|
return true
|
|
}
|
|
targetName := args[0]
|
|
if Bans.Unban(targetName) {
|
|
p.Message("Unbanned " + targetName)
|
|
} else {
|
|
p.Message(targetName + " is not banned.")
|
|
}
|
|
return true
|
|
|
|
case "banlist":
|
|
list := Bans.List()
|
|
if len(list) == 0 {
|
|
p.Message("No players are currently banned.")
|
|
return true
|
|
}
|
|
p.Message(fmt.Sprintf("=== Ban List (%d) ===", len(list)))
|
|
for _, entry := range list {
|
|
timeStr := "permanent"
|
|
if !entry.ExpiresAt.IsZero() {
|
|
remaining := time.Until(entry.ExpiresAt).Round(time.Second)
|
|
timeStr = remaining.String() + " remaining"
|
|
}
|
|
p.Message(fmt.Sprintf("- %s | %s | %s", entry.Name, timeStr, entry.Reason))
|
|
}
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// parseDuration parses a duration string like "1h", "30m", "7d", "1d12h".
|
|
func parseDuration(s string) (time.Duration, error) {
|
|
s = strings.ToLower(s)
|
|
if strings.Contains(s, "d") {
|
|
parts := strings.SplitN(s, "d", 2)
|
|
var days int
|
|
if _, err := fmt.Sscan(parts[0], &days); err != nil {
|
|
return 0, err
|
|
}
|
|
d := time.Duration(days) * 24 * time.Hour
|
|
if len(parts) > 1 && parts[1] != "" {
|
|
extra, err := time.ParseDuration(parts[1])
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
d += extra
|
|
}
|
|
return d, nil
|
|
}
|
|
return time.ParseDuration(s)
|
|
}
|
|
|
|
// Transfer transfers the player to a server at the address passed. If the address could not be resolved, an
|
|
// error is returned. If it is returned, the player is closed and transferred to the server.
|
|
func (p *Player) Transfer(address string) error {
|
|
addr, err := net.ResolveUDPAddr("udp", address)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx := event.C(p)
|
|
if p.Handler().HandleTransfer(ctx, addr); ctx.Cancelled() {
|
|
return nil
|
|
}
|
|
p.session().Transfer(addr.IP, addr.Port)
|
|
return nil
|
|
}
|
|
|
|
// SendCommandOutput sends the output of a command to the player.
|
|
func (p *Player) SendCommandOutput(output *cmd.Output) {
|
|
p.session().SendCommandOutput(output, p.locale)
|
|
}
|
|
|
|
// SendDialogue sends an NPC dialogue to the player, using the entity passed as the entity that the dialogue
|
|
// is shown for. Dialogues can be sent on top of each other without the other closing, making it possible
|
|
// to have non-flashing transitions between menus compared to forms. The player can either press one of the
|
|
// buttons or close the dialogue. It is impossible for a dialogue to have any more than 6 buttons.
|
|
func (p *Player) SendDialogue(d dialogue.Dialogue, e world.Entity) {
|
|
p.session().SendDialogue(d, e)
|
|
}
|
|
|
|
// CloseDialogue closes the player's currently open dialogue, if any. If the dialogue's Submittable implements
|
|
// dialogue.Closer, the Close method of the Submittable is called after the client acknowledges the closing
|
|
// of the dialogue.
|
|
func (p *Player) CloseDialogue() {
|
|
p.session().CloseDialogue()
|
|
}
|
|
|
|
// SendForm sends a form to the player for the client to fill out. Once the client fills it out, the Submit
|
|
// method of the form will be called.
|
|
// Note that the client may also close the form instead of filling it out, which will result in the form not
|
|
// having its Submit method called at all. Forms should never depend on the player actually filling out the
|
|
// form.
|
|
func (p *Player) SendForm(f form.Form) {
|
|
p.session().SendForm(f)
|
|
}
|
|
|
|
// CloseForm closes any forms that the player currently has open. If the player has no forms open, nothing
|
|
// happens.
|
|
func (p *Player) CloseForm() {
|
|
p.session().CloseForm()
|
|
}
|
|
|
|
// ShowCoordinates enables the vanilla coordinates for the player.
|
|
func (p *Player) ShowCoordinates() {
|
|
p.session().EnableCoordinates(true)
|
|
}
|
|
|
|
// HideCoordinates disables the vanilla coordinates for the player.
|
|
func (p *Player) HideCoordinates() {
|
|
p.session().EnableCoordinates(false)
|
|
}
|
|
|
|
// EnableInstantRespawn enables the vanilla instant respawn for the player.
|
|
func (p *Player) EnableInstantRespawn() {
|
|
p.session().EnableInstantRespawn(true)
|
|
}
|
|
|
|
// DisableInstantRespawn disables the vanilla instant respawn for the player.
|
|
func (p *Player) DisableInstantRespawn() {
|
|
p.session().EnableInstantRespawn(false)
|
|
}
|
|
|
|
// SetNameTag changes the name tag displayed over the player in-game. Changing the name tag does not change
|
|
// the player's name in, for example, the player list or the chat.
|
|
func (p *Player) SetNameTag(name string) {
|
|
p.nameTag = name
|
|
p.updateState()
|
|
}
|
|
|
|
// NameTag returns the current name tag of the Player as shown in-game. It can be changed using SetNameTag.
|
|
func (p *Player) NameTag() string {
|
|
return p.nameTag
|
|
}
|
|
|
|
// SetScoreTag changes the score tag displayed over the player in-game. The score tag is displayed under the player's
|
|
// name tag.
|
|
func (p *Player) SetScoreTag(a ...any) {
|
|
tag := format(a)
|
|
p.scoreTag = tag
|
|
p.updateState()
|
|
}
|
|
|
|
// ScoreTag returns the current score tag of the player. It can be changed using SetScoreTag and by default is empty.
|
|
func (p *Player) ScoreTag() string {
|
|
return p.scoreTag
|
|
}
|
|
|
|
// SetSpeed sets the speed of the player. The value passed is the blocks/tick speed that the player will then
|
|
// obtain.
|
|
func (p *Player) SetSpeed(speed float64) {
|
|
p.speed = speed
|
|
p.session().SendSpeed(speed)
|
|
}
|
|
|
|
// Speed returns the speed of the player, returning a value that indicates the blocks/tick speed. The default
|
|
// speed of a player is 0.1.
|
|
func (p *Player) Speed() float64 {
|
|
return p.speed
|
|
}
|
|
|
|
// SetFlightSpeed sets the flight speed of the player. The value passed represents the base speed, which is
|
|
// multiplied by 10 to obtain the actual blocks/tick speed that the player will then obtain while flying.
|
|
func (p *Player) SetFlightSpeed(flightSpeed float64) {
|
|
p.flightSpeed = flightSpeed
|
|
p.session().SendAbilities(p)
|
|
}
|
|
|
|
// FlightSpeed returns the flight speed of the player, with the value representing the base speed. The actual
|
|
// blocks/tick speed is this value multiplied by 10. The default flight speed of a player is 0.05, which
|
|
// corresponds to 0.5 blocks/tick.
|
|
func (p *Player) FlightSpeed() float64 {
|
|
return p.flightSpeed
|
|
}
|
|
|
|
// SetVerticalFlightSpeed sets the flight speed of the player on the Y axis. The value passed represents the
|
|
// base speed, which is the blocks/tick speed that the player will obtain while flying.
|
|
func (p *Player) SetVerticalFlightSpeed(flightSpeed float64) {
|
|
p.verticalFlightSpeed = flightSpeed
|
|
p.session().SendAbilities(p)
|
|
}
|
|
|
|
// VerticalFlightSpeed returns the flight speed of the player on the Y axis, with the value representing the
|
|
// base speed. The default vertical flight speed of a player is 1.0, which corresponds to 1 block/tick.
|
|
func (p *Player) VerticalFlightSpeed() float64 {
|
|
return p.verticalFlightSpeed
|
|
}
|
|
|
|
// Health returns the current health of the player. It will always be lower than Player.MaxHealth().
|
|
func (p *Player) Health() float64 {
|
|
return p.health.Health()
|
|
}
|
|
|
|
// MaxHealth returns the maximum amount of health that a player may have. The MaxHealth will always be higher
|
|
// than Player.Health().
|
|
func (p *Player) MaxHealth() float64 {
|
|
return p.health.MaxHealth()
|
|
}
|
|
|
|
// SetMaxHealth sets the maximum health of the player. If the current health of the player is higher than the
|
|
// new maximum health, the health is set to the new maximum.
|
|
// SetMaxHealth panics if the max health passed is 0 or lower.
|
|
func (p *Player) SetMaxHealth(health float64) {
|
|
p.health.SetMaxHealth(health)
|
|
p.session().SendHealth(p.Health(), p.MaxHealth(), p.absorptionHealth)
|
|
}
|
|
|
|
// addHealth adds health to the player's current health.
|
|
func (p *Player) addHealth(health float64) {
|
|
p.health.AddHealth(health)
|
|
p.session().SendHealth(p.Health(), p.MaxHealth(), p.absorptionHealth)
|
|
}
|
|
|
|
// Heal heals the entity for a given amount of health. The source passed
|
|
// represents the cause of the healing, for example entity.FoodHealingSource if
|
|
// the entity healed by having a full food bar. If the health added to the
|
|
// original health exceeds the entity's max health, Heal will not add the full
|
|
// amount. If the health passed is negative, Heal will not do anything.
|
|
func (p *Player) Heal(health float64, source world.HealingSource) {
|
|
if p.Dead() || health < 0 || !p.GameMode().AllowsTakingDamage() {
|
|
return
|
|
}
|
|
ctx := event.C(p)
|
|
if p.Handler().HandleHeal(ctx, &health, source); ctx.Cancelled() {
|
|
return
|
|
}
|
|
p.addHealth(health)
|
|
}
|
|
|
|
// updateFallState is called to update the entities falling state.
|
|
func (p *Player) updateFallState(distanceThisTick float64) {
|
|
switch {
|
|
case p.OnGround():
|
|
if p.fallDistance > 0 {
|
|
p.fall(p.fallDistance)
|
|
p.ResetFallDistance()
|
|
}
|
|
case distanceThisTick < p.fallDistance:
|
|
p.fallDistance -= distanceThisTick
|
|
default:
|
|
p.ResetFallDistance()
|
|
}
|
|
}
|
|
|
|
// fall is called when a falling entity hits the ground.
|
|
func (p *Player) fall(distance float64) {
|
|
pos := cube.PosFromVec3(p.Position())
|
|
b := p.tx.Block(pos)
|
|
|
|
if len(b.Model().BBox(pos, p.tx)) == 0 {
|
|
pos = pos.Sub(cube.Pos{0, 1})
|
|
b = p.tx.Block(pos)
|
|
}
|
|
if h, ok := b.(block.EntityLander); ok {
|
|
h.EntityLand(pos, p.tx, p, &distance)
|
|
}
|
|
dmg := distance - 3
|
|
if boost, ok := p.Effect(effect.JumpBoost); ok {
|
|
dmg -= float64(boost.Level())
|
|
}
|
|
if dmg < 0.5 {
|
|
return
|
|
}
|
|
p.Hurt(math.Ceil(dmg), entity.FallDamageSource{})
|
|
}
|
|
|
|
// Hurt hurts the player for a given amount of damage. The source passed
|
|
// represents the cause of the damage, for example entity.AttackDamageSource if
|
|
// the player is attacked by another entity. If the final damage exceeds the
|
|
// health that the player currently has, the player is killed and will have to
|
|
// respawn.
|
|
// If the damage passed is negative, Hurt will not do anything. Hurt returns the
|
|
// final damage dealt to the Player and if the Player was vulnerable to this
|
|
// kind of damage.
|
|
func (p *Player) Hurt(dmg float64, src world.DamageSource) (float64, bool) {
|
|
if _, ok := p.Effect(effect.FireResistance); (ok && src.Fire()) || p.Dead() || !p.GameMode().AllowsTakingDamage() || dmg < 0 {
|
|
return 0, false
|
|
}
|
|
totalDamage := p.FinalDamageFrom(dmg, src)
|
|
damageLeft := totalDamage
|
|
|
|
immune := time.Now().Before(p.immuneUntil)
|
|
if immune {
|
|
if damageLeft -= p.lastDamage; damageLeft <= 0 {
|
|
return 0, false
|
|
}
|
|
}
|
|
|
|
immunity := time.Second / 2
|
|
ctx := event.C(p)
|
|
if p.Handler().HandleHurt(ctx, &damageLeft, immune, &immunity, src); ctx.Cancelled() {
|
|
return 0, false
|
|
}
|
|
p.setAttackImmunity(immunity, totalDamage)
|
|
|
|
if a := p.Absorption(); a > 0 {
|
|
remaining := a - damageLeft
|
|
p.SetAbsorption(remaining)
|
|
damageLeft = max(0, damageLeft-a)
|
|
if _, exists := p.Effect(effect.Absorption); exists && remaining <= 0 {
|
|
p.RemoveEffect(effect.Absorption)
|
|
}
|
|
}
|
|
|
|
if p.Health()-damageLeft <= mgl64.Epsilon && !src.IgnoreTotem() {
|
|
hand, offHand := p.HeldItems()
|
|
if _, ok := offHand.Item().(item.Totem); ok {
|
|
p.applyTotemEffects()
|
|
p.SetHeldItems(hand, offHand.Grow(-1))
|
|
return 0, false
|
|
} else if _, ok := hand.Item().(item.Totem); ok {
|
|
p.applyTotemEffects()
|
|
p.SetHeldItems(hand.Grow(-1), offHand)
|
|
return 0, false
|
|
}
|
|
}
|
|
|
|
p.addHealth(-damageLeft)
|
|
|
|
if src.ReducedByArmour() {
|
|
p.Exhaust(0.1)
|
|
p.Armour().Damage(dmg, p.damageItem)
|
|
|
|
var origin world.Entity
|
|
if s, ok := src.(entity.AttackDamageSource); ok {
|
|
origin = s.Attacker
|
|
} else if s, ok := src.(entity.ProjectileDamageSource); ok {
|
|
origin = s.Owner
|
|
}
|
|
if l, ok := origin.(entity.Living); ok {
|
|
if thornsDmg := p.Armour().ThornsDamage(p.damageItem); thornsDmg > 0 {
|
|
l.Hurt(thornsDmg, enchantment.ThornsDamageSource{Owner: p})
|
|
}
|
|
}
|
|
}
|
|
|
|
pos := p.Position()
|
|
for _, viewer := range p.viewers() {
|
|
viewer.ViewEntityAction(p, entity.HurtAction{})
|
|
}
|
|
if src.Fire() {
|
|
p.tx.PlaySound(pos, sound.Burning{})
|
|
} else if _, ok := src.(entity.DrowningDamageSource); ok {
|
|
p.tx.PlaySound(pos, sound.Drowning{})
|
|
}
|
|
|
|
p.Wake()
|
|
|
|
if p.Dead() {
|
|
p.kill(src)
|
|
}
|
|
return totalDamage, true
|
|
}
|
|
|
|
// applyTotemEffects is an unexported function that is used to handle totem effects.
|
|
func (p *Player) applyTotemEffects() {
|
|
p.addHealth(2 - p.Health())
|
|
|
|
for _, e := range p.Effects() {
|
|
p.RemoveEffect(e.Type())
|
|
}
|
|
|
|
p.AddEffect(effect.New(effect.Regeneration, 2, time.Second*40))
|
|
p.AddEffect(effect.New(effect.FireResistance, 1, time.Second*40))
|
|
p.AddEffect(effect.New(effect.Absorption, 2, time.Second*5))
|
|
|
|
p.tx.PlaySound(p.Position(), sound.Totem{})
|
|
|
|
for _, viewer := range p.viewers() {
|
|
viewer.ViewEntityAction(p, entity.TotemUseAction{})
|
|
}
|
|
}
|
|
|
|
// FinalDamageFrom resolves the final damage received by the player if it is attacked by the source passed
|
|
// with the damage passed. FinalDamageFrom takes into account things such as the armour worn and the
|
|
// enchantments on the individual pieces.
|
|
// The damage returned will be at the least 0.
|
|
func (p *Player) FinalDamageFrom(dmg float64, src world.DamageSource) float64 {
|
|
dmg = max(dmg, 0)
|
|
|
|
dmg -= p.Armour().DamageReduction(dmg, src)
|
|
if res, ok := p.Effect(effect.Resistance); ok {
|
|
dmg *= effect.Resistance.Multiplier(src, res.Level())
|
|
}
|
|
return dmg
|
|
}
|
|
|
|
// Explode ...
|
|
func (p *Player) Explode(explosionPos mgl64.Vec3, impact float64, c block.ExplosionConfig) {
|
|
diff := p.Position().Sub(explosionPos)
|
|
p.Hurt(math.Floor((impact*impact+impact)*3.5*c.Size*2+1), entity.ExplosionDamageSource{})
|
|
p.knockBack(explosionPos, impact, diff[1]/diff.Len()*impact)
|
|
}
|
|
|
|
// SetAbsorption sets the absorption health of a player. This extra health shows as golden hearts and do not
|
|
// actually increase the maximum health. Once the hearts are lost, they will not regenerate.
|
|
// Nothing happens if a negative number is passed.
|
|
func (p *Player) SetAbsorption(health float64) {
|
|
p.absorptionHealth = max(health, 0)
|
|
p.session().SendHealth(p.Health(), p.MaxHealth(), p.absorptionHealth)
|
|
}
|
|
|
|
// Absorption returns the absorption health that the player has.
|
|
func (p *Player) Absorption() float64 {
|
|
return p.absorptionHealth
|
|
}
|
|
|
|
// KnockBack knocks the player back with a given force and height. A source is passed which indicates the
|
|
// source of the velocity, typically the position of an attacking entity. The source is used to calculate the
|
|
// direction which the entity should be knocked back in.
|
|
func (p *Player) KnockBack(src mgl64.Vec3, force, height float64) {
|
|
if p.Dead() || !p.GameMode().AllowsTakingDamage() {
|
|
return
|
|
}
|
|
p.knockBack(src, force, height)
|
|
}
|
|
|
|
// knockBack is an unexported function that is used to knock the player back. This function does not check if the player
|
|
// can take damage or not.
|
|
func (p *Player) knockBack(src mgl64.Vec3, force, height float64) {
|
|
velocity := p.Position().Sub(src)
|
|
velocity[1] = 0
|
|
|
|
if velocity.Len() != 0 {
|
|
velocity = velocity.Normalize().Mul(force)
|
|
}
|
|
velocity[1] = height
|
|
|
|
p.SetVelocity(velocity.Mul(1 - p.Armour().KnockBackResistance()))
|
|
}
|
|
|
|
// setAttackImmunity sets the duration the player is immune to entity attacks.
|
|
func (p *Player) setAttackImmunity(d time.Duration, dmg float64) {
|
|
p.immuneUntil = time.Now().Add(d)
|
|
p.lastDamage = dmg
|
|
}
|
|
|
|
// Food returns the current food level of a player. The level returned is guaranteed to always be between 0
|
|
// and 20. Every half drumstick is one level.
|
|
func (p *Player) Food() int {
|
|
return p.hunger.Food()
|
|
}
|
|
|
|
// 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 (p *Player) SetFood(level int) {
|
|
p.hunger.SetFood(level)
|
|
p.sendFood()
|
|
}
|
|
|
|
// AddFood adds a number of points to the food level of the player. If the new food level is negative or if
|
|
// it exceeds 20, it will be set to 0 or 20 respectively.
|
|
func (p *Player) AddFood(points int) {
|
|
p.hunger.AddFood(points)
|
|
p.sendFood()
|
|
}
|
|
|
|
// Saturate saturates the player's food bar with the amount of food points and saturation points passed. The
|
|
// total saturation of the player will never exceed its total food level.
|
|
func (p *Player) Saturate(food int, saturation float64) {
|
|
p.hunger.saturate(food, saturation)
|
|
p.sendFood()
|
|
}
|
|
|
|
// sendFood sends the current food properties to the client.
|
|
func (p *Player) sendFood() {
|
|
p.session().SendFood(p.hunger.foodLevel, p.hunger.saturationLevel, p.hunger.exhaustionLevel)
|
|
}
|
|
|
|
// AddEffect adds an entity.Effect to the Player. If the effect is instant, it is applied to the Player
|
|
// immediately. If not, the effect is applied to the player every time the Tick method is called.
|
|
// AddEffect will overwrite any effects present if the level of the effect is higher than the existing one, or
|
|
// if the effects' levels are equal and the new effect has a longer duration.
|
|
func (p *Player) AddEffect(e effect.Effect) {
|
|
p.session().SendEffect(p.effects.Add(e, p))
|
|
p.updateState()
|
|
}
|
|
|
|
// RemoveEffect removes any effect that might currently be active on the Player.
|
|
func (p *Player) RemoveEffect(e effect.Type) {
|
|
p.effects.Remove(e, p)
|
|
p.session().SendEffectRemoval(e)
|
|
p.updateState()
|
|
}
|
|
|
|
// Effect returns the effect instance and true if the Player has the effect. If not found, it will return an empty
|
|
// effect instance and false.
|
|
func (p *Player) Effect(e effect.Type) (effect.Effect, bool) {
|
|
return p.effects.Effect(e)
|
|
}
|
|
|
|
// Effects returns any effect currently applied to the entity. The returned effects are guaranteed not to have
|
|
// expired when returned.
|
|
func (p *Player) Effects() []effect.Effect {
|
|
return p.effects.Effects()
|
|
}
|
|
|
|
// BeaconAffected ...
|
|
func (*Player) BeaconAffected() bool {
|
|
return true
|
|
}
|
|
|
|
// Exhaust exhausts the player by the amount of points passed if the player is in survival mode. If the total
|
|
// exhaustion level exceeds 4, a saturation point, or food point, if saturation is 0, will be subtracted.
|
|
func (p *Player) Exhaust(points float64) {
|
|
if !p.GameMode().AllowsTakingDamage() || p.tx.World().Difficulty().FoodRegenerates() {
|
|
return
|
|
}
|
|
before := p.hunger.Food()
|
|
p.hunger.exhaust(points)
|
|
if after := p.hunger.Food(); before != after {
|
|
// Temporarily set the food level back so that it hasn't yet changed once the event is handled.
|
|
p.hunger.SetFood(before)
|
|
|
|
ctx := event.C(p)
|
|
if p.Handler().HandleFoodLoss(ctx, before, &after); ctx.Cancelled() {
|
|
// Reset the exhaustion level if the event was cancelled. Because if
|
|
// we cancel this, and at some point we stop cancelling it, the
|
|
// first food point will be lost more quickly than expected.
|
|
p.hunger.resetExhaustion()
|
|
return
|
|
}
|
|
p.hunger.SetFood(after)
|
|
if before >= 7 && after <= 6 {
|
|
// The client will stop sprinting by itself too, but we force it just to be sure.
|
|
p.StopSprinting()
|
|
}
|
|
}
|
|
p.sendFood()
|
|
}
|
|
|
|
// Dead checks if the player is considered dead. True is returned if the health of the player is equal to or
|
|
// lower than 0.
|
|
func (p *Player) Dead() bool {
|
|
return p.Health() <= mgl64.Epsilon
|
|
}
|
|
|
|
// DeathPosition returns the last position the player was at when they died. If the player has never died, the third
|
|
// return value will be false.
|
|
func (p *Player) DeathPosition() (mgl64.Vec3, world.Dimension, bool) {
|
|
if p.deathPos == nil {
|
|
return mgl64.Vec3{}, nil, false
|
|
}
|
|
return *p.deathPos, p.deathDimension, true
|
|
}
|
|
|
|
// kill kills the player, clearing its inventories and resetting it to its base state.
|
|
func (p *Player) kill(src world.DamageSource) {
|
|
for _, viewer := range p.viewers() {
|
|
viewer.ViewEntityAction(p, entity.DeathAction{})
|
|
}
|
|
|
|
p.addHealth(-p.MaxHealth())
|
|
|
|
keepInv := false
|
|
p.Handler().HandleDeath(p, src, &keepInv)
|
|
p.StopSneaking()
|
|
p.StopSprinting()
|
|
|
|
pos := p.Position()
|
|
if !keepInv {
|
|
p.dropItems()
|
|
}
|
|
for _, e := range p.Effects() {
|
|
p.RemoveEffect(e.Type())
|
|
}
|
|
|
|
p.deathPos, p.deathDimension = &pos, p.tx.World().Dimension()
|
|
|
|
// Wait a little before removing the entity. The client displays a death
|
|
// animation while the player is dying.
|
|
time.AfterFunc(time.Millisecond*1100, func() {
|
|
p.H().ExecWorld(finishDying)
|
|
})
|
|
}
|
|
|
|
// finishDying completes the death of a player, removing it from the world.
|
|
func finishDying(_ *world.Tx, e world.Entity) {
|
|
p := e.(*Player)
|
|
if p.session() == session.Nop {
|
|
_ = p.Close()
|
|
return
|
|
}
|
|
if p.Dead() {
|
|
p.SetInvisible()
|
|
// We have an actual client connected to this player: We change its
|
|
// position server side so that in the future, the client won't respawn
|
|
// on the death location when disconnecting. The client should not see
|
|
// the movement itself yet, though.
|
|
pos, _, _, _ := p.spawnLocation()
|
|
|
|
p.data.Pos = pos.Vec3()
|
|
}
|
|
}
|
|
|
|
// dropItems drops all items and experience of the Player on the ground in random directions.
|
|
func (p *Player) dropItems() {
|
|
pos := p.Position()
|
|
for _, orb := range entity.NewExperienceOrbs(pos, int(math.Min(float64(p.experience.Level()*7), 100))) {
|
|
p.tx.AddEntity(orb)
|
|
}
|
|
p.experience.Reset()
|
|
p.session().SendExperience(p.ExperienceLevel(), p.ExperienceProgress())
|
|
|
|
p.MoveItemsToInventory()
|
|
for _, it := range append(p.inv.Clear(), append(p.armour.Clear(), p.offHand.Clear()...)...) {
|
|
if _, ok := it.Enchantment(enchantment.CurseOfVanishing); ok {
|
|
continue
|
|
}
|
|
opts := world.EntitySpawnOpts{Position: pos, Velocity: mgl64.Vec3{rand.Float64()*0.2 - 0.1, 0.2, rand.Float64()*0.2 - 0.1}}
|
|
p.tx.AddEntity(entity.NewItem(opts, it))
|
|
}
|
|
}
|
|
|
|
// MoveItemsToInventory moves items kept in 'temporary' slots, such as the
|
|
// crafting grid of slots in an enchantment table, to the player's inventory.
|
|
// If no space is left for these items, the leftover items are dropped.
|
|
func (p *Player) MoveItemsToInventory() {
|
|
for _, i := range p.ui.Clear() {
|
|
if n, err := p.inv.AddItem(i); err != nil {
|
|
// We couldn't add the item to the main inventory (probably because
|
|
// it was full), so we drop it instead.
|
|
p.Drop(i.Grow(i.Count() - n))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Respawn spawns the player after it dies, so that its health is replenished,
|
|
// and it is spawned in the world again. Nothing will happen if the player does
|
|
// not have a session connected to it.
|
|
// Calling Respawn may lead to the player being removed from its world and being
|
|
// added to a new world. This means that p cannot be assumed to be valid after
|
|
// a call to Respawn.
|
|
func (p *Player) Respawn() *world.EntityHandle {
|
|
p.respawn(nil)
|
|
return p.handle
|
|
}
|
|
|
|
func (p *Player) respawn(f func(p *Player)) {
|
|
if !p.Dead() || p.session() == session.Nop {
|
|
return
|
|
}
|
|
|
|
blockPos, w, spawnObstructed, _ := p.spawnLocation()
|
|
pos := blockPos.Vec3Middle()
|
|
|
|
if spawnObstructed {
|
|
p.Messaget(chat.MessageBedNotValid)
|
|
}
|
|
|
|
p.addHealth(p.MaxHealth())
|
|
p.hunger.Reset()
|
|
p.sendFood()
|
|
p.Extinguish()
|
|
p.ResetFallDistance()
|
|
|
|
p.Handler().HandleRespawn(p, &pos, &w)
|
|
|
|
handle := p.tx.RemoveEntity(p)
|
|
w.Exec(func(tx *world.Tx) {
|
|
np := tx.AddEntity(handle).(*Player)
|
|
np.Teleport(pos)
|
|
np.session().SendRespawn(pos, p)
|
|
np.SetVisible()
|
|
if f != nil {
|
|
f(np)
|
|
}
|
|
})
|
|
}
|
|
|
|
// spawnLocation designates a players safe spawn location.
|
|
func (p *Player) spawnLocation() (playerSpawn cube.Pos, w *world.World, spawnBlockBroken bool, previousDimension world.Dimension) {
|
|
tx := p.tx
|
|
w = tx.World()
|
|
previousDimension = w.Dimension()
|
|
playerSpawn = w.PlayerSpawn(p.UUID())
|
|
if b, ok := tx.Block(playerSpawn).(block.Bed); ok && b.CanRespawnOn() {
|
|
pos, ok := b.SafeSpawn(playerSpawn, tx)
|
|
if ok {
|
|
return pos, w, false, previousDimension
|
|
}
|
|
}
|
|
|
|
// We can use the principle here that returning through a portal of a specific dimension inside that dimension will
|
|
// always bring us back to the overworld.
|
|
w = w.PortalDestination(w.Dimension())
|
|
worldSpawn := w.Spawn()
|
|
return worldSpawn, w, playerSpawn != worldSpawn, previousDimension
|
|
}
|
|
|
|
// StartSprinting makes a player start sprinting, increasing the speed of the player by 30% and making
|
|
// particles show up under the feet. The player will only start sprinting if its food level is high enough.
|
|
// If the player is sneaking when calling StartSprinting, it is stopped from sneaking.
|
|
func (p *Player) StartSprinting() {
|
|
if !p.hunger.canSprint() && p.GameMode().AllowsTakingDamage() || p.crawling || p.sprinting {
|
|
return
|
|
}
|
|
ctx := event.C(p)
|
|
if p.Handler().HandleToggleSprint(ctx, true); ctx.Cancelled() {
|
|
return
|
|
}
|
|
p.StopSneaking()
|
|
p.sprinting = true
|
|
p.SetSpeed(p.speed * 1.3)
|
|
p.updateState()
|
|
}
|
|
|
|
// Sprinting checks if the player is currently sprinting.
|
|
func (p *Player) Sprinting() bool {
|
|
return p.sprinting
|
|
}
|
|
|
|
// StopSprinting makes a player stop sprinting, setting back the speed of the player to its original value.
|
|
func (p *Player) StopSprinting() {
|
|
if !p.sprinting {
|
|
return
|
|
}
|
|
ctx := event.C(p)
|
|
if p.Handler().HandleToggleSprint(ctx, false); ctx.Cancelled() {
|
|
return
|
|
}
|
|
p.sprinting = false
|
|
p.SetSpeed(p.speed / 1.3)
|
|
p.updateState()
|
|
}
|
|
|
|
// StartSneaking makes a player start sneaking. If the player is already sneaking, StartSneaking will not do
|
|
// anything.
|
|
// If the player is sprinting while StartSneaking is called, the sprinting is stopped.
|
|
func (p *Player) StartSneaking() {
|
|
if p.sneaking {
|
|
return
|
|
}
|
|
ctx := event.C(p)
|
|
if p.Handler().HandleToggleSneak(ctx, true); ctx.Cancelled() {
|
|
return
|
|
}
|
|
if !p.Flying() {
|
|
p.StopSprinting()
|
|
}
|
|
p.sneaking = true
|
|
p.updateState()
|
|
}
|
|
|
|
// Sneaking checks if the player is currently sneaking.
|
|
func (p *Player) Sneaking() bool {
|
|
return p.sneaking
|
|
}
|
|
|
|
// StopSneaking makes a player stop sneaking if it currently is. If the player is not sneaking, StopSneaking
|
|
// will not do anything.
|
|
func (p *Player) StopSneaking() {
|
|
if !p.sneaking {
|
|
return
|
|
}
|
|
ctx := event.C(p)
|
|
if p.Handler().HandleToggleSneak(ctx, false); ctx.Cancelled() {
|
|
return
|
|
}
|
|
p.sneaking = false
|
|
p.updateState()
|
|
}
|
|
|
|
// StartSwimming makes the player start swimming if it is not currently doing so. If the player is sneaking
|
|
// while StartSwimming is called, the sneaking is stopped.
|
|
func (p *Player) StartSwimming() {
|
|
if p.swimming {
|
|
return
|
|
}
|
|
p.StopSneaking()
|
|
p.swimming = true
|
|
p.updateState()
|
|
}
|
|
|
|
// Swimming checks if the player is currently swimming.
|
|
func (p *Player) Swimming() bool {
|
|
return p.swimming
|
|
}
|
|
|
|
// StopSwimming makes the player stop swimming if it is currently doing so.
|
|
func (p *Player) StopSwimming() {
|
|
if !p.swimming {
|
|
return
|
|
}
|
|
p.swimming = false
|
|
p.updateState()
|
|
}
|
|
|
|
// Splash is called when a water bottle splashes onto the player.
|
|
func (p *Player) Splash(*world.Tx, mgl64.Vec3) {
|
|
if d := p.OnFireDuration(); d.Seconds() <= 0 {
|
|
return
|
|
}
|
|
p.Extinguish()
|
|
}
|
|
|
|
// StartCrawling makes the player start crawling if it is not currently doing so. If the player is sneaking
|
|
// while StartCrawling is called, the sneaking is stopped.
|
|
func (p *Player) StartCrawling() {
|
|
if p.crawling {
|
|
return
|
|
}
|
|
for _, corner := range p.H().Type().BBox(p).Translate(p.Position()).Corners() {
|
|
if _, isAir := p.tx.Block(cube.PosFromVec3(corner).Add(cube.Pos{0, 1, 0})).(block.Air); !isAir {
|
|
p.crawling = true
|
|
break
|
|
}
|
|
}
|
|
p.StopSneaking()
|
|
p.updateState()
|
|
}
|
|
|
|
// StopCrawling makes the player stop crawling if it is currently doing so.
|
|
func (p *Player) StopCrawling() {
|
|
if !p.crawling {
|
|
return
|
|
}
|
|
p.crawling = false
|
|
p.updateState()
|
|
}
|
|
|
|
// Crawling checks if the player is currently crawling.
|
|
func (p *Player) Crawling() bool {
|
|
return p.crawling
|
|
}
|
|
|
|
// StartGliding makes the player start gliding if it is not currently doing so.
|
|
func (p *Player) StartGliding() {
|
|
if p.gliding {
|
|
return
|
|
}
|
|
chest := p.Armour().Chestplate()
|
|
if _, ok := chest.Item().(item.Elytra); !ok || chest.Durability() < 2 {
|
|
return
|
|
}
|
|
p.gliding = true
|
|
p.updateState()
|
|
}
|
|
|
|
// Gliding checks if the player is currently gliding.
|
|
func (p *Player) Gliding() bool {
|
|
return p.gliding
|
|
}
|
|
|
|
// StopGliding makes the player stop gliding if it is currently doing so.
|
|
func (p *Player) StopGliding() {
|
|
if !p.gliding {
|
|
return
|
|
}
|
|
p.gliding = false
|
|
p.glideTicks = 0
|
|
p.updateState()
|
|
}
|
|
|
|
// StartFlying makes the player start flying if they aren't already. It requires the player to be in a gamemode which
|
|
// allows flying.
|
|
func (p *Player) StartFlying() {
|
|
if !p.GameMode().AllowsFlying() || p.Flying() {
|
|
return
|
|
}
|
|
p.flying = true
|
|
p.session().SendGameMode(p)
|
|
}
|
|
|
|
// Flying checks if the player is currently flying.
|
|
func (p *Player) Flying() bool {
|
|
return p.flying
|
|
}
|
|
|
|
// StopFlying makes the player stop flying if it currently is.
|
|
func (p *Player) StopFlying() {
|
|
if !p.flying {
|
|
return
|
|
}
|
|
p.flying = false
|
|
p.session().SendGameMode(p)
|
|
}
|
|
|
|
// Jump makes the player jump if they are on ground. It exhausts the player by 0.05 food points, an additional 0.15
|
|
// is exhausted if the player is sprint jumping.
|
|
func (p *Player) Jump() {
|
|
if p.Dead() {
|
|
return
|
|
}
|
|
|
|
p.Handler().HandleJump(p)
|
|
if p.OnGround() {
|
|
jumpVel := 0.42
|
|
if e, ok := p.Effect(effect.JumpBoost); ok {
|
|
jumpVel = float64(e.Level()) / 10
|
|
}
|
|
p.data.Vel = mgl64.Vec3{0, jumpVel}
|
|
}
|
|
if p.Sprinting() {
|
|
p.Exhaust(0.2)
|
|
} else {
|
|
p.Exhaust(0.05)
|
|
}
|
|
}
|
|
|
|
// Sleep makes the player sleep at the given position. If the position does not map to a bed (specifically the head side),
|
|
// the player will not sleep.
|
|
func (p *Player) Sleep(pos cube.Pos) {
|
|
if p.sleeping {
|
|
// The player is already sleeping.
|
|
return
|
|
}
|
|
|
|
tx := p.tx
|
|
b, ok := tx.Block(pos).(block.Bed)
|
|
if !ok || b.Sleeper != nil {
|
|
// The player cannot sleep here.
|
|
return
|
|
}
|
|
|
|
ctx, sendReminder := event.C(p), true
|
|
if p.Handler().HandleSleep(ctx, &sendReminder); ctx.Cancelled() {
|
|
return
|
|
}
|
|
|
|
b.Sleeper = p.H()
|
|
tx.SetBlock(pos, b, nil)
|
|
|
|
tx.World().SetRequiredSleepDuration(time.Millisecond * 5050)
|
|
|
|
p.data.Pos = pos.Vec3Middle().Add(mgl64.Vec3{0, 0.5625})
|
|
p.sleeping = true
|
|
p.sleepPos = pos
|
|
|
|
p.session().SendPlayerSpawn(pos.Vec3())
|
|
|
|
if sendReminder {
|
|
tx.BroadcastSleepingReminder(p)
|
|
}
|
|
|
|
tx.BroadcastSleepingIndicator()
|
|
p.updateState()
|
|
}
|
|
|
|
// Wake forces the player out of bed if they are sleeping.
|
|
func (p *Player) Wake() {
|
|
if !p.sleeping {
|
|
return
|
|
}
|
|
p.sleeping = false
|
|
|
|
tx := p.tx
|
|
tx.BroadcastSleepingIndicator()
|
|
|
|
for _, v := range p.viewers() {
|
|
v.ViewEntityWake(p)
|
|
}
|
|
p.updateState()
|
|
|
|
pos := p.sleepPos
|
|
if b, ok := tx.Block(pos).(block.Bed); ok {
|
|
b.Sleeper = nil
|
|
tx.SetBlock(pos, b, nil)
|
|
}
|
|
}
|
|
|
|
// Sleeping returns true if the player is currently sleeping, along with the position of the bed the player is sleeping
|
|
// on.
|
|
func (p *Player) Sleeping() (cube.Pos, bool) {
|
|
if !p.sleeping {
|
|
return cube.Pos{}, false
|
|
}
|
|
return p.sleepPos, true
|
|
}
|
|
|
|
// SendSleepingIndicator displays a notification to the player on the amount of sleeping players in the world.
|
|
func (p *Player) SendSleepingIndicator(sleeping, max int) {
|
|
p.session().ViewSleepingPlayers(sleeping, max)
|
|
}
|
|
|
|
// SetInvisible sets the player invisible, so that other players will not be able to see it.
|
|
func (p *Player) SetInvisible() {
|
|
if p.Invisible() {
|
|
return
|
|
}
|
|
p.invisible = true
|
|
p.updateState()
|
|
}
|
|
|
|
// SetVisible sets the player visible again, so that other players can see it again. If the player was already
|
|
// visible, or if the player is in spectator mode, nothing happens.
|
|
func (p *Player) SetVisible() {
|
|
if _, ok := p.Effect(effect.Invisibility); ok || !p.GameMode().Visible() || !p.invisible {
|
|
return
|
|
}
|
|
p.invisible = false
|
|
p.updateState()
|
|
}
|
|
|
|
// Invisible checks if the Player is currently invisible.
|
|
func (p *Player) Invisible() bool {
|
|
return p.invisible
|
|
}
|
|
|
|
// SetImmobile prevents the player from moving around, but still allows them to look around.
|
|
func (p *Player) SetImmobile() {
|
|
if p.Immobile() {
|
|
return
|
|
}
|
|
p.immobile = true
|
|
p.updateState()
|
|
}
|
|
|
|
// SetMobile allows the player to freely move around again after being immobile.
|
|
func (p *Player) SetMobile() {
|
|
if !p.Immobile() {
|
|
return
|
|
}
|
|
p.immobile = false
|
|
p.updateState()
|
|
}
|
|
|
|
// Immobile checks if the Player is currently immobile.
|
|
func (p *Player) Immobile() bool {
|
|
return p.immobile
|
|
}
|
|
|
|
// FireProof checks if the Player is currently fireproof. True is returned if the player has a fireResistance effect or
|
|
// if it is in creative mode.
|
|
func (p *Player) FireProof() bool {
|
|
if _, ok := p.Effect(effect.FireResistance); ok {
|
|
return true
|
|
}
|
|
return !p.GameMode().AllowsTakingDamage()
|
|
}
|
|
|
|
// OnFireDuration ...
|
|
func (p *Player) OnFireDuration() time.Duration {
|
|
return time.Duration(p.fireTicks) * time.Second / 20
|
|
}
|
|
|
|
// SetOnFire ...
|
|
func (p *Player) SetOnFire(duration time.Duration) {
|
|
ticks := int64(duration.Seconds() * 20)
|
|
if level := p.Armour().HighestEnchantmentLevel(enchantment.FireProtection); level > 0 {
|
|
ticks -= int64(math.Floor(float64(ticks) * float64(level) * 0.15))
|
|
}
|
|
p.fireTicks = ticks
|
|
p.updateState()
|
|
}
|
|
|
|
// Extinguish ...
|
|
func (p *Player) Extinguish() {
|
|
p.SetOnFire(0)
|
|
}
|
|
|
|
// Inventory returns the inventory of the player. This inventory holds the items stored in the normal part of
|
|
// the inventory and the hotbar. It also includes the item in the main hand as returned by Player.HeldItems().
|
|
func (p *Player) Inventory() *inventory.Inventory {
|
|
return p.inv
|
|
}
|
|
|
|
// Armour returns the armour inventory of the player. This inventory yields 4 slots, for the helmet,
|
|
// chestplate, leggings and boots respectively.
|
|
func (p *Player) Armour() *inventory.Armour {
|
|
return p.armour
|
|
}
|
|
|
|
// HeldItems returns the items currently held in the hands of the player. The first item stack returned is the
|
|
// one held in the main hand, the second is held in the off-hand.
|
|
// If no item was held in a hand, the stack returned has a count of 0. Stack.Empty() may be used to check if
|
|
// the hand held anything.
|
|
func (p *Player) HeldItems() (mainHand, offHand item.Stack) {
|
|
offHand, _ = p.offHand.Item(0)
|
|
mainHand, _ = p.inv.Item(int(*p.heldSlot))
|
|
return mainHand, offHand
|
|
}
|
|
|
|
// SetHeldItems sets items to the main hand and the off-hand of the player. The Stacks passed may be empty
|
|
// (Stack.Empty()) to clear the held item.
|
|
func (p *Player) SetHeldItems(mainHand, offHand item.Stack) {
|
|
_ = p.inv.SetItem(int(*p.heldSlot), mainHand)
|
|
_ = p.offHand.SetItem(0, offHand)
|
|
}
|
|
|
|
// SetHeldSlot updates the held slot of the player to the slot provided. The
|
|
// slot must be between 0 and 8.
|
|
func (p *Player) SetHeldSlot(to int) error {
|
|
// The slot that the player might have selected must be within the hotbar:
|
|
// The held item cannot be in a different place in the inventory.
|
|
if to < 0 || to > 8 {
|
|
return fmt.Errorf("held slot exceeds hotbar range 0-8: slot is %v", to)
|
|
}
|
|
from := int(*p.heldSlot)
|
|
if from == to {
|
|
// Old slot was the same as new slot, so don't do anything.
|
|
return nil
|
|
}
|
|
|
|
ctx := event.C(p)
|
|
p.Handler().HandleHeldSlotChange(ctx, from, to)
|
|
if ctx.Cancelled() {
|
|
// The slot change was cancelled, resend held slot.
|
|
p.session().SendHeldSlot(from, p, true)
|
|
return nil
|
|
}
|
|
*p.heldSlot = uint32(to)
|
|
p.usingItem = false
|
|
|
|
for _, viewer := range p.viewers() {
|
|
viewer.ViewEntityItems(p)
|
|
}
|
|
p.session().SendHeldSlot(to, p, false)
|
|
return nil
|
|
}
|
|
|
|
// EnderChestInventory returns the player's ender chest inventory. Its accessed by the player when opening
|
|
// ender chests anywhere.
|
|
func (p *Player) EnderChestInventory() *inventory.Inventory {
|
|
return p.enderChest
|
|
}
|
|
|
|
// SetGameMode sets the game mode of a player. The game mode specifies the way that the player can interact
|
|
// with the world that it is in.
|
|
func (p *Player) SetGameMode(mode world.GameMode) {
|
|
previous := p.GameMode()
|
|
p.gameMode = mode
|
|
|
|
if !mode.AllowsFlying() {
|
|
p.StopFlying()
|
|
}
|
|
if !mode.Visible() {
|
|
p.SetInvisible()
|
|
} else if !previous.Visible() {
|
|
p.SetVisible()
|
|
}
|
|
|
|
p.session().SendGameMode(p)
|
|
for _, v := range p.viewers() {
|
|
v.ViewEntityGameMode(p)
|
|
}
|
|
if mode.AllowsTakingDamage() {
|
|
p.session().SendHealth(p.Health(), p.MaxHealth(), p.absorptionHealth)
|
|
}
|
|
}
|
|
|
|
// GameMode returns the current game mode assigned to the player. If not changed, the game mode returned will
|
|
// be the same as that of the world that the player spawns in.
|
|
// The game mode may be changed using Player.SetGameMode().
|
|
func (p *Player) GameMode() world.GameMode {
|
|
return p.gameMode
|
|
}
|
|
|
|
// HasCooldown returns true if the item passed has an active cooldown, meaning it currently cannot be used again. If the
|
|
// world.Item passed is nil, HasCooldown always returns false.
|
|
func (p *Player) HasCooldown(item world.Item) bool {
|
|
if item == nil {
|
|
return false
|
|
}
|
|
name, _ := item.EncodeItem()
|
|
otherTime, ok := p.cooldowns[name]
|
|
if !ok {
|
|
return false
|
|
}
|
|
if time.Now().After(otherTime) {
|
|
delete(p.cooldowns, name)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// SetCooldown sets a cooldown for an item. If the world.Item passed is nil, nothing happens.
|
|
func (p *Player) SetCooldown(item world.Item, cooldown time.Duration) {
|
|
if item == nil {
|
|
return
|
|
}
|
|
name, _ := item.EncodeItem()
|
|
p.cooldowns[name] = time.Now().Add(cooldown)
|
|
p.session().ViewItemCooldown(item, cooldown)
|
|
}
|
|
|
|
// UseItem uses the item currently held in the player's main hand in the air. Generally, nothing happens,
|
|
// unless the held item implements the item.Usable interface, in which case it will be activated.
|
|
// This generally happens for items such as throwable items like snowballs.
|
|
func (p *Player) UseItem() {
|
|
i, _ := p.HeldItems()
|
|
ctx := event.C(p)
|
|
if p.HasCooldown(i.Item()) {
|
|
return
|
|
}
|
|
if p.Handler().HandleItemUse(ctx); ctx.Cancelled() {
|
|
return
|
|
}
|
|
i, left := p.HeldItems()
|
|
it := i.Item()
|
|
|
|
if cd, ok := it.(item.Cooldown); ok {
|
|
p.SetCooldown(it, cd.Cooldown())
|
|
}
|
|
|
|
if _, ok := it.(item.Releasable); ok {
|
|
if !p.canRelease() {
|
|
return
|
|
}
|
|
p.usingSince, p.usingItem = time.Now(), true
|
|
p.updateState()
|
|
}
|
|
switch usable := it.(type) {
|
|
case item.Chargeable:
|
|
useCtx := p.useContext()
|
|
if !p.usingItem {
|
|
if !usable.ReleaseCharge(p, p.tx, useCtx) && usable.CanCharge(p, p.tx, useCtx) {
|
|
// If the item was not charged yet, start charging.
|
|
p.usingSince, p.usingItem = time.Now(), true
|
|
}
|
|
p.handleUseContext(useCtx)
|
|
p.updateState()
|
|
return
|
|
}
|
|
|
|
// Stop charging and determine if the item is ready.
|
|
p.usingItem = false
|
|
dur := p.useDuration()
|
|
if usable.Charge(p, p.tx, useCtx, dur) {
|
|
p.session().SendChargeItemComplete()
|
|
}
|
|
p.handleUseContext(useCtx)
|
|
p.updateState()
|
|
case item.Usable:
|
|
useCtx := p.useContext()
|
|
if !usable.Use(p.tx, p, useCtx) {
|
|
return
|
|
}
|
|
// We only swing the player's arm if the item held actually does something. If it doesn't, there is no
|
|
// reason to swing the arm.
|
|
p.SwingArm()
|
|
p.SetHeldItems(p.subtractItem(p.damageItem(i, useCtx.Damage), useCtx.CountSub), left)
|
|
p.addNewItem(useCtx)
|
|
case item.Consumable:
|
|
if c, ok := usable.(interface{ CanConsume() bool }); ok && !c.CanConsume() {
|
|
p.ReleaseItem()
|
|
return
|
|
}
|
|
if !usable.AlwaysConsumable() && p.GameMode().AllowsTakingDamage() && p.Food() >= 20 {
|
|
// The item.Consumable is not always consumable, the player is not in creative mode and the
|
|
// food bar is filled: The item cannot be consumed.
|
|
p.ReleaseItem()
|
|
return
|
|
}
|
|
if !p.usingItem {
|
|
// Consumable starts being consumed: Set the start timestamp and update the using state to viewers.
|
|
p.usingItem, p.usingSince = true, time.Now()
|
|
p.updateState()
|
|
return
|
|
}
|
|
// The player is currently using the item held. This is a signal the item was consumed, so we
|
|
// consume it and start using it again.
|
|
useCtx, dur := p.useContext(), p.useDuration()
|
|
if dur < usable.ConsumeDuration() {
|
|
// The required duration for consuming this item was not met, so we don't consume it.
|
|
return
|
|
}
|
|
// Reset the duration for the next item to be consumed.
|
|
p.usingSince = time.Now()
|
|
ctx := event.C(p)
|
|
if p.Handler().HandleItemConsume(ctx, i); ctx.Cancelled() {
|
|
return
|
|
}
|
|
useCtx.CountSub, useCtx.NewItem = 1, usable.Consume(p.tx, p)
|
|
p.handleUseContext(useCtx)
|
|
p.tx.PlaySound(p.Position().Add(mgl64.Vec3{0, 1.5}), sound.Burp{})
|
|
}
|
|
}
|
|
|
|
// ReleaseItem makes the Player release the item it is currently using. This is only applicable for items that
|
|
// implement the item.Releasable interface.
|
|
// If the Player is not currently using any item, ReleaseItem returns immediately.
|
|
// ReleaseItem either aborts the using of the item or finished it, depending on the time that elapsed since
|
|
// the item started being used.
|
|
func (p *Player) ReleaseItem() {
|
|
if !p.usingItem || !p.canRelease() || !p.GameMode().AllowsInteraction() {
|
|
p.usingItem = false
|
|
return
|
|
}
|
|
p.usingItem = false
|
|
|
|
useCtx, dur := p.useContext(), p.useDuration()
|
|
i, _ := p.HeldItems()
|
|
ctx := event.C(p)
|
|
if p.Handler().HandleItemRelease(ctx, i, dur); ctx.Cancelled() {
|
|
return
|
|
}
|
|
i.Item().(item.Releasable).Release(p, p.tx, useCtx, dur)
|
|
p.handleUseContext(useCtx)
|
|
p.updateState()
|
|
}
|
|
|
|
// canRelease returns whether the player can release the item currently held in the main hand.
|
|
func (p *Player) canRelease() bool {
|
|
held, left := p.HeldItems()
|
|
releasable, ok := held.Item().(item.Releasable)
|
|
if !ok {
|
|
return false
|
|
}
|
|
if p.GameMode().CreativeInventory() {
|
|
return true
|
|
}
|
|
for _, req := range releasable.Requirements() {
|
|
reqName, _ := req.Item().EncodeItem()
|
|
|
|
if !left.Empty() {
|
|
leftName, _ := left.Item().EncodeItem()
|
|
if leftName == reqName {
|
|
continue
|
|
}
|
|
}
|
|
|
|
_, found := p.Inventory().FirstFunc(func(stack item.Stack) bool {
|
|
name, _ := stack.Item().EncodeItem()
|
|
return name == reqName
|
|
})
|
|
if !found {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// handleUseContext handles the item.UseContext after the item has been used.
|
|
func (p *Player) handleUseContext(ctx *item.UseContext) {
|
|
i, left := p.HeldItems()
|
|
|
|
p.SetHeldItems(p.subtractItem(p.damageItem(i, ctx.Damage), ctx.CountSub), left)
|
|
p.addNewItem(ctx)
|
|
for _, it := range ctx.ConsumedItems {
|
|
_, offHand := p.HeldItems()
|
|
if offHand.Comparable(it) {
|
|
if err := p.offHand.RemoveItem(it); err == nil {
|
|
continue
|
|
}
|
|
|
|
it = it.Grow(-offHand.Count())
|
|
}
|
|
|
|
_ = p.Inventory().RemoveItem(it)
|
|
}
|
|
}
|
|
|
|
// useDuration returns the duration the player has been using the item in the main hand.
|
|
func (p *Player) useDuration() time.Duration {
|
|
return time.Since(p.usingSince) + time.Second/20
|
|
}
|
|
|
|
// UsingItem checks if the Player is currently using an item. True is returned if the Player is currently eating an
|
|
// item or using it over a longer duration such as when using a bow.
|
|
func (p *Player) UsingItem() bool {
|
|
return p.usingItem
|
|
}
|
|
|
|
// UseItemOnBlock uses the item held in the main hand of the player on a block at the position passed. The
|
|
// player is assumed to have clicked the face passed with the relative click position clickPos.
|
|
// If the item could not be used successfully, for example when the position is out of range, the method
|
|
// returns immediately.
|
|
// UseItemOnBlock does nothing if the block at the cube.Pos passed is of the type block.Air.
|
|
func (p *Player) UseItemOnBlock(pos cube.Pos, face cube.Face, clickPos mgl64.Vec3) {
|
|
if _, ok := p.tx.Block(pos).(block.Air); ok || !p.canReach(pos.Vec3Centre()) {
|
|
// The client used its item on a block that does not exist server-side or one it couldn't reach. Stop trying
|
|
// to use the item immediately.
|
|
p.resendNearbyBlocks(pos, face)
|
|
return
|
|
}
|
|
ctx := event.C(p)
|
|
if p.Handler().HandleItemUseOnBlock(ctx, pos, face, clickPos); ctx.Cancelled() {
|
|
p.resendNearbyBlocks(pos, face)
|
|
return
|
|
}
|
|
i, left := p.HeldItems()
|
|
b := p.tx.Block(pos)
|
|
if act, ok := b.(block.Activatable); ok {
|
|
// If a player is sneaking, it will not activate the block clicked, unless it is not holding any
|
|
// items, in which case the block will be activated as usual.
|
|
if !p.Sneaking() || i.Empty() {
|
|
p.SwingArm()
|
|
|
|
// The block was activated: Blocks such as doors must always have precedence over the item being
|
|
// used.
|
|
if useCtx := p.useContext(); act.Activate(pos, face, p.tx, p, useCtx) {
|
|
p.SetHeldItems(p.subtractItem(p.damageItem(i, useCtx.Damage), useCtx.CountSub), left)
|
|
p.addNewItem(useCtx)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
if i.Empty() {
|
|
return
|
|
}
|
|
switch ib := i.Item().(type) {
|
|
case item.UsableOnBlock:
|
|
// The item does something when used on a block.
|
|
useCtx := p.useContext()
|
|
if !ib.UseOnBlock(pos, face, clickPos, p.tx, p, useCtx) {
|
|
return
|
|
}
|
|
p.SwingArm()
|
|
p.SetHeldItems(p.subtractItem(p.damageItem(i, useCtx.Damage), useCtx.CountSub), left)
|
|
p.addNewItem(useCtx)
|
|
case world.Block:
|
|
// The item IS a block, meaning it is being placed.
|
|
replacedPos := pos
|
|
if replaceable, ok := b.(block.Replaceable); !ok || !replaceable.ReplaceableBy(ib) {
|
|
// The block clicked was either not replaceable, or not replaceable using the block passed.
|
|
replacedPos = pos.Side(face)
|
|
}
|
|
if replaceable, ok := p.tx.Block(replacedPos).(block.Replaceable); !ok || !replaceable.ReplaceableBy(ib) || replacedPos.OutOfBounds(p.tx.Range()) {
|
|
return
|
|
}
|
|
if !p.placeBlock(replacedPos, ib, false) || p.GameMode().CreativeInventory() {
|
|
return
|
|
}
|
|
p.SetHeldItems(p.subtractItem(i, 1), left)
|
|
}
|
|
}
|
|
|
|
// UseItemOnEntity uses the item held in the main hand of the player on the entity passed, provided it is
|
|
// within range of the player.
|
|
// If the item held in the main hand of the player does nothing when used on an entity, nothing will happen.
|
|
func (p *Player) UseItemOnEntity(e world.Entity) bool {
|
|
if !p.canReach(e.Position()) {
|
|
return false
|
|
}
|
|
ctx := event.C(p)
|
|
if p.Handler().HandleItemUseOnEntity(ctx, e); ctx.Cancelled() {
|
|
return false
|
|
}
|
|
i, left := p.HeldItems()
|
|
usable, ok := i.Item().(item.UsableOnEntity)
|
|
if !ok {
|
|
return true
|
|
}
|
|
useCtx := p.useContext()
|
|
if !usable.UseOnEntity(e, p.tx, p, useCtx) {
|
|
return true
|
|
}
|
|
p.SwingArm()
|
|
p.SetHeldItems(p.subtractItem(p.damageItem(i, useCtx.Damage), useCtx.CountSub), left)
|
|
p.addNewItem(useCtx)
|
|
return true
|
|
}
|
|
|
|
// AttackEntity uses the item held in the main hand of the player to attack the entity passed, provided it is
|
|
// within range of the player.
|
|
// The damage dealt to the entity will depend on the item held by the player and any effects the player may
|
|
// have.
|
|
// If the player cannot reach the entity at its position, the method returns immediately.
|
|
func (p *Player) AttackEntity(e world.Entity) bool {
|
|
if !p.canReach(e.Position()) {
|
|
return false
|
|
}
|
|
|
|
living, isLiving := e.(entity.Living)
|
|
if isLiving && living.Dead() {
|
|
return false
|
|
}
|
|
|
|
var (
|
|
force, height = 0.45, 0.3608
|
|
_, slowFalling = p.Effect(effect.SlowFalling)
|
|
_, blind = p.Effect(effect.Blindness)
|
|
critical = !p.Sprinting() && !p.Flying() && p.FallDistance() > 0 && !slowFalling && !blind
|
|
)
|
|
|
|
i, _ := p.HeldItems()
|
|
if k, ok := i.Enchantment(enchantment.Knockback); ok {
|
|
inc := enchantment.Knockback.Force(k.Level())
|
|
force += inc
|
|
height += inc
|
|
}
|
|
|
|
ctx := event.C(p)
|
|
if p.Handler().HandleAttackEntity(ctx, e, &force, &height, &critical); ctx.Cancelled() {
|
|
return false
|
|
}
|
|
p.SwingArm()
|
|
|
|
if !isLiving {
|
|
return false
|
|
}
|
|
|
|
dmg := i.AttackDamage()
|
|
if strength, ok := p.Effect(effect.Strength); ok {
|
|
dmg += dmg * effect.Strength.Multiplier(strength.Level())
|
|
}
|
|
if weakness, ok := p.Effect(effect.Weakness); ok {
|
|
dmg -= dmg * effect.Weakness.Multiplier(weakness.Level())
|
|
}
|
|
if s, ok := i.Enchantment(enchantment.Sharpness); ok {
|
|
dmg += enchantment.Sharpness.Addend(s.Level())
|
|
for _, v := range p.tx.Viewers(living.Position()) {
|
|
v.ViewEntityAction(living, entity.EnchantedHitAction{})
|
|
}
|
|
}
|
|
if critical {
|
|
dmg *= 1.5
|
|
}
|
|
|
|
n, vulnerable := living.Hurt(dmg, entity.AttackDamageSource{Attacker: p})
|
|
i, left := p.HeldItems()
|
|
|
|
if durable, ok := i.Item().(item.Durable); ok {
|
|
p.SetHeldItems(p.damageItem(i, durable.DurabilityInfo().AttackDurability), left)
|
|
}
|
|
|
|
p.tx.PlaySound(entity.EyePosition(e), sound.Attack{Damage: !mgl64.FloatEqual(n, 0)})
|
|
if !vulnerable {
|
|
return true
|
|
}
|
|
if critical {
|
|
for _, v := range p.tx.Viewers(living.Position()) {
|
|
v.ViewEntityAction(living, entity.CriticalHitAction{})
|
|
}
|
|
}
|
|
|
|
p.Exhaust(0.1)
|
|
|
|
living.KnockBack(p.Position(), force, height)
|
|
|
|
if f, ok := i.Enchantment(enchantment.FireAspect); ok {
|
|
if flammable, ok := living.(entity.Flammable); ok {
|
|
flammable.SetOnFire(enchantment.FireAspect.Duration(f.Level()))
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// StartBreaking makes the player start breaking the block at the position passed using the item currently
|
|
// held in its main hand.
|
|
// If no block is present at the position, or if the block is out of range, StartBreaking will return
|
|
// immediately and the block will not be broken. StartBreaking will stop the breaking of any block that the
|
|
// player might be breaking before this method is called.
|
|
func (p *Player) StartBreaking(pos cube.Pos, face cube.Face) {
|
|
p.AbortBreaking()
|
|
if _, air := p.tx.Block(pos).(block.Air); air || !p.canReach(pos.Vec3Centre()) {
|
|
// The block was either out of range or air, so it can't be broken by the player.
|
|
return
|
|
}
|
|
if _, ok := p.tx.Block(pos.Side(face)).(block.Fire); ok {
|
|
ctx := event.C(p)
|
|
if p.Handler().HandleFireExtinguish(ctx, pos); ctx.Cancelled() {
|
|
// Resend the block because on client side that was extinguished
|
|
p.resendNearbyBlocks(pos, face)
|
|
return
|
|
}
|
|
|
|
p.tx.SetBlock(pos.Side(face), nil, nil)
|
|
p.tx.PlaySound(pos.Vec3(), sound.FireExtinguish{})
|
|
return
|
|
}
|
|
|
|
held, _ := p.HeldItems()
|
|
if _, ok := held.Item().(item.Sword); ok && p.GameMode().CreativeInventory() {
|
|
// Can't break blocks with a sword in creative mode.
|
|
return
|
|
}
|
|
// Note: We intentionally store this regardless of whether the breaking proceeds, so that we
|
|
// can resend the block to the client when it tries to break the block regardless.
|
|
p.breakingPos = pos
|
|
|
|
ctx := event.C(p)
|
|
if p.Handler().HandleStartBreak(ctx, pos); ctx.Cancelled() {
|
|
return
|
|
}
|
|
if punchable, ok := p.tx.Block(pos).(block.Punchable); ok {
|
|
punchable.Punch(pos, face, p.tx, p)
|
|
}
|
|
|
|
p.breaking, p.breakingFace = true, face
|
|
p.SwingArm()
|
|
|
|
if p.GameMode().CreativeInventory() {
|
|
return
|
|
}
|
|
p.lastBreakDuration = p.breakTime(pos)
|
|
for _, viewer := range p.viewers() {
|
|
viewer.ViewBlockAction(pos, block.StartCrackAction{BreakTime: p.lastBreakDuration})
|
|
}
|
|
}
|
|
|
|
// breakTime returns the time needed to break a block at the position passed, taking into account the item
|
|
// held, if the player is on the ground/underwater and if the player has any effects.
|
|
func (p *Player) breakTime(pos cube.Pos) time.Duration {
|
|
held, _ := p.HeldItems()
|
|
breakTime := block.BreakDuration(p.tx.Block(pos), held)
|
|
if !p.OnGround() {
|
|
breakTime *= 5
|
|
}
|
|
if _, ok := p.Armour().Helmet().Enchantment(enchantment.AquaAffinity); p.insideOfWater() && !ok {
|
|
breakTime *= 5
|
|
}
|
|
for _, e := range p.Effects() {
|
|
lvl := e.Level()
|
|
switch e.Type() {
|
|
case effect.Haste:
|
|
breakTime = time.Duration(float64(breakTime) * effect.Haste.Multiplier(lvl))
|
|
case effect.MiningFatigue:
|
|
breakTime = time.Duration(float64(breakTime) * effect.MiningFatigue.Multiplier(lvl))
|
|
case effect.ConduitPower:
|
|
breakTime = time.Duration(float64(breakTime) * effect.ConduitPower.Multiplier(lvl))
|
|
}
|
|
}
|
|
return breakTime
|
|
}
|
|
|
|
// FinishBreaking makes the player finish breaking the block it is currently breaking, or returns immediately
|
|
// if the player isn't breaking anything.
|
|
// FinishBreaking will stop the animation and break the block.
|
|
func (p *Player) FinishBreaking() {
|
|
if !p.breaking {
|
|
p.resendNearbyBlock(p.breakingPos)
|
|
return
|
|
}
|
|
p.AbortBreaking()
|
|
p.BreakBlock(p.breakingPos)
|
|
}
|
|
|
|
// AbortBreaking makes the player stop breaking the block it is currently breaking, or returns immediately
|
|
// if the player isn't breaking anything.
|
|
// Unlike FinishBreaking, AbortBreaking does not stop the animation.
|
|
func (p *Player) AbortBreaking() {
|
|
if !p.breaking {
|
|
return
|
|
}
|
|
p.breaking, p.breakCounter = false, 0
|
|
for _, viewer := range p.viewers() {
|
|
viewer.ViewBlockAction(p.breakingPos, block.StopCrackAction{})
|
|
}
|
|
}
|
|
|
|
// ContinueBreaking makes the player continue breaking the block it started breaking after a call to
|
|
// Player.StartBreaking().
|
|
// The face passed is used to display particles on the side of the block broken.
|
|
func (p *Player) ContinueBreaking(face cube.Face) {
|
|
if !p.breaking {
|
|
return
|
|
}
|
|
pos := p.breakingPos
|
|
b := p.tx.Block(pos)
|
|
p.tx.AddParticle(pos.Vec3(), particle.PunchBlock{Block: b, Face: face})
|
|
|
|
if p.breakCounter++; p.breakCounter%5 == 0 {
|
|
p.SwingArm()
|
|
|
|
// We send this sound only every so often. Vanilla doesn't send it every tick while breaking
|
|
// either. Every 5 ticks seems accurate.
|
|
p.tx.PlaySound(pos.Vec3(), sound.BlockBreaking{Block: b})
|
|
}
|
|
if breakTime := p.breakTime(pos); breakTime != p.lastBreakDuration {
|
|
for _, viewer := range p.viewers() {
|
|
viewer.ViewBlockAction(pos, block.ContinueCrackAction{BreakTime: breakTime})
|
|
}
|
|
p.lastBreakDuration = breakTime
|
|
}
|
|
}
|
|
|
|
// PlaceBlock makes the player place the block passed at the position passed, granted it is within the range
|
|
// of the player.
|
|
// An item.UseContext may be passed to obtain information on if the block placement was successful. (SubCount will
|
|
// be incremented). Nil may also be passed for the context parameter.
|
|
func (p *Player) PlaceBlock(pos cube.Pos, b world.Block, ctx *item.UseContext) {
|
|
ignoreBBox := ctx != nil && ctx.IgnoreBBox
|
|
if !p.placeBlock(pos, b, ignoreBBox) {
|
|
return
|
|
}
|
|
if ctx != nil {
|
|
ctx.CountSub++
|
|
}
|
|
}
|
|
|
|
// placeBlock makes the player place the block passed at the position passed, granted it is within the range
|
|
// of the player. A bool is returned indicating if a block was placed successfully.
|
|
func (p *Player) placeBlock(pos cube.Pos, b world.Block, ignoreBBox bool) bool {
|
|
if !p.canReach(pos.Vec3Centre()) || !p.GameMode().AllowsEditing() {
|
|
p.resendNearbyBlocks(pos, cube.Faces()...)
|
|
return false
|
|
}
|
|
if obstructed, selfOnly := p.obstructedPos(pos, b); obstructed && !ignoreBBox {
|
|
if !selfOnly {
|
|
// Only resend blocks if there were other entities blocking the
|
|
// placement than the player itself. Resending blocks placed inside
|
|
// the player itself leads to synchronisation issues.
|
|
p.resendNearbyBlocks(pos, cube.Faces()...)
|
|
}
|
|
return false
|
|
}
|
|
|
|
ctx := event.C(p)
|
|
if p.Handler().HandleBlockPlace(ctx, pos, b); ctx.Cancelled() {
|
|
p.resendNearbyBlocks(pos, cube.Faces()...)
|
|
return false
|
|
}
|
|
p.tx.SetBlock(pos, b, nil)
|
|
p.tx.PlaySound(pos.Vec3(), sound.BlockPlace{Block: b})
|
|
p.SwingArm()
|
|
return true
|
|
}
|
|
|
|
// obstructedPos checks if the position passed is obstructed if the block
|
|
// passed is attempted to be placed. The function returns true as the first
|
|
// bool if there is an entity in the way that could prevent the block from
|
|
// being placed.
|
|
// If the only entity preventing the block from being placed is the player
|
|
// itself, the second bool returned is true too.
|
|
func (p *Player) obstructedPos(pos cube.Pos, b world.Block) (obstructed, selfOnly bool) {
|
|
blockBoxes := b.Model().BBox(pos, p.tx)
|
|
for i, box := range blockBoxes {
|
|
blockBoxes[i] = box.Translate(pos.Vec3())
|
|
}
|
|
|
|
for e := range p.tx.EntitiesWithin(cube.Box(-3, -3, -3, 3, 3, 3).Translate(pos.Vec3())) {
|
|
t := e.H().Type()
|
|
switch t {
|
|
case entity.ItemType, entity.ArrowType, entity.ExperienceOrbType:
|
|
continue
|
|
default:
|
|
if cube.AnyIntersections(blockBoxes, t.BBox(e).Translate(e.Position()).Grow(-1e-4)) {
|
|
obstructed = true
|
|
if e.H() == p.handle {
|
|
continue
|
|
}
|
|
return true, false
|
|
}
|
|
}
|
|
}
|
|
return obstructed, true
|
|
}
|
|
|
|
// BreakBlock makes the player break a block in the world at a position passed. If the player is unable to
|
|
// reach the block passed, the method returns immediately.
|
|
func (p *Player) BreakBlock(pos cube.Pos) {
|
|
b := p.tx.Block(pos)
|
|
if _, air := b.(block.Air); air {
|
|
// Don't do anything if the position broken is already air.
|
|
return
|
|
}
|
|
if !p.canReach(pos.Vec3Centre()) || !p.GameMode().AllowsEditing() {
|
|
p.resendNearbyBlocks(pos)
|
|
return
|
|
}
|
|
if _, breakable := b.(block.Breakable); !breakable && !p.GameMode().CreativeInventory() {
|
|
p.resendNearbyBlocks(pos)
|
|
return
|
|
}
|
|
held, _ := p.HeldItems()
|
|
drops := p.drops(held, b)
|
|
|
|
xp := 0
|
|
if breakable, ok := b.(block.Breakable); ok && !p.GameMode().CreativeInventory() {
|
|
if _, hasSilkTouch := held.Enchantment(enchantment.SilkTouch); !hasSilkTouch {
|
|
xp = breakable.BreakInfo().XPDrops.RandomValue()
|
|
}
|
|
}
|
|
|
|
ctx := event.C(p)
|
|
if p.Handler().HandleBlockBreak(ctx, pos, &drops, &xp); ctx.Cancelled() {
|
|
p.resendNearbyBlocks(pos)
|
|
return
|
|
}
|
|
held, left := p.HeldItems()
|
|
|
|
p.SwingArm()
|
|
p.tx.SetBlock(pos, nil, nil)
|
|
p.tx.AddParticle(pos.Vec3Centre(), particle.BlockBreak{Block: b})
|
|
|
|
if breakable, ok := b.(block.Breakable); ok {
|
|
info := breakable.BreakInfo()
|
|
if info.BreakHandler != nil {
|
|
info.BreakHandler(pos, p.tx, p)
|
|
}
|
|
for _, orb := range entity.NewExperienceOrbs(pos.Vec3Centre(), xp) {
|
|
p.tx.AddEntity(orb)
|
|
}
|
|
}
|
|
for _, drop := range drops {
|
|
opts := world.EntitySpawnOpts{Position: pos.Vec3Centre(), Velocity: mgl64.Vec3{rand.Float64()*0.2 - 0.1, 0.2, rand.Float64()*0.2 - 0.1}}
|
|
p.tx.AddEntity(entity.NewItem(opts, drop))
|
|
}
|
|
|
|
p.Exhaust(0.005)
|
|
if block.BreaksInstantly(b, held) {
|
|
return
|
|
}
|
|
if durable, ok := held.Item().(item.Durable); ok {
|
|
p.SetHeldItems(p.damageItem(held, durable.DurabilityInfo().BreakDurability), left)
|
|
}
|
|
}
|
|
|
|
// drops returns the drops that the player can get from the block passed using the item held.
|
|
func (p *Player) drops(held item.Stack, b world.Block) []item.Stack {
|
|
t, ok := held.Item().(item.Tool)
|
|
if !ok {
|
|
t = item.ToolNone{}
|
|
}
|
|
var drops []item.Stack
|
|
if breakable, ok := b.(block.Breakable); ok && !p.GameMode().CreativeInventory() {
|
|
if breakable.BreakInfo().Harvestable(t) {
|
|
drops = breakable.BreakInfo().Drops(t, held.Enchantments())
|
|
}
|
|
} else if it, ok := b.(world.Item); ok && !p.GameMode().CreativeInventory() {
|
|
drops = []item.Stack{item.NewStack(it, 1)}
|
|
}
|
|
return drops
|
|
}
|
|
|
|
// PickBlock makes the player pick a block in the world at a position passed. If the player is unable to
|
|
// pick the block, the method returns immediately.
|
|
func (p *Player) PickBlock(pos cube.Pos) {
|
|
if !p.canReach(pos.Vec3()) {
|
|
return
|
|
}
|
|
|
|
b := p.tx.Block(pos)
|
|
|
|
var pickedItem item.Stack
|
|
if pi, ok := b.(block.Pickable); ok {
|
|
pickedItem = pi.Pick()
|
|
} else if i, ok := b.(world.Item); ok {
|
|
it, _ := world.ItemByName(i.EncodeItem())
|
|
pickedItem = item.NewStack(it, 1)
|
|
} else {
|
|
return
|
|
}
|
|
|
|
slot, found := p.Inventory().First(pickedItem)
|
|
if !found && !p.GameMode().CreativeInventory() {
|
|
return
|
|
}
|
|
|
|
ctx := event.C(p)
|
|
if p.Handler().HandleBlockPick(ctx, pos, b); ctx.Cancelled() {
|
|
return
|
|
}
|
|
_, offhand := p.HeldItems()
|
|
|
|
if found {
|
|
if slot < 9 {
|
|
_ = p.SetHeldSlot(slot)
|
|
return
|
|
}
|
|
_ = p.Inventory().Swap(slot, int(*p.heldSlot))
|
|
return
|
|
}
|
|
|
|
firstEmpty, emptyFound := p.Inventory().FirstEmpty()
|
|
if !emptyFound {
|
|
p.SetHeldItems(pickedItem, offhand)
|
|
return
|
|
}
|
|
if firstEmpty < 9 {
|
|
_ = p.SetHeldSlot(firstEmpty)
|
|
_ = p.Inventory().SetItem(firstEmpty, pickedItem)
|
|
return
|
|
}
|
|
_ = p.Inventory().Swap(firstEmpty, int(*p.heldSlot))
|
|
p.SetHeldItems(pickedItem, offhand)
|
|
}
|
|
|
|
// Teleport teleports the player to a target position in the world. Unlike Move, it immediately changes the
|
|
// position of the player, rather than showing an animation.
|
|
func (p *Player) Teleport(pos mgl64.Vec3) {
|
|
ctx := event.C(p)
|
|
if p.Handler().HandleTeleport(ctx, pos); ctx.Cancelled() {
|
|
return
|
|
}
|
|
p.Wake()
|
|
p.teleport(pos)
|
|
}
|
|
|
|
// teleport teleports the player to a target position in the world. It does not call the Handler of the
|
|
// player.
|
|
func (p *Player) teleport(pos mgl64.Vec3) {
|
|
for _, v := range p.viewers() {
|
|
v.ViewEntityTeleport(p, pos)
|
|
}
|
|
p.data.Pos = pos
|
|
p.data.Vel = mgl64.Vec3{}
|
|
p.ResetFallDistance()
|
|
}
|
|
|
|
// Move moves the player from one position to another in the world, by adding the delta passed to the current
|
|
// position of the player.
|
|
// Move also rotates the player, adding deltaYaw and deltaPitch to the respective values.
|
|
func (p *Player) Move(deltaPos mgl64.Vec3, deltaYaw, deltaPitch float64) {
|
|
if p.Dead() || (deltaPos.ApproxEqual(mgl64.Vec3{}) && mgl64.FloatEqual(deltaYaw, 0) && mgl64.FloatEqual(deltaPitch, 0)) {
|
|
return
|
|
}
|
|
if p.immobile {
|
|
if mgl64.FloatEqual(deltaYaw, 0) && mgl64.FloatEqual(deltaPitch, 0) {
|
|
// If only the position was changed, don't continue with the movement when immobile.
|
|
return
|
|
}
|
|
// Still update rotation if it was changed.
|
|
deltaPos = mgl64.Vec3{}
|
|
}
|
|
var (
|
|
pos = p.Position()
|
|
res, resRot = pos.Add(deltaPos), p.Rotation().Add(cube.Rotation{deltaYaw, deltaPitch})
|
|
)
|
|
ctx := event.C(p)
|
|
if p.Handler().HandleMove(ctx, res, resRot); ctx.Cancelled() {
|
|
if p.session() != session.Nop && pos.ApproxEqual(p.Position()) {
|
|
// The position of the player was changed and the event cancelled. This means we still need to notify the
|
|
// player of this movement change.
|
|
p.teleport(pos)
|
|
}
|
|
return
|
|
}
|
|
for _, v := range p.viewers() {
|
|
v.ViewEntityMovement(p, res, resRot, p.OnGround())
|
|
}
|
|
|
|
p.data.Pos = res
|
|
p.data.Rot = resRot
|
|
if deltaPos.Len() <= 3 {
|
|
// Only update velocity if the player is not moving too fast to prevent potential OOMs.
|
|
p.data.Vel = deltaPos
|
|
p.checkBlockCollisions(deltaPos)
|
|
}
|
|
|
|
horizontalVel := deltaPos
|
|
horizontalVel[1] = 0
|
|
if p.Gliding() {
|
|
if deltaPos.Y() >= -0.5 {
|
|
p.fallDistance = 1.0
|
|
}
|
|
if p.collidedHorizontally {
|
|
if force := horizontalVel.Len()*10.0 - 3.0; force > 0.0 {
|
|
p.tx.PlaySound(p.Position(), sound.Fall{Distance: force})
|
|
p.Hurt(force, entity.GlideDamageSource{})
|
|
}
|
|
}
|
|
}
|
|
|
|
_, submergedBefore := p.tx.Liquid(cube.PosFromVec3(pos.Add(mgl64.Vec3{0, p.EyeHeight()})))
|
|
_, submergedAfter := p.tx.Liquid(cube.PosFromVec3(res.Add(mgl64.Vec3{0, p.EyeHeight()})))
|
|
if submergedBefore != submergedAfter {
|
|
// Player wasn't either breathing before and no longer isn't, or wasn't breathing before and now is,
|
|
// so send the updated metadata.
|
|
p.session().ViewEntityState(p)
|
|
}
|
|
|
|
p.onGround = p.checkOnGround(deltaPos)
|
|
p.updateFallState(deltaPos[1])
|
|
|
|
if p.Swimming() {
|
|
p.Exhaust(0.01 * horizontalVel.Len())
|
|
} else if p.Sprinting() {
|
|
p.Exhaust(0.1 * horizontalVel.Len())
|
|
}
|
|
}
|
|
|
|
// Position returns the current position of the player. It may be changed as the player moves or is moved
|
|
// around the world.
|
|
func (p *Player) Position() mgl64.Vec3 {
|
|
return p.data.Pos
|
|
}
|
|
|
|
// Velocity returns the players current velocity. If there is an attached session, this will be empty.
|
|
func (p *Player) Velocity() mgl64.Vec3 {
|
|
return p.data.Vel
|
|
}
|
|
|
|
// SetVelocity updates the player's velocity. If there is an attached session, this will just send
|
|
// the velocity to the player session for the player to update.
|
|
func (p *Player) SetVelocity(velocity mgl64.Vec3) {
|
|
if p.session() == session.Nop {
|
|
p.data.Vel = velocity
|
|
return
|
|
}
|
|
for _, v := range p.viewers() {
|
|
v.ViewEntityVelocity(p, velocity)
|
|
}
|
|
}
|
|
|
|
// Rotation returns the yaw and pitch of the player in degrees. Yaw is horizontal rotation (rotation around the
|
|
// vertical axis, 0 when facing forward), pitch is vertical rotation (rotation around the horizontal axis, also 0
|
|
// when facing forward).
|
|
func (p *Player) Rotation() cube.Rotation {
|
|
return p.data.Rot
|
|
}
|
|
|
|
// Collect makes the player collect the item stack passed, adding it to the inventory. The amount of items that could
|
|
// be added is returned.
|
|
func (p *Player) Collect(s item.Stack) (int, bool) {
|
|
if p.Dead() || !p.GameMode().AllowsInteraction() {
|
|
return 0, false
|
|
}
|
|
ctx := event.C(p)
|
|
if p.Handler().HandleItemPickup(ctx, &s); ctx.Cancelled() {
|
|
return 0, false
|
|
}
|
|
var added int
|
|
if _, offHand := p.HeldItems(); !offHand.Empty() && offHand.Comparable(s) {
|
|
added, _ = p.offHand.AddItem(s)
|
|
}
|
|
if s.Count() != added {
|
|
n, _ := p.Inventory().AddItem(s.Grow(-added))
|
|
added += n
|
|
}
|
|
return added, true
|
|
}
|
|
|
|
// Experience returns the amount of experience the player has.
|
|
func (p *Player) Experience() int {
|
|
return p.experience.Experience()
|
|
}
|
|
|
|
// EnchantmentSeed is a seed used to calculate random enchantments with enchantment tables.
|
|
func (p *Player) EnchantmentSeed() int64 {
|
|
return p.enchantSeed
|
|
}
|
|
|
|
// ResetEnchantmentSeed resets the enchantment seed to a new random value.
|
|
func (p *Player) ResetEnchantmentSeed() {
|
|
p.enchantSeed = rand.Int64()
|
|
}
|
|
|
|
// AddExperience adds experience to the player.
|
|
func (p *Player) AddExperience(amount int) int {
|
|
ctx := event.C(p)
|
|
if p.Handler().HandleExperienceGain(ctx, &amount); ctx.Cancelled() {
|
|
return 0
|
|
}
|
|
before := p.experience.Level()
|
|
level, _ := p.experience.Add(amount)
|
|
if level/5 > before/5 {
|
|
p.PlaySound(sound.LevelUp{})
|
|
} else if amount > 0 {
|
|
p.PlaySound(sound.Experience{})
|
|
}
|
|
p.session().SendExperience(p.ExperienceLevel(), p.ExperienceProgress())
|
|
return amount
|
|
}
|
|
|
|
// RemoveExperience removes experience from the player.
|
|
func (p *Player) RemoveExperience(amount int) {
|
|
p.experience.Add(-amount)
|
|
p.session().SendExperience(p.ExperienceLevel(), p.ExperienceProgress())
|
|
}
|
|
|
|
// ExperienceLevel returns the experience level of the player.
|
|
func (p *Player) ExperienceLevel() int {
|
|
return p.experience.Level()
|
|
}
|
|
|
|
// SetExperienceLevel sets the experience level of the player. The level must have a value between 0 and 2,147,483,647,
|
|
// otherwise the method panics.
|
|
func (p *Player) SetExperienceLevel(level int) {
|
|
p.experience.SetLevel(level)
|
|
p.session().SendExperience(p.ExperienceLevel(), p.ExperienceProgress())
|
|
}
|
|
|
|
// ExperienceProgress returns the experience progress of the player.
|
|
func (p *Player) ExperienceProgress() float64 {
|
|
return p.experience.Progress()
|
|
}
|
|
|
|
// SetExperienceProgress sets the experience progress of the player. The progress must have a value between 0.0 and 1.0, otherwise
|
|
// the method panics.
|
|
func (p *Player) SetExperienceProgress(progress float64) {
|
|
p.experience.SetProgress(progress)
|
|
p.session().SendExperience(p.ExperienceLevel(), p.ExperienceProgress())
|
|
}
|
|
|
|
// CanCollectExperience checks if the player can collect experience, which is true if the player is not dead,
|
|
// is in a game mode that allows interaction and if 100ms have passed since the last experience collection.
|
|
func (p *Player) CanCollectExperience() bool {
|
|
if p.Dead() || !p.GameMode().AllowsInteraction() {
|
|
return false
|
|
}
|
|
if last := p.lastXPPickup; last != nil && time.Since(*last) < time.Millisecond*100 {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// CollectExperience makes the player collect the experience points passed, adding it to the experience manager. A bool
|
|
// is returned indicating whether the player was able to collect the experience or not, due to the 100ms delay between
|
|
// experience collection or if the player was dead or in a game mode that doesn't allow collection.
|
|
func (p *Player) CollectExperience(value int) bool {
|
|
if !p.CanCollectExperience() {
|
|
return false
|
|
}
|
|
value = p.mendItems(value)
|
|
now := time.Now()
|
|
p.lastXPPickup = &now
|
|
if value > 0 {
|
|
return p.AddExperience(value) > 0
|
|
}
|
|
|
|
p.PlaySound(sound.Experience{})
|
|
return true
|
|
}
|
|
|
|
// mendItems handles the mending enchantment when collecting experience, it then returns the leftover experience.
|
|
func (p *Player) mendItems(xp int) int {
|
|
mendingItems := make([]item.Stack, 0, 6)
|
|
held, offHand := p.HeldItems()
|
|
if _, ok := offHand.Enchantment(enchantment.Mending); ok && offHand.Durability() < offHand.MaxDurability() {
|
|
mendingItems = append(mendingItems, offHand)
|
|
}
|
|
if _, ok := held.Enchantment(enchantment.Mending); ok && held.Durability() < held.MaxDurability() {
|
|
mendingItems = append(mendingItems, held)
|
|
}
|
|
for _, i := range p.Armour().Items() {
|
|
if i.Durability() == i.MaxDurability() {
|
|
continue
|
|
}
|
|
if _, ok := i.Enchantment(enchantment.Mending); ok {
|
|
mendingItems = append(mendingItems, i)
|
|
}
|
|
}
|
|
length := len(mendingItems)
|
|
if length == 0 {
|
|
return xp
|
|
}
|
|
foundItem := mendingItems[rand.IntN(length)]
|
|
repairAmount := math.Min(float64(foundItem.MaxDurability()-foundItem.Durability()), float64(xp*2))
|
|
repairedItem := foundItem.WithDurability(foundItem.Durability() + int(repairAmount))
|
|
if repairAmount >= 2 {
|
|
// mending removes 1 experience point for every 2 durability points. If the repaired durability is less than 2,
|
|
// then no experience is removed.
|
|
xp -= int(math.Ceil(repairAmount / 2))
|
|
}
|
|
if offHand.Equal(foundItem) {
|
|
p.SetHeldItems(held, repairedItem)
|
|
} else if held.Equal(foundItem) {
|
|
p.SetHeldItems(repairedItem, offHand)
|
|
} else if slot, ok := p.Armour().Inventory().First(foundItem); ok {
|
|
_ = p.Armour().Inventory().SetItem(slot, repairedItem)
|
|
}
|
|
return xp
|
|
}
|
|
|
|
// Drop makes the player drop the item.Stack passed as an entity.Item, so that it may be picked up from the
|
|
// ground.
|
|
// The dropped item entity has a pickup delay of 2 seconds.
|
|
// The number of items that was dropped in the end is returned. It is generally the count of the stack passed
|
|
// or 0 if dropping the item.Stack was cancelled.
|
|
func (p *Player) Drop(s item.Stack) int {
|
|
ctx := event.C(p)
|
|
if p.Handler().HandleItemDrop(ctx, s); ctx.Cancelled() {
|
|
return 0
|
|
}
|
|
opts := world.EntitySpawnOpts{Position: p.Position().Add(mgl64.Vec3{0, 1.4}), Velocity: p.Rotation().Vec3().Mul(0.4)}
|
|
p.tx.AddEntity(entity.NewItemPickupDelay(opts, s, time.Second*2))
|
|
return s.Count()
|
|
}
|
|
|
|
// OpenBlockContainer opens a block container, such as a chest, at the position passed. If no container was
|
|
// present at that location, OpenBlockContainer does nothing.
|
|
// OpenBlockContainer will also do nothing if the player has no session connected to it.
|
|
func (p *Player) OpenBlockContainer(pos cube.Pos, tx *world.Tx) {
|
|
if p.session() != session.Nop {
|
|
p.session().OpenBlockContainer(pos, tx)
|
|
}
|
|
}
|
|
|
|
// HideEntity hides a world.Entity from the Player so that it can under no circumstance see it. Hidden entities can be
|
|
// made visible again through a call to ShowEntity.
|
|
func (p *Player) HideEntity(e world.Entity) {
|
|
if p.session() != session.Nop && p.H() != e.H() {
|
|
p.session().StopShowingEntity(e)
|
|
}
|
|
}
|
|
|
|
// ShowEntity shows a world.Entity previously hidden from the Player using HideEntity. It does nothing if the entity
|
|
// wasn't currently hidden.
|
|
func (p *Player) ShowEntity(e world.Entity) {
|
|
if p.session() != session.Nop {
|
|
p.session().StartShowingEntity(e)
|
|
}
|
|
}
|
|
|
|
// Latency returns a rolling average of latency between the sending and the receiving end of the connection of
|
|
// the player.
|
|
// The latency returned is updated continuously and is half the round trip time (RTT).
|
|
// If the Player does not have a session associated with it, Latency returns 0.
|
|
func (p *Player) Latency() time.Duration {
|
|
if p.session() == session.Nop {
|
|
return 0
|
|
}
|
|
return p.session().Latency()
|
|
}
|
|
|
|
// Tick ticks the entity, performing actions such as checking if the player is still breaking a block.
|
|
func (p *Player) Tick(tx *world.Tx, current int64) {
|
|
if p.Dead() {
|
|
return
|
|
}
|
|
if _, ok := p.tx.Liquid(cube.PosFromVec3(p.Position())); !ok {
|
|
p.StopSwimming()
|
|
if _, ok := p.Armour().Helmet().Item().(item.TurtleShell); ok {
|
|
p.AddEffect(effect.New(effect.WaterBreathing, 1, time.Second*10).WithoutParticles())
|
|
}
|
|
}
|
|
|
|
if _, ok := p.Armour().Chestplate().Item().(item.Elytra); ok && p.Gliding() {
|
|
if p.glideTicks += 1; p.glideTicks%20 == 0 {
|
|
d := p.damageItem(p.Armour().Chestplate(), 1)
|
|
p.armour.SetChestplate(d)
|
|
if d.Durability() < 2 {
|
|
p.StopGliding()
|
|
}
|
|
}
|
|
}
|
|
|
|
p.checkBlockCollisions(p.data.Vel)
|
|
p.onGround = p.checkOnGround(mgl64.Vec3{})
|
|
p.checkEntitySteppers()
|
|
|
|
p.effects.Tick(p, p.tx)
|
|
|
|
p.tickFood()
|
|
p.tickAirSupply()
|
|
|
|
if p.Position()[1] < float64(p.tx.Range()[0]) {
|
|
p.Hurt(4, entity.VoidDamageSource{})
|
|
}
|
|
if p.insideOfSolid() {
|
|
p.Hurt(1, entity.SuffocationDamageSource{})
|
|
}
|
|
|
|
if p.OnFireDuration() > 0 {
|
|
p.fireTicks -= 1
|
|
if !p.GameMode().AllowsTakingDamage() || p.OnFireDuration() <= 0 || p.tx.RainingAt(cube.PosFromVec3(p.Position())) {
|
|
p.Extinguish()
|
|
}
|
|
if p.OnFireDuration()%time.Second == 0 {
|
|
p.Hurt(1, block.FireDamageSource{})
|
|
}
|
|
}
|
|
|
|
held, _ := p.HeldItems()
|
|
if current%4 == 0 && p.usingItem {
|
|
if _, ok := held.Item().(item.Consumable); ok {
|
|
// Eating particles seem to happen roughly every 4 ticks.
|
|
for _, v := range p.viewers() {
|
|
v.ViewEntityAction(p, entity.EatAction{})
|
|
}
|
|
}
|
|
}
|
|
|
|
if p.usingItem {
|
|
if c, ok := held.Item().(item.Chargeable); ok {
|
|
c.ContinueCharge(p, tx, p.useContext(), p.useDuration())
|
|
}
|
|
}
|
|
if p.breaking {
|
|
p.ContinueBreaking(p.breakingFace)
|
|
}
|
|
|
|
for it, ti := range p.cooldowns {
|
|
if time.Now().After(ti) {
|
|
delete(p.cooldowns, it)
|
|
}
|
|
}
|
|
|
|
p.session().SendDebugShapes(tx.World().Dimension())
|
|
p.session().SendHudUpdates()
|
|
|
|
if p.prevWorld != tx.World() && p.prevWorld != nil {
|
|
p.Handler().HandleChangeWorld(p, p.prevWorld, tx.World())
|
|
}
|
|
p.prevWorld = tx.World()
|
|
|
|
if p.session() == session.Nop && !p.Immobile() {
|
|
m := p.mc.TickMovement(p, p.Position(), p.Velocity(), p.Rotation(), p.tx)
|
|
m.Send()
|
|
|
|
p.data.Vel = m.Velocity()
|
|
p.Move(m.Position().Sub(p.Position()), 0, 0)
|
|
} else {
|
|
p.data.Vel = mgl64.Vec3{}
|
|
}
|
|
}
|
|
|
|
// ViewLayer returns the ViewLayer attached to the player's session.
|
|
func (p *Player) ViewLayer() *world.ViewLayer {
|
|
return p.session().ViewLayer()
|
|
}
|
|
|
|
// ViewNameTag overrides the public name tag of the entity for this player.
|
|
func (p *Player) ViewNameTag(entity world.Entity, nameTag string) {
|
|
p.session().ViewNameTag(entity, nameTag)
|
|
}
|
|
|
|
// ViewPublicNameTag removes the name tag override of the entity for this player.
|
|
func (p *Player) ViewPublicNameTag(entity world.Entity) {
|
|
p.session().ViewPublicNameTag(entity)
|
|
}
|
|
|
|
// ViewScoreTag overrides the public score tag of the entity for this player.
|
|
func (p *Player) ViewScoreTag(entity world.Entity, scoreTag string) {
|
|
p.session().ViewScoreTag(entity, scoreTag)
|
|
}
|
|
|
|
// ViewPublicScoreTag removes the score tag override of the entity for this player.
|
|
func (p *Player) ViewPublicScoreTag(entity world.Entity) {
|
|
p.session().ViewPublicScoreTag(entity)
|
|
}
|
|
|
|
// ViewVisibility overrides the public visibility of the entity for this player.
|
|
func (p *Player) ViewVisibility(entity world.Entity, level world.VisibilityLevel) {
|
|
p.session().ViewVisibility(entity, level)
|
|
}
|
|
|
|
// RemoveViewLayer removes all view-layer overrides of the entity for this player.
|
|
func (p *Player) RemoveViewLayer(entity world.Entity) {
|
|
p.session().RemoveViewLayer(entity)
|
|
}
|
|
|
|
// tickAirSupply tick's the player's air supply, consuming it when underwater, and replenishing it when out of water.
|
|
func (p *Player) tickAirSupply() {
|
|
if !p.canBreathe() {
|
|
if r, ok := p.Armour().Helmet().Enchantment(enchantment.Respiration); ok && rand.Float64() <= enchantment.Respiration.Chance(r.Level()) {
|
|
// respiration grants a chance to avoid drowning damage every tick.
|
|
return
|
|
}
|
|
if p.airSupplyTicks -= 1; p.airSupplyTicks <= -20 {
|
|
p.airSupplyTicks = 0
|
|
p.Hurt(2, entity.DrowningDamageSource{})
|
|
}
|
|
p.breathing = false
|
|
p.updateState()
|
|
} else if !p.breathing && p.airSupplyTicks < p.maxAirSupplyTicks {
|
|
p.airSupplyTicks = min(p.airSupplyTicks+5, p.maxAirSupplyTicks)
|
|
p.breathing = p.airSupplyTicks == p.maxAirSupplyTicks
|
|
p.updateState()
|
|
}
|
|
}
|
|
|
|
// tickFood ticks food related functionality, such as the depletion of the food bar and regeneration if it
|
|
// is full enough.
|
|
func (p *Player) tickFood() {
|
|
if p.hunger.foodTick%10 == 0 && (p.hunger.canQuicklyRegenerate() || p.tx.World().Difficulty().FoodRegenerates()) {
|
|
if p.tx.World().Difficulty().FoodRegenerates() {
|
|
p.AddFood(1)
|
|
}
|
|
if p.hunger.foodTick%20 == 0 {
|
|
p.regenerate(true)
|
|
}
|
|
}
|
|
if p.hunger.foodTick == 1 {
|
|
if p.hunger.canRegenerate() {
|
|
p.regenerate(false)
|
|
} else if p.hunger.starving() {
|
|
p.starve()
|
|
}
|
|
}
|
|
|
|
if !p.hunger.canSprint() {
|
|
p.StopSprinting()
|
|
}
|
|
|
|
p.hunger.foodTick++
|
|
if p.hunger.foodTick > 80 {
|
|
p.hunger.foodTick = 1
|
|
}
|
|
}
|
|
|
|
// regenerate attempts to regenerate half a heart of health, typically caused by a full food bar.
|
|
func (p *Player) regenerate(exhaust bool) {
|
|
if p.Health() == p.MaxHealth() {
|
|
return
|
|
}
|
|
p.Heal(1, entity.FoodHealingSource{})
|
|
if exhaust {
|
|
p.Exhaust(6)
|
|
}
|
|
}
|
|
|
|
// starve deals starvation damage to the player if the difficult allows it. In peaceful mode, no damage will
|
|
// ever be dealt. In easy mode, damage will only be dealt if the player has more than 10 health. In normal
|
|
// mode, damage will only be dealt if the player has more than 2 health and in hard mode, damage will always
|
|
// be dealt.
|
|
func (p *Player) starve() {
|
|
if p.Health() > p.tx.World().Difficulty().StarvationHealthLimit() {
|
|
p.Hurt(1, StarvationDamageSource{})
|
|
}
|
|
}
|
|
|
|
// AirSupply returns the player's remaining air supply.
|
|
func (p *Player) AirSupply() time.Duration {
|
|
return time.Duration(p.airSupplyTicks) * time.Second / 20
|
|
}
|
|
|
|
// SetAirSupply sets the player's remaining air supply.
|
|
func (p *Player) SetAirSupply(duration time.Duration) {
|
|
p.airSupplyTicks = int(duration.Milliseconds() / 50)
|
|
p.updateState()
|
|
}
|
|
|
|
// MaxAirSupply returns the player's maximum air supply.
|
|
func (p *Player) MaxAirSupply() time.Duration {
|
|
return time.Duration(p.maxAirSupplyTicks) * time.Second / 20
|
|
}
|
|
|
|
// SetMaxAirSupply sets the player's maximum air supply.
|
|
func (p *Player) SetMaxAirSupply(duration time.Duration) {
|
|
p.maxAirSupplyTicks = int(duration.Milliseconds() / 50)
|
|
p.updateState()
|
|
}
|
|
|
|
// canBreathe returns true if the player can currently breathe.
|
|
func (p *Player) canBreathe() bool {
|
|
canTakeDamage := p.GameMode().AllowsTakingDamage()
|
|
_, waterBreathing := p.effects.Effect(effect.WaterBreathing)
|
|
_, conduitPower := p.effects.Effect(effect.ConduitPower)
|
|
return !canTakeDamage || waterBreathing || conduitPower || (!p.insideOfWater() && !p.insideOfSolid())
|
|
}
|
|
|
|
// breathingDistanceBelowEyes is the lowest distance the player can be in water and still be able to breathe based on
|
|
// the player's eye height.
|
|
const breathingDistanceBelowEyes = 0.11111111
|
|
|
|
// insideOfWater returns true if the player is currently underwater.
|
|
func (p *Player) insideOfWater() bool {
|
|
pos := cube.PosFromVec3(entity.EyePosition(p))
|
|
if l, ok := p.tx.Liquid(pos); ok {
|
|
if _, ok := l.(block.Water); ok {
|
|
d := float64(l.SpreadDecay()) + 1
|
|
if l.LiquidFalling() {
|
|
d = 1
|
|
}
|
|
return p.Position().Y() < (pos.Side(cube.FaceUp).Vec3().Y())-(d/9-breathingDistanceBelowEyes)
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// insideOfSolid returns true if the player is inside a solid block.
|
|
func (p *Player) insideOfSolid() bool {
|
|
pos := cube.PosFromVec3(entity.EyePosition(p))
|
|
b, box := p.tx.Block(pos), p.handle.Type().BBox(p).Translate(p.Position())
|
|
|
|
_, solid := b.Model().(model.Solid)
|
|
if !solid {
|
|
// Not solid.
|
|
return false
|
|
}
|
|
d, diffuses := b.(block.LightDiffuser)
|
|
if diffuses && d.LightDiffusionLevel() == 0 {
|
|
// Transparent.
|
|
return false
|
|
}
|
|
for _, blockBox := range b.Model().BBox(pos, p.tx) {
|
|
if blockBox.Translate(pos.Vec3()).IntersectsWith(box) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// checkCollisions checks the player's block collisions.
|
|
func (p *Player) checkBlockCollisions(vel mgl64.Vec3) {
|
|
entityBBox := Type.BBox(p).Translate(p.Position())
|
|
deltaX, deltaY, deltaZ := vel[0], vel[1], vel[2]
|
|
|
|
p.checkEntityInsiders(entityBBox)
|
|
|
|
grown := entityBBox.Extend(vel).Grow(0.25)
|
|
low, high := grown.Min(), grown.Max()
|
|
minX, minY, minZ := int(math.Floor(low[0])), int(math.Floor(low[1])), int(math.Floor(low[2]))
|
|
maxX, maxY, maxZ := int(math.Ceil(high[0])), int(math.Ceil(high[1])), int(math.Ceil(high[2]))
|
|
|
|
// A prediction of one BBox per block, plus an additional 2, in case
|
|
blocks := make([]cube.BBox, 0, (maxX-minX)*(maxY-minY)*(maxZ-minZ)+2)
|
|
for y := minY; y <= maxY; y++ {
|
|
for x := minX; x <= maxX; x++ {
|
|
for z := minZ; z <= maxZ; z++ {
|
|
pos := cube.Pos{x, y, z}
|
|
boxes := p.tx.Block(pos).Model().BBox(pos, p.tx)
|
|
for _, box := range boxes {
|
|
blocks = append(blocks, box.Translate(pos.Vec3()))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// epsilon is the epsilon used for thresholds for change used for change in position and velocity.
|
|
const epsilon = 0.001
|
|
|
|
if !mgl64.FloatEqualThreshold(deltaY, 0, epsilon) {
|
|
// First we move the entity BBox on the Y axis.
|
|
for _, blockBBox := range blocks {
|
|
deltaY = entityBBox.YOffset(blockBBox, deltaY)
|
|
}
|
|
entityBBox = entityBBox.Translate(mgl64.Vec3{0, deltaY})
|
|
}
|
|
if !mgl64.FloatEqualThreshold(deltaX, 0, epsilon) {
|
|
// Then on the X axis.
|
|
for _, blockBBox := range blocks {
|
|
deltaX = entityBBox.XOffset(blockBBox, deltaX)
|
|
}
|
|
entityBBox = entityBBox.Translate(mgl64.Vec3{deltaX})
|
|
}
|
|
if !mgl64.FloatEqualThreshold(deltaZ, 0, epsilon) {
|
|
// And finally on the Z axis.
|
|
for _, blockBBox := range blocks {
|
|
deltaZ = entityBBox.ZOffset(blockBBox, deltaZ)
|
|
}
|
|
}
|
|
|
|
p.collidedHorizontally = !mgl64.FloatEqual(deltaX, vel[0]) || !mgl64.FloatEqual(deltaZ, vel[2])
|
|
p.collidedVertically = !mgl64.FloatEqual(deltaY, vel[1])
|
|
}
|
|
|
|
// checkEntityInsiders checks if the player is colliding with any EntityInsider blocks.
|
|
func (p *Player) checkEntityInsiders(entityBBox cube.BBox) {
|
|
box := entityBBox.Grow(-0.0001)
|
|
low, high := cube.PosFromVec3(box.Min()), cube.PosFromVec3(box.Max())
|
|
|
|
for y := low[1]; y <= high[1]; y++ {
|
|
for x := low[0]; x <= high[0]; x++ {
|
|
for z := low[2]; z <= high[2]; z++ {
|
|
blockPos := cube.Pos{x, y, z}
|
|
b := p.tx.Block(blockPos)
|
|
if collide, ok := b.(block.EntityInsider); ok {
|
|
collide.EntityInside(blockPos, p.tx, p)
|
|
if _, liquid := b.(world.Liquid); liquid {
|
|
continue
|
|
}
|
|
}
|
|
|
|
if l, ok := p.tx.Liquid(blockPos); ok {
|
|
if collide, ok := l.(block.EntityInsider); ok {
|
|
collide.EntityInside(blockPos, p.tx, p)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// checkEntitySteppers checks if the player is standing on any EntityStepper blocks.
|
|
func (p *Player) checkEntitySteppers() {
|
|
if !p.OnGround() {
|
|
return
|
|
}
|
|
box := Type.BBox(p).Translate(p.Position()).Grow(-0.0001)
|
|
low, high := cube.PosFromVec3(box.Min()), cube.PosFromVec3(box.Max())
|
|
y := int(math.Floor(box.Min()[1] - 0.0001))
|
|
|
|
for x := low[0]; x <= high[0]; x++ {
|
|
for z := low[2]; z <= high[2]; z++ {
|
|
pos := cube.Pos{x, y, z}
|
|
if stepper, ok := p.tx.Block(pos).(block.EntityStepper); ok {
|
|
stepper.EntityStepOn(pos, p.tx, p)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// checkOnGround checks if the player is currently considered to be on the ground.
|
|
func (p *Player) checkOnGround(deltaPos mgl64.Vec3) bool {
|
|
box := Type.BBox(p).Translate(p.Position()).Extend(mgl64.Vec3{0, -0.05}).Extend(deltaPos.Mul(-1.0))
|
|
b := box.Grow(1)
|
|
|
|
epsilon := mgl64.Vec3{mgl64.Epsilon, mgl64.Epsilon, mgl64.Epsilon}
|
|
low, high := cube.PosFromVec3(b.Min().Add(epsilon)), cube.PosFromVec3(b.Max().Sub(epsilon))
|
|
for x := low[0]; x <= high[0]; x++ {
|
|
for z := low[2]; z <= high[2]; z++ {
|
|
for y := low[1]; y < high[1]; y++ {
|
|
pos := cube.Pos{x, y, z}
|
|
for _, bb := range p.tx.Block(pos).Model().BBox(pos, p.tx) {
|
|
if bb.Translate(pos.Vec3()).IntersectsWith(box) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Scale returns the scale modifier of the Player. The default value for a normal scale is 1. A scale of 0
|
|
// will make the Player completely invisible.
|
|
func (p *Player) Scale() float64 {
|
|
return p.scale
|
|
}
|
|
|
|
// SetScale changes the scale modifier of the Player. The default value for a normal scale is 1. A scale of 0
|
|
// will make the Player completely invisible.
|
|
func (p *Player) SetScale(s float64) {
|
|
p.scale = s
|
|
p.updateState()
|
|
}
|
|
|
|
// OnGround checks if the player is considered to be on the ground.
|
|
func (p *Player) OnGround() bool {
|
|
if p.session() == session.Nop {
|
|
return p.mc.OnGround()
|
|
}
|
|
return p.onGround
|
|
}
|
|
|
|
// EyeHeight returns the eye height of the player: 1.62, 1.26 if player is sneaking or 0.52 if the player is
|
|
// swimming, gliding or crawling.
|
|
func (p *Player) EyeHeight() float64 {
|
|
switch {
|
|
case p.swimming || p.crawling || p.gliding:
|
|
return 0.4
|
|
case p.sneaking:
|
|
return 1.27
|
|
default:
|
|
return 1.62
|
|
}
|
|
}
|
|
|
|
// TorsoHeight returns the player's torso offset above its feet. This is 0.1 blocks below the eye height.
|
|
func (p *Player) TorsoHeight() float64 {
|
|
return p.EyeHeight() - 0.1
|
|
}
|
|
|
|
// PlaySound plays a world.Sound that only this Player can hear. Unlike World.PlaySound, it is not broadcast
|
|
// to players around it.
|
|
func (p *Player) PlaySound(sound world.Sound) {
|
|
p.session().PlaySound(sound, entity.EyePosition(p))
|
|
}
|
|
|
|
// ShowParticle shows a particle that only this Player can see. Unlike World.AddParticle, it is not broadcast
|
|
// to players around it.
|
|
func (p *Player) ShowParticle(pos mgl64.Vec3, particle world.Particle) {
|
|
p.session().ViewParticle(pos, particle)
|
|
}
|
|
|
|
// OpenSign makes the player open the sign at the cube.Pos passed, with the specific side provided. The client will not
|
|
// show the interface if it is not aware of a sign at the position.
|
|
func (p *Player) OpenSign(pos cube.Pos, frontSide bool) {
|
|
p.session().OpenSign(pos, frontSide)
|
|
}
|
|
|
|
// EditSign edits the sign at the cube.Pos passed and writes the text passed to a sign at that position. If no sign is
|
|
// present, an error is returned.
|
|
func (p *Player) EditSign(pos cube.Pos, frontText, backText string) error {
|
|
sign, ok := p.tx.Block(pos).(block.Sign)
|
|
if !ok {
|
|
return fmt.Errorf("edit sign: no sign at position %v", pos)
|
|
}
|
|
|
|
if sign.Waxed {
|
|
return nil
|
|
} else if frontText == sign.Front.Text && backText == sign.Back.Text {
|
|
return nil
|
|
}
|
|
|
|
ctx := event.C(p)
|
|
if frontText != sign.Front.Text {
|
|
if p.Handler().HandleSignEdit(ctx, pos, true, sign.Front.Text, frontText); ctx.Cancelled() {
|
|
p.resendNearbyBlock(pos)
|
|
return nil
|
|
}
|
|
sign.Front.Text = frontText
|
|
sign.Front.Owner = p.XUID()
|
|
} else {
|
|
if p.Handler().HandleSignEdit(ctx, pos, false, sign.Back.Text, backText); ctx.Cancelled() {
|
|
p.resendNearbyBlock(pos)
|
|
return nil
|
|
}
|
|
sign.Back.Text = backText
|
|
sign.Back.Owner = p.XUID()
|
|
}
|
|
p.tx.SetBlock(pos, sign, nil)
|
|
return nil
|
|
}
|
|
|
|
// TurnLecternPage edits the lectern at the cube.Pos passed by turning the page to the page passed. If no lectern is
|
|
// present, an error is returned.
|
|
func (p *Player) TurnLecternPage(pos cube.Pos, page int) error {
|
|
lectern, ok := p.tx.Block(pos).(block.Lectern)
|
|
if !ok {
|
|
return fmt.Errorf("edit lectern: no lectern at position %v", pos)
|
|
}
|
|
|
|
ctx := event.C(p)
|
|
if p.Handler().HandleLecternPageTurn(ctx, pos, lectern.Page, &page); ctx.Cancelled() {
|
|
return nil
|
|
}
|
|
|
|
lectern.Page = page
|
|
p.tx.SetBlock(pos, lectern, nil)
|
|
return nil
|
|
}
|
|
|
|
// updateState updates the state of the player to all viewers of the player.
|
|
func (p *Player) updateState() {
|
|
for _, v := range p.viewers() {
|
|
v.ViewEntityState(p)
|
|
}
|
|
}
|
|
|
|
// Breathing checks if the player is currently able to breathe. If it's underwater and the player does not
|
|
// have the water breathing or conduit power effect, this returns false.
|
|
// If the player is in creative or spectator mode, Breathing always returns true.
|
|
func (p *Player) Breathing() bool {
|
|
_, breathing := p.Effect(effect.WaterBreathing)
|
|
_, conduitPower := p.Effect(effect.ConduitPower)
|
|
_, submerged := p.tx.Liquid(cube.PosFromVec3(entity.EyePosition(p)))
|
|
return !p.GameMode().AllowsTakingDamage() || !submerged || breathing || conduitPower
|
|
}
|
|
|
|
// SwingArm makes the player swing its arm.
|
|
func (p *Player) SwingArm() {
|
|
if p.Dead() {
|
|
return
|
|
}
|
|
for _, v := range p.viewers() {
|
|
v.ViewEntityAction(p, entity.SwingArmAction{})
|
|
}
|
|
}
|
|
|
|
// PunchAir makes the player punch the air and plays the sound for attacking with no damage.
|
|
func (p *Player) PunchAir() {
|
|
if p.Dead() {
|
|
return
|
|
}
|
|
ctx := event.C(p)
|
|
if p.Handler().HandlePunchAir(ctx); ctx.Cancelled() {
|
|
return
|
|
}
|
|
p.SwingArm()
|
|
p.tx.PlaySound(p.Position(), sound.Attack{})
|
|
}
|
|
|
|
// UpdateDiagnostics updates the diagnostics of the player.
|
|
func (p *Player) UpdateDiagnostics(d session.Diagnostics) {
|
|
p.Handler().HandleDiagnostics(p, d)
|
|
}
|
|
|
|
// ShowHudElement shows a HUD element to the player if it is not already shown.
|
|
func (p *Player) ShowHudElement(e hud.Element) {
|
|
p.session().ShowHudElement(e)
|
|
}
|
|
|
|
// HideHudElement hides a HUD element from the player if it is not already hidden.
|
|
func (p *Player) HideHudElement(e hud.Element) {
|
|
p.session().HideHudElement(e)
|
|
}
|
|
|
|
// HudElementHidden checks if a HUD element is currently hidden from the player.
|
|
func (p *Player) HudElementHidden(e hud.Element) bool {
|
|
return p.session().HudElementHidden(e)
|
|
}
|
|
|
|
// AddDebugShape adds a debug shape to be rendered to the player. If the shape already exists, it will be
|
|
// updated with the new information.
|
|
func (p *Player) AddDebugShape(shape debug.Shape) {
|
|
p.session().AddDebugShape(shape)
|
|
}
|
|
|
|
// RemoveDebugShape removes a debug shape from the player by its unique identifier.
|
|
func (p *Player) RemoveDebugShape(shape debug.Shape) {
|
|
p.session().RemoveDebugShape(shape)
|
|
}
|
|
|
|
// VisibleDebugShapes returns a slice of all debug shapes that are currently being shown to the player.
|
|
func (p *Player) VisibleDebugShapes() []debug.Shape {
|
|
return p.session().VisibleDebugShapes()
|
|
}
|
|
|
|
// RemoveAllDebugShapes removes all rendered debug shapes from the player, as well as any shapes that have
|
|
// not yet been rendered.
|
|
func (p *Player) RemoveAllDebugShapes() {
|
|
p.session().RemoveAllDebugShapes()
|
|
}
|
|
|
|
// damageItem damages the item stack passed with the damage passed and returns the new stack. If the item
|
|
// broke, a breaking sound is played.
|
|
// If the player is not survival, the original stack is returned.
|
|
func (p *Player) damageItem(s item.Stack, d int) item.Stack {
|
|
if p.GameMode().CreativeInventory() || d == 0 || s.MaxDurability() == -1 {
|
|
return s
|
|
}
|
|
ctx := event.C(p)
|
|
if p.Handler().HandleItemDamage(ctx, s, &d); ctx.Cancelled() || d <= 0 {
|
|
return s
|
|
}
|
|
if e, ok := s.Enchantment(enchantment.Unbreaking); ok {
|
|
d = enchantment.Unbreaking.Reduce(s.Item(), e.Level(), d)
|
|
}
|
|
if s = s.Damage(d); s.Empty() {
|
|
p.tx.PlaySound(p.Position(), sound.ItemBreak{})
|
|
}
|
|
return s
|
|
}
|
|
|
|
// subtractItem subtracts d from the count of the item stack passed and returns it, if the player is in
|
|
// survival or adventure mode.
|
|
func (p *Player) subtractItem(s item.Stack, d int) item.Stack {
|
|
if !p.GameMode().CreativeInventory() && d != 0 {
|
|
return s.Grow(-d)
|
|
}
|
|
return s
|
|
}
|
|
|
|
// addNewItem adds the new item of the context passed to the inventory.
|
|
func (p *Player) addNewItem(ctx *item.UseContext) {
|
|
if (ctx.NewItemSurvivalOnly && p.GameMode().CreativeInventory()) || ctx.NewItem.Empty() {
|
|
return
|
|
}
|
|
held, left := p.HeldItems()
|
|
if held.Empty() {
|
|
p.SetHeldItems(ctx.NewItem, left)
|
|
return
|
|
}
|
|
n, err := p.Inventory().AddItem(ctx.NewItem)
|
|
if err != nil {
|
|
// Not all items could be added to the inventory, so drop the rest.
|
|
p.Drop(ctx.NewItem.Grow(ctx.NewItem.Count() - n))
|
|
}
|
|
if p.Dead() {
|
|
p.dropItems()
|
|
}
|
|
}
|
|
|
|
// canReach checks if a player can reach a position with its current range. The range depends on if the player
|
|
// is either survival or creative mode.
|
|
func (p *Player) canReach(pos mgl64.Vec3) bool {
|
|
dist := entity.EyePosition(p).Sub(pos).Len()
|
|
return !p.Dead() && p.GameMode().AllowsInteraction() &&
|
|
(dist <= 8.0 || (dist <= 14.0 && p.GameMode().CreativeInventory()))
|
|
}
|
|
|
|
// Disconnect closes the player and removes it from the world.
|
|
// Disconnect, unlike Close, allows a custom message to be passed to show to the player when it is
|
|
// disconnected. The message is formatted following the rules of fmt.Sprintln without a newline at the end.
|
|
func (p *Player) Disconnect(msg ...any) {
|
|
p.once.Do(func() {
|
|
p.close(format(msg))
|
|
})
|
|
}
|
|
|
|
// Close closes the player and removes it from the world.
|
|
// Close disconnects the player with a 'Connection closed.' message. Disconnect should be used to disconnect a
|
|
// player with a custom message.
|
|
func (p *Player) Close() error {
|
|
p.once.Do(func() {
|
|
p.close("Connection closed.")
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// close closes the player without disconnecting it. It executes code shared by both the closing and the
|
|
// disconnecting of players.
|
|
func (p *Player) close(msg string) {
|
|
// If the player is being disconnected while they are dead, we respawn the player
|
|
// so that the player logic works correctly the next time they join.
|
|
if p.Dead() && p.session() != nil {
|
|
p.respawn(func(np *Player) {
|
|
np.quit(msg)
|
|
})
|
|
return
|
|
}
|
|
p.quit(msg)
|
|
}
|
|
|
|
func (p *Player) quit(msg string) {
|
|
p.h.HandleQuit(p)
|
|
p.h = NopHandler{}
|
|
|
|
if s := p.s; s != nil {
|
|
s.Disconnect(msg)
|
|
s.CloseConnection()
|
|
return
|
|
}
|
|
// Only remove the player from the world if it's not attached to a session. If it is attached to a session, the
|
|
// session will remove the player once ready.
|
|
p.tx.RemoveEntity(p)
|
|
_ = p.handle.Close()
|
|
}
|
|
|
|
// Data returns the player data that needs to be saved. This is used when the player
|
|
// gets disconnected and the player provider needs to save the data.
|
|
func (p *Player) Data() Config {
|
|
p.hunger.mu.RLock()
|
|
defer p.hunger.mu.RUnlock()
|
|
return Config{
|
|
Session: p.s,
|
|
Skin: p.skin,
|
|
XUID: p.xuid,
|
|
UUID: p.UUID(),
|
|
Name: p.nameTag,
|
|
Locale: p.locale,
|
|
GameMode: p.gameMode,
|
|
Position: p.Position(),
|
|
Rotation: p.Rotation(),
|
|
Velocity: p.Velocity(),
|
|
Health: p.Health(),
|
|
MaxHealth: p.MaxHealth(),
|
|
FoodTick: p.hunger.foodTick,
|
|
Food: p.hunger.foodLevel,
|
|
Exhaustion: p.hunger.exhaustionLevel,
|
|
Saturation: p.hunger.saturationLevel,
|
|
AirSupply: p.airSupplyTicks,
|
|
MaxAirSupply: p.maxAirSupplyTicks,
|
|
EnchantmentSeed: p.enchantSeed,
|
|
Experience: p.experience.Experience(),
|
|
HeldSlot: int(*p.heldSlot),
|
|
Inventory: p.inv,
|
|
OffHand: p.offHand,
|
|
Armour: p.armour,
|
|
EnderChestInventory: p.enderChest,
|
|
FireTicks: p.fireTicks,
|
|
FallDistance: p.fallDistance,
|
|
Effects: p.Effects(),
|
|
}
|
|
}
|
|
|
|
// session returns the network session of the player. If it has one, it is returned. If not, a no-op session
|
|
// is returned.
|
|
func (p *Player) session() *session.Session {
|
|
if s := p.s; s != nil {
|
|
return s
|
|
}
|
|
return session.Nop
|
|
}
|
|
|
|
// useContext returns an item.UseContext initialised for a Player.
|
|
func (p *Player) useContext() *item.UseContext {
|
|
call := func(ctx *inventory.Context, slot int, it item.Stack, f func(ctx *inventory.Context, slot int, it item.Stack)) error {
|
|
if ctx.Cancelled() {
|
|
return fmt.Errorf("action was cancelled")
|
|
}
|
|
f(ctx, slot, it)
|
|
if ctx.Cancelled() {
|
|
return fmt.Errorf("action was cancelled")
|
|
}
|
|
return nil
|
|
}
|
|
return &item.UseContext{
|
|
SwapHeldWithArmour: func(i int) {
|
|
src, dst, srcInv, dstInv := int(*p.heldSlot), i, p.inv, p.armour.Inventory()
|
|
srcIt, _ := srcInv.Item(src)
|
|
dstIt, _ := dstInv.Item(dst)
|
|
|
|
ctx := event.C(inventory.Holder(p))
|
|
_ = call(ctx, src, srcIt, srcInv.Handler().HandleTake)
|
|
_ = call(ctx, src, dstIt, srcInv.Handler().HandlePlace)
|
|
_ = call(ctx, dst, dstIt, dstInv.Handler().HandleTake)
|
|
if err := call(ctx, dst, srcIt, dstInv.Handler().HandlePlace); err == nil {
|
|
_ = srcInv.SetItem(src, dstIt)
|
|
_ = dstInv.SetItem(dst, srcIt)
|
|
p.PlaySound(sound.EquipItem{Item: srcIt.Item()})
|
|
}
|
|
},
|
|
FirstFunc: func(comparable func(item.Stack) bool) (item.Stack, bool) {
|
|
_, left := p.HeldItems()
|
|
if !left.Empty() && comparable(left) {
|
|
return left, true
|
|
}
|
|
inv := p.Inventory()
|
|
s, ok := inv.FirstFunc(comparable)
|
|
if !ok {
|
|
return item.Stack{}, false
|
|
}
|
|
it, _ := inv.Item(s)
|
|
return it, ok
|
|
},
|
|
}
|
|
}
|
|
|
|
// Handler returns the Handler of the player.
|
|
func (p *Player) Handler() Handler {
|
|
return p.h
|
|
}
|
|
|
|
// broadcastItems broadcasts the items held to viewers.
|
|
func (p *Player) broadcastItems(int, item.Stack, item.Stack) {
|
|
for _, viewer := range p.viewers() {
|
|
viewer.ViewEntityItems(p)
|
|
}
|
|
}
|
|
|
|
// broadcastArmour broadcasts the armour equipped to viewers.
|
|
func (p *Player) broadcastArmour(_ int, before, after item.Stack) {
|
|
if before.Comparable(after) && before.Empty() == after.Empty() {
|
|
// Only send armour if the type of the armour changed.
|
|
return
|
|
}
|
|
for _, viewer := range p.viewers() {
|
|
viewer.ViewEntityArmour(p)
|
|
}
|
|
}
|
|
|
|
// viewers returns a list of all viewers of the Player.
|
|
func (p *Player) viewers() []world.Viewer {
|
|
viewers := p.tx.Viewers(p.Position())
|
|
var s world.Viewer = p.session()
|
|
if slices.Index(viewers, s) == -1 && p.s != nil {
|
|
return append(viewers, p.s)
|
|
}
|
|
return viewers
|
|
}
|
|
|
|
// withinChunkRadius checks if the position provided is within the chunk radius of the player.
|
|
func (p *Player) withinChunkRadius(pos mgl64.Vec3) bool {
|
|
playerChunkX, playerChunkZ := int(p.Position().X())>>4, int(p.Position().Z())>>4
|
|
posChunkX, posChunkZ := int(pos.X())>>4, int(pos.Z())>>4
|
|
dx, dz := playerChunkX-posChunkX, playerChunkZ-posChunkZ
|
|
if dx < 0 {
|
|
dx = -dx
|
|
}
|
|
if dz < 0 {
|
|
dz = -dz
|
|
}
|
|
r := int(p.session().ChunkRadius())
|
|
return dx <= r && dz <= r
|
|
}
|
|
|
|
// resendNearbyBlocks resends the block at cube.Pos and its adjacent blocks (if faces provided),
|
|
// but only if they are within the player's render distance.
|
|
func (p *Player) resendNearbyBlocks(pos cube.Pos, faces ...cube.Face) {
|
|
if p.session() == session.Nop {
|
|
return
|
|
}
|
|
p.resendNearbyBlock(pos)
|
|
for _, f := range faces {
|
|
p.resendNearbyBlock(pos.Side(f))
|
|
}
|
|
}
|
|
|
|
// resendNearbyBlock resends a block at cube.Pos if it is within the player's render distance.
|
|
func (p *Player) resendNearbyBlock(pos cube.Pos) {
|
|
if p.session() == session.Nop {
|
|
return
|
|
}
|
|
if !p.withinChunkRadius(pos.Vec3()) {
|
|
// This is a safety check. Without it, clients could request block resends for arbitrary world positions
|
|
// (including unloaded chunks). A malicious client could repeatedly trigger such requests and force the server
|
|
// to allocate memory for chunks, potentially exhausting RAM.
|
|
return
|
|
}
|
|
b := p.tx.Block(pos)
|
|
p.session().ViewBlockUpdate(pos, b, 0)
|
|
if _, ok := b.(world.LiquidDisplacer); ok {
|
|
liq, _ := p.tx.Liquid(pos)
|
|
p.session().ViewBlockUpdate(pos, liq, 1)
|
|
}
|
|
}
|
|
|
|
// 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(strings.TrimSuffix(fmt.Sprintln(a...), "\n"), "\n")
|
|
}
|