diff --git a/server/player/handler.go b/server/player/handler.go index ada7a6546..3329d729e 100644 --- a/server/player/handler.go +++ b/server/player/handler.go @@ -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) @@ -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) {} diff --git a/server/player/player.go b/server/player/player.go index d3c535fc3..645c70d7f 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -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 } @@ -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) } @@ -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 } @@ -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 @@ -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 @@ -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 @@ -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)) @@ -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) { @@ -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) diff --git a/server/session/chunk.go b/server/session/chunk.go index 150142ae3..24054b03f 100644 --- a/server/session/chunk.go +++ b/server/session/chunk.go @@ -2,6 +2,7 @@ package session import ( "bytes" + "maps" "github.com/cespare/xxhash/v2" "github.com/df-mc/dragonfly/server/block/cube" @@ -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 @@ -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() @@ -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 } } } @@ -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, @@ -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) @@ -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) diff --git a/server/session/session.go b/server/session/session.go index 0bfcc7934..bbe523687 100644 --- a/server/session/session.go +++ b/server/session/session.go @@ -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), @@ -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{}) @@ -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) diff --git a/server/session/view_layer.go b/server/session/view_layer.go index 632b58012..94dee2cba 100644 --- a/server/session/view_layer.go +++ b/server/session/view_layer.go @@ -1,6 +1,9 @@ package session -import "github.com/df-mc/dragonfly/server/world" +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" +) // ViewLayer returns the session's ViewLayer. The layer may be used to override how entities are viewed // by this session, such as with a different name tag or visibility state. @@ -48,6 +51,22 @@ func (s *Session) ViewVisibility(entity world.Entity, level world.VisibilityLeve s.viewLayer.ViewVisibility(entity, level) } +// ViewBlock overwrites the public block at the position passed and immediately refreshes it for this session. +func (s *Session) ViewBlock(pos cube.Pos, b world.Block) { + if s.viewLayer == nil { + return + } + s.viewLayer.ViewBlock(pos, b) +} + +// ViewPublicBlock removes the block override at the position passed and immediately refreshes it for this session. +func (s *Session) ViewPublicBlock(pos cube.Pos) { + if s.viewLayer == nil { + return + } + s.viewLayer.ViewPublicBlock(pos) +} + // RemoveViewLayer removes all overrides for the entity and immediately refreshes it for this session. func (s *Session) RemoveViewLayer(entity world.Entity) { if s.viewLayer == nil { @@ -64,6 +83,17 @@ func (s *Session) ViewLayerEntityChanged(e world.Entity) { s.ViewEntityState(e) } +// ViewLayerBlockChanged refreshes the block for this session if its chunk is currently visible. +func (s *Session) ViewLayerBlockChanged(pos cube.Pos) { + if b, ok := s.viewLayer.Block(pos); ok { + s.viewBlockUpdate(pos, b, 0) + return + } + if b, ok := s.publicBlock(pos); ok { + s.viewBlockUpdate(pos, b, 0) + } +} + // viewingEntity checks if this session currently has a runtime ID assigned to the entity handle. func (s *Session) viewingEntity(handle *world.EntityHandle) bool { s.entityMutex.RLock() @@ -71,3 +101,16 @@ func (s *Session) viewingEntity(handle *world.EntityHandle) bool { s.entityMutex.RUnlock() return ok } + +// publicBlock returns the public block at pos from the loaded chunk for this session. +func (s *Session) publicBlock(pos cube.Pos) (world.Block, bool) { + col, ok := s.chunkLoader.Chunk(world.ChunkPos{int32(pos[0] >> 4), int32(pos[2] >> 4)}) + if !ok { + return nil, false + } + if b, ok := col.BlockEntities[pos]; ok { + return b, true + } + b, ok := s.br.BlockByRuntimeID(col.Block(uint8(pos[0]), int16(pos[1]), uint8(pos[2]), 0)) + return b, ok +} diff --git a/server/session/world.go b/server/session/world.go index 1ecdfb728..69e04778f 100644 --- a/server/session/world.go +++ b/server/session/world.go @@ -959,6 +959,16 @@ func (s *Session) ViewBrewingUpdate(prevBrewTime, brewTime time.Duration, prevFu // ViewBlockUpdate ... func (s *Session) ViewBlockUpdate(pos cube.Pos, b world.Block, layer int) { + if s.viewLayer != nil && layer == 0 { + if viewed, ok := s.viewLayer.Block(pos); ok { + b = viewed + } + } + s.viewBlockUpdate(pos, b, layer) +} + +// viewBlockUpdate sends a block update to the session without applying view-layer overrides. +func (s *Session) viewBlockUpdate(pos cube.Pos, b world.Block, layer int) { blockPos := protocol.BlockPos{int32(pos[0]), int32(pos[1]), int32(pos[2])} s.writePacket(&packet.UpdateBlock{ Position: blockPos, diff --git a/server/world/chunk/chunk.go b/server/world/chunk/chunk.go index 17ef1f10f..ab3d6d9ff 100644 --- a/server/world/chunk/chunk.go +++ b/server/world/chunk/chunk.go @@ -49,6 +49,26 @@ func New(br BlockRegistry, r cube.Range) *Chunk { } } +// Clone returns an independent copy of the Chunk. +func (chunk *Chunk) Clone() *Chunk { + clone := &Chunk{ + r: chunk.r, + br: chunk.br, + air: chunk.air, + recalculateHeightMap: chunk.recalculateHeightMap, + heightMap: append(HeightMap(nil), chunk.heightMap...), + sub: make([]*SubChunk, len(chunk.sub)), + biomes: make([]*PalettedStorage, len(chunk.biomes)), + } + for i, sub := range chunk.sub { + clone.sub[i] = sub.Clone() + } + for i, biomes := range chunk.biomes { + clone.biomes[i] = biomes.Clone() + } + return clone +} + // Equals returns if the chunk passed is equal to the current one func (chunk *Chunk) Equals(c *Chunk) bool { if !chunk.recalculateHeightMap && !c.recalculateHeightMap && !slices.Equal(c.heightMap, chunk.heightMap) { diff --git a/server/world/chunk/palette.go b/server/world/chunk/palette.go index 3b3d32b17..a9fe6e2d9 100644 --- a/server/world/chunk/palette.go +++ b/server/world/chunk/palette.go @@ -23,6 +23,11 @@ func newPalette(size paletteSize, values []uint32) *Palette { return &Palette{size: size, values: values, last: math.MaxUint32} } +// Clone returns an independent copy of the Palette. +func (palette *Palette) Clone() *Palette { + return newPalette(palette.size, append([]uint32(nil), palette.values...)) +} + // Len returns the amount of unique values in the Palette. func (palette *Palette) Len() int { return len(palette.values) diff --git a/server/world/chunk/paletted_storage.go b/server/world/chunk/paletted_storage.go index 92153e7fb..a1047c2b1 100644 --- a/server/world/chunk/paletted_storage.go +++ b/server/world/chunk/paletted_storage.go @@ -59,6 +59,11 @@ func emptyStorage(v uint32) *PalettedStorage { return newPalettedStorage([]uint32{}, newPalette(0, []uint32{v})) } +// Clone returns an independent copy of the PalettedStorage. +func (storage *PalettedStorage) Clone() *PalettedStorage { + return newPalettedStorage(append([]uint32(nil), storage.indices...), storage.palette.Clone()) +} + // Palette returns the Palette of the PalettedStorage. func (storage *PalettedStorage) Palette() *Palette { return storage.palette diff --git a/server/world/chunk/sub_chunk.go b/server/world/chunk/sub_chunk.go index 21436cecb..7a118a520 100644 --- a/server/world/chunk/sub_chunk.go +++ b/server/world/chunk/sub_chunk.go @@ -29,6 +29,20 @@ func NewSubChunk(air uint32) *SubChunk { return &SubChunk{air: air} } +// Clone returns an independent copy of the SubChunk. +func (sub *SubChunk) Clone() *SubChunk { + clone := &SubChunk{ + air: sub.air, + blockLight: append([]uint8(nil), sub.blockLight...), + skyLight: append([]uint8(nil), sub.skyLight...), + storages: make([]*PalettedStorage, len(sub.storages)), + } + for i, storage := range sub.storages { + clone.storages[i] = storage.Clone() + } + return clone +} + // Empty checks if the SubChunk is considered empty. This is the case if the SubChunk has 0 block storages or if it has // a single one that is completely filled with air. func (sub *SubChunk) Empty() bool { diff --git a/server/world/view_layer.go b/server/world/view_layer.go index cb9098f1b..0b1007701 100644 --- a/server/world/view_layer.go +++ b/server/world/view_layer.go @@ -4,6 +4,8 @@ import ( "maps" "slices" "sync" + + "github.com/df-mc/dragonfly/server/block/cube" ) // layer stores the appearance overrides that a ViewLayer applies to an entity. @@ -17,6 +19,8 @@ type layer struct { type ViewLayerUpdater interface { // ViewLayerEntityChanged handles an entity whose view-layer overrides changed. ViewLayerEntityChanged(entity Entity) + // ViewLayerBlockChanged handles a block whose view-layer override changed. + ViewLayerBlockChanged(pos cube.Pos) } type viewLayerViewer interface { @@ -26,16 +30,29 @@ type viewLayerViewer interface { // ViewLayer holds overrides for how entities are viewed by a single viewer. It allows entities to be // viewed differently by different players, such as with a different name tag or visibility state. type ViewLayer struct { - mu sync.RWMutex - entities map[*EntityHandle]layer - updater ViewLayerUpdater + mu sync.RWMutex + entities map[*EntityHandle]layer + blocks map[cube.Pos]uint32 + blockRegistry BlockRegistry + updater ViewLayerUpdater } // NewViewLayer returns a new ViewLayer. func NewViewLayer(updater ViewLayerUpdater) *ViewLayer { + return NewViewLayerWithBlockRegistry(updater, DefaultBlockRegistry) +} + +// NewViewLayerWithBlockRegistry returns a new ViewLayer using the BlockRegistry passed for block runtime ID +// conversions. +func NewViewLayerWithBlockRegistry(updater ViewLayerUpdater, br BlockRegistry) *ViewLayer { + if br == nil { + br = DefaultBlockRegistry + } return &ViewLayer{ - entities: map[*EntityHandle]layer{}, - updater: updater, + entities: map[*EntityHandle]layer{}, + blocks: map[cube.Pos]uint32{}, + blockRegistry: br, + updater: updater, } } @@ -119,6 +136,52 @@ func (v *ViewLayer) Visibility(entity Entity) VisibilityLevel { return v.entities[entity.H()].visibility } +// ViewBlock overwrites the public block at the position passed for this ViewLayer. +// Passing nil removes the block override, causing the public block to be viewed again. +func (v *ViewLayer) ViewBlock(pos cube.Pos, b Block) { + v.mu.Lock() + if b == nil { + delete(v.blocks, pos) + } else { + v.blocks[pos] = v.blockRegistry.BlockRuntimeID(b) + } + v.mu.Unlock() + + v.refreshBlock(pos) +} + +// ViewPublicBlock removes the block override at the position passed, causing the public block to be viewed again. +func (v *ViewLayer) ViewPublicBlock(pos cube.Pos) { + v.ViewBlock(pos, nil) +} + +// Block returns the overwritten block at the position passed and whether an override was set. +func (v *ViewLayer) Block(pos cube.Pos) (Block, bool) { + v.mu.RLock() + defer v.mu.RUnlock() + + rid, ok := v.blocks[pos] + if !ok { + return nil, false + } + b, ok := v.blockRegistry.BlockByRuntimeID(rid) + return b, ok +} + +// Blocks returns all block overrides in the view layer. +func (v *ViewLayer) Blocks() map[cube.Pos]Block { + v.mu.RLock() + defer v.mu.RUnlock() + + blocks := make(map[cube.Pos]Block, len(v.blocks)) + for pos, rid := range v.blocks { + if b, ok := v.blockRegistry.BlockByRuntimeID(rid); ok { + blocks[pos] = b + } + } + return blocks +} + // Remove removes all overrides for the entity from the ViewLayer. func (v *ViewLayer) Remove(entity Entity) { if v.remove(entity) { @@ -162,6 +225,7 @@ func (v *ViewLayer) Close() error { defer v.mu.Unlock() clear(v.entities) + clear(v.blocks) return nil } @@ -175,3 +239,9 @@ func (v *ViewLayer) refresh(entity Entity) { v.updater.ViewLayerEntityChanged(entity) } } + +func (v *ViewLayer) refreshBlock(pos cube.Pos) { + if v.updater != nil { + v.updater.ViewLayerBlockChanged(pos) + } +}