diff --git a/server/block/nether_sprouts.go b/server/block/nether_sprouts.go index b22e41166..11b051dd0 100644 --- a/server/block/nether_sprouts.go +++ b/server/block/nether_sprouts.go @@ -17,7 +17,7 @@ type NetherSprouts struct { // NeighbourUpdateTick ... func (n NetherSprouts) NeighbourUpdateTick(pos, _ cube.Pos, w *world.World) { if !supportsVegetation(n, w.Block(pos.Side(cube.FaceDown))) { - w.SetBlock(pos, nil, nil) //TODO: Nylium & mycelium + w.SetBlock(pos, nil, nil) // TODO: Nylium & mycelium } } @@ -28,7 +28,7 @@ func (n NetherSprouts) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, w return false } if !supportsVegetation(n, w.Block(pos.Side(cube.FaceDown))) { - return false //TODO: Nylium & mycelium + return false // TODO: Nylium & mycelium } place(w, pos, n, user, ctx) diff --git a/server/block/wall.go b/server/block/wall.go index 3bfd32ace..332edbe12 100644 --- a/server/block/wall.go +++ b/server/block/wall.go @@ -155,7 +155,7 @@ func (w Wall) calculateConnections(wo *world.World, pos cube.Pos) (Wall, bool) { var connectionType WallConnectionType if connected { // If the wall is connected to the side, it has the possibility of having a tall connection. This is - //calculated by checking for any overlapping blocks in the area of the connection. + // calculated by checking for any overlapping blocks in the area of the connection. connectionType = ShortWallConnection() boxes := above.Model().BBox(abovePos, wo) for _, bb := range boxes { diff --git a/server/player/player.go b/server/player/player.go index 3f630ee3c..625c12eee 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -7,7 +7,6 @@ import ( "math/rand" "net" "strings" - "sync" "time" "github.com/df-mc/dragonfly/server/block" @@ -34,123 +33,71 @@ import ( "github.com/go-gl/mathgl/mgl64" "github.com/google/uuid" "golang.org/x/text/language" - "sync/atomic" ) -// 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 { - name string - uuid uuid.UUID - xuid string - locale language.Tag - pos, vel atomic.Pointer[mgl64.Vec3] - nameTag atomic.Pointer[string] - scoreTag atomic.Pointer[string] - yaw, pitch, absorptionHealth, scale atomic.Uint64 - once sync.Once - - gameMode atomic.Pointer[world.GameMode] - - skin atomic.Pointer[skin.Skin] - // s holds the session of the player. This field should not be used directly, but instead, - // Player.session() should be called. - s atomic.Pointer[session.Session] - // h holds the current Handler of the player. It may be changed at any time by calling the Handle method. - h atomic.Pointer[Handler] +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 *inventory.Inventory armour *inventory.Armour - heldSlot *atomic.Uint32 + heldSlot *uint32 sneaking, sprinting, swimming, gliding, flying, - invisible, immobile, onGround, usingItem atomic.Bool - usingSince atomic.Int64 + invisible, immobile, onGround, usingItem bool + usingSince time.Time - glideTicks atomic.Int64 - fireTicks atomic.Int64 - fallDistance atomic.Uint64 + glideTicks int64 + fireTicks int64 + fallDistance float64 breathing bool - airSupplyTicks atomic.Int64 - maxAirSupplyTicks atomic.Int64 + airSupplyTicks int64 + maxAirSupplyTicks int64 - cooldownMu sync.Mutex - cooldowns map[string]time.Time - // lastTickedWorld holds the world that the player was in, in the last tick. - lastTickedWorld *world.World + cooldowns map[string]time.Time - speed atomic.Uint64 + speed float64 health *entity.HealthManager experience *entity.ExperienceManager effects *entity.EffectManager - lastXPPickup atomic.Pointer[time.Time] - immunityTicks atomic.Int64 + lastXPPickup *time.Time + immunityTicks int64 - deathMu sync.Mutex deathPos *mgl64.Vec3 deathDimension world.Dimension - enchantSeed atomic.Int64 + enchantSeed int64 mc *entity.MovementComputer - collidedVertically, collidedHorizontally atomic.Bool + collidedVertically, collidedHorizontally bool - breaking atomic.Bool - breakingPos atomic.Pointer[cube.Pos] + breaking bool + breakingPos cube.Pos lastBreakDuration time.Duration - breakParticleCounter atomic.Uint32 + breakParticleCounter uint32 hunger *hungerManager } -// New returns a new initialised player. A random UUID is generated for the player, so that it may be -// identified over network. You can either pass on player data you want to load or -// you can leave the data as nil to use default data. -func New(name string, skin skin.Skin, pos mgl64.Vec3) *Player { - p := &Player{} - *p = Player{ - inv: inventory.New(36, func(slot int, before, after item.Stack) { - if slot == int(p.heldSlot.Load()) { - p.broadcastItems(slot, before, after) - } - }), - enderChest: inventory.New(27, nil), - uuid: uuid.New(), - offHand: inventory.New(1, p.broadcastItems), - armour: inventory.NewArmour(p.broadcastArmour), - hunger: newHungerManager(), - health: entity.NewHealthManager(20, 20), - experience: entity.NewExperienceManager(), - effects: entity.NewEffectManager(), - name: name, - locale: language.BritishEnglish, - breathing: true, - cooldowns: make(map[string]time.Time), - mc: &entity.MovementComputer{Gravity: 0.08, Drag: 0.02, DragBeforeGravity: true}, - heldSlot: &atomic.Uint32{}, - } - var scoreTag string - var heldSlot uint32 - var vel mgl64.Vec3 - var gm world.GameMode = world.GameModeSurvival - p.gameMode.Store(&gm) - p.Handle(nil) - p.skin.Store(&skin) - p.speed.Store(math.Float64bits(0.1)) - p.nameTag.Store(&name) - p.scoreTag.Store(&scoreTag) - p.airSupplyTicks.Store(300) - p.maxAirSupplyTicks.Store(300) - p.enchantSeed.Store(rand.Int63()) - p.scale.Store(math.Float64bits(1)) - p.pos.Store(&pos) - p.vel.Store(&vel) - p.heldSlot.Store(heldSlot) - return p +// 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 + *world.EntityHandle + data *world.EntityData + *playerData } // NewWithSession returns a new player for a network session, so that the network session can control the @@ -179,15 +126,7 @@ func (p *Player) Type() world.EntityType { // 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 { - return p.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.uuid + return p.data.Name } // XUID returns the XBOX Live user ID of the player. It will remain consistent with the XBOX Live account, @@ -239,21 +178,18 @@ func (p *Player) Addr() net.Addr { // 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.Load() + 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) { - if p.Dead() { - return - } ctx := event.C() if p.Handler().HandleSkinChange(ctx, &skin); ctx.Cancelled() { p.session().ViewSkin(p) return } - p.skin.Store(&skin) + p.skin = skin for _, v := range p.viewers() { v.ViewSkin(p) } @@ -268,6 +204,7 @@ func (p *Player) Locale() language.Tag { // handlers of the Handler passed. // Handle sets the player's Handler to NopHandler if nil is passed. func (p *Player) Handle(h Handler) { + // TODO: Decide where handlers go! if h == nil { h = NopHandler{} } @@ -313,12 +250,12 @@ func (p *Player) SendToast(title, message string) { // ResetFallDistance resets the player's fall distance. func (p *Player) ResetFallDistance() { - p.fallDistance.Store(0) + p.fallDistance = 0 } // FallDistance returns the player's fall distance. func (p *Player) FallDistance() float64 { - return math.Float64frombits(p.fallDistance.Load()) + return p.fallDistance } // SendTitle sends a title to the player. The title may be configured to change the duration it is displayed @@ -372,7 +309,7 @@ func (p *Player) Chat(msg ...any) { if p.Handler().HandleChat(ctx, &message); ctx.Cancelled() { return } - _, _ = fmt.Fprintf(chat.Global, "<%v> %v\n", p.name, message) + _, _ = 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 @@ -456,39 +393,39 @@ func (p *Player) DisableInstantRespawn() { // 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.Store(&name) + 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.Load() + 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.Store(&tag) + 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.Load() + 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.Store(math.Float64bits(speed)) + 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 math.Float64frombits(p.speed.Load()) + return p.speed } // Health returns the current health of the player. It will always be lower than Player.MaxHealth(). @@ -534,14 +471,13 @@ func (p *Player) Heal(health float64, source world.HealingSource) { // updateFallState is called to update the entities falling state. func (p *Player) updateFallState(distanceThisTick float64) { - fallDistance := math.Float64frombits(p.fallDistance.Load()) if p.OnGround() { - if fallDistance > 0 { - p.fall(fallDistance) + if p.fallDistance > 0 { + p.fall(p.fallDistance) p.ResetFallDistance() } - } else if distanceThisTick < fallDistance { - p.fallDistance.Store(math.Float64bits(fallDistance - distanceThisTick)) + } else if distanceThisTick < p.fallDistance { + p.fallDistance -= distanceThisTick } else { p.ResetFallDistance() } @@ -552,11 +488,11 @@ func (p *Player) fall(distance float64) { var ( w = p.World() pos = cube.PosFromVec3(p.Position()) - b = w.Block(pos) + b = p.tx.Block(pos) ) if len(b.Model().BBox(pos, w)) == 0 { pos = pos.Sub(cube.Pos{0, 1}) - b = w.Block(pos) + b = p.tx.Block(pos) } if h, ok := b.(block.EntityLander); ok { h.EntityLand(pos, w, p, &distance) @@ -638,14 +574,14 @@ func (p *Player) Hurt(dmg float64, src world.DamageSource) (float64, bool) { } } - w, pos := p.World(), p.Position() + pos := p.Position() for _, viewer := range p.viewers() { viewer.ViewEntityAction(p, entity.HurtAction{}) } if src.Fire() { - w.PlaySound(pos, sound.Burning{}) + p.tx.PlaySound(pos, sound.Burning{}) } else if _, ok := src.(entity.DrowningDamageSource); ok { - w.PlaySound(pos, sound.Drowning{}) + p.tx.PlaySound(pos, sound.Drowning{}) } p.SetAttackImmunity(immunity) @@ -667,7 +603,7 @@ func (p *Player) applyTotemEffects() { p.AddEffect(effect.New(effect.FireResistance{}, 1, time.Second*40)) p.AddEffect(effect.New(effect.Absorption{}, 2, time.Second*5)) - p.World().PlaySound(p.Position(), sound.Totem{}) + p.tx.PlaySound(p.Position(), sound.Totem{}) for _, viewer := range p.viewers() { viewer.ViewEntityAction(p, entity.TotemUseAction{}) @@ -699,14 +635,13 @@ func (p *Player) Explode(explosionPos mgl64.Vec3, impact float64, c block.Explos // 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) { - health = math.Max(health, 0) - p.absorptionHealth.Store(math.Float64bits(health)) - p.session().SendAbsorption(health) + p.absorptionHealth = math.Max(health, 0) + p.session().SendAbsorption(p.absorptionHealth) } // Absorption returns the absorption health that the player has. func (p *Player) Absorption() float64 { - return math.Float64frombits(p.absorptionHealth.Load()) + return p.absorptionHealth } // KnockBack knocks the player back with a given force and height. A source is passed which indicates the @@ -735,17 +670,17 @@ func (p *Player) knockBack(src mgl64.Vec3, force, height float64) { // AttackImmune checks if the player is currently immune to entity attacks, meaning it was recently attacked. func (p *Player) AttackImmune() bool { - return p.immunityTicks.Load() > 0 + return p.immunityTicks > 0 } // AttackImmunity returns the duration the player is immune to entity attacks. func (p *Player) AttackImmunity() time.Duration { - return time.Duration(p.immunityTicks.Load()) * time.Second / 20 + return time.Duration(p.immunityTicks) * time.Second / 20 } // SetAttackImmunity sets the duration the player is immune to entity attacks. func (p *Player) SetAttackImmunity(d time.Duration) { - p.immunityTicks.Store(d.Milliseconds() / 50) + p.immunityTicks = d.Milliseconds() / 50 } // Food returns the current food level of a player. The level returned is guaranteed to always be between 0 @@ -777,8 +712,6 @@ func (p *Player) Saturate(food int, saturation float64) { // sendFood sends the current food properties to the client. func (p *Player) sendFood() { - p.hunger.mu.RLock() - defer p.hunger.mu.RUnlock() p.session().SendFood(p.hunger.foodLevel, p.hunger.saturationLevel, p.hunger.exhaustionLevel) } @@ -849,8 +782,6 @@ func (p *Player) Dead() bool { // 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) { - p.deathMu.Lock() - defer p.deathMu.Unlock() if p.deathPos == nil { return mgl64.Vec3{}, nil, false } @@ -878,8 +809,6 @@ func (p *Player) kill(src world.DamageSource) { p.RemoveEffect(e.Type()) } - p.deathMu.Lock() - defer p.deathMu.Unlock() p.deathPos, p.deathDimension = &pos, w.Dimension() // Wait a little before removing the entity. The client displays a death animation while the player is dying. @@ -893,18 +822,17 @@ func (p *Player) kill(src world.DamageSource) { // 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. - newPos := w.Spawn().Vec3() - p.pos.Store(&newPos) + p.data.Pos = w.Spawn().Vec3() } }) } // dropContents drops all items and experience of the Player on the ground in random directions. func (p *Player) dropContents() { - w, pos := p.World(), p.Position() + pos := p.Position() for _, orb := range entity.NewExperienceOrbs(pos, int(math.Min(float64(p.experience.Level()*7), 100))) { orb.SetVelocity(mgl64.Vec3{(rand.Float64()*0.2 - 0.1) * 2, rand.Float64() * 0.4, (rand.Float64()*0.2 - 0.1) * 2}) - w.AddEntity(orb) + p.tx.AddEntity(orb) } p.experience.Reset() p.session().SendExperience(p.experience) @@ -916,7 +844,7 @@ func (p *Player) dropContents() { } ent := entity.NewItem(it, pos) ent.SetVelocity(mgl64.Vec3{rand.Float64()*0.2 - 0.1, 0.2, rand.Float64()*0.2 - 0.1}) - w.AddEntity(ent) + p.tx.AddEntity(ent) } } @@ -935,12 +863,12 @@ func (p *Player) Respawn() { // 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()) + w = w.PortalDestination(p.tx.World().Dimension()) pos := w.PlayerSpawn(p.UUID()).Vec3Middle() p.Handler().HandleRespawn(&pos, &w) - w.AddEntity(p) + p.tx.AddEntity(p) p.Teleport(pos) p.session().SendRespawn(pos) @@ -951,38 +879,35 @@ func (p *Player) Respawn() { // 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() { + if !p.hunger.canSprint() && p.GameMode().AllowsTakingDamage() || p.Sprinting() { return } ctx := event.C() if p.Handler().HandleToggleSprint(ctx, true); ctx.Cancelled() { return } - if !p.sprinting.CompareAndSwap(false, true) { - 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.Load() + return p.sprinting } // StopSprinting makes a player stop sprinting, setting back the speed of the player to its original value. func (p *Player) StopSprinting() { - ctx := event.C() - if p.Handler().HandleToggleSprint(ctx, false); ctx.Cancelled() { + if !p.sprinting { return } - if !p.sprinting.CompareAndSwap(true, false) { + ctx := event.C() + if p.Handler().HandleToggleSprint(ctx, false); ctx.Cancelled() { return } + p.sprinting = true p.SetSpeed(p.Speed() / 1.3) - p.updateState() } @@ -990,106 +915,114 @@ func (p *Player) StopSprinting() { // anything. // If the player is sprinting while StartSneaking is called, the sprinting is stopped. func (p *Player) StartSneaking() { - ctx := event.C() - if p.Handler().HandleToggleSneak(ctx, true); ctx.Cancelled() { + if p.sneaking { return } - if !p.sneaking.CompareAndSwap(false, true) { + ctx := event.C() + 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.Load() + 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() { - ctx := event.C() - if p.Handler().HandleToggleSneak(ctx, false); ctx.Cancelled() { + if !p.sneaking { return } - if !p.sneaking.CompareAndSwap(true, false) { + ctx := event.C() + 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.CompareAndSwap(false, true) { + 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.Load() + return p.swimming } // StopSwimming makes the player stop swimming if it is currently doing so. func (p *Player) StopSwimming() { - if !p.swimming.CompareAndSwap(true, false) { + if !p.swimming { return } + p.swimming = false p.updateState() } // StartGliding makes the player start gliding if it is not currently doing so. func (p *Player) StartGliding() { - if !p.gliding.CompareAndSwap(false, true) { + 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.Load() + return p.gliding } // StopGliding makes the player stop gliding if it is currently doing so. func (p *Player) StopGliding() { - if !p.gliding.CompareAndSwap(true, false) { + if !p.gliding { return } - p.glideTicks.Store(0) + 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.CompareAndSwap(false, true) { + if !p.GameMode().AllowsFlying() || p.Flying() { return } - p.session().SendGameMode(p.GameMode()) + p.flying = true + p.session().SendGameMode(p) } // Flying checks if the player is currently flying. func (p *Player) Flying() bool { - return p.flying.Load() + return p.flying } // StopFlying makes the player stop flying if it currently is. func (p *Player) StopFlying() { - if !p.flying.CompareAndSwap(true, false) { + if !p.flying { return } - p.session().SendGameMode(p.GameMode()) + 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 @@ -1105,7 +1038,7 @@ func (p *Player) Jump() { if e, ok := p.Effect(effect.JumpBoost{}); ok { jumpVel = float64(e.Level()) / 10 } - p.vel.Store(&mgl64.Vec3{0, jumpVel}) + p.data.Vel = mgl64.Vec3{0, jumpVel} } if p.Sprinting() { p.Exhaust(0.2) @@ -1116,51 +1049,49 @@ func (p *Player) Jump() { // SetInvisible sets the player invisible, so that other players will not be able to see it. func (p *Player) SetInvisible() { - if !p.invisible.CompareAndSwap(false, true) { + 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 !p.GameMode().Visible() { - return - } - if _, ok := p.Effect(effect.Invisibility{}); ok { - return - } - if !p.invisible.CompareAndSwap(true, false) { + 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.Load() + return p.invisible } // SetImmobile prevents the player from moving around, but still allows them to look around. func (p *Player) SetImmobile() { - if !p.immobile.CompareAndSwap(false, true) { + 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.CompareAndSwap(true, false) { + 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.Load() + return p.immobile } // FireProof checks if the Player is currently fireproof. True is returned if the player has a FireResistance effect or @@ -1174,7 +1105,7 @@ func (p *Player) FireProof() bool { // OnFireDuration ... func (p *Player) OnFireDuration() time.Duration { - return time.Duration(p.fireTicks.Load()) * time.Second / 20 + return time.Duration(p.fireTicks) * time.Second / 20 } // SetOnFire ... @@ -1183,7 +1114,7 @@ func (p *Player) SetOnFire(duration time.Duration) { if level := p.Armour().HighestEnchantmentLevel(enchantment.FireProtection{}); level > 0 { ticks -= int64(math.Floor(float64(ticks) * float64(level) * 0.15)) } - p.fireTicks.Store(ticks) + p.fireTicks = ticks p.updateState() } @@ -1210,14 +1141,14 @@ func (p *Player) Armour() *inventory.Armour { // the hand held anything. func (p *Player) HeldItems() (mainHand, offHand item.Stack) { offHand, _ = p.offHand.Item(0) - mainHand, _ = p.inv.Item(int(p.heldSlot.Load())) + 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.Load()), mainHand) + _ = p.inv.SetItem(int(*p.heldSlot), mainHand) _ = p.offHand.SetItem(0, offHand) } @@ -1230,11 +1161,8 @@ func (p *Player) EnderChestInventory() *inventory.Inventory { // 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.Swap(&mode) - p.session().SendGameMode(mode) - for _, v := range p.viewers() { - v.ViewEntityGameMode(p) - } + previous := p.GameMode() + p.gameMode = mode if !mode.AllowsFlying() { p.StopFlying() @@ -1244,13 +1172,18 @@ func (p *Player) SetGameMode(mode world.GameMode) { } else if !previous.Visible() { p.SetVisible() } + + p.session().SendGameMode(p) + for _, v := range p.viewers() { + v.ViewEntityGameMode(p) + } } // 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.Load() + return p.gameMode } // HasCooldown returns true if the item passed has an active cooldown, meaning it currently cannot be used again. If the @@ -1259,9 +1192,6 @@ func (p *Player) HasCooldown(item world.Item) bool { if item == nil { return false } - p.cooldownMu.Lock() - defer p.cooldownMu.Unlock() - name, _ := item.EncodeItem() otherTime, ok := p.cooldowns[name] if !ok { @@ -1279,9 +1209,6 @@ func (p *Player) SetCooldown(item world.Item, cooldown time.Duration) { if item == nil { return } - p.cooldownMu.Lock() - defer p.cooldownMu.Unlock() - name, _ := item.EncodeItem() p.cooldowns[name] = time.Now().Add(cooldown) p.session().ViewItemCooldown(item, cooldown) @@ -1293,7 +1220,6 @@ func (p *Player) SetCooldown(item world.Item, cooldown time.Duration) { func (p *Player) UseItem() { var ( i, left = p.HeldItems() - w = p.World() ctx = event.C() ) if p.HasCooldown(i.Item()) { @@ -1313,15 +1239,15 @@ func (p *Player) UseItem() { if !p.canRelease() { return } - p.usingSince.Store(time.Now().UnixNano()) - p.usingItem.Store(true) + p.usingSince = time.Now() + p.usingItem = true p.updateState() } switch usable := it.(type) { case item.Usable: useCtx := p.useContext() - if !usable.Use(w, p, useCtx) { + 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 @@ -1340,9 +1266,9 @@ func (p *Player) UseItem() { p.ReleaseItem() return } - if p.usingItem.CompareAndSwap(false, true) { + if !p.usingItem { // Consumable starts being consumed: Set the start timestamp and update the using state to viewers. - p.usingSince.Store(time.Now().UnixNano()) + p.usingItem, p.usingSince = true, time.Now() p.updateState() return } @@ -1357,15 +1283,15 @@ func (p *Player) UseItem() { ctx = event.C() if p.Handler().HandleItemConsume(ctx, i); ctx.Cancelled() { // Consuming was cancelled, but the client will continue consuming the next item. - p.usingSince.Store(time.Now().UnixNano()) + p.usingSince = time.Now() return } p.SetHeldItems(p.subtractItem(i, 1), left) useCtx := p.useContext() - useCtx.NewItem = usable.Consume(w, p) + useCtx.NewItem = usable.Consume(p.tx, p) p.addNewItem(useCtx) - w.PlaySound(p.Position().Add(mgl64.Vec3{0, 1.5}), sound.Burp{}) + p.tx.PlaySound(p.Position().Add(mgl64.Vec3{0, 1.5}), sound.Burp{}) } } @@ -1375,9 +1301,10 @@ func (p *Player) UseItem() { // 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.CompareAndSwap(true, false) || !p.canRelease() || !p.GameMode().AllowsInteraction() { + if !p.usingItem || !p.canRelease() || !p.GameMode().AllowsInteraction() { return } + p.usingItem = false ctx := p.useContext() i, _ := p.HeldItems() i.Item().(item.Releasable).Release(p, p.useDuration(), ctx) @@ -1422,13 +1349,13 @@ func (p *Player) handleUseContext(ctx *item.UseContext) { // useDuration returns the duration the player has been using the item in the main hand. func (p *Player) useDuration() time.Duration { - return time.Duration(time.Now().UnixNano()-p.usingSince.Load()) + time.Second/20 + return time.Now().Sub(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.Load() + return p.usingItem } // UseItemOnBlock uses the item held in the main hand of the player on a block at the position passed. The @@ -1437,20 +1364,19 @@ func (p *Player) UsingItem() bool { // 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) { - w := p.World() - if _, ok := w.Block(pos).(block.Air); ok || !p.canReach(pos.Vec3Centre()) { + 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.resendBlocks(pos, w, face) + p.resendBlocks(pos, face) return } ctx := event.C() if p.Handler().HandleItemUseOnBlock(ctx, pos, face, clickPos); ctx.Cancelled() { - p.resendBlocks(pos, w, face) + p.resendBlocks(pos, face) return } i, left := p.HeldItems() - b := w.Block(pos) + 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. @@ -1459,7 +1385,7 @@ func (p *Player) UseItemOnBlock(pos cube.Pos, face cube.Face, clickPos mgl64.Vec // 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.World(), p, useCtx) { + 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 @@ -1473,7 +1399,7 @@ func (p *Player) UseItemOnBlock(pos cube.Pos, face cube.Face, clickPos mgl64.Vec case item.UsableOnBlock: // The item does something when used on a block. useCtx := p.useContext() - if !ib.UseOnBlock(pos, face, clickPos, p.World(), p, useCtx) { + if !ib.UseOnBlock(pos, face, clickPos, p.tx, p, useCtx) { return } p.SwingArm() @@ -1486,7 +1412,7 @@ func (p *Player) UseItemOnBlock(pos cube.Pos, face cube.Face, clickPos mgl64.Vec // The block clicked was either not replaceable, or not replaceable using the block passed. replacedPos = pos.Side(face) } - if replaceable, ok := w.Block(replacedPos).(block.Replaceable); !ok || !replaceable.ReplaceableBy(ib) || replacedPos.OutOfBounds(w.Range()) { + if replaceable, ok := p.tx.Block(replacedPos).(block.Replaceable); !ok || !replaceable.ReplaceableBy(ib) || replacedPos.OutOfBounds(w.Range()) { return } if !p.placeBlock(replacedPos, ib, false) || p.GameMode().CreativeInventory() { @@ -1570,12 +1496,12 @@ func (p *Player) AttackEntity(e world.Entity) bool { n, vulnerable := living.Hurt(dmg, entity.AttackDamageSource{Attacker: p}) i, left := p.HeldItems() - p.World().PlaySound(entity.EyePosition(e), sound.Attack{Damage: !mgl64.FloatEqual(n, 0)}) + p.tx.PlaySound(entity.EyePosition(e), sound.Attack{Damage: !mgl64.FloatEqual(n, 0)}) if !vulnerable { return true } if critical { - for _, v := range p.World().Viewers(living.Position()) { + for _, v := range p.tx.Viewers(living.Position()) { v.ViewEntityAction(living, entity.CriticalHitAction{}) } } @@ -1608,21 +1534,20 @@ func (p *Player) AttackEntity(e world.Entity) bool { // player might be breaking before this method is called. func (p *Player) StartBreaking(pos cube.Pos, face cube.Face) { p.AbortBreaking() - w := p.World() - if _, air := w.Block(pos).(block.Air); air || !p.canReach(pos.Vec3Centre()) { + 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 := w.Block(pos.Side(face)).(block.Fire); ok { + if _, ok := p.tx.Block(pos.Side(face)).(block.Fire); ok { ctx := event.C() if p.Handler().HandleFireExtinguish(ctx, pos); ctx.Cancelled() { // Resend the block because on client side that was extinguished - p.resendBlocks(pos, w, face) + p.resendBlocks(pos, face) return } - w.SetBlock(pos.Side(face), nil, nil) - w.PlaySound(pos.Vec3(), sound.FireExtinguish{}) + p.tx.SetBlock(pos.Side(face), nil, nil) + p.tx.PlaySound(pos.Vec3(), sound.FireExtinguish{}) return } @@ -1633,17 +1558,17 @@ func (p *Player) StartBreaking(pos cube.Pos, face cube.Face) { } // 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.Store(&pos) + p.breakingPos = pos ctx := event.C() if p.Handler().HandleStartBreak(ctx, pos); ctx.Cancelled() { return } - if punchable, ok := w.Block(pos).(block.Punchable); ok { - punchable.Punch(pos, face, w, p) + if punchable, ok := p.tx.Block(pos).(block.Punchable); ok { + punchable.Punch(pos, face, p.tx, p) } - p.breaking.Store(true) + p.breaking = true p.SwingArm() if p.GameMode().CreativeInventory() { @@ -1659,12 +1584,11 @@ func (p *Player) StartBreaking(pos cube.Pos, face cube.Face) { // 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() - w := p.World() - breakTime := block.BreakDuration(w.Block(pos), held) + breakTime := block.BreakDuration(p.tx.Block(pos), held) if !p.OnGround() { breakTime *= 5 } - if _, ok := p.Armour().Helmet().Enchantment(enchantment.AquaAffinity{}); p.insideOfWater(w) && !ok { + if _, ok := p.Armour().Helmet().Enchantment(enchantment.AquaAffinity{}); p.insideOfWater() && !ok { breakTime *= 5 } for _, e := range p.Effects() { @@ -1685,26 +1609,24 @@ func (p *Player) breakTime(pos cube.Pos) time.Duration { // if the player isn't breaking anything. // FinishBreaking will stop the animation and break the block. func (p *Player) FinishBreaking() { - pos := *p.breakingPos.Load() - if !p.breaking.Load() { - p.resendBlock(pos, p.World()) + if !p.breaking { + p.resendBlock(p.breakingPos) return } p.AbortBreaking() - p.BreakBlock(pos) + 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.CompareAndSwap(true, false) { + if !p.breaking { return } - p.breakParticleCounter.Store(0) - pos := *p.breakingPos.Load() + p.breaking, p.breakParticleCounter = true, 0 for _, viewer := range p.viewers() { - viewer.ViewBlockAction(pos, block.StopCrackAction{}) + viewer.ViewBlockAction(p.breakingPos, block.StopCrackAction{}) } } @@ -1712,21 +1634,20 @@ func (p *Player) AbortBreaking() { // 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.Load() { + if !p.breaking { return } - pos := *p.breakingPos.Load() + pos := p.breakingPos p.SwingArm() - w := p.World() - b := w.Block(pos) - w.AddParticle(pos.Vec3(), particle.PunchBlock{Block: b, Face: face}) + b := p.tx.Block(pos) + p.tx.AddParticle(pos.Vec3(), particle.PunchBlock{Block: b, Face: face}) - if p.breakParticleCounter.Add(1)%5 == 0 { + if p.breakParticleCounter += 1; p.breakParticleCounter%5 == 0 { // We send this sound only every so often. Vanilla doesn't send it every tick while breaking // either. Every 5 ticks seems accurate. - w.PlaySound(pos.Vec3(), sound.BlockBreaking{Block: w.Block(pos)}) + p.tx.PlaySound(pos.Vec3(), sound.BlockBreaking{Block: p.tx.Block(pos)}) } breakTime := p.breakTime(pos) if breakTime != p.lastBreakDuration { @@ -1753,23 +1674,22 @@ func (p *Player) PlaceBlock(pos cube.Pos, b world.Block, ctx *item.UseContext) { // 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 { - w := p.World() if !p.canReach(pos.Vec3Centre()) || !p.GameMode().AllowsEditing() { - p.resendBlocks(pos, w, cube.Faces()...) + p.resendBlocks(pos, cube.Faces()...) return false } if !ignoreBBox && p.obstructedPos(pos, b) { - p.resendBlocks(pos, w, cube.Faces()...) + p.resendBlocks(pos, cube.Faces()...) return false } ctx := event.C() if p.Handler().HandleBlockPlace(ctx, pos, b); ctx.Cancelled() { - p.resendBlocks(pos, w, cube.Faces()...) + p.resendBlocks(pos, cube.Faces()...) return false } - w.SetBlock(pos, b, nil) - w.PlaySound(pos.Vec3(), sound.BlockPlace{Block: b}) + p.tx.SetBlock(pos, b, nil) + p.tx.PlaySound(pos.Vec3(), sound.BlockPlace{Block: b}) p.SwingArm() return true } @@ -1777,13 +1697,12 @@ func (p *Player) placeBlock(pos cube.Pos, b world.Block, ignoreBBox bool) bool { // obstructedPos checks if the position passed is obstructed if the block passed is attempted to be placed. // The function returns true if there is an entity in the way that could prevent the block from being placed. func (p *Player) obstructedPos(pos cube.Pos, b world.Block) bool { - w := p.World() - blockBoxes := b.Model().BBox(pos, w) + blockBoxes := b.Model().BBox(pos, p.tx) for i, box := range blockBoxes { blockBoxes[i] = box.Translate(pos.Vec3()) } - around := w.EntitiesWithin(cube.Box(-3, -3, -3, 3, 3, 3).Translate(pos.Vec3()), nil) + around := p.tx.EntitiesWithin(cube.Box(-3, -3, -3, 3, 3, 3).Translate(pos.Vec3()), nil) for _, e := range around { switch e.Type().(type) { case entity.ItemType, entity.ArrowType: @@ -1800,18 +1719,17 @@ func (p *Player) obstructedPos(pos cube.Pos, b world.Block) bool { // 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) { - w := p.World() - b := w.Block(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.resendBlocks(pos, w) + p.resendBlocks(pos) return } if _, breakable := b.(block.Breakable); !breakable && !p.GameMode().CreativeInventory() { - p.resendBlocks(pos, w) + p.resendBlocks(pos) return } held, _ := p.HeldItems() @@ -1824,29 +1742,29 @@ func (p *Player) BreakBlock(pos cube.Pos) { ctx := event.C() if p.Handler().HandleBlockBreak(ctx, pos, &drops, &xp); ctx.Cancelled() { - p.resendBlocks(pos, w) + p.resendBlocks(pos) return } held, left := p.HeldItems() p.SwingArm() - w.SetBlock(pos, nil, nil) - w.AddParticle(pos.Vec3Centre(), particle.BlockBreak{Block: b}) + 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, w, p) + info.BreakHandler(pos, p.tx, p) } for _, orb := range entity.NewExperienceOrbs(pos.Vec3Centre(), xp) { orb.SetVelocity(mgl64.Vec3{(rand.Float64()*0.2 - 0.1) * 2, rand.Float64() * 0.4, (rand.Float64()*0.2 - 0.1) * 2}) - w.AddEntity(orb) + p.tx.AddEntity(orb) } } for _, drop := range drops { ent := entity.NewItem(drop, pos.Vec3Centre()) ent.SetVelocity(mgl64.Vec3{rand.Float64()*0.2 - 0.1, 0.2, rand.Float64()*0.2 - 0.1}) - w.AddEntity(ent) + p.tx.AddEntity(ent) } p.Exhaust(0.005) @@ -1882,7 +1800,7 @@ func (p *Player) PickBlock(pos cube.Pos) { return } - b := p.World().Block(pos) + b := p.tx.Block(pos) var pickedItem item.Stack if pi, ok := b.(block.Pickable); ok { @@ -1910,7 +1828,7 @@ func (p *Player) PickBlock(pos cube.Pos) { _ = p.session().SetHeldSlot(slot) return } - _ = p.Inventory().Swap(slot, int(p.heldSlot.Load())) + _ = p.Inventory().Swap(slot, int(*p.heldSlot)) return } @@ -1924,7 +1842,7 @@ func (p *Player) PickBlock(pos cube.Pos) { _ = p.Inventory().SetItem(firstEmpty, pickedItem) return } - _ = p.Inventory().Swap(firstEmpty, int(p.heldSlot.Load())) + _ = p.Inventory().Swap(firstEmpty, int(*p.heldSlot)) p.SetHeldItems(pickedItem, offhand) } @@ -1944,8 +1862,8 @@ func (p *Player) teleport(pos mgl64.Vec3) { for _, v := range p.viewers() { v.ViewEntityTeleport(p, pos) } - p.pos.Store(&pos) - p.vel.Store(&mgl64.Vec3{}) + p.data.Pos = pos + p.data.Vel = mgl64.Vec3{} p.ResetFallDistance() } @@ -1956,7 +1874,7 @@ 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.Load() { + 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 @@ -1965,7 +1883,6 @@ func (p *Player) Move(deltaPos mgl64.Vec3, deltaYaw, deltaPitch float64) { deltaPos = mgl64.Vec3{} } var ( - w = p.World() pos = p.Position() yaw, pitch = p.Rotation().Elem() res, resYaw, resPitch = pos.Add(deltaPos), yaw + deltaYaw, pitch + deltaPitch @@ -1983,38 +1900,37 @@ func (p *Player) Move(deltaPos mgl64.Vec3, deltaYaw, deltaPitch float64) { v.ViewEntityMovement(p, res, cube.Rotation{resYaw, resPitch}, p.OnGround()) } - p.pos.Store(&res) - p.yaw.Store(math.Float64bits(resYaw)) - p.pitch.Store(math.Float64bits(resPitch)) + p.data.Pos = res + p.data.Rot = cube.Rotation{resYaw, resPitch} if deltaPos.Len() <= 3 { // Only update velocity if the player is not moving too fast to prevent potential OOMs. - p.vel.Store(&deltaPos) - p.checkBlockCollisions(deltaPos, w) + p.data.Vel = deltaPos + p.checkBlockCollisions(deltaPos) } horizontalVel := deltaPos horizontalVel[1] = 0 if p.Gliding() { if deltaPos.Y() >= -0.5 { - p.fallDistance.Store(1.0) + p.fallDistance = 1.0 } - if p.collidedHorizontally.Load() { + if p.collidedHorizontally { if force := horizontalVel.Len()*10.0 - 3.0; force > 0.0 && !p.AttackImmune() { - w.PlaySound(p.Position(), sound.Fall{Distance: force}) + p.tx.PlaySound(p.Position(), sound.Fall{Distance: force}) p.Hurt(force, entity.GlideDamageSource{}) } } } - _, submergedBefore := w.Liquid(cube.PosFromVec3(pos.Add(mgl64.Vec3{0, p.EyeHeight()}))) - _, submergedAfter := w.Liquid(cube.PosFromVec3(res.Add(mgl64.Vec3{0, p.EyeHeight()}))) + _, 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.Store(p.checkOnGround(w)) + p.onGround = p.checkOnGround() p.updateFallState(deltaPos[1]) if p.Swimming() { @@ -2033,19 +1949,19 @@ func (p *Player) World() *world.World { // 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.pos.Load() + 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.vel.Load() + 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.vel.Store(&velocity) + p.data.Vel = velocity return } for _, v := range p.viewers() { @@ -2057,7 +1973,7 @@ func (p *Player) SetVelocity(velocity mgl64.Vec3) { // 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 cube.Rotation{math.Float64frombits(p.yaw.Load()), math.Float64frombits(p.pitch.Load())} + return p.data.Rot } // ChangingDimension returns whether the player is currently changing dimension or not. @@ -2089,12 +2005,12 @@ func (p *Player) Experience() int { // EnchantmentSeed is a seed used to calculate random enchantments with enchantment tables. func (p *Player) EnchantmentSeed() int64 { - return p.enchantSeed.Load() + return p.enchantSeed } // ResetEnchantmentSeed resets the enchantment seed to a new random value. func (p *Player) ResetEnchantmentSeed() { - p.enchantSeed.Store(rand.Int63()) + p.enchantSeed = rand.Int63() } // AddExperience adds experience to the player. @@ -2151,13 +2067,12 @@ func (p *Player) CollectExperience(value int) bool { if p.Dead() || !p.GameMode().AllowsInteraction() { return false } - last := p.lastXPPickup.Load() - if last != nil && time.Since(*last) < time.Millisecond*100 { + if last := p.lastXPPickup; last != nil && time.Since(*last) < time.Millisecond*100 { return false } value = p.mendItems(value) now := time.Now() - p.lastXPPickup.Store(&now) + p.lastXPPickup = &now if value > 0 { return p.AddExperience(value) > 0 } @@ -2219,7 +2134,7 @@ func (p *Player) Drop(s item.Stack) int { if p.Handler().HandleItemDrop(ctx, e); ctx.Cancelled() { return 0 } - p.World().AddEntity(e) + p.tx.AddEntity(e) return s.Count() } @@ -2260,15 +2175,11 @@ func (p *Player) Latency() time.Duration { } // Tick ticks the entity, performing actions such as checking if the player is still breaking a block. -func (p *Player) Tick(w *world.World, current int64) { +func (p *Player) Tick(_ *world.World, current int64) { if p.Dead() { return } - if p.lastTickedWorld != w { - p.Handler().HandleChangeWorld(p.lastTickedWorld, w) - } - p.lastTickedWorld = w - if _, ok := w.Liquid(cube.PosFromVec3(p.Position())); !ok { + 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()) @@ -2276,7 +2187,7 @@ func (p *Player) Tick(w *world.World, current int64) { } if _, ok := p.Armour().Chestplate().Item().(item.Elytra); ok && p.Gliding() { - if t := p.glideTicks.Add(1); t%20 == 0 { + if p.glideTicks += 1; p.glideTicks%20 == 0 { d := p.damageItem(p.Armour().Chestplate(), 1) p.armour.SetChestplate(d) if d.Durability() < 2 { @@ -2285,26 +2196,24 @@ func (p *Player) Tick(w *world.World, current int64) { } } - p.checkBlockCollisions(*p.vel.Load(), w) - p.onGround.Store(p.checkOnGround(w)) + p.checkBlockCollisions(p.data.Vel) + p.onGround = p.checkOnGround() p.effects.Tick(p) - p.tickFood(w) - p.tickAirSupply(w) - if p.immunityTicks.Load() > 0 { - p.immunityTicks.Add(-1) - } - if p.Position()[1] < float64(w.Range()[0]) && p.GameMode().AllowsTakingDamage() && current%10 == 0 { + p.tickFood() + p.tickAirSupply() + p.immunityTicks = max(p.immunityTicks-1, 0) + if p.Position()[1] < float64(p.tx.Range()[0]) && p.GameMode().AllowsTakingDamage() && current%10 == 0 { p.Hurt(4, entity.VoidDamageSource{}) } - if !p.AttackImmune() && p.insideOfSolid(w) { + if !p.AttackImmune() && p.insideOfSolid() { p.Hurt(1, entity.SuffocationDamageSource{}) } if p.OnFireDuration() > 0 { - p.fireTicks.Add(-1) - if !p.GameMode().AllowsTakingDamage() || p.OnFireDuration() <= 0 || w.RainingAt(cube.PosFromVec3(p.Position())) { + 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.AttackImmune() { @@ -2312,7 +2221,7 @@ func (p *Player) Tick(w *world.World, current int64) { } } - if current%4 == 0 && p.usingItem.Load() { + if current%4 == 0 && p.usingItem { held, _ := p.HeldItems() if _, ok := held.Item().(item.Consumable); ok { // Eating particles seem to happen roughly every 4 ticks. @@ -2321,49 +2230,41 @@ func (p *Player) Tick(w *world.World, current int64) { } } } - - p.cooldownMu.Lock() for it, ti := range p.cooldowns { if time.Now().After(ti) { delete(p.cooldowns, it) } } - p.cooldownMu.Unlock() if p.session() == session.Nop && !p.Immobile() { - m := p.mc.TickMovement(p, p.Position(), p.Velocity(), cube.Rotation{math.Float64frombits(p.yaw.Load()), math.Float64frombits(p.pitch.Load())}) + m := p.mc.TickMovement(p, p.Position(), p.Velocity(), p.Rotation()) m.Send() - vel := m.Velocity() - p.vel.Store(&vel) + p.data.Vel = m.Velocity() p.Move(m.Position().Sub(p.Position()), 0, 0) } else { - p.vel.Store(&mgl64.Vec3{}) + p.data.Vel = mgl64.Vec3{} } } // tickAirSupply tick's the player's air supply, consuming it when underwater, and replenishing it when out of water. -func (p *Player) tickAirSupply(w *world.World) { - if !p.canBreathe(w) { +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 ticks := p.airSupplyTicks.Add(-1); ticks <= -20 { - p.airSupplyTicks.Store(0) + if p.airSupplyTicks -= 1; p.airSupplyTicks <= -20 { + p.airSupplyTicks = 0 if !p.AttackImmune() { p.Hurt(2, entity.DrowningDamageSource{}) } } p.breathing = false p.updateState() - } else if max := p.maxAirSupplyTicks.Load(); !p.breathing && p.airSupplyTicks.Load() < max { - p.airSupplyTicks.Add(5) - if p.airSupplyTicks.Load() >= max { - p.breathing = true - p.airSupplyTicks.Store(max) - } + } else if !p.breathing && p.airSupplyTicks < p.maxAirSupplyTicks { + p.airSupplyTicks = min(p.airSupplyTicks+5, p.maxAirSupplyTicks) + p.breathing = p.airSupplyTicks == p.maxAirSupplyTicks p.updateState() } } @@ -2420,32 +2321,32 @@ func (p *Player) starve(w *world.World) { // AirSupply returns the player's remaining air supply. func (p *Player) AirSupply() time.Duration { - return time.Duration(p.airSupplyTicks.Load()) * time.Second / 20 + 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.Store(duration.Milliseconds() / 50) + p.airSupplyTicks = duration.Milliseconds() / 50 p.updateState() } // MaxAirSupply returns the player's maximum air supply. func (p *Player) MaxAirSupply() time.Duration { - return time.Duration(p.maxAirSupplyTicks.Load()) * time.Second / 20 + 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.Store(duration.Milliseconds() / 50) + p.maxAirSupplyTicks = duration.Milliseconds() / 50 p.updateState() } // canBreathe returns true if the player can currently breathe. -func (p *Player) canBreathe(w *world.World) bool { +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(w) && !p.insideOfSolid(w)) + 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 @@ -2453,9 +2354,9 @@ func (p *Player) canBreathe(w *world.World) bool { const breathingDistanceBelowEyes = 0.11111111 // insideOfWater returns true if the player is currently underwater. -func (p *Player) insideOfWater(w *world.World) bool { +func (p *Player) insideOfWater() bool { pos := cube.PosFromVec3(entity.EyePosition(p)) - if l, ok := w.Liquid(pos); ok { + if l, ok := p.tx.Liquid(pos); ok { if _, ok := l.(block.Water); ok { d := float64(l.SpreadDecay()) + 1 if l.LiquidFalling() { @@ -2468,9 +2369,9 @@ func (p *Player) insideOfWater(w *world.World) bool { } // insideOfSolid returns true if the player is inside a solid block. -func (p *Player) insideOfSolid(w *world.World) bool { +func (p *Player) insideOfSolid() bool { pos := cube.PosFromVec3(entity.EyePosition(p)) - b, box := w.Block(pos), p.Type().BBox(p).Translate(p.Position()) + b, box := p.tx.Block(pos), p.Type().BBox(p).Translate(p.Position()) _, solid := b.Model().(model.Solid) if !solid { @@ -2482,7 +2383,7 @@ func (p *Player) insideOfSolid(w *world.World) bool { // Transparent. return false } - for _, blockBox := range b.Model().BBox(pos, w) { + for _, blockBox := range b.Model().BBox(pos, p.tx) { if blockBox.Translate(pos.Vec3()).IntersectsWith(box) { return true } @@ -2491,16 +2392,16 @@ func (p *Player) insideOfSolid(w *world.World) bool { } // checkCollisions checks the player's block collisions. -func (p *Player) checkBlockCollisions(vel mgl64.Vec3, w *world.World) { +func (p *Player) checkBlockCollisions(vel mgl64.Vec3) { entityBBox := p.Type().BBox(p).Translate(p.Position()) deltaX, deltaY, deltaZ := vel[0], vel[1], vel[2] - p.checkEntityInsiders(w, entityBBox) + p.checkEntityInsiders(entityBBox) grown := entityBBox.Extend(vel).Grow(0.25) - min, max := grown.Min(), grown.Max() - minX, minY, minZ := int(math.Floor(min[0])), int(math.Floor(min[1])), int(math.Floor(min[2])) - maxX, maxY, maxZ := int(math.Ceil(max[0])), int(math.Ceil(max[1])), int(math.Ceil(max[2])) + 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) @@ -2508,7 +2409,7 @@ func (p *Player) checkBlockCollisions(vel mgl64.Vec3, w *world.World) { for x := minX; x <= maxX; x++ { for z := minZ; z <= maxZ; z++ { pos := cube.Pos{x, y, z} - boxes := w.Block(pos).Model().BBox(pos, w) + boxes := p.tx.Block(pos).Model().BBox(pos, p.tx) for _, box := range boxes { blocks = append(blocks, box.Translate(pos.Vec3())) } @@ -2540,30 +2441,30 @@ func (p *Player) checkBlockCollisions(vel mgl64.Vec3, w *world.World) { } } - p.collidedHorizontally.Store(!mgl64.FloatEqual(deltaX, vel[0]) || !mgl64.FloatEqual(deltaZ, vel[2])) - p.collidedVertically.Store(!mgl64.FloatEqual(deltaY, vel[1])) + 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(w *world.World, entityBBox cube.BBox) { +func (p *Player) checkEntityInsiders(entityBBox cube.BBox) { box := entityBBox.Grow(-0.0001) - min, max := cube.PosFromVec3(box.Min()), cube.PosFromVec3(box.Max()) + low, high := cube.PosFromVec3(box.Min()), cube.PosFromVec3(box.Max()) - for y := min[1]; y <= max[1]; y++ { - for x := min[0]; x <= max[0]; x++ { - for z := min[2]; z <= max[2]; z++ { + 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 := w.Block(blockPos) + b := p.tx.Block(blockPos) if collide, ok := b.(block.EntityInsider); ok { - collide.EntityInside(blockPos, w, p) + collide.EntityInside(blockPos, p.tx, p) if _, liquid := b.(world.Liquid); liquid { continue } } - if l, ok := w.Liquid(blockPos); ok { + if l, ok := p.tx.Liquid(blockPos); ok { if collide, ok := l.(block.EntityInsider); ok { - collide.EntityInside(blockPos, w, p) + collide.EntityInside(blockPos, p.tx, p) } } } @@ -2572,17 +2473,16 @@ func (p *Player) checkEntityInsiders(w *world.World, entityBBox cube.BBox) { } // checkOnGround checks if the player is currently considered to be on the ground. -func (p *Player) checkOnGround(w *world.World) bool { +func (p *Player) checkOnGround() bool { box := p.Type().BBox(p).Translate(p.Position()) - b := box.Grow(1) - min, max := cube.PosFromVec3(b.Min()), cube.PosFromVec3(b.Max()) - for x := min[0]; x <= max[0]; x++ { - for z := min[2]; z <= max[2]; z++ { - for y := min[1]; y < max[1]; y++ { + low, high := cube.PosFromVec3(b.Min()), cube.PosFromVec3(b.Max()) + 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} - boxList := w.Block(pos).Model().BBox(pos, w) + boxList := p.tx.Block(pos).Model().BBox(pos, p.tx) for _, bb := range boxList { if bb.GrowVec3(mgl64.Vec3{0, 0.05}).Translate(pos.Vec3()).IntersectsWith(box) { return true @@ -2597,13 +2497,13 @@ func (p *Player) checkOnGround(w *world.World) bool { // 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 math.Float64frombits(p.scale.Load()) + 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.Store(math.Float64bits(s)) + p.scale = s p.updateState() } @@ -2612,12 +2512,12 @@ func (p *Player) OnGround() bool { if p.session() == session.Nop { return p.mc.OnGround() } - return p.onGround.Load() + return p.onGround } // EyeHeight returns the eye height of the player: 1.62, or 0.52 if the player is swimming. func (p *Player) EyeHeight() float64 { - if p.swimming.Load() { + if p.swimming { return 0.52 } return 1.62 @@ -2644,8 +2544,7 @@ func (p *Player) OpenSign(pos cube.Pos, frontSide bool) { // 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 { - w := p.World() - sign, ok := w.Block(pos).(block.Sign) + sign, ok := p.tx.Block(pos).(block.Sign) if !ok { return fmt.Errorf("edit sign: no sign at position %v", pos) } @@ -2659,28 +2558,27 @@ func (p *Player) EditSign(pos cube.Pos, frontText, backText string) error { ctx := event.C() if frontText != sign.Front.Text { if p.Handler().HandleSignEdit(ctx, pos, true, sign.Front.Text, frontText); ctx.Cancelled() { - p.resendBlock(pos, w) + p.resendBlock(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.resendBlock(pos, w) + p.resendBlock(pos) return nil } sign.Back.Text = backText sign.Back.Owner = p.XUID() } - w.SetBlock(pos, sign, nil) + 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 { - w := p.World() - lectern, ok := w.Block(pos).(block.Lectern) + lectern, ok := p.tx.Block(pos).(block.Lectern) if !ok { return fmt.Errorf("edit lectern: no lectern at position %v", pos) } @@ -2691,7 +2589,7 @@ func (p *Player) TurnLecternPage(pos cube.Pos, page int) error { } lectern.Page = page - w.SetBlock(pos, lectern, nil) + p.tx.SetBlock(pos, lectern, nil) return nil } @@ -2708,7 +2606,7 @@ func (p *Player) updateState() { func (p *Player) Breathing() bool { _, breathing := p.Effect(effect.WaterBreathing{}) _, conduitPower := p.Effect(effect.ConduitPower{}) - _, submerged := p.World().Liquid(cube.PosFromVec3(entity.EyePosition(p))) + _, submerged := p.tx.Liquid(cube.PosFromVec3(entity.EyePosition(p))) return !p.GameMode().AllowsTakingDamage() || !submerged || breathing || conduitPower } @@ -2732,7 +2630,7 @@ func (p *Player) PunchAir() { return } p.SwingArm() - p.World().PlaySound(p.Position(), sound.Attack{}) + p.tx.PlaySound(p.Position(), sound.Attack{}) } // UpdateDiagnostics updates the diagnostics of the player. @@ -2755,7 +2653,7 @@ func (p *Player) damageItem(s item.Stack, d int) item.Stack { d = (enchantment.Unbreaking{}).Reduce(s.Item(), e.Level(), d) } if s = s.Damage(d); s.Empty() { - p.World().PlaySound(p.Position(), sound.ItemBreak{}) + p.tx.PlaySound(p.Position(), sound.ItemBreak{}) } return s } @@ -2792,19 +2690,9 @@ func (p *Player) addNewItem(ctx *item.UseContext) { // 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 { - const ( - creativeRange = 14.0 - survivalRange = 8.0 - ) - if !p.GameMode().AllowsInteraction() { - return false - } - eyes := entity.EyePosition(p) - - if p.GameMode().CreativeInventory() { - return eyes.Sub(pos).Len() <= creativeRange && !p.Dead() - } - return eyes.Sub(pos).Len() <= survivalRange && !p.Dead() + 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. @@ -2834,51 +2722,49 @@ func (p *Player) close(msg string) { if p.Dead() && p.session() != nil { p.Respawn() } - var h Handler = NopHandler{} - (*p.h.Swap(&h)).HandleQuit() + p.h.HandleQuit() + p.h = NopHandler{} - if s := p.s.Swap(nil); s != nil { - (*s).Disconnect(msg) - (*s).CloseConnection() + if s := p.s; s != nil { + p.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.World().RemoveEntity(p) + p.tx.RemoveEntity(p) } // load reads the player data from the provider. It uses the default values if the provider // returns false. func (p *Player) load(data Data) { - p.yaw.Store(math.Float64bits(data.Yaw)) - p.pitch.Store(math.Float64bits(data.Pitch)) + p.data.Rot = cube.Rotation{data.Yaw, data.Pitch} p.health.SetMaxHealth(data.MaxHealth) p.health.AddHealth(data.Health - p.Health()) p.session().SendHealth(p.health) - p.absorptionHealth.Store(math.Float64bits(data.AbsorptionLevel)) + p.absorptionHealth = data.AbsorptionLevel p.session().SendAbsorption(data.AbsorptionLevel) - p.hunger.SetFood(data.Hunger) - p.hunger.foodTick = data.FoodTick + p.hunger.foodLevel, p.hunger.foodTick = data.Hunger, data.FoodTick p.hunger.exhaustionLevel, p.hunger.saturationLevel = data.ExhaustionLevel, data.SaturationLevel p.sendFood() - p.airSupplyTicks.Store(data.AirSupply) - p.maxAirSupplyTicks.Store(data.MaxAirSupply) + p.airSupplyTicks, p.maxAirSupplyTicks = data.AirSupply, data.MaxAirSupply p.experience.Add(data.Experience) p.session().SendExperience(p.experience) - p.enchantSeed.Store(data.EnchantmentSeed) + p.enchantSeed = data.EnchantmentSeed - p.gameMode.Store(&data.GameMode) + p.gameMode = data.GameMode for _, potion := range data.Effects { p.AddEffect(potion) } - p.fireTicks.Store(data.FireTicks) - p.fallDistance.Store(math.Float64bits(data.FallDistance)) + p.fireTicks = data.FireTicks + p.fallDistance = data.FallDistance p.loadInventory(data.Inventory) for slot, stack := range data.EnderChestInventory { @@ -2917,8 +2803,8 @@ func (p *Player) Data() Data { Experience: p.Experience(), EnchantmentSeed: p.EnchantmentSeed(), FoodTick: p.hunger.foodTick, - AirSupply: p.airSupplyTicks.Load(), - MaxAirSupply: p.maxAirSupplyTicks.Load(), + AirSupply: p.airSupplyTicks, + MaxAirSupply: p.maxAirSupplyTicks, ExhaustionLevel: p.hunger.exhaustionLevel, SaturationLevel: p.hunger.saturationLevel, AbsorptionLevel: p.Absorption(), @@ -2930,20 +2816,20 @@ func (p *Player) Data() Data { Chestplate: p.armour.Chestplate(), Helmet: p.armour.Helmet(), OffHand: offHand, - MainHandSlot: p.heldSlot.Load(), + MainHandSlot: *p.heldSlot, }, EnderChestInventory: p.enderChest.Slots(), Effects: p.Effects(), - FireTicks: p.fireTicks.Load(), - FallDistance: math.Float64frombits(p.fallDistance.Load()), - World: p.World(), + FireTicks: p.fireTicks, + FallDistance: p.fallDistance, + World: p.tx.World(), } } // 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.Load(); s != nil { + if s := p.s; s != nil { return s } return session.Nop @@ -2963,7 +2849,7 @@ func (p *Player) useContext() *item.UseContext { } return &item.UseContext{ SwapHeldWithArmour: func(i int) { - src, dst, srcInv, dstInv := int(p.heldSlot.Load()), i, p.inv, p.armour.Inventory() + src, dst, srcInv, dstInv := int(*p.heldSlot), i, p.inv, p.armour.Inventory() srcIt, _ := srcInv.Item(src) dstIt, _ := dstInv.Item(dst) @@ -2991,7 +2877,8 @@ func (p *Player) useContext() *item.UseContext { // Handler returns the Handler of the player. func (p *Player) Handler() Handler { - return *p.h.Load() + // TODO: Figure out handlers. + return p.h } // broadcastItems broadcasts the items held to viewers. @@ -3014,7 +2901,7 @@ func (p *Player) broadcastArmour(_ int, before, after item.Stack) { // viewers returns a list of all viewers of the Player. func (p *Player) viewers() []world.Viewer { - viewers := p.World().Viewers(p.Position()) + viewers := p.tx.Viewers(p.Position()) var s world.Viewer = p.session() if sliceutil.Index(viewers, s) == -1 { @@ -3024,22 +2911,22 @@ func (p *Player) viewers() []world.Viewer { } // resendBlocks resends blocks in a world.World at the cube.Pos passed and the block next to it at the cube.Face passed. -func (p *Player) resendBlocks(pos cube.Pos, w *world.World, faces ...cube.Face) { +func (p *Player) resendBlocks(pos cube.Pos, faces ...cube.Face) { if p.session() == session.Nop { return } - p.resendBlock(pos, w) + p.resendBlock(pos) for _, f := range faces { - p.resendBlock(pos.Side(f), w) + p.resendBlock(pos.Side(f)) } } // resendBlock resends the block at a cube.Pos in the world.World passed. -func (p *Player) resendBlock(pos cube.Pos, w *world.World) { - b := w.Block(pos) +func (p *Player) resendBlock(pos cube.Pos) { + b := p.tx.Block(pos) p.session().ViewBlockUpdate(pos, b, 0) if _, ok := b.(world.Liquid); !ok { - if liq, ok := w.Liquid(pos); ok { + if liq, ok := p.tx.Liquid(pos); ok { p.session().ViewBlockUpdate(pos, liq, 1) } } diff --git a/server/player/type.go b/server/player/type.go index 1f1a2c17d..b8655e86d 100644 --- a/server/player/type.go +++ b/server/player/type.go @@ -2,12 +2,102 @@ package player import ( "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/entity" + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/item/inventory" + "github.com/df-mc/dragonfly/server/player/skin" + "github.com/df-mc/dragonfly/server/session" "github.com/df-mc/dragonfly/server/world" + "github.com/go-gl/mathgl/mgl64" + "github.com/google/uuid" + "golang.org/x/text/language" + "math/rand" + "strings" + "time" ) // Type is a world.EntityType implementation for Player. type Type struct{} +type Config struct { + Name, XUID, Locale string + UUID uuid.UUID + Skin skin.Skin + Data *Data + Pos mgl64.Vec3 + Session *session.Session +} + +func (t Type) Init(conf any, data *world.EntityData) { + cfg, _ := conf.(Config) + locale, err := language.Parse(strings.Replace(cfg.Locale, "_", "-", 1)) + if err != nil { + locale = language.BritishEnglish + } + data.Name = cfg.Name + data.Pos = cfg.Pos + data.Data = &playerData{ + inv: inventory.New(36, nil), + enderChest: inventory.New(27, nil), + offHand: inventory.New(1, nil), + armour: inventory.NewArmour(nil), + hunger: newHungerManager(), + health: entity.NewHealthManager(20, 20), + experience: entity.NewExperienceManager(), + effects: entity.NewEffectManager(), + locale: locale, + breathing: true, + cooldowns: make(map[string]time.Time), + mc: &entity.MovementComputer{Gravity: 0.08, Drag: 0.02, DragBeforeGravity: true}, + heldSlot: new(uint32), + gameMode: world.GameModeSurvival, + skin: cfg.Skin, + airSupplyTicks: 300, + maxAirSupplyTicks: 300, + enchantSeed: rand.Int63(), + scale: 1.0, + s: cfg.Session, + } +} + +func (t Type) From(tx *world.Tx, handle *world.EntityHandle, data *world.EntityData) world.Entity { + pd := data.Data.(*playerData) + + // With session: + // p := New(name, skin, pos) + // p.s.Store(s) + // p.skin.Store(&skin) + // p.uuid, p.xuid = uuid, xuid + // p.inv, p.offHand, p.enderChest, p.armour, p.heldSlot = s.HandleInventories() + // p.locale, _ = language.Parse(strings.Replace(s.ClientData().LanguageCode, "_", "-", 1)) + // if data != nil { + // p.load(*data) + // } + // return p + p := &Player{ + tx: tx, + EntityHandle: handle, + data: data, + playerData: pd, + } + + // TODO: Make sure inventories don't get recreated everytime. + if pd.s != nil { + pd.inv, pd.offHand, pd.enderChest, pd.armour, pd.heldSlot = pd.s.HandleInventories(tx, p) + } else { + pd.inv = inventory.New(36, func(slot int, before, after item.Stack) { + if slot == int(*p.heldSlot) { + p.broadcastItems(slot, before, after) + } + }) + pd.enderChest = inventory.New(27, nil) + pd.offHand = inventory.New(1, p.broadcastItems) + pd.armour = inventory.NewArmour(p.broadcastArmour) + } + + return p +} + func (Type) EncodeEntity() string { return "minecraft:player" } func (Type) NetworkOffset() float64 { return 1.62 } func (Type) BBox(e world.Entity) cube.BBox { diff --git a/server/server.go b/server/server.go index 1801e27ad..5c6dba85a 100644 --- a/server/server.go +++ b/server/server.go @@ -455,7 +455,16 @@ func (srv *Server) createPlayer(id uuid.UUID, conn session.Conn, data *player.Da w, gm, pos = data.World, data.GameMode, data.Position } s := session.New(conn, srv.conf.MaxChunkRadius, srv.conf.Log, srv.conf.JoinMessage, srv.conf.QuitMessage) - p := player.NewWithSession(conn.IdentityData().DisplayName, conn.IdentityData().XUID, id, srv.parseSkin(conn.ClientData()), s, pos, data) + p := world.NewEntity(player.Type{}, player.Config{ + Name: conn.IdentityData().DisplayName, + XUID: conn.IdentityData().XUID, + UUID: id, + Locale: conn.ClientData().LanguageCode, + Skin: srv.parseSkin(conn.ClientData()), + Data: data, + Pos: pos, + Session: s, + }) s.Spawn(p, pos, w, gm, srv.handleSessionClose) srv.pwg.Add(1) diff --git a/server/session/chunk.go b/server/session/chunk.go index 909d8740d..1b47fb304 100644 --- a/server/session/chunk.go +++ b/server/session/chunk.go @@ -25,9 +25,8 @@ func (s *Session) ViewChunk(pos world.ChunkPos, c *chunk.Chunk, blockEntities ma } // ViewSubChunks ... -func (s *Session) ViewSubChunks(center world.SubChunkPos, offsets []protocol.SubChunkOffset) { - w := s.c.World() - r := w.Range() +func (s *Session) ViewSubChunks(center world.SubChunkPos, offsets []protocol.SubChunkOffset, tx *world.Tx) { + r := tx.Range() entries := make([]protocol.SubChunkEntry, 0, len(offsets)) transaction := make(map[uint64]struct{}) @@ -45,16 +44,14 @@ func (s *Session) ViewSubChunks(center world.SubChunkPos, offsets []protocol.Sub entries = append(entries, protocol.SubChunkEntry{Result: protocol.SubChunkResultChunkNotFound, Offset: offset}) continue } - col.Lock() entries = append(entries, s.subChunkEntry(offset, ind, col, transaction)) - col.Unlock() } if s.conn.ClientCacheEnabled() && len(transaction) > 0 { s.blobMu.Lock() s.openChunkTransactions = append(s.openChunkTransactions, transaction) s.blobMu.Unlock() } - dim, _ := world.DimensionID(w.Dimension()) + dim, _ := world.DimensionID(tx.World().Dimension()) s.writePacket(&packet.SubChunk{ Dimension: int32(dim), Position: protocol.SubChunkPos(center), diff --git a/server/session/handler.go b/server/session/handler.go index 8a9fa3c4b..712cabf5a 100644 --- a/server/session/handler.go +++ b/server/session/handler.go @@ -1,6 +1,7 @@ package session import ( + "github.com/df-mc/dragonfly/server/world" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -8,5 +9,5 @@ import ( type packetHandler interface { // Handle handles an incoming packet from the client. The session of the client is passed to the packetHandler. // Handle returns an error if the packet was in any way invalid. - Handle(p packet.Packet, s *Session) error + Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error } diff --git a/server/session/handler_block_actor_data.go b/server/session/handler_block_actor_data.go index 443ca36aa..a5b0afb1e 100644 --- a/server/session/handler_block_actor_data.go +++ b/server/session/handler_block_actor_data.go @@ -5,6 +5,7 @@ import ( "github.com/df-mc/dragonfly/server/block" "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/entity" + "github.com/df-mc/dragonfly/server/world" "github.com/go-gl/mathgl/mgl64" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" "strings" @@ -16,7 +17,7 @@ import ( type BlockActorDataHandler struct{} // Handle ... -func (b BlockActorDataHandler) Handle(p packet.Packet, s *Session) error { +func (b BlockActorDataHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error { pk := p.(*packet.BlockActorData) if id, ok := pk.NBTData["id"]; ok { pos := blockPosFromProtocol(pk.Position) diff --git a/server/session/handler_block_pick_request.go b/server/session/handler_block_pick_request.go index c927c8b7f..2c95b3b01 100644 --- a/server/session/handler_block_pick_request.go +++ b/server/session/handler_block_pick_request.go @@ -2,6 +2,7 @@ package session import ( "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -9,7 +10,7 @@ import ( type BlockPickRequestHandler struct{} // Handle ... -func (b BlockPickRequestHandler) Handle(p packet.Packet, s *Session) error { +func (b BlockPickRequestHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error { pk := p.(*packet.BlockPickRequest) s.c.PickBlock(cube.Pos{int(pk.Position.X()), int(pk.Position.Y()), int(pk.Position.Z())}) return nil diff --git a/server/session/handler_book_edit.go b/server/session/handler_book_edit.go index 8f7385104..99e35679b 100644 --- a/server/session/handler_book_edit.go +++ b/server/session/handler_book_edit.go @@ -3,6 +3,7 @@ package session import ( "fmt" "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/world" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -10,7 +11,7 @@ import ( type BookEditHandler struct{} // Handle ... -func (b BookEditHandler) Handle(p packet.Packet, s *Session) error { +func (b BookEditHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error { pk := p.(*packet.BookEdit) it, err := s.inv.Item(int(pk.InventorySlot)) diff --git a/server/session/handler_client_cache_blob_status.go b/server/session/handler_client_cache_blob_status.go index babda4d12..2219058a4 100644 --- a/server/session/handler_client_cache_blob_status.go +++ b/server/session/handler_client_cache_blob_status.go @@ -1,6 +1,7 @@ package session import ( + "github.com/df-mc/dragonfly/server/world" "github.com/sandertv/gophertunnel/minecraft/protocol" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -10,7 +11,7 @@ type ClientCacheBlobStatusHandler struct { } // Handle ... -func (c *ClientCacheBlobStatusHandler) Handle(p packet.Packet, s *Session) error { +func (c *ClientCacheBlobStatusHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error { pk := p.(*packet.ClientCacheBlobStatus) resp := &packet.ClientCacheMissResponse{Blobs: make([]protocol.CacheBlob, 0, len(pk.MissHashes))} diff --git a/server/session/handler_command_request.go b/server/session/handler_command_request.go index 5c4872df0..9484913d9 100644 --- a/server/session/handler_command_request.go +++ b/server/session/handler_command_request.go @@ -2,6 +2,7 @@ package session import ( "fmt" + "github.com/df-mc/dragonfly/server/world" "github.com/sandertv/gophertunnel/minecraft/protocol" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -12,7 +13,7 @@ type CommandRequestHandler struct { } // Handle ... -func (h *CommandRequestHandler) Handle(p packet.Packet, s *Session) error { +func (h *CommandRequestHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error { pk := p.(*packet.CommandRequest) if pk.Internal { return fmt.Errorf("command request packet must never have the internal field set to true") diff --git a/server/session/handler_container_close.go b/server/session/handler_container_close.go index 617015295..8c4490e35 100644 --- a/server/session/handler_container_close.go +++ b/server/session/handler_container_close.go @@ -2,6 +2,7 @@ package session import ( "fmt" + "github.com/df-mc/dragonfly/server/world" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -9,7 +10,7 @@ import ( type ContainerCloseHandler struct{} // Handle ... -func (h *ContainerCloseHandler) Handle(p packet.Packet, s *Session) error { +func (h *ContainerCloseHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error { pk := p.(*packet.ContainerClose) s.EmptyUIInventory() diff --git a/server/session/handler_emote.go b/server/session/handler_emote.go index 931fb0897..c689fc460 100644 --- a/server/session/handler_emote.go +++ b/server/session/handler_emote.go @@ -1,6 +1,7 @@ package session import ( + "github.com/df-mc/dragonfly/server/world" "github.com/google/uuid" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" "time" @@ -12,7 +13,7 @@ type EmoteHandler struct { } // Handle ... -func (h *EmoteHandler) Handle(p packet.Packet, s *Session) error { +func (h *EmoteHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error { pk := p.(*packet.Emote) if pk.EntityRuntimeID != selfEntityRuntimeID { diff --git a/server/session/handler_interact.go b/server/session/handler_interact.go index d0aedcfa3..e0dcda85d 100644 --- a/server/session/handler_interact.go +++ b/server/session/handler_interact.go @@ -2,6 +2,7 @@ package session import ( "fmt" + "github.com/df-mc/dragonfly/server/world" "github.com/sandertv/gophertunnel/minecraft/protocol" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -10,7 +11,7 @@ import ( type InteractHandler struct{} // Handle ... -func (h *InteractHandler) Handle(p packet.Packet, s *Session) error { +func (h *InteractHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error { pk := p.(*packet.Interact) pos := s.c.Position() diff --git a/server/session/handler_inventory_transaction.go b/server/session/handler_inventory_transaction.go index 715b68e73..fe25f8260 100644 --- a/server/session/handler_inventory_transaction.go +++ b/server/session/handler_inventory_transaction.go @@ -5,6 +5,7 @@ import ( "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/event" "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/world" "github.com/sandertv/gophertunnel/minecraft/protocol" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -13,7 +14,7 @@ import ( type InventoryTransactionHandler struct{} // Handle ... -func (h *InventoryTransactionHandler) Handle(p packet.Packet, s *Session) error { +func (h *InventoryTransactionHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error { pk := p.(*packet.InventoryTransaction) switch data := pk.TransactionData.(type) { @@ -21,7 +22,7 @@ func (h *InventoryTransactionHandler) Handle(p packet.Packet, s *Session) error h.resendInventories(s) // Always resend inventories with normal transactions. Most of the time we do not use these // transactions, so we're best off making sure the client and server stay in sync. - if err := h.handleNormalTransaction(pk, s); err != nil { + if err := h.handleNormalTransaction(pk, s, c); err != nil { s.log.Debug("process packet: InventoryTransaction: verify Normal transaction actions: " + err.Error()) return nil } @@ -31,20 +32,20 @@ func (h *InventoryTransactionHandler) Handle(p packet.Packet, s *Session) error h.resendInventories(s) return nil case *protocol.UseItemOnEntityTransactionData: - if err := s.UpdateHeldSlot(int(data.HotBarSlot), stackToItem(data.HeldItem.Stack)); err != nil { + if err := s.UpdateHeldSlot(int(data.HotBarSlot), stackToItem(data.HeldItem.Stack), tx, c); err != nil { return err } - return h.handleUseItemOnEntityTransaction(data, s) + return h.handleUseItemOnEntityTransaction(data, s, c) case *protocol.UseItemTransactionData: - if err := s.UpdateHeldSlot(int(data.HotBarSlot), stackToItem(data.HeldItem.Stack)); err != nil { + if err := s.UpdateHeldSlot(int(data.HotBarSlot), stackToItem(data.HeldItem.Stack), tx, c); err != nil { return err } - return h.handleUseItemTransaction(data, s) + return h.handleUseItemTransaction(data, s, c) case *protocol.ReleaseItemTransactionData: - if err := s.UpdateHeldSlot(int(data.HotBarSlot), stackToItem(data.HeldItem.Stack)); err != nil { + if err := s.UpdateHeldSlot(int(data.HotBarSlot), stackToItem(data.HeldItem.Stack), tx, c); err != nil { return err } - return h.handleReleaseItemTransaction(s) + return h.handleReleaseItemTransaction(s, c) } return fmt.Errorf("unhandled inventory transaction type %T", pk.TransactionData) } @@ -58,7 +59,7 @@ func (h *InventoryTransactionHandler) resendInventories(s *Session) { } // handleNormalTransaction ... -func (h *InventoryTransactionHandler) handleNormalTransaction(pk *packet.InventoryTransaction, s *Session) error { +func (h *InventoryTransactionHandler) handleNormalTransaction(pk *packet.InventoryTransaction, s *Session, c Controllable) error { if len(pk.Actions) != 2 { return fmt.Errorf("expected two actions for dropping an item, got %d", len(pk.Actions)) } @@ -99,17 +100,17 @@ func (h *InventoryTransactionHandler) handleNormalTransaction(pk *packet.Invento // logic in the Comparable() method was flawed, users would be able to cheat with item properties. // Only grow or shrink the held item to prevent any such issues. res := actual.Grow(count - actual.Count()) - if err := call(event.C(), int(s.heldSlot.Load()), res, s.inv.Handler().HandleDrop); err != nil { + if err := call(event.C(), int(*s.heldSlot), res, s.inv.Handler().HandleDrop); err != nil { return err } - n := s.c.Drop(res) + n := c.Drop(res) _ = s.inv.SetItem(slot, actual.Grow(-n)) return nil } // handleUseItemOnEntityTransaction ... -func (h *InventoryTransactionHandler) handleUseItemOnEntityTransaction(data *protocol.UseItemOnEntityTransactionData, s *Session) error { +func (h *InventoryTransactionHandler) handleUseItemOnEntityTransaction(data *protocol.UseItemOnEntityTransactionData, s *Session, c Controllable) error { s.swingingArm.Store(true) defer s.swingingArm.Store(false) @@ -127,22 +128,22 @@ func (h *InventoryTransactionHandler) handleUseItemOnEntityTransaction(data *pro var valid bool switch data.ActionType { case protocol.UseItemOnEntityActionInteract: - valid = s.c.UseItemOnEntity(e) + valid = c.UseItemOnEntity(e) case protocol.UseItemOnEntityActionAttack: - valid = s.c.AttackEntity(e) + valid = c.AttackEntity(e) default: return fmt.Errorf("unhandled UseItemOnEntity ActionType %v", data.ActionType) } if !valid { - slot := int(s.heldSlot.Load()) - item, _ := s.inv.Item(slot) - s.sendItem(item, slot, protocol.WindowIDInventory) + slot := int(*s.heldSlot) + it, _ := s.inv.Item(slot) + s.sendItem(it, slot, protocol.WindowIDInventory) } return nil } // handleUseItemTransaction ... -func (h *InventoryTransactionHandler) handleUseItemTransaction(data *protocol.UseItemTransactionData, s *Session) error { +func (h *InventoryTransactionHandler) handleUseItemTransaction(data *protocol.UseItemTransactionData, s *Session, c Controllable) error { pos := cube.Pos{int(data.BlockPosition[0]), int(data.BlockPosition[1]), int(data.BlockPosition[2])} s.swingingArm.Store(true) defer s.swingingArm.Store(false) @@ -155,11 +156,11 @@ func (h *InventoryTransactionHandler) handleUseItemTransaction(data *protocol.Us switch data.ActionType { case protocol.UseItemActionBreakBlock: - s.c.BreakBlock(pos) + c.BreakBlock(pos) case protocol.UseItemActionClickBlock: - s.c.UseItemOnBlock(pos, cube.Face(data.BlockFace), vec32To64(data.ClickedPosition)) + c.UseItemOnBlock(pos, cube.Face(data.BlockFace), vec32To64(data.ClickedPosition)) case protocol.UseItemActionClickAir: - s.c.UseItem() + c.UseItem() default: return fmt.Errorf("unhandled UseItem ActionType %v", data.ActionType) } @@ -167,7 +168,7 @@ func (h *InventoryTransactionHandler) handleUseItemTransaction(data *protocol.Us } // handleReleaseItemTransaction ... -func (h *InventoryTransactionHandler) handleReleaseItemTransaction(s *Session) error { - s.c.ReleaseItem() +func (h *InventoryTransactionHandler) handleReleaseItemTransaction(s *Session, c Controllable) error { + c.ReleaseItem() return nil } diff --git a/server/session/handler_item_stack_request.go b/server/session/handler_item_stack_request.go index a34908ae1..a7df2b85b 100644 --- a/server/session/handler_item_stack_request.go +++ b/server/session/handler_item_stack_request.go @@ -7,6 +7,7 @@ import ( "github.com/df-mc/dragonfly/server/event" "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/item/inventory" + "github.com/df-mc/dragonfly/server/world" "github.com/go-gl/mathgl/mgl64" "github.com/sandertv/gophertunnel/minecraft/protocol" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" @@ -44,7 +45,7 @@ type changeInfo struct { } // Handle ... -func (h *ItemStackRequestHandler) Handle(p packet.Packet, s *Session) error { +func (h *ItemStackRequestHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error { pk := p.(*packet.ItemStackRequest) h.current = time.Now() @@ -62,7 +63,7 @@ func (h *ItemStackRequestHandler) Handle(p packet.Packet, s *Session) error { } // handleRequest resolves a single item stack request from the client. -func (h *ItemStackRequestHandler) handleRequest(req protocol.ItemStackRequest, s *Session) (err error) { +func (h *ItemStackRequestHandler) handleRequest(req protocol.ItemStackRequest, s *Session, tx *world.Tx, c Controllable) (err error) { h.currentRequest = req.RequestID defer func() { if err != nil { @@ -90,7 +91,7 @@ func (h *ItemStackRequestHandler) handleRequest(req protocol.ItemStackRequest, s case *protocol.CraftRecipeStackRequestAction: if s.containerOpened.Load() { var special bool - switch s.c.World().Block(*s.openedPos.Load()).(type) { + switch tx.Block(*s.openedPos.Load()).(type) { case block.SmithingTable: err, special = h.handleSmithing(a, s), true case block.Stonecutter: @@ -109,7 +110,7 @@ func (h *ItemStackRequestHandler) handleRequest(req protocol.ItemStackRequest, s case *protocol.CraftRecipeOptionalStackRequestAction: err = h.handleCraftRecipeOptional(a, s, req.FilterStrings) case *protocol.CraftLoomRecipeStackRequestAction: - err = h.handleLoomCraft(a, s) + err = h.handleLoomCraft(a, s, tx, c) case *protocol.CraftGrindstoneRecipeStackRequestAction: err = h.handleGrindstoneCraft(s) case *protocol.CraftCreativeStackRequestAction: diff --git a/server/session/handler_lectern_update.go b/server/session/handler_lectern_update.go index 40e36461d..fee56ce6f 100644 --- a/server/session/handler_lectern_update.go +++ b/server/session/handler_lectern_update.go @@ -3,6 +3,7 @@ package session import ( "fmt" "github.com/df-mc/dragonfly/server/block" + "github.com/df-mc/dragonfly/server/world" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -10,14 +11,14 @@ import ( type LecternUpdateHandler struct{} // Handle ... -func (LecternUpdateHandler) Handle(p packet.Packet, s *Session) error { +func (LecternUpdateHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error { pk := p.(*packet.LecternUpdate) pos := blockPosFromProtocol(pk.Position) - if !canReach(s.c, pos.Vec3Middle()) { + if !canReach(c, pos.Vec3Middle()) { return fmt.Errorf("block at %v is not within reach", pos) } - if _, ok := s.c.World().Block(pos).(block.Lectern); !ok { + if _, ok := tx.Block(pos).(block.Lectern); !ok { return fmt.Errorf("block at %v is not a lectern", pos) } - return s.c.TurnLecternPage(pos, int(pk.Page)) + return c.TurnLecternPage(pos, int(pk.Page)) } diff --git a/server/session/handler_loom.go b/server/session/handler_loom.go index 4d73ee14e..db097cf0b 100644 --- a/server/session/handler_loom.go +++ b/server/session/handler_loom.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/df-mc/dragonfly/server/block" "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/world" "github.com/sandertv/gophertunnel/minecraft/protocol" ) @@ -17,12 +18,12 @@ const ( ) // handleLoomCraft handles a CraftLoomRecipe stack request action made using a loom table. -func (h *ItemStackRequestHandler) handleLoomCraft(a *protocol.CraftLoomRecipeStackRequestAction, s *Session) error { +func (h *ItemStackRequestHandler) handleLoomCraft(a *protocol.CraftLoomRecipeStackRequestAction, s *Session, tx *world.Tx, c Controllable) error { // First check if there actually is a loom opened. if !s.containerOpened.Load() { return fmt.Errorf("no loom container opened") } - if _, ok := s.c.World().Block(*s.openedPos.Load()).(block.Loom); !ok { + if _, ok := tx.Block(*s.openedPos.Load()).(block.Loom); !ok { return fmt.Errorf("no loom container opened") } diff --git a/server/session/handler_mob_equipment.go b/server/session/handler_mob_equipment.go index 1ef6fe96d..19c130cf0 100644 --- a/server/session/handler_mob_equipment.go +++ b/server/session/handler_mob_equipment.go @@ -2,6 +2,7 @@ package session import ( "fmt" + "github.com/df-mc/dragonfly/server/world" "github.com/sandertv/gophertunnel/minecraft/protocol" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -10,7 +11,7 @@ import ( type MobEquipmentHandler struct{} // Handle ... -func (*MobEquipmentHandler) Handle(p packet.Packet, s *Session) error { +func (*MobEquipmentHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error { pk := p.(*packet.MobEquipment) if pk.EntityRuntimeID != selfEntityRuntimeID { @@ -21,7 +22,7 @@ func (*MobEquipmentHandler) Handle(p packet.Packet, s *Session) error { // This window ID is expected, but we don't handle it. return nil case protocol.WindowIDInventory: - return s.UpdateHeldSlot(int(pk.InventorySlot), stackToItem(pk.NewItem.Stack)) + return s.UpdateHeldSlot(int(pk.InventorySlot), stackToItem(pk.NewItem.Stack), tx, c) default: return fmt.Errorf("only main inventory should be involved in slot chnage, got window ID %v", pk.WindowID) } diff --git a/server/session/handler_modal_form_response.go b/server/session/handler_modal_form_response.go index aed3d450f..1467d9043 100644 --- a/server/session/handler_modal_form_response.go +++ b/server/session/handler_modal_form_response.go @@ -3,6 +3,7 @@ package session import ( "fmt" "github.com/df-mc/dragonfly/server/player/form" + "github.com/df-mc/dragonfly/server/world" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" "sync" "sync/atomic" @@ -16,7 +17,7 @@ type ModalFormResponseHandler struct { } // Handle ... -func (h *ModalFormResponseHandler) Handle(p packet.Packet, s *Session) error { +func (h *ModalFormResponseHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error { pk := p.(*packet.ModalFormResponse) h.mu.Lock() @@ -37,7 +38,7 @@ func (h *ModalFormResponseHandler) Handle(p packet.Packet, s *Session) error { if !ok { return fmt.Errorf("no form with ID %v currently opened", pk.FormID) } - if err := f.SubmitJSON(resp, s.c); err != nil { + if err := f.SubmitJSON(resp, c); err != nil { return fmt.Errorf("error submitting form data: %w", err) } return nil diff --git a/server/session/handler_player_action.go b/server/session/handler_player_action.go index 5dfc30bc7..5ba7d3abd 100644 --- a/server/session/handler_player_action.go +++ b/server/session/handler_player_action.go @@ -3,6 +3,7 @@ package session import ( "fmt" "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" "github.com/sandertv/gophertunnel/minecraft/protocol" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -11,14 +12,14 @@ import ( type PlayerActionHandler struct{} // Handle ... -func (*PlayerActionHandler) Handle(p packet.Packet, s *Session) error { +func (*PlayerActionHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error { pk := p.(*packet.PlayerAction) - return handlePlayerAction(pk.ActionType, pk.BlockFace, pk.BlockPosition, pk.EntityRuntimeID, s) + return handlePlayerAction(pk.ActionType, pk.BlockFace, pk.BlockPosition, pk.EntityRuntimeID, s, c) } // handlePlayerAction handles an action performed by a player, found in packet.PlayerAction and packet.PlayerAuthInput. -func handlePlayerAction(action int32, face int32, pos protocol.BlockPos, entityRuntimeID uint64, s *Session) error { +func handlePlayerAction(action int32, face int32, pos protocol.BlockPos, entityRuntimeID uint64, s *Session, c Controllable) error { if entityRuntimeID != selfEntityRuntimeID { return errSelfRuntimeID } @@ -26,7 +27,7 @@ func handlePlayerAction(action int32, face int32, pos protocol.BlockPos, entityR case protocol.PlayerActionRespawn, protocol.PlayerActionDimensionChangeDone: // Don't do anything for these actions. case protocol.PlayerActionStopSleeping: - if mode := s.c.GameMode(); !mode.Visible() && !mode.HasCollision() { + if mode := c.GameMode(); !mode.Visible() && !mode.HasCollision() { // As of v1.19.50, the client sends this packet when switching to spectator mode... even if it wasn't // sleeping in the first place. This accounts for that. return nil @@ -36,13 +37,13 @@ func handlePlayerAction(action int32, face int32, pos protocol.BlockPos, entityR defer s.swingingArm.Store(false) s.breakingPos = cube.Pos{int(pos[0]), int(pos[1]), int(pos[2])} - s.c.StartBreaking(s.breakingPos, cube.Face(face)) + c.StartBreaking(s.breakingPos, cube.Face(face)) case protocol.PlayerActionAbortBreak: - s.c.AbortBreaking() + c.AbortBreaking() case protocol.PlayerActionPredictDestroyBlock, protocol.PlayerActionStopBreak: s.swingingArm.Store(true) defer s.swingingArm.Store(false) - s.c.FinishBreaking() + c.FinishBreaking() case protocol.PlayerActionCrackBreak: s.swingingArm.Store(true) defer s.swingingArm.Store(false) @@ -53,10 +54,10 @@ func handlePlayerAction(action int32, face int32, pos protocol.BlockPos, entityR // block to be broken by comparing positions. if newPos != s.breakingPos { s.breakingPos = newPos - s.c.StartBreaking(newPos, cube.Face(face)) + c.StartBreaking(newPos, cube.Face(face)) return nil } - s.c.ContinueBreaking(cube.Face(face)) + c.ContinueBreaking(cube.Face(face)) case protocol.PlayerActionStartItemUseOn, protocol.PlayerActionStopItemUseOn: // TODO: Properly utilize these actions. case protocol.PlayerActionStartBuildingBlock: @@ -66,7 +67,7 @@ func handlePlayerAction(action int32, face int32, pos protocol.BlockPos, entityR case protocol.PlayerActionMissedSwing: s.swingingArm.Store(true) defer s.swingingArm.Store(false) - s.c.PunchAir() + c.PunchAir() default: return fmt.Errorf("unhandled ActionType %v", action) } diff --git a/server/session/handler_player_auth_input.go b/server/session/handler_player_auth_input.go index bb8fa9d4e..946aef813 100644 --- a/server/session/handler_player_auth_input.go +++ b/server/session/handler_player_auth_input.go @@ -3,6 +3,7 @@ package session import ( "fmt" "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" "github.com/go-gl/mathgl/mgl32" "github.com/go-gl/mathgl/mgl64" "github.com/sandertv/gophertunnel/minecraft/protocol" @@ -14,18 +15,18 @@ import ( type PlayerAuthInputHandler struct{} // Handle ... -func (h PlayerAuthInputHandler) Handle(p packet.Packet, s *Session) error { +func (h PlayerAuthInputHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error { pk := p.(*packet.PlayerAuthInput) - if err := h.handleMovement(pk, s); err != nil { + if err := h.handleMovement(pk, s, c); err != nil { return err } - return h.handleActions(pk, s) + return h.handleActions(pk, s, c) } // handleMovement handles the movement part of the packet.PlayerAuthInput. -func (h PlayerAuthInputHandler) handleMovement(pk *packet.PlayerAuthInput, s *Session) error { - yaw, pitch := s.c.Rotation().Elem() - pos := s.c.Position() +func (h PlayerAuthInputHandler) handleMovement(pk *packet.PlayerAuthInput, s *Session, c Controllable) error { + yaw, pitch := c.Rotation().Elem() + pos := c.Position() reference := []float64{pitch, yaw, yaw, pos[0], pos[1], pos[2]} for i, v := range [...]*float32{&pk.Pitch, &pk.Yaw, &pk.HeadYaw, &pk.Position[0], &pk.Position[1], &pk.Position[2]} { @@ -60,23 +61,23 @@ func (h PlayerAuthInputHandler) handleMovement(pk *packet.PlayerAuthInput, s *Se s.teleportPos.Store(nil) } - s.c.Move(deltaPos, deltaYaw, deltaPitch) + c.Move(deltaPos, deltaYaw, deltaPitch) return nil } // handleActions handles the actions with the world that are present in the PlayerAuthInput packet. -func (h PlayerAuthInputHandler) handleActions(pk *packet.PlayerAuthInput, s *Session) error { +func (h PlayerAuthInputHandler) handleActions(pk *packet.PlayerAuthInput, s *Session, c Controllable) error { if pk.InputData&packet.InputFlagPerformItemInteraction != 0 { - if err := h.handleUseItemData(pk.ItemInteractionData, s); err != nil { + if err := h.handleUseItemData(pk.ItemInteractionData, s, c); err != nil { return err } } if pk.InputData&packet.InputFlagPerformBlockActions != 0 { - if err := h.handleBlockActions(pk.BlockActions, s); err != nil { + if err := h.handleBlockActions(pk.BlockActions, s, c); err != nil { return err } } - h.handleInputFlags(pk.InputData, s) + h.handleInputFlags(pk.InputData, s, c) if pk.InputData&packet.InputFlagPerformItemStackRequest != 0 { s.inTransaction.Store(true) @@ -94,47 +95,47 @@ func (h PlayerAuthInputHandler) handleActions(pk *packet.PlayerAuthInput, s *Ses } // handleInputFlags handles the toggleable input flags set in a PlayerAuthInput packet. -func (h PlayerAuthInputHandler) handleInputFlags(flags uint64, s *Session) { +func (h PlayerAuthInputHandler) handleInputFlags(flags uint64, s *Session, c Controllable) { if flags&packet.InputFlagStartSprinting != 0 { - s.c.StartSprinting() + c.StartSprinting() } if flags&packet.InputFlagStopSprinting != 0 { - s.c.StopSprinting() + c.StopSprinting() } if flags&packet.InputFlagStartSneaking != 0 { - s.c.StartSneaking() + c.StartSneaking() } if flags&packet.InputFlagStopSneaking != 0 { - s.c.StopSneaking() + c.StopSneaking() } if flags&packet.InputFlagStartSwimming != 0 { - s.c.StartSwimming() + c.StartSwimming() } if flags&packet.InputFlagStopSwimming != 0 { - s.c.StopSwimming() + c.StopSwimming() } if flags&packet.InputFlagStartGliding != 0 { - s.c.StartGliding() + c.StartGliding() } if flags&packet.InputFlagStopGliding != 0 { - s.c.StopGliding() + c.StopGliding() } if flags&packet.InputFlagStartJumping != 0 { - s.c.Jump() + c.Jump() } if flags&packet.InputFlagMissedSwing != 0 { s.swingingArm.Store(true) defer s.swingingArm.Store(false) - s.c.PunchAir() + c.PunchAir() } } // handleUseItemData handles the protocol.UseItemTransactionData found in a packet.PlayerAuthInput. -func (h PlayerAuthInputHandler) handleUseItemData(data protocol.UseItemTransactionData, s *Session) error { +func (h PlayerAuthInputHandler) handleUseItemData(data protocol.UseItemTransactionData, s *Session, c Controllable) error { s.swingingArm.Store(true) defer s.swingingArm.Store(false) - held, _ := s.c.HeldItems() + held, _ := c.HeldItems() if !held.Equal(stackToItem(data.HeldItem.Stack)) { s.log.Debug("process packet: PlayerAuthInput: UseItemTransaction: mismatch between actual held item and client held item") return nil @@ -144,7 +145,7 @@ func (h PlayerAuthInputHandler) handleUseItemData(data protocol.UseItemTransacti // Seems like this is only used for breaking blocks at the moment. switch data.ActionType { case protocol.UseItemActionBreakBlock: - s.c.BreakBlock(pos) + c.BreakBlock(pos) default: return fmt.Errorf("unhandled UseItem ActionType for PlayerAuthInput packet %v", data.ActionType) } @@ -152,9 +153,9 @@ func (h PlayerAuthInputHandler) handleUseItemData(data protocol.UseItemTransacti } // handleBlockActions handles a slice of protocol.PlayerBlockAction present in a PlayerAuthInput packet. -func (h PlayerAuthInputHandler) handleBlockActions(a []protocol.PlayerBlockAction, s *Session) error { +func (h PlayerAuthInputHandler) handleBlockActions(a []protocol.PlayerBlockAction, s *Session, c Controllable) error { for _, action := range a { - if err := handlePlayerAction(action.Action, action.Face, action.BlockPos, selfEntityRuntimeID, s); err != nil { + if err := handlePlayerAction(action.Action, action.Face, action.BlockPos, selfEntityRuntimeID, s, c); err != nil { return err } } diff --git a/server/session/handler_player_skin.go b/server/session/handler_player_skin.go index 4d912ada8..c536308f0 100644 --- a/server/session/handler_player_skin.go +++ b/server/session/handler_player_skin.go @@ -2,6 +2,7 @@ package session import ( "fmt" + "github.com/df-mc/dragonfly/server/world" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -9,7 +10,7 @@ import ( type PlayerSkinHandler struct{} // Handle ... -func (PlayerSkinHandler) Handle(p packet.Packet, s *Session) error { +func (PlayerSkinHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error { pk := p.(*packet.PlayerSkin) playerSkin, err := protocolToSkin(pk.Skin) @@ -17,7 +18,7 @@ func (PlayerSkinHandler) Handle(p packet.Packet, s *Session) error { return fmt.Errorf("error decoding skin: %w", err) } - s.c.SetSkin(playerSkin) + c.SetSkin(playerSkin) return nil } diff --git a/server/session/handler_request_ability.go b/server/session/handler_request_ability.go index 244bd9998..c4523cad0 100644 --- a/server/session/handler_request_ability.go +++ b/server/session/handler_request_ability.go @@ -1,6 +1,7 @@ package session import ( + "github.com/df-mc/dragonfly/server/world" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -8,15 +9,15 @@ import ( type RequestAbilityHandler struct{} // Handle ... -func (a RequestAbilityHandler) Handle(p packet.Packet, s *Session) error { +func (a RequestAbilityHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error { pk := p.(*packet.RequestAbility) if pk.Ability == packet.AbilityFlying { - if !s.c.GameMode().AllowsFlying() { + if !c.GameMode().AllowsFlying() { s.log.Debug("process packet: RequestAbility: flying flag enabled while unable to fly") s.sendAbilities() return nil } - s.c.StartFlying() + c.StartFlying() } return nil } diff --git a/server/session/handler_request_chunk_radius.go b/server/session/handler_request_chunk_radius.go index 0fcfb733f..30acf8e60 100644 --- a/server/session/handler_request_chunk_radius.go +++ b/server/session/handler_request_chunk_radius.go @@ -1,6 +1,7 @@ package session import ( + "github.com/df-mc/dragonfly/server/world" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -8,7 +9,7 @@ import ( type RequestChunkRadiusHandler struct{} // Handle ... -func (*RequestChunkRadiusHandler) Handle(p packet.Packet, s *Session) error { +func (*RequestChunkRadiusHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error { pk := p.(*packet.RequestChunkRadius) if pk.ChunkRadius > s.maxChunkRadius { diff --git a/server/session/handler_respawn.go b/server/session/handler_respawn.go index 59c2d10bd..93be9a752 100644 --- a/server/session/handler_respawn.go +++ b/server/session/handler_respawn.go @@ -2,6 +2,7 @@ package session import ( "fmt" + "github.com/df-mc/dragonfly/server/world" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -9,15 +10,14 @@ import ( type RespawnHandler struct{} // Handle ... -func (*RespawnHandler) Handle(p packet.Packet, s *Session) error { +func (*RespawnHandler) Handle(p packet.Packet, _ *Session, _ *world.Tx, c Controllable) error { pk := p.(*packet.Respawn) - if pk.EntityRuntimeID != selfEntityRuntimeID { return errSelfRuntimeID } if pk.State != packet.RespawnStateClientReadyToSpawn { return fmt.Errorf("respawn state must always be %v, but got %v", packet.RespawnStateClientReadyToSpawn, pk.State) } - s.c.Respawn() + c.Respawn() return nil } diff --git a/server/session/handler_server_bound_diagnostics.go b/server/session/handler_server_bound_diagnostics.go index a156ce8a1..8caf66249 100644 --- a/server/session/handler_server_bound_diagnostics.go +++ b/server/session/handler_server_bound_diagnostics.go @@ -2,6 +2,7 @@ package session import ( "github.com/df-mc/dragonfly/server/player/diagnostics" + "github.com/df-mc/dragonfly/server/world" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -9,9 +10,9 @@ import ( type ServerBoundDiagnosticsHandler struct{} // Handle ... -func (h *ServerBoundDiagnosticsHandler) Handle(p packet.Packet, s *Session) error { +func (h *ServerBoundDiagnosticsHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error { pk := p.(*packet.ServerBoundDiagnostics) - s.c.UpdateDiagnostics(diagnostics.Diagnostics{ + c.UpdateDiagnostics(diagnostics.Diagnostics{ AverageFramesPerSecond: float64(pk.AverageFramesPerSecond), AverageServerSimTickTime: float64(pk.AverageServerSimTickTime), AverageClientSimTickTime: float64(pk.AverageClientSimTickTime), diff --git a/server/session/handler_server_bound_loading_screen.go b/server/session/handler_server_bound_loading_screen.go index fd9c46663..abd18d9e4 100644 --- a/server/session/handler_server_bound_loading_screen.go +++ b/server/session/handler_server_bound_loading_screen.go @@ -2,6 +2,7 @@ package session import ( "fmt" + "github.com/df-mc/dragonfly/server/world" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" "sync/atomic" ) @@ -14,7 +15,7 @@ type ServerBoundLoadingScreenHandler struct { } // Handle ... -func (h *ServerBoundLoadingScreenHandler) Handle(p packet.Packet, s *Session) error { +func (h *ServerBoundLoadingScreenHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error { pk := p.(*packet.ServerBoundLoadingScreen) v, ok := pk.LoadingScreenID.Value() if !ok || h.expectedID.Load() == 0 { diff --git a/server/session/handler_sub_chunk_request.go b/server/session/handler_sub_chunk_request.go index bef89d23f..8c23513ff 100644 --- a/server/session/handler_sub_chunk_request.go +++ b/server/session/handler_sub_chunk_request.go @@ -10,8 +10,8 @@ import ( type SubChunkRequestHandler struct{} // Handle ... -func (*SubChunkRequestHandler) Handle(p packet.Packet, s *Session) error { +func (*SubChunkRequestHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error { pk := p.(*packet.SubChunkRequest) - s.ViewSubChunks(world.SubChunkPos(pk.Position), pk.Offsets) + s.ViewSubChunks(world.SubChunkPos(pk.Position), pk.Offsets, tx) return nil } diff --git a/server/session/handler_text.go b/server/session/handler_text.go index ee4ea992b..124b74863 100644 --- a/server/session/handler_text.go +++ b/server/session/handler_text.go @@ -2,6 +2,7 @@ package session import ( "fmt" + "github.com/df-mc/dragonfly/server/world" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) @@ -9,7 +10,7 @@ import ( type TextHandler struct{} // Handle ... -func (TextHandler) Handle(p packet.Packet, s *Session) error { +func (TextHandler) Handle(p packet.Packet, s *Session, _ *world.Tx, c Controllable) error { pk := p.(*packet.Text) if pk.TextType != packet.TextTypeChat { @@ -21,6 +22,6 @@ func (TextHandler) Handle(p packet.Packet, s *Session) error { if pk.XUID != s.conn.IdentityData().XUID { return fmt.Errorf("XUID must be equal to player's XUID") } - s.c.Chat(pk.Message) + c.Chat(pk.Message) return nil } diff --git a/server/session/player.go b/server/session/player.go index 27f878c9b..7371bb82c 100644 --- a/server/session/player.go +++ b/server/session/player.go @@ -20,7 +20,6 @@ import ( "github.com/sandertv/gophertunnel/minecraft/protocol/packet" "math" "net" - "sync/atomic" "time" _ "unsafe" // Imported for compiler directives. ) @@ -58,19 +57,18 @@ func (s *Session) StartShowingEntity(e world.Entity) { } // closeCurrentContainer closes the container the player might currently have open. -func (s *Session) closeCurrentContainer() { +func (s *Session) closeCurrentContainer(tx *world.Tx) { if !s.containerOpened.Load() { return } s.closeWindow() pos := *s.openedPos.Load() - w := s.c.World() - b := w.Block(pos) + b := tx.Block(pos) if container, ok := b.(block.Container); ok { - container.RemoveViewer(s, w, pos) + container.RemoveViewer(s, tx, pos) } else if enderChest, ok := b.(block.EnderChest); ok { - enderChest.RemoveViewer(w, pos) + enderChest.RemoveViewer(tx, pos) } } @@ -80,18 +78,13 @@ func (s *Session) EmptyUIInventory() { if s == Nop { return } - items := s.ui.Clear() - leftover := make([]item.Stack, 0, len(items)) - for _, i := range items { + for _, i := range s.ui.Clear() { if n, err := s.inv.AddItem(i); err != nil { - leftover = append(leftover, i.Grow(i.Count()-n)) + // We couldn't add the item to the main inventory (probably because + // it was full), so we drop it instead. + s.c.Drop(i.Grow(i.Count() - n)) } } - for _, i := range leftover { - // We can't put this back in the inventory, so the best option here is to simply get rid of the item if - // dropping was cancelled. - s.c.Drop(i) - } } // SendRespawn spawns the Controllable entity of the session client-side in the world, provided it has died. @@ -231,7 +224,7 @@ type smelter interface { // invByID attempts to return an inventory by the ID passed. If found, the inventory is returned and the bool // returned is true. -func (s *Session) invByID(id int32) (*inventory.Inventory, bool) { +func (s *Session) invByID(id int32, tx *world.Tx) (*inventory.Inventory, bool) { switch id { case protocol.ContainerCraftingInput, protocol.ContainerCreatedOutput, protocol.ContainerCursor: // UI inventory. @@ -244,62 +237,48 @@ func (s *Session) invByID(id int32) (*inventory.Inventory, bool) { case protocol.ContainerArmor: // Armour inventory. return s.armour.Inventory(), true - case protocol.ContainerLevelEntity: - if s.containerOpened.Load() { - return s.openedWindow.Load(), true + default: + if !s.containerOpened.Load() { + return nil, false } - case protocol.ContainerBarrel: - if s.containerOpened.Load() { - if _, barrel := s.c.World().Block(*s.openedPos.Load()).(block.Barrel); barrel { + switch id { + case protocol.ContainerLevelEntity: + return s.openedWindow.Load(), true + case protocol.ContainerBarrel: + if _, barrel := tx.Block(*s.openedPos.Load()).(block.Barrel); barrel { return s.openedWindow.Load(), true } - } - case protocol.ContainerBeaconPayment: - if s.containerOpened.Load() { - if _, beacon := s.c.World().Block(*s.openedPos.Load()).(block.Beacon); beacon { + case protocol.ContainerBeaconPayment: + if _, beacon := tx.Block(*s.openedPos.Load()).(block.Beacon); beacon { return s.ui, true } - } - case protocol.ContainerAnvilInput, protocol.ContainerAnvilMaterial: - if s.containerOpened.Load() { - if _, anvil := s.c.World().Block(*s.openedPos.Load()).(block.Anvil); anvil { + case protocol.ContainerAnvilInput, protocol.ContainerAnvilMaterial: + if _, anvil := tx.Block(*s.openedPos.Load()).(block.Anvil); anvil { return s.ui, true } - } - case protocol.ContainerSmithingTableTemplate, protocol.ContainerSmithingTableInput, protocol.ContainerSmithingTableMaterial: - if s.containerOpened.Load() { - if _, smithing := s.c.World().Block(*s.openedPos.Load()).(block.SmithingTable); smithing { + case protocol.ContainerSmithingTableTemplate, protocol.ContainerSmithingTableInput, protocol.ContainerSmithingTableMaterial: + if _, smithing := tx.Block(*s.openedPos.Load()).(block.SmithingTable); smithing { return s.ui, true } - } - case protocol.ContainerLoomInput, protocol.ContainerLoomDye, protocol.ContainerLoomMaterial: - if s.containerOpened.Load() { - if _, loom := s.c.World().Block(*s.openedPos.Load()).(block.Loom); loom { + case protocol.ContainerLoomInput, protocol.ContainerLoomDye, protocol.ContainerLoomMaterial: + if _, loom := tx.Block(*s.openedPos.Load()).(block.Loom); loom { return s.ui, true } - } - case protocol.ContainerStonecutterInput: - if s.containerOpened.Load() { - if _, ok := s.c.World().Block(*s.openedPos.Load()).(block.Stonecutter); ok { + case protocol.ContainerStonecutterInput: + if _, ok := tx.Block(*s.openedPos.Load()).(block.Stonecutter); ok { return s.ui, true } - } - case protocol.ContainerGrindstoneInput, protocol.ContainerGrindstoneAdditional: - if s.containerOpened.Load() { - if _, ok := s.c.World().Block(*s.openedPos.Load()).(block.Grindstone); ok { + case protocol.ContainerGrindstoneInput, protocol.ContainerGrindstoneAdditional: + if _, ok := tx.Block(*s.openedPos.Load()).(block.Grindstone); ok { return s.ui, true } - } - case protocol.ContainerEnchantingInput, protocol.ContainerEnchantingMaterial: - if s.containerOpened.Load() { - if _, enchanting := s.c.World().Block(*s.openedPos.Load()).(block.EnchantingTable); enchanting { + case protocol.ContainerEnchantingInput, protocol.ContainerEnchantingMaterial: + if _, enchanting := tx.Block(*s.openedPos.Load()).(block.EnchantingTable); enchanting { return s.ui, true } - } - case protocol.ContainerFurnaceIngredient, protocol.ContainerFurnaceFuel, protocol.ContainerFurnaceResult, - protocol.ContainerBlastFurnaceIngredient, protocol.ContainerSmokerIngredient: - if s.containerOpened.Load() { - if _, ok := s.c.World().Block(*s.openedPos.Load()).(smelter); ok { + case protocol.ContainerFurnaceIngredient, protocol.ContainerFurnaceFuel, protocol.ContainerFurnaceResult, + protocol.ContainerBlastFurnaceIngredient, protocol.ContainerSmokerIngredient: + if _, ok := tx.Block(*s.openedPos.Load()).(smelter); ok { return s.openedWindow.Load(), true } } @@ -411,29 +390,29 @@ func (s *Session) Transfer(ip net.IP, port int) { // SendGameMode sends the game mode of the Controllable entity of the session to the client. It makes sure the right // flags are set to create the full game mode. -func (s *Session) SendGameMode(mode world.GameMode) { +func (s *Session) SendGameMode(c Controllable) { if s == Nop { return } - s.writePacket(&packet.SetPlayerGameType{GameType: gameTypeFromMode(mode)}) - s.sendAbilities() + s.writePacket(&packet.SetPlayerGameType{GameType: gameTypeFromMode(c.GameMode())}) + s.sendAbilities(c) } // sendAbilities sends the abilities of the Controllable entity of the session to the client. -func (s *Session) sendAbilities() { - mode, abilities := s.c.GameMode(), uint32(0) +func (s *Session) sendAbilities(c Controllable) { + mode, abilities := c.GameMode(), uint32(0) if mode.AllowsFlying() { abilities |= protocol.AbilityMayFly - if s.c.Flying() { + if c.Flying() { abilities |= protocol.AbilityFlying } } if !mode.HasCollision() { abilities |= protocol.AbilityNoClip - defer s.c.StartFlying() + defer c.StartFlying() // If the client is currently on the ground and turned to spectator mode, it will be unable to sprint during // flight. In order to allow this, we force the client to be flying through a MovePlayer packet. - s.ViewEntityTeleport(s.c, s.c.Position()) + s.ViewEntityTeleport(c, c.Position()) } if !mode.AllowsTakingDamage() { abilities |= protocol.AbilityInvulnerable @@ -611,7 +590,7 @@ func skinToProtocol(s skin.Skin) protocol.Skin { // removeFromPlayerList removes the player of a session from the player list of this session. It will no // longer be shown in the in-game pause menu screen. func (s *Session) removeFromPlayerList(session *Session) { - c := session.c + c := session.ent s.entityMutex.Lock() delete(s.entities, s.entityRuntimeIDs[c]) @@ -620,59 +599,60 @@ func (s *Session) removeFromPlayerList(session *Session) { s.writePacket(&packet.PlayerList{ ActionType: packet.PlayerListActionRemove, - Entries: []protocol.PlayerListEntry{{ - UUID: c.UUID(), - }}, + Entries: []protocol.PlayerListEntry{{UUID: c.UUID()}}, }) } // HandleInventories starts handling the inventories of the Controllable entity of the session. It sends packets when // slots in the inventory are changed. -func (s *Session) HandleInventories() (inv, offHand, enderChest *inventory.Inventory, armour *inventory.Armour, heldSlot *atomic.Uint32) { - s.inv = inventory.New(36, func(slot int, _, item item.Stack) { - if s.c == nil { - return - } - if slot == int(s.heldSlot.Load()) { - for _, viewer := range s.c.World().Viewers(s.c.Position()) { - viewer.ViewEntityItems(s.c) +func (s *Session) HandleInventories(tx *world.Tx, c Controllable) (inv, offHand, enderChest *inventory.Inventory, armour *inventory.Armour, heldSlot *uint32) { + s.inv = inventory.New(36, s.broadcastInvFunc(tx, c)) + s.offHand = inventory.New(1, s.broadcastOffHandFunc(tx, c)) + s.enderChest = inventory.New(27, s.broadcastEnderChestFunc(tx, c)) + s.armour = inventory.NewArmour(s.broadcastArmourFunc(tx, c)) + return s.inv, s.offHand, s.enderChest, s.armour, s.heldSlot +} + +func (s *Session) broadcastInvFunc(tx *world.Tx, c Controllable) func(slot int, before, after item.Stack) { + return func(slot int, _, after item.Stack) { + if slot == int(*s.heldSlot) { + for _, viewer := range tx.Viewers(c.Position()) { + viewer.ViewEntityItems(c) } } if !s.inTransaction.Load() { - s.sendItem(item, slot, protocol.WindowIDInventory) + s.sendItem(after, slot, protocol.WindowIDInventory) } - }) - s.offHand = inventory.New(1, func(slot int, _, item item.Stack) { - if s.c == nil { - return + } +} + +func (s *Session) broadcastEnderChestFunc(tx *world.Tx, _ Controllable) func(slot int, before, after item.Stack) { + return func(slot int, _, after item.Stack) { + if !s.inTransaction.Load() { + if _, ok := tx.Block(*s.openedPos.Load()).(block.EnderChest); ok { + s.ViewSlotChange(slot, after) + } } - for _, viewer := range s.c.World().Viewers(s.c.Position()) { - viewer.ViewEntityItems(s.c) + } +} + +func (s *Session) broadcastOffHandFunc(tx *world.Tx, c Controllable) func(slot int, before, after item.Stack) { + return func(slot int, _, after item.Stack) { + for _, viewer := range tx.Viewers(c.Position()) { + viewer.ViewEntityItems(c) } if !s.inTransaction.Load() { i, _ := s.offHand.Item(0) s.writePacket(&packet.InventoryContent{ WindowID: protocol.WindowIDOffHand, - Content: []protocol.ItemInstance{ - instanceFromItem(i), - }, + Content: []protocol.ItemInstance{instanceFromItem(i)}, }) } - }) - s.enderChest = inventory.New(27, func(slot int, _, item item.Stack) { - if s.c == nil { - return - } - if !s.inTransaction.Load() { - if _, ok := s.c.World().Block(*s.openedPos.Load()).(block.EnderChest); ok { - s.ViewSlotChange(slot, item) - } - } - }) - s.armour = inventory.NewArmour(func(slot int, before, after item.Stack) { - if s.c == nil { - return - } + } +} + +func (s *Session) broadcastArmourFunc(tx *world.Tx, c Controllable) func(slot int, before, after item.Stack) { + return func(slot int, before, after item.Stack) { if !s.inTransaction.Load() { s.sendItem(after, slot, protocol.WindowIDArmour) } @@ -680,11 +660,10 @@ func (s *Session) HandleInventories() (inv, offHand, enderChest *inventory.Inven // Only send armour if the item type actually changed. return } - for _, viewer := range s.c.World().Viewers(s.c.Position()) { - viewer.ViewEntityArmour(s.c) + for _, viewer := range tx.Viewers(c.Position()) { + viewer.ViewEntityArmour(c) } - }) - return s.inv, s.offHand, s.enderChest, s.armour, s.heldSlot + } } // SetHeldSlot sets the currently held hotbar slot. @@ -711,20 +690,20 @@ func (s *Session) SetHeldSlot(slot int) error { // UpdateHeldSlot updates the held slot of the Session to the slot passed. It also verifies that the item in that slot // matches an expected item stack. -func (s *Session) UpdateHeldSlot(slot int, expected item.Stack) error { +func (s *Session) UpdateHeldSlot(slot int, expected item.Stack, tx *world.Tx, c Controllable) 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 slot > 8 { return fmt.Errorf("new held slot exceeds hotbar range 0-8: slot is %v", slot) } - if s.heldSlot.Load() == uint32(slot) { + if *s.heldSlot == uint32(slot) { // Old slot was the same as new slot, so don't do anything. return nil } // The user swapped changed held slots so stop using item right away. - s.c.ReleaseItem() + c.ReleaseItem() - s.heldSlot.Store(uint32(slot)) + *s.heldSlot = uint32(slot) clientSideItem := expected actual, _ := s.inv.Item(slot) @@ -735,8 +714,8 @@ func (s *Session) UpdateHeldSlot(slot int, expected item.Stack) error { // out of sync. s.log.Debug("update held slot: client-side item must be identical to server-side item, but got differences", "client-held", clientSideItem.String(), "server-held", actual.String()) } - for _, viewer := range s.c.World().Viewers(s.c.Position()) { - viewer.ViewEntityItems(s.c) + for _, viewer := range tx.Viewers(c.Position()) { + viewer.ViewEntityItems(c) } return nil } diff --git a/server/session/session.go b/server/session/session.go index 8937804fb..709c16a16 100644 --- a/server/session/session.go +++ b/server/session/session.go @@ -34,7 +34,7 @@ type Session struct { log *slog.Logger once, connOnce sync.Once - c Controllable + ent *world.EntityHandle conn Conn handlers map[uint32]packetHandler @@ -60,7 +60,7 @@ type Session struct { hiddenEntities map[world.Entity]struct{} // heldSlot is the slot in the inventory that the controllable is holding. - heldSlot *atomic.Uint32 + heldSlot *uint32 inv, offHand, enderChest, ui *inventory.Inventory armour *inventory.Armour @@ -237,13 +237,7 @@ func (s *Session) Close() error { func (s *Session) close() { _ = s.c.Close() - // Move UI inventory items to the main inventory. - for _, it := range s.ui.Items() { - if _, err := s.inv.AddItem(it); err != nil { - // We couldn't add the item to the main inventory (probably because it was full), so we drop it instead. - s.c.Drop(it) - } - } + s.EmptyUIInventory() s.onStop(s.c) @@ -312,9 +306,11 @@ func (s *Session) handlePackets() { if err != nil { return } - if err := s.handlePacket(pk); err != nil { - // An error occurred during the handling of a packet. - // Print the error and stop handling any more packets. + s.ent.World().Query(func(tx *world.Tx) { + c := s.ent.Entity(tx) + err = s.handlePacket(pk, tx, c) + }) + if err != nil { s.log.Debug("process packet: " + err.Error()) return } @@ -429,7 +425,7 @@ func (s *Session) ChangingDimension() bool { // handlePacket handles an incoming packet, processing it accordingly. If the packet had invalid data or was // otherwise not valid in its context, an error is returned. -func (s *Session) handlePacket(pk packet.Packet) error { +func (s *Session) handlePacket(pk packet.Packet, tx *world.Tx, c Controllable) (err error) { handler, ok := s.handlers[pk.ID()] if !ok { s.log.Debug("unhandled packet", "packet", fmt.Sprintf("%T", pk), "data", fmt.Sprintf("%+v", pk)[1:]) @@ -439,7 +435,7 @@ func (s *Session) handlePacket(pk packet.Packet) error { // A nil handler means it was explicitly unhandled. return nil } - if err := handler.Handle(pk, s); err != nil { + if err := handler.Handle(pk, s, tx, c); err != nil { return fmt.Errorf("%T: %w", pk, err) } return nil diff --git a/server/world/entity.go b/server/world/entity.go index 0233388dc..289fa5251 100644 --- a/server/world/entity.go +++ b/server/world/entity.go @@ -3,16 +3,72 @@ package world import ( "github.com/df-mc/dragonfly/server/block/cube" "github.com/go-gl/mathgl/mgl64" + "github.com/google/uuid" "golang.org/x/exp/maps" "io" + "sync/atomic" "time" ) +type AdvancedEntityType interface { + EntityType + + Init(conf any, data *EntityData) + From(tx *Tx, handle *EntityHandle, data *EntityData) Entity +} + +type EntityHandle struct { + id uuid.UUID + t AdvancedEntityType + + w atomic.Pointer[World] + + data EntityData + + // HANDLER?? HANDLE WORLD CHANGE HERE +} + +func NewEntity(t AdvancedEntityType, conf any) *EntityHandle { + handle := &EntityHandle{id: uuid.New(), t: t} + t.Init(conf, &handle.data) + return handle +} + +type EntityData struct { + Pos, Vel mgl64.Vec3 + Rot cube.Rotation + Name string + FireDuration time.Duration + Age time.Duration + + Data any +} + +func (e *EntityHandle) Entity(tx *Tx) Entity { + if e.World() != tx.World() { + panic("can't load entity with Tx of different world") + } + return e.t.From(tx, e, &e.data) +} + +func (e *EntityHandle) UUID() uuid.UUID { + return e.id +} + +func (e *EntityHandle) World() *World { + return e.w.Load() +} + +func (e *EntityHandle) Handle() *EntityHandle { + return e +} + // Entity represents an entity in the world, typically an object that may be moved around and can be // interacted with by other entities. // Viewers of a world may view an entity when near it. type Entity interface { io.Closer + Handle() *EntityHandle // Type returns the EntityType of the Entity. Type() EntityType diff --git a/server/world/tx.go b/server/world/tx.go new file mode 100644 index 000000000..87a5af451 --- /dev/null +++ b/server/world/tx.go @@ -0,0 +1,120 @@ +package world + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/go-gl/mathgl/mgl64" + "sync/atomic" + "time" +) + +type Tx struct { + complete atomic.Bool + w *World +} + +// Range returns the lower and upper bounds of the World that the Tx is +// operating on. +func (tx *Tx) Range() cube.Range { + return tx.w.ra +} + +func (tx *Tx) SetBlock(pos cube.Pos, b Block, opts *SetOpts) { + tx.World().setBlock(pos, b, opts) +} + +func (tx *Tx) Block(pos cube.Pos) Block { + return tx.World().block(pos) +} + +func (tx *Tx) Liquid(pos cube.Pos) (Liquid, bool) { + return tx.World().liquid(pos) +} + +func (tx *Tx) SetLiquid(pos cube.Pos, b Liquid) { + tx.World().setLiquid(pos, b) +} + +func (tx *Tx) BuildStructure(pos cube.Pos, s Structure) { + tx.World().buildStructure(pos, s) +} + +func (tx *Tx) ScheduleBlockUpdate(pos cube.Pos, delay time.Duration) { + tx.World().scheduleBlockUpdate(pos, delay) +} + +func (tx *Tx) HighestLightBlocker(x, z int) int { + return tx.World().highestLightBlocker(x, z) +} + +func (tx *Tx) HighestBlock(x, z int) int { + return tx.World().highestBlock(x, z) +} + +func (tx *Tx) Light(pos cube.Pos) uint8 { + return tx.World().light(pos) +} + +func (tx *Tx) Skylight(pos cube.Pos) uint8 { + return tx.World().skyLight(pos) +} + +func (tx *Tx) SetBiome(pos cube.Pos, b Biome) { + tx.World().setBiome(pos, b) +} + +func (tx *Tx) Biome(pos cube.Pos) Biome { + return tx.World().biome(pos) +} + +func (tx *Tx) Temperature(pos cube.Pos) float64 { + return tx.World().temperature(pos) +} + +func (tx *Tx) RainingAt(pos cube.Pos) bool { + return tx.World().rainingAt(pos) +} + +func (tx *Tx) SnowingAt(pos cube.Pos) bool { + return tx.World().snowingAt(pos) +} + +func (tx *Tx) ThunderingAt(pos cube.Pos) bool { + return tx.World().thunderingAt(pos) +} + +func (tx *Tx) AddParticle(pos mgl64.Vec3, p Particle) { + tx.World().addParticle(pos, p) +} + +func (tx *Tx) PlaySound(pos mgl64.Vec3, s Sound) { + tx.World().playSound(pos, s) +} + +func (tx *Tx) AddEntity(e Entity) { + tx.World().addEntity(e) +} + +func (tx *Tx) RemoveEntity(e Entity) { + tx.World().removeEntity(e) +} + +func (tx *Tx) EntitiesWithin(box cube.BBox, ignore func(Entity) bool) []Entity { + return tx.World().entitiesWithin(tx, box, ignore) +} + +func (tx *Tx) Entities() []Entity { + return tx.World().allEntities(tx) +} + +func (tx *Tx) Viewers(pos mgl64.Vec3) []Viewer { + return tx.World().viewersOf(pos) +} + +// World returns the World of the Tx. It panics if the transaction was already +// marked complete. +func (tx *Tx) World() *World { + if tx.complete.Load() { + panic("transaction already completed") + } + return tx.w +} diff --git a/server/world/weather.go b/server/world/weather.go index f12ccc03f..a543dacca 100644 --- a/server/world/weather.go +++ b/server/world/weather.go @@ -22,11 +22,11 @@ func (w weather) StartWeatherCycle() { // SnowingAt checks if it is snowing at a specific cube.Pos in the World. True is returned if the temperature in the // biome at that position is sufficiently low, if it is raining and if it's above the top-most obstructing block. -func (w weather) SnowingAt(pos cube.Pos) bool { +func (w weather) snowingAt(pos cube.Pos) bool { if w.w == nil || !w.w.Dimension().WeatherCycle() { return false } - if b := w.w.Biome(pos); b.Rainfall() == 0 || w.w.Temperature(pos) > 0.15 { + if b := w.w.biome(pos); b.Rainfall() == 0 || w.w.temperature(pos) > 0.15 { return false } w.w.set.Lock() @@ -38,11 +38,11 @@ func (w weather) SnowingAt(pos cube.Pos) bool { // RainingAt checks if it is raining at a specific cube.Pos in the World. True is returned if it is raining, if the // temperature is high enough in the biome for it not to be snow and if the block is above the top-most obstructing // block. -func (w weather) RainingAt(pos cube.Pos) bool { +func (w weather) rainingAt(pos cube.Pos) bool { if w.w == nil || !w.w.Dimension().WeatherCycle() { return false } - if b := w.w.Biome(pos); b.Rainfall() == 0 || w.w.Temperature(pos) <= 0.15 { + if b := w.w.biome(pos); b.Rainfall() == 0 || w.w.temperature(pos) <= 0.15 { return false } w.w.set.Lock() @@ -53,8 +53,8 @@ func (w weather) RainingAt(pos cube.Pos) bool { // ThunderingAt checks if it is thundering at a specific cube.Pos in the World. True is returned if RainingAt returns // true and if it is thundering in the world. -func (w weather) ThunderingAt(pos cube.Pos) bool { - raining := w.RainingAt(pos) +func (w weather) thunderingAt(pos cube.Pos) bool { + raining := w.rainingAt(pos) w.w.set.Lock() a := w.w.set.Thundering && raining w.w.set.Unlock() @@ -200,7 +200,7 @@ func (w weather) lightningPosition(c ChunkPos) mgl64.Vec3 { // from the mgl64.Vec3. If multiple entities are found, the position of one of the entities is selected randomly. func (w weather) adjustPositionToEntities(vec mgl64.Vec3) mgl64.Vec3 { max := vec.Add(mgl64.Vec3{0, float64(w.w.Range().Max())}) - ent := w.w.EntitiesWithin(cube.Box(vec[0], vec[1], vec[2], max[0], max[1], max[2]).GrowVec3(mgl64.Vec3{3, 3, 3}), nil) + ent := w.w.entitiesWithin(cube.Box(vec[0], vec[1], vec[2], max[0], max[1], max[2]).GrowVec3(mgl64.Vec3{3, 3, 3}), nil) list := make([]mgl64.Vec3, 0, len(ent)/3) for _, e := range ent { @@ -208,7 +208,7 @@ func (w weather) adjustPositionToEntities(vec mgl64.Vec3) mgl64.Vec3 { // Any (living) entity that is positioned higher than the highest block at its position is eligible to be // struck by lightning. We first save all entity positions where this is the case. pos := cube.PosFromVec3(e.Position()) - if w.w.HighestBlock(pos[0], pos[1]) < pos[2] { + if w.w.highestBlock(pos[0], pos[1]) < pos[2] { list = append(list, e.Position()) } } diff --git a/server/world/world.go b/server/world/world.go index fce62e3a7..b04b4a1fb 100644 --- a/server/world/world.go +++ b/server/world/world.go @@ -46,27 +46,23 @@ type World struct { closing chan struct{} running sync.WaitGroup - chunkMu sync.Mutex // chunks holds a cache of chunks currently loaded. These chunks are cleared from this map after some time // of not being used. chunks map[ChunkPos]*Column - entityMu sync.RWMutex // entities holds a map of entities currently loaded and the last ChunkPos that the Entity was in. // These are tracked so that a call to RemoveEntity can find the correct entity. - entities map[Entity]ChunkPos + entities map[*EntityHandle]ChunkPos r *rand.Rand - updateMu sync.Mutex // scheduledUpdates is a map of tick time values indexed by the block position at which an update is // scheduled. If the current tick exceeds the tick value passed, the block update will be performed // and the entry will be removed from the map. scheduledUpdates map[cube.Pos]int64 neighbourUpdates []neighbourUpdate - viewersMu sync.Mutex - viewers map[*Loader]Viewer + viewers map[*Loader]Viewer } // New creates a new initialised world. The world may be used right away, but it will not be saved or loaded @@ -103,6 +99,12 @@ func (w *World) Range() cube.Range { return w.ra } +func (w *World) Query(func(tx *Tx)) { + if w == nil { + return + } +} + // EntityRegistry returns the EntityRegistry that was passed to the World's // Config upon construction. func (w *World) EntityRegistry() EntityRegistry { @@ -112,7 +114,7 @@ func (w *World) EntityRegistry() EntityRegistry { // Block reads a block from the position passed. If a chunk is not yet loaded at that position, the chunk is // loaded, or generated if it could not be found in the world save, and the block returned. Chunks will be // loaded synchronously. -func (w *World) Block(pos cube.Pos) Block { +func (w *World) block(pos cube.Pos) Block { if w == nil || pos.OutOfBounds(w.Range()) { // Fast way out. return air() @@ -144,7 +146,7 @@ func (w *World) Block(pos cube.Pos) Block { // Biome reads the biome at the position passed. If a chunk is not yet loaded at that position, the chunk is // loaded, or generated if it could not be found in the world save, and the biome returned. Chunks will be // loaded synchronously. -func (w *World) Biome(pos cube.Pos) Biome { +func (w *World) biome(pos cube.Pos) Biome { if w == nil || pos.OutOfBounds(w.Range()) { // Fast way out. return ocean() @@ -180,7 +182,7 @@ func (w *World) blockInChunk(c *Column, pos cube.Pos) Block { // HighestLightBlocker gets the Y value of the highest fully light blocking block at the x and z values // passed in the world. -func (w *World) HighestLightBlocker(x, z int) int { +func (w *World) highestLightBlocker(x, z int) int { if w == nil { return w.Range()[0] } @@ -191,7 +193,7 @@ func (w *World) HighestLightBlocker(x, z int) int { // HighestBlock looks up the highest non-air block in the world at a specific x and z in the world. The y // value of the highest block is returned, or 0 if no blocks were present in the column. -func (w *World) HighestBlock(x, z int) int { +func (w *World) highestBlock(x, z int) int { if w == nil { return w.Range()[0] } @@ -206,10 +208,10 @@ func (w *World) highestObstructingBlock(x, z int) int { if w == nil { return 0 } - yHigh := w.HighestBlock(x, z) + yHigh := w.highestBlock(x, z) for y := yHigh; y >= w.Range()[0]; y-- { pos := cube.Pos{x, y, z} - m := w.Block(pos).Model() + m := w.block(pos).Model() if m.FaceSolid(pos, cube.FaceUp, w) || m.FaceSolid(pos, cube.FaceDown, w) { return y } @@ -239,7 +241,7 @@ type SetOpts struct { // // SetBlock should be avoided in situations where performance is critical when needing to set a lot of blocks // to the world. BuildStructure may be used instead. -func (w *World) SetBlock(pos cube.Pos, b Block, opts *SetOpts) { +func (w *World) setBlock(pos cube.Pos, b Block, opts *SetOpts) { if w == nil || pos.OutOfBounds(w.Range()) { // Fast way out. return @@ -307,7 +309,7 @@ func (w *World) SetBlock(pos cube.Pos, b Block, opts *SetOpts) { // SetBiome sets the biome at the position passed. If a chunk is not yet loaded at that position, the chunk is // first loaded or generated if it could not be found in the world save. -func (w *World) SetBiome(pos cube.Pos, b Biome) { +func (w *World) setBiome(pos cube.Pos, b Biome) { if w == nil || pos.OutOfBounds(w.Range()) { // Fast way out. return @@ -325,7 +327,7 @@ func (w *World) SetBiome(pos cube.Pos, b Biome) { // will do so within much less time than separate SetBlock calls would. // The method operates on a per-chunk basis, setting all blocks within a single chunk part of the structure // before moving on to the next chunk. -func (w *World) BuildStructure(pos cube.Pos, s Structure) { +func (w *World) buildStructure(pos cube.Pos, s Structure) { if w == nil { return } @@ -345,7 +347,7 @@ func (w *World) BuildStructure(pos cube.Pos, s Structure) { if actual[0]>>4 == chunkX && actual[2]>>4 == chunkZ { return w.blockInChunk(c, actual) } - return w.Block(actual) + return w.block(actual) } baseX, baseZ := chunkX<<4, chunkZ<<4 subs := c.Sub() @@ -414,7 +416,7 @@ func (w *World) BuildStructure(pos cube.Pos, s Structure) { // Liquid attempts to return any liquid block at the position passed. This liquid may be in the foreground or // in any other layer. // If found, the liquid is returned. If not, the bool returned is false and the liquid is nil. -func (w *World) Liquid(pos cube.Pos) (Liquid, bool) { +func (w *World) liquid(pos cube.Pos) (Liquid, bool) { if w == nil || pos.OutOfBounds(w.Range()) { // Fast way out. return nil, false @@ -447,7 +449,7 @@ func (w *World) Liquid(pos cube.Pos) (Liquid, bool) { // overwrite any existing blocks. It will instead be in the same position as a block currently there, unless // there already is a liquid at that position, in which case it will be overwritten. // If nil is passed for the liquid, any liquid currently present will be removed. -func (w *World) SetLiquid(pos cube.Pos, b Liquid) { +func (w *World) setLiquid(pos cube.Pos, b Liquid) { if w == nil || pos.OutOfBounds(w.Range()) { // Fast way out. return @@ -547,7 +549,7 @@ func (w *World) additionalLiquid(pos cube.Pos) (Liquid, bool) { // Light returns the light level at the position passed. This is the highest of the sky and block light. // The light value returned is a value in the range 0-15, where 0 means there is no light present, whereas // 15 means the block is fully lit. -func (w *World) Light(pos cube.Pos) uint8 { +func (w *World) light(pos cube.Pos) uint8 { if w == nil || pos[1] < w.Range()[0] { // Fast way out. return 0 @@ -564,7 +566,7 @@ func (w *World) Light(pos cube.Pos) uint8 { // SkyLight returns the skylight level at the position passed. This light level is not influenced by blocks // that emit light, such as torches or glowstone. The light value, similarly to Light, is a value in the // range 0-15, where 0 means no light is present. -func (w *World) SkyLight(pos cube.Pos) uint8 { +func (w *World) skyLight(pos cube.Pos) uint8 { if w == nil || pos[1] < w.Range()[0] { // Fast way out. return 0 @@ -580,7 +582,7 @@ func (w *World) SkyLight(pos cube.Pos) uint8 { // Time returns the current time of the world. The time is incremented every 1/20th of a second, unless // World.StopTime() is called. -func (w *World) Time() int { +func (w *World) time() int { if w == nil { return 0 } @@ -591,7 +593,7 @@ func (w *World) Time() int { // SetTime sets the new time of the world. SetTime will always work, regardless of whether the time is stopped // or not. -func (w *World) SetTime(new int) { +func (w *World) setTime(new int) { if w == nil { return } @@ -608,14 +610,14 @@ func (w *World) SetTime(new int) { // StopTime stops the time in the world. When called, the time will no longer cycle and the world will remain // at the time when StopTime is called. The time may be restarted by calling World.StartTime(). // StopTime will not do anything if the time is already stopped. -func (w *World) StopTime() { +func (w *World) stopTime() { w.enableTimeCycle(false) } // StartTime restarts the time in the world. When called, the time will start cycling again and the day/night // cycle will continue. The time may be stopped again by calling World.StopTime(). // StartTime will not do anything if the time is already started. -func (w *World) StartTime() { +func (w *World) startTime() { w.enableTimeCycle(true) } @@ -631,7 +633,7 @@ func (w *World) enableTimeCycle(v bool) { // Temperature returns the temperature in the World at a specific position. Higher altitudes and different biomes // influence the temperature returned. -func (w *World) Temperature(pos cube.Pos) float64 { +func (w *World) temperature(pos cube.Pos) float64 { const ( tempDrop = 1.0 / 600 seaLevel = 64 @@ -640,29 +642,29 @@ func (w *World) Temperature(pos cube.Pos) float64 { if diff < 0 { diff = 0 } - return w.Biome(pos).Temperature() - float64(diff)*tempDrop + return w.biome(pos).Temperature() - float64(diff)*tempDrop } // AddParticle spawns a particle at a given position in the world. Viewers that are viewing the chunk will be // shown the particle. -func (w *World) AddParticle(pos mgl64.Vec3, p Particle) { +func (w *World) addParticle(pos mgl64.Vec3, p Particle) { if w == nil { return } p.Spawn(w, pos) - for _, viewer := range w.Viewers(pos) { + for _, viewer := range w.viewersOf(pos) { viewer.ViewParticle(pos, p) } } // PlaySound plays a sound at a specific position in the world. Viewers of that position will be able to hear // the sound if they're close enough. -func (w *World) PlaySound(pos mgl64.Vec3, s Sound) { +func (w *World) playSound(pos mgl64.Vec3, s Sound) { ctx := event.C() if w.Handler().HandleSound(ctx, s, pos); ctx.Cancelled() { return } - for _, viewer := range w.Viewers(pos) { + for _, viewer := range w.viewersOf(pos) { viewer.ViewSound(pos, s) } } @@ -678,40 +680,21 @@ var ( // all viewers of the world that have the chunk of the entity loaded. // If the chunk that the entity is in is not yet loaded, it will first be loaded. // If the entity passed to AddEntity is currently in a world, it is first removed from that world. -func (w *World) AddEntity(e Entity) { - if w == nil { - return - } +func (w *World) addEntity(ent Entity) { + e := ent.Handle() + e.w.Store(w) - // Remove the Entity from any previous World it might be in. - e.World().RemoveEntity(e) + pos := chunkPosFromVec3(e.data.Pos) + w.entities[e] = pos - add(e, w) + c := w.chunk(pos) + c.Entities, c.modified = append(c.Entities, e), true - chunkPos := chunkPosFromVec3(e.Position()) - w.entityMu.Lock() - w.entities[e] = chunkPos - w.entityMu.Unlock() - - c := w.chunk(chunkPos) - c.Entities = append(c.Entities, e) - viewers := slices.Clone(c.viewers) - c.modified = true - c.Unlock() - - for _, v := range viewers { + for _, v := range c.viewers { // We show the entity to all viewers currently in the chunk that the entity is spawned in. - showEntity(e, v) + showEntity(ent, v) } - - w.Handler().HandleEntitySpawn(e) -} - -// add maps an Entity to a World in the entityWorlds map. -func add(e Entity, w *World) { - worldsMu.Lock() - entityWorlds[e] = w - worldsMu.Unlock() + w.Handler().HandleEntitySpawn(ent) } // RemoveEntity removes an entity from the world that is currently present in it. Any viewers of the entity @@ -720,49 +703,33 @@ func add(e Entity, w *World) { // world. If it can not find it there, it will loop through all entities and try to find it. // RemoveEntity assumes the entity is currently loaded and in a loaded chunk. If not, the function will not do // anything. -func (w *World) RemoveEntity(e Entity) { - if w == nil { - return - } - w.entityMu.Lock() - chunkPos, found := w.entities[e] - w.entityMu.Unlock() +func (w *World) removeEntity(ent Entity) { + e := ent.Handle() + pos, found := w.entities[e] if !found { // The entity currently isn't in this world. return } - w.Handler().HandleEntityDespawn(e) - - worldsMu.Lock() - delete(entityWorlds, e) - worldsMu.Unlock() - - c, ok := w.chunkFromCache(chunkPos) + w.Handler().HandleEntityDespawn(ent) + c, ok := w.chunkFromCache(pos) if !ok { // The chunk wasn't loaded, so we can't remove any entity from the chunk. return } - c.Entities = sliceutil.DeleteVal(c.Entities, e) - viewers := slices.Clone(c.viewers) - c.modified = true - c.Unlock() + c.Entities, c.modified = sliceutil.DeleteVal(c.Entities, e), true - w.entityMu.Lock() - delete(w.entities, e) - w.entityMu.Unlock() - - for _, v := range viewers { - v.HideEntity(e) + for _, v := range c.viewers { + v.HideEntity(ent) } + + e.w.Store(nil) + delete(w.entities, e) } // EntitiesWithin does a lookup through the entities in the chunks touched by the BBox passed, returning all // those which are contained within the BBox when it comes to their position. -func (w *World) EntitiesWithin(box cube.BBox, ignored func(Entity) bool) []Entity { - if w == nil { - return nil - } +func (w *World) entitiesWithin(tx *Tx, box cube.BBox, ignored func(Entity) bool) []Entity { // Make an estimate of 16 entities on average. m := make([]Entity, 0, 16) @@ -775,14 +742,12 @@ func (w *World) EntitiesWithin(box cube.BBox, ignored func(Entity) bool) []Entit // The chunk wasn't loaded, so there are no entities here. continue } - entities := slices.Clone(c.Entities) - c.Unlock() - - for _, entity := range entities { + for _, handle := range c.Entities { + entity := handle.Entity(tx) if ignored != nil && ignored(entity) { continue } - if box.Vec3Within(entity.Position()) { + if box.Vec3Within(handle.data.Pos) { // The entity position was within the BBox, so we add it to the slice to return. m = append(m, entity) } @@ -793,15 +758,10 @@ func (w *World) EntitiesWithin(box cube.BBox, ignored func(Entity) bool) []Entit } // Entities returns a list of all entities currently added to the World. -func (w *World) Entities() []Entity { - if w == nil { - return nil - } - w.entityMu.RLock() - defer w.entityMu.RUnlock() +func (w *World) allEntities(tx *Tx) []Entity { m := make([]Entity, 0, len(w.entities)) for e := range w.entities { - m = append(m, e) + m = append(m, e.Entity(tx)) } return m } @@ -847,13 +807,13 @@ func (w *World) SetSpawn(pos cube.Pos) { } // PlayerSpawn returns the spawn position of a player with a UUID in this World. -func (w *World) PlayerSpawn(uuid uuid.UUID) cube.Pos { +func (w *World) PlayerSpawn(id uuid.UUID) cube.Pos { if w == nil { return cube.Pos{} } - pos, exist, err := w.conf.Provider.LoadPlayerSpawnPosition(uuid) + pos, exist, err := w.conf.Provider.LoadPlayerSpawnPosition(id) if err != nil { - w.conf.Log.Error("load player spawn: " + err.Error()) + w.conf.Log.Error("load player spawn: "+err.Error(), "ID", id) return w.Spawn() } if !exist { @@ -864,12 +824,12 @@ func (w *World) PlayerSpawn(uuid uuid.UUID) cube.Pos { // SetPlayerSpawn sets the spawn position of a player with a UUID in this World. If the player has a spawn in the world, // the player will be teleported to this location on respawn. -func (w *World) SetPlayerSpawn(uuid uuid.UUID, pos cube.Pos) { +func (w *World) SetPlayerSpawn(id uuid.UUID, pos cube.Pos) { if w == nil { return } - if err := w.conf.Provider.SavePlayerSpawnPosition(uuid, pos); err != nil { - w.conf.Log.Error("set player spawn: " + err.Error()) + if err := w.conf.Provider.SavePlayerSpawnPosition(id, pos); err != nil { + w.conf.Log.Error("save player spawn: "+err.Error(), "ID", id) } } @@ -937,7 +897,7 @@ func (w *World) SetDifficulty(d Difficulty) { // ScheduleBlockUpdate schedules a block update at the position passed after a specific delay. If the block at // that position does not handle block updates, nothing will happen. -func (w *World) ScheduleBlockUpdate(pos cube.Pos, delay time.Duration) { +func (w *World) scheduleBlockUpdate(pos cube.Pos, delay time.Duration) { if w == nil || pos.OutOfBounds(w.Range()) { return } @@ -994,7 +954,7 @@ func (w *World) Handle(h Handler) { // Viewers returns a list of all viewers viewing the position passed. A viewer will be assumed to be watching // if the position is within one of the chunks that the viewer is watching. -func (w *World) Viewers(pos mgl64.Vec3) (viewers []Viewer) { +func (w *World) viewersOf(pos mgl64.Vec3) (viewers []Viewer) { if w == nil { return nil } @@ -1096,7 +1056,7 @@ func (w *World) addWorldViewer(l *Loader) { w.viewersMu.Lock() w.viewers[l] = l.viewer w.viewersMu.Unlock() - l.viewer.ViewTime(w.Time()) + l.viewer.ViewTime(w.time()) w.set.Lock() raining, thundering := w.set.Raining, w.set.Raining && w.set.Thundering w.set.Unlock() @@ -1388,11 +1348,10 @@ func (w *World) chunkCacheJanitor() { // Column represents the data of a chunk including the block entities and loaders. This data is protected // by the mutex present in the chunk.Chunk held. type Column struct { - sync.Mutex modified bool *chunk.Chunk - Entities []Entity + Entities []*EntityHandle BlockEntities map[cube.Pos]Block viewers []Viewer