Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions server/player/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,15 @@ type Handler interface {
// be called to cancel the fire being extinguished.
// cube.Pos can be used to see where was the fire extinguished, may be used to cancel this on specific positions.
HandleFireExtinguish(ctx *Context, pos cube.Pos)
// HandleStartBreak handles the player starting to break a block at the position passed. ctx.Cancel() may
// be called to stop the player from breaking the block completely.
HandleStartBreak(ctx *Context, pos cube.Pos)
// HandleBlockBreak handles a block that is being broken by a player. ctx.Cancel() may be called to cancel
// the block being broken. A pointer to a slice of the block's drops is passed, and may be altered
// to change what items will actually be dropped.
HandleBlockBreak(ctx *Context, pos cube.Pos, drops *[]item.Stack, xp *int)
// HandleStartBreak handles the player starting to break a block at the position passed. Private is true if the
// block is a private view-layer block, and false if it is the public world block. ctx.Cancel() may be called to
// stop the player from breaking the block completely.
HandleStartBreak(ctx *Context, pos cube.Pos, private bool)
// HandleBlockBreak handles a block that is being broken by a player. Private is true if the block broken is
// a private view-layer block, and false if it is the public world block. ctx.Cancel() may be called to cancel
// the block being broken. A pointer to a slice of the block's drops is passed, and may be altered to change
// what items will actually be dropped.
HandleBlockBreak(ctx *Context, pos cube.Pos, private bool, drops *[]item.Stack, xp *int)
// HandleBlockPlace handles the player placing a specific block at a position in its world. ctx.Cancel()
// may be called to cancel the block being placed.
HandleBlockPlace(ctx *Context, pos cube.Pos, b world.Block)
Expand Down Expand Up @@ -176,8 +178,8 @@ func (NopHandler) HandleTransfer(*Context, *net.UDPAddr)
func (NopHandler) HandleChat(*Context, *string) {}
func (NopHandler) HandleSkinChange(*Context, *skin.Skin) {}
func (NopHandler) HandleFireExtinguish(*Context, cube.Pos) {}
func (NopHandler) HandleStartBreak(*Context, cube.Pos) {}
func (NopHandler) HandleBlockBreak(*Context, cube.Pos, *[]item.Stack, *int) {}
func (NopHandler) HandleStartBreak(*Context, cube.Pos, bool) {}
func (NopHandler) HandleBlockBreak(*Context, cube.Pos, bool, *[]item.Stack, *int) {}
func (NopHandler) HandleBlockPlace(*Context, cube.Pos, world.Block) {}
func (NopHandler) HandleBlockPick(*Context, cube.Pos, world.Block) {}
func (NopHandler) HandleSignEdit(*Context, cube.Pos, bool, string, string) {}
Expand Down
114 changes: 84 additions & 30 deletions server/player/player.go
Original file line number Diff line number Diff line change
Expand Up @@ -1838,7 +1838,9 @@ 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()
if _, air := p.tx.Block(pos).(block.Air); air || !p.canReach(pos.Vec3Centre()) {
b := p.breakingBlock(pos)
_, private := p.privateBlock(pos)
if _, air := b.(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
}
Expand All @@ -1865,10 +1867,10 @@ func (p *Player) StartBreaking(pos cube.Pos, face cube.Face) {
p.breakingPos = pos

ctx := event.C(p)
if p.Handler().HandleStartBreak(ctx, pos); ctx.Cancelled() {
if p.Handler().HandleStartBreak(ctx, pos, private); ctx.Cancelled() {
return
}
if punchable, ok := p.tx.Block(pos).(block.Punchable); ok {
if punchable, ok := b.(block.Punchable); ok && !private {
punchable.Punch(pos, face, p.tx, p)
}

Expand All @@ -1878,17 +1880,15 @@ func (p *Player) StartBreaking(pos cube.Pos, face cube.Face) {
if p.GameMode().CreativeInventory() {
return
}
p.lastBreakDuration = p.breakTime(pos)
for _, viewer := range p.viewers() {
viewer.ViewBlockAction(pos, block.StartCrackAction{BreakTime: p.lastBreakDuration})
}
p.lastBreakDuration = p.breakTime(b)
p.viewBreakingBlockAction(pos, private, block.StartCrackAction{BreakTime: p.lastBreakDuration})
}

// breakTime returns the time needed to break a block at the position passed, taking into account the item
// held, if the player is on the ground/underwater and if the player has any effects.
func (p *Player) breakTime(pos cube.Pos) time.Duration {
func (p *Player) breakTime(b world.Block) time.Duration {
held, _ := p.HeldItems()
breakTime := block.BreakDuration(p.tx.Block(pos), held)
breakTime := block.BreakDuration(b, held)
if !p.OnGround() {
breakTime *= 5
}
Expand Down Expand Up @@ -1928,10 +1928,9 @@ func (p *Player) AbortBreaking() {
if !p.breaking {
return
}
_, private := p.privateBlock(p.breakingPos)
p.breaking, p.breakCounter = false, 0
for _, viewer := range p.viewers() {
viewer.ViewBlockAction(p.breakingPos, block.StopCrackAction{})
}
p.viewBreakingBlockAction(p.breakingPos, private, block.StopCrackAction{})
}

// ContinueBreaking makes the player continue breaking the block it started breaking after a call to
Expand All @@ -1942,24 +1941,51 @@ func (p *Player) ContinueBreaking(face cube.Face) {
return
}
pos := p.breakingPos
b := p.tx.Block(pos)
p.tx.AddParticle(pos.Vec3(), particle.PunchBlock{Block: b, Face: face})
b := p.breakingBlock(pos)
_, private := p.privateBlock(pos)
if private {
p.s.ViewParticle(pos.Vec3(), particle.PunchBlock{Block: b, Face: face})
} else {
p.tx.AddParticle(pos.Vec3(), particle.PunchBlock{Block: b, Face: face})
}

if p.breakCounter++; p.breakCounter%5 == 0 {
p.SwingArm()

// We send this sound only every so often. Vanilla doesn't send it every tick while breaking
// either. Every 5 ticks seems accurate.
p.tx.PlaySound(pos.Vec3(), sound.BlockBreaking{Block: b})
}
if breakTime := p.breakTime(pos); breakTime != p.lastBreakDuration {
for _, viewer := range p.viewers() {
viewer.ViewBlockAction(pos, block.ContinueCrackAction{BreakTime: breakTime})
if private {
p.s.ViewSound(pos.Vec3(), sound.BlockBreaking{Block: b})
} else {
p.tx.PlaySound(pos.Vec3(), sound.BlockBreaking{Block: b})
}
}
if breakTime := p.breakTime(b); breakTime != p.lastBreakDuration {
p.viewBreakingBlockAction(pos, private, block.ContinueCrackAction{BreakTime: breakTime})
p.lastBreakDuration = breakTime
}
}

// viewBreakingBlockAction shows a breaking action to the player only if the block is a private view-layer
// block, or to all viewers if it is a public world block.
func (p *Player) viewBreakingBlockAction(pos cube.Pos, private bool, a world.BlockAction) {
if private {
p.s.ViewBlockAction(pos, a)
return
}
for _, viewer := range p.viewers() {
viewer.ViewBlockAction(pos, a)
}
}

// breakingBlock returns the block that should be used for the player's breaking progress.
func (p *Player) breakingBlock(pos cube.Pos) world.Block {
if b, ok := p.privateBlock(pos); ok {
return b
}
return p.tx.Block(pos)
}

// PlaceBlock makes the player place the block passed at the position passed, granted it is within the range
// of the player.
// An item.UseContext may be passed to obtain information on if the block placement was successful. (SubCount will
Expand Down Expand Up @@ -2036,6 +2062,11 @@ func (p *Player) obstructedPos(pos cube.Pos, b world.Block) (obstructed, selfOnl
// reach the block passed, the method returns immediately.
func (p *Player) BreakBlock(pos cube.Pos) {
b := p.tx.Block(pos)

privateBlock, private := p.privateBlock(pos)
if private {
b = privateBlock
}
if _, air := b.(block.Air); air {
// Don't do anything if the position broken is already air.
return
Expand All @@ -2059,25 +2090,30 @@ func (p *Player) BreakBlock(pos cube.Pos) {
}

ctx := event.C(p)
if p.Handler().HandleBlockBreak(ctx, pos, &drops, &xp); ctx.Cancelled() {
if p.Handler().HandleBlockBreak(ctx, pos, private, &drops, &xp); ctx.Cancelled() {
p.resendNearbyBlocks(pos)
return
}
held, left := p.HeldItems()

p.SwingArm()
p.tx.SetBlock(pos, nil, nil)
p.tx.AddParticle(pos.Vec3Centre(), particle.BlockBreak{Block: b})

if breakable, ok := b.(block.Breakable); ok {
info := breakable.BreakInfo()
if info.BreakHandler != nil {
info.BreakHandler(pos, p.tx, p)
}
for _, orb := range entity.NewExperienceOrbs(pos.Vec3Centre(), xp) {
p.tx.AddEntity(orb)
if private {
p.ViewPublicBlock(pos)
p.s.ViewParticle(pos.Vec3Centre(), particle.BlockBreak{Block: b})
} else {
p.tx.SetBlock(pos, nil, nil)
p.tx.AddParticle(pos.Vec3Centre(), particle.BlockBreak{Block: b})
if breakable, ok := b.(block.Breakable); ok {
info := breakable.BreakInfo()
if info.BreakHandler != nil {
info.BreakHandler(pos, p.tx, p)
}
}
}

for _, orb := range entity.NewExperienceOrbs(pos.Vec3Centre(), xp) {
p.tx.AddEntity(orb)
}
for _, drop := range drops {
opts := world.EntitySpawnOpts{Position: pos.Vec3Centre(), Velocity: mgl64.Vec3{rand.Float64()*0.2 - 0.1, 0.2, rand.Float64()*0.2 - 0.1}}
p.tx.AddEntity(entity.NewItem(opts, drop))
Expand Down Expand Up @@ -2109,6 +2145,14 @@ func (p *Player) drops(held item.Stack, b world.Block) []item.Stack {
return drops
}

// privateBlock returns this player's view-layer block override at pos, if present.
func (p *Player) privateBlock(pos cube.Pos) (world.Block, bool) {
if p.session() == session.Nop {
return nil, false
}
return p.ViewLayer().Block(pos)
}

// PickBlock makes the player pick a block in the world at a position passed. If the player is unable to
// pick the block, the method returns immediately.
func (p *Player) PickBlock(pos cube.Pos) {
Expand Down Expand Up @@ -2612,6 +2656,16 @@ func (p *Player) ViewVisibility(entity world.Entity, level world.VisibilityLevel
p.session().ViewVisibility(entity, level)
}

// ViewBlock overrides the public block at the position passed for this player.
func (p *Player) ViewBlock(pos cube.Pos, b world.Block) {
p.session().ViewBlock(pos, b)
}

// ViewPublicBlock removes the block override at the position passed for this player.
func (p *Player) ViewPublicBlock(pos cube.Pos) {
p.session().ViewPublicBlock(pos)
}

// RemoveViewLayer removes all view-layer overrides of the entity for this player.
func (p *Player) RemoveViewLayer(entity world.Entity) {
p.session().RemoveViewLayer(entity)
Expand Down
54 changes: 45 additions & 9 deletions server/session/chunk.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package session

import (
"bytes"
"maps"

"github.com/cespare/xxhash/v2"
"github.com/df-mc/dragonfly/server/block/cube"
Expand All @@ -18,6 +19,7 @@ const subChunkRequests = true

// ViewChunk ...
func (s *Session) ViewChunk(pos world.ChunkPos, dim world.Dimension, blockEntities map[cube.Pos]world.Block, c *chunk.Chunk) {
c, blockEntities = s.applyViewLayerToChunk(pos, c, blockEntities)
if !s.conn.ClientCacheEnabled() {
s.sendNetworkChunk(pos, dim, c, blockEntities)
return
Expand Down Expand Up @@ -45,7 +47,11 @@ func (s *Session) ViewSubChunks(centre world.SubChunkPos, offsets []protocol.Sub
entries = append(entries, protocol.SubChunkEntry{Result: protocol.SubChunkResultChunkNotFound, Offset: offset})
continue
}
entries = append(entries, s.subChunkEntry(offset, ind, col, transaction))
ch, blockEntities := s.applyViewLayerToChunk(world.ChunkPos{
centre.X() + int32(offset[0]),
centre.Z() + int32(offset[2]),
}, col.Chunk, col.BlockEntities)
entries = append(entries, s.subChunkEntry(offset, ind, ch, blockEntities, transaction))
}
if s.conn.ClientCacheEnabled() && len(transaction) > 0 {
s.blobMu.Lock()
Expand All @@ -61,21 +67,21 @@ func (s *Session) ViewSubChunks(centre world.SubChunkPos, offsets []protocol.Sub
})
}

func (s *Session) subChunkEntry(offset protocol.SubChunkOffset, ind int16, col *world.Column, transaction map[uint64]struct{}) protocol.SubChunkEntry {
chunkMap := col.HeightMap()
func (s *Session) subChunkEntry(offset protocol.SubChunkOffset, ind int16, c *chunk.Chunk, blockEntities map[cube.Pos]world.Block, transaction map[uint64]struct{}) protocol.SubChunkEntry {
chunkMap := c.HeightMap()
subMapType, subMap := byte(protocol.HeightMapDataHasData), make([]int8, 256)
higher, lower := true, true
for x := uint8(0); x < 16; x++ {
for z := uint8(0); z < 16; z++ {
y, i := chunkMap.At(x, z), (uint16(z)<<4)|uint16(x)
otherInd := col.SubIndex(y)
otherInd := c.SubIndex(y)
switch {
case otherInd > ind:
subMap[i], lower = 16, false
case otherInd < ind:
subMap[i], higher = -1, false
default:
subMap[i], lower, higher = int8(y-col.SubY(otherInd)), false, false
subMap[i], lower, higher = int8(y-c.SubY(otherInd)), false, false
}
}
}
Expand All @@ -85,7 +91,7 @@ func (s *Session) subChunkEntry(offset protocol.SubChunkOffset, ind int16, col *
subMapType, subMap = protocol.HeightMapDataTooLow, nil
}

sub := col.Sub()[ind]
sub := c.Sub()[ind]
if sub.Empty() {
return protocol.SubChunkEntry{
Result: protocol.SubChunkResultSuccessAllAir,
Expand All @@ -97,12 +103,12 @@ func (s *Session) subChunkEntry(offset protocol.SubChunkOffset, ind int16, col *
}
}

serialisedSubChunk := chunk.EncodeSubChunk(col.Chunk, chunk.NetworkEncoding, int(ind))
serialisedSubChunk := chunk.EncodeSubChunk(c, chunk.NetworkEncoding, int(ind))

blockEntityBuf := bytes.NewBuffer(nil)
enc := nbt.NewEncoderWithEncoding(blockEntityBuf, nbt.NetworkLittleEndian)
for pos, b := range col.BlockEntities {
if n, ok := b.(world.NBTer); ok && col.SubIndex(int16(pos.Y())) == ind {
for pos, b := range blockEntities {
if n, ok := b.(world.NBTer); ok && c.SubIndex(int16(pos.Y())) == ind {
d := n.EncodeNBT()
d["x"], d["y"], d["z"] = int32(pos[0]), int32(pos[1]), int32(pos[2])
_ = enc.Encode(d)
Expand All @@ -129,6 +135,36 @@ func (s *Session) subChunkEntry(offset protocol.SubChunkOffset, ind int16, col *
return entry
}

// applyViewLayerToChunk returns a chunk and block entity map with this session's view-layer block overrides applied.
func (s *Session) applyViewLayerToChunk(pos world.ChunkPos, c *chunk.Chunk, blockEntities map[cube.Pos]world.Block) (*chunk.Chunk, map[cube.Pos]world.Block) {
if s.viewLayer == nil {
return c, blockEntities
}
overrides := s.viewLayer.Blocks()
if len(overrides) == 0 {
return c, blockEntities
}

var cloned bool
for blockPos, b := range overrides {
if (world.ChunkPos{int32(blockPos[0] >> 4), int32(blockPos[2] >> 4)}) != pos {
continue
}
if !cloned {
c = c.Clone()
blockEntities = maps.Clone(blockEntities)
cloned = true
}
c.SetBlock(uint8(blockPos[0]), int16(blockPos[1]), uint8(blockPos[2]), 0, s.br.BlockRuntimeID(b))
if _, ok := b.(world.NBTer); ok {
blockEntities[blockPos] = b
} else {
delete(blockEntities, blockPos)
}
}
return c, blockEntities
}

// dimensionID returns the dimension ID of the world that the session is in.
func (s *Session) dimensionID(dim world.Dimension) int32 {
d, _ := world.DimensionID(dim)
Expand Down
14 changes: 7 additions & 7 deletions server/session/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,11 @@ func (conf Config) New(conn Conn) *Session {
}
conf.Log = conf.Log.With("name", conn.IdentityData().DisplayName, "uuid", conn.IdentityData().Identity, "raddr", conn.RemoteAddr().String())

br := conf.BlockRegistry
if br == nil {
br = world.DefaultBlockRegistry
}

s := &Session{}
*s = Session{
openChunkTransactions: make([]map[uint64]struct{}, 0, 8),
Expand All @@ -200,8 +205,9 @@ func (conf Config) New(conn Conn) *Session {
hiddenHud: make(map[hud.Element]struct{}),
debugShapes: make(map[int]debug.Shape),
debugShapeUpdates: make([]debugShapeUpdate, 0, 256),
br: br,
}
s.viewLayer = world.NewViewLayer(s)
s.viewLayer = world.NewViewLayerWithBlockRegistry(s, br)
s.openedWindow.Store(inventory.New(1, nil))
s.openedPos.Store(&cube.Pos{})

Expand All @@ -210,12 +216,6 @@ func (conf Config) New(conn Conn) *Session {
s.currentScoreboard.Store(&scoreboardName)
s.currentLines.Store(&scoreboardLines)

if conf.BlockRegistry == nil {
s.br = world.DefaultBlockRegistry
} else {
s.br = conf.BlockRegistry
}

s.registerHandlers()
s.sendBiomes()
groups, items := creativeContent(s.br)
Expand Down
Loading