diff --git a/cmd/blockhash/main.go b/cmd/blockhash/main.go index a82126baf..a4a95af76 100644 --- a/cmd/blockhash/main.go +++ b/cmd/blockhash/main.go @@ -228,6 +228,8 @@ func (b *hashBuilder) ftype(structName, s string, expr ast.Expr, directives map[ return "uint64(" + s + ".Uint8())", 3 case "AnvilType", "SandstoneType", "PrismarineType", "StoneBricksType", "NetherBricksType", "FroglightType", "WallConnectionType", "BlackstoneType", "DeepslateType", "TallGrassType": return "uint64(" + s + ".Uint8())", 2 + case "BellAttachment": + return "uint64(" + s + ".Uint8())", 2 case "OreType", "FireType", "DoubleTallGrassType": return "uint64(" + s + ".Uint8())", 1 case "Direction", "Axis": diff --git a/server/block/action.go b/server/block/action.go index 5242340e6..53595146f 100644 --- a/server/block/action.go +++ b/server/block/action.go @@ -1,6 +1,9 @@ package block -import "time" +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "time" +) // OpenAction is a world.BlockAction to open a block at a position. It is sent for blocks such as chests. type OpenAction struct{ action } @@ -8,10 +11,20 @@ type OpenAction struct{ action } // CloseAction is a world.BlockAction to close a block at a position, complementary to the OpenAction action. type CloseAction struct{ action } +// BellRing is a world.BlockAction to ring a bell at a position. +type BellRing struct { + action + + // Face is the face at which the bell was rung from. + Face cube.Face +} + // StartCrackAction is a world.BlockAction to make the cracks in a block start forming, following the break time set in // the action. type StartCrackAction struct { action + + // BreakTime ... BreakTime time.Duration } @@ -20,6 +33,8 @@ type StartCrackAction struct { // ground, submerged or is using a different item than at first. type ContinueCrackAction struct { action + + // BreakTime ... BreakTime time.Duration } diff --git a/server/block/bell.go b/server/block/bell.go new file mode 100644 index 000000000..840976635 --- /dev/null +++ b/server/block/bell.go @@ -0,0 +1,164 @@ +package block + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/model" + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/world" + "github.com/df-mc/dragonfly/server/world/particle" + "github.com/df-mc/dragonfly/server/world/sound" + "github.com/go-gl/mathgl/mgl64" +) + +// Bell is a transparent, animated block entity that produces a sound when used. Unlike most utility blocks, bells +// cannot be crafted. +type Bell struct { + transparent + + // Attach represents the attachment type of the Bell. + Attach BellAttachment + // Facing represents the direction the Bell is facing. + Facing cube.Direction +} + +// Model ... +func (b Bell) Model() world.BlockModel { + return model.Bell{Attach: b.Attach.String(), Facing: b.Facing} +} + +// BreakInfo ... +func (b Bell) BreakInfo() BreakInfo { + return newBreakInfo(1, pickaxeHarvestable, pickaxeEffective, oneOf(b)).withBlastResistance(15) +} + +// UseOnBlock ... +func (b Bell) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, w *world.World, user item.User, ctx *item.UseContext) (used bool) { + pos, face, used = firstReplaceable(w, pos, face, b) + if !used { + return false + } + b.Facing = user.Rotation().Direction().Opposite() + if face == cube.FaceUp { + if _, ok := w.Block(pos.Side(cube.FaceDown)).Model().(model.Solid); !ok { + return false + } + } else if face == cube.FaceDown { + if _, ok := w.Block(pos.Side(cube.FaceUp)).Model().(model.Solid); !ok { + return false + } + b.Attach = HangingBellAttachment() + } else { + if _, ok := w.Block(pos.Side(face.Opposite())).Model().(model.Solid); !ok { + return false + } + b.Facing = face.Direction() + b.Attach = WallBellAttachment() + if _, ok := w.Block(pos.Side(face)).Model().(model.Solid); ok { + b.Attach = WallsBellAttachment() + } + } + place(w, pos, b, user, ctx) + return placed(ctx) +} + +// NeighbourUpdateTick ... +func (b Bell) NeighbourUpdateTick(pos, _ cube.Pos, w *world.World) { + var supportFaces []cube.Face + switch b.Attach { + case HangingBellAttachment(): + supportFaces = append(supportFaces, cube.FaceUp) + case StandingBellAttachment(): + supportFaces = append(supportFaces, cube.FaceDown) + case WallBellAttachment(), WallsBellAttachment(): + supportFaces = append(supportFaces, b.Facing.Face().Opposite()) + if b.Attach == WallsBellAttachment() { + supportFaces = append(supportFaces, b.Facing.Face()) + } + } + for _, supportFace := range supportFaces { + if _, ok := w.Block(pos.Side(supportFace)).Model().(model.Solid); !ok { + w.SetBlock(pos, nil, nil) + w.AddParticle(pos.Vec3Centre(), particle.BlockBreak{Block: b}) + dropItem(w, item.NewStack(b, 1), pos.Vec3Centre()) + break + } + } +} + +// Activate ... +func (b Bell) Activate(pos cube.Pos, _ cube.Face, w *world.World, u item.User, _ *item.UseContext) bool { + s, f := u.Rotation().Direction().Opposite().Face(), b.Facing.Face() + switch b.Attach { + case HangingBellAttachment(): + b.Ring(pos, s, w) + return true + case StandingBellAttachment(): + if s.Axis() == f.Axis() { + b.Ring(pos, s, w) + return true + } + case WallBellAttachment(), WallsBellAttachment(): + if s == f.RotateLeft() || s == f.RotateRight() { + b.Ring(pos, s, w) + return true + } + } + return false +} + +// EntityInside ... +func (b Bell) EntityInside(pos cube.Pos, w *world.World, e world.Entity) { + if ejector, ok := e.Type().(EntityEjector); ok { + ejector.EntityEject(e, pos) + + // BDS always rings the bell on it's facing direction, even if the entity is ejected in a different direction. + b.Ring(pos, b.Facing.Face(), w) + } +} + +// ProjectileHit ... +func (b Bell) ProjectileHit(w *world.World, _ world.Entity, pos cube.Pos, face cube.Face) { + b.Ring(pos, face, w) +} + +// Ring rings the bell on the face passed. +func (b Bell) Ring(pos cube.Pos, face cube.Face, w *world.World) { + w.PlaySound(pos.Vec3Centre(), sound.BellRing{}) + for _, v := range w.Viewers(pos.Vec3Centre()) { + v.ViewBlockAction(pos, BellRing{Face: face}) + } +} + +// EncodeNBT encodes the Bell's block entity ID. There are other properties, but we can skip those. +func (b Bell) EncodeNBT() map[string]any { + return map[string]any{"id": "Bell"} +} + +// DecodeNBT ... +func (b Bell) DecodeNBT(map[string]any) any { + return b +} + +// EncodeItem ... +func (b Bell) EncodeItem() (name string, meta int16) { + return "minecraft:bell", 0 +} + +// EncodeBlock ... +func (b Bell) EncodeBlock() (string, map[string]any) { + return "minecraft:bell", map[string]any{ + "toggle_bit": uint8(0), // Useless property, updated on ring in vanilla. + "attachment": b.Attach.String(), + "direction": int32(horizontalDirection(b.Facing)), + } +} + +// allBells ... +func allBells() (bells []world.Block) { + for _, a := range BellAttachments() { + for _, d := range cube.Directions() { + bells = append(bells, Bell{Attach: a, Facing: d}) + } + } + return +} diff --git a/server/block/bell_attachment.go b/server/block/bell_attachment.go new file mode 100644 index 000000000..e52a4652a --- /dev/null +++ b/server/block/bell_attachment.go @@ -0,0 +1,53 @@ +package block + +// BellAttachment represents a type of attachment for a Bell. +type BellAttachment struct { + bellAttachment +} + +// StandingBellAttachment is a type of attachment for a standing Bell. +func StandingBellAttachment() BellAttachment { + return BellAttachment{0} +} + +// HangingBellAttachment is a type of attachment for a hanging Bell. +func HangingBellAttachment() BellAttachment { + return BellAttachment{1} +} + +// WallBellAttachment is a type of attachment for a wall Bell. +func WallBellAttachment() BellAttachment { + return BellAttachment{2} +} + +// WallsBellAttachment is a type of attachment for a two-wall Bell. +func WallsBellAttachment() BellAttachment { + return BellAttachment{3} +} + +// BellAttachments returns all possible BellAttachments. +func BellAttachments() []BellAttachment { + return []BellAttachment{StandingBellAttachment(), HangingBellAttachment(), WallBellAttachment(), WallsBellAttachment()} +} + +type bellAttachment uint8 + +// Uint8 returns the BellAttachment as a uint8. +func (g bellAttachment) Uint8() uint8 { + return uint8(g) +} + +// String returns the BellAttachment as a string. +func (g bellAttachment) String() string { + switch g { + case 0: + return "standing" + case 1: + return "hanging" + case 2: + return "side" + case 3: + return "multiple" + } + panic("should never happen") +} diff --git a/server/block/block.go b/server/block/block.go index f1812b4d5..13b411fa4 100644 --- a/server/block/block.go +++ b/server/block/block.go @@ -73,6 +73,18 @@ type EntityInsider interface { EntityInside(pos cube.Pos, w *world.World, e world.Entity) } +// ProjectileHitter represents a block that handles being hit by a projectile. +type ProjectileHitter interface { + // ProjectileHit is called when a projectile hits the block. + ProjectileHit(w *world.World, e world.Entity, pos cube.Pos, face cube.Face) +} + +// EntityEjector represents a block that ejects entities when they are inside it. +type EntityEjector interface { + // EntityEject is called when an entity is inside the block's 1x1x1 axis aligned bounding box. + EntityEject(e world.Entity, pos cube.Pos) +} + // Frictional represents a block that may have a custom friction value, friction is used for entity drag when the // entity is on ground. If a block does not implement this interface, it should be assumed that its friction is 0.6. type Frictional interface { diff --git a/server/block/hash.go b/server/block/hash.go index baf995445..73ee58010 100644 --- a/server/block/hash.go +++ b/server/block/hash.go @@ -15,6 +15,7 @@ const ( hashBeacon hashBedrock hashBeetrootSeeds + hashBell hashBlackstone hashBlastFurnace hashBlueIce @@ -219,6 +220,10 @@ func (b BeetrootSeeds) Hash() uint64 { return hashBeetrootSeeds | uint64(b.Growth)<<8 } +func (b Bell) Hash() uint64 { + return hashBell | uint64(b.Attach.Uint8())<<8 | uint64(b.Facing)<<10 +} + func (b Blackstone) Hash() uint64 { return hashBlackstone | uint64(b.Type.Uint8())<<8 } diff --git a/server/block/model/bell.go b/server/block/model/bell.go new file mode 100644 index 000000000..0d3dc55fb --- /dev/null +++ b/server/block/model/bell.go @@ -0,0 +1,38 @@ +package model + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" + "github.com/go-gl/mathgl/mgl64" +) + +// Bell represents the block model of a bell. +type Bell struct { + // Attach represents the attachment type of the Bell. + Attach string + // Facing represents the direction the Bell is facing. + Facing cube.Direction +} + +// BBox ... +func (b Bell) BBox(cube.Pos, *world.World) []cube.BBox { + if b.Attach == "standing" { + return []cube.BBox{full.Stretch(b.Facing.Face().Axis(), -0.25).ExtendTowards(cube.FaceUp, -0.1875)} + } + if b.Attach == "hanging" { + return []cube.BBox{full.GrowVec3(mgl64.Vec3{-0.25, 0, -0.25}).ExtendTowards(cube.FaceDown, -0.25)} + } + + box := full.Stretch(b.Facing.RotateLeft().Face().Axis(), -0.25). + ExtendTowards(cube.FaceUp, -0.0625). + ExtendTowards(cube.FaceDown, -0.25) + if b.Attach == "side" { + return []cube.BBox{box.ExtendTowards(b.Facing.Face(), -0.1875)} + } + return []cube.BBox{box} +} + +// FaceSolid ... +func (Bell) FaceSolid(cube.Pos, cube.Face, *world.World) bool { + return false +} diff --git a/server/block/register.go b/server/block/register.go index 9a2dddc67..e44128c1d 100644 --- a/server/block/register.go +++ b/server/block/register.go @@ -117,6 +117,7 @@ func init() { registerAll(allBarrels()) registerAll(allBasalt()) registerAll(allBeetroot()) + registerAll(allBells()) registerAll(allBlackstone()) registerAll(allBlastFurnaces()) registerAll(allBoneBlock()) @@ -205,6 +206,7 @@ func init() { world.RegisterItem(Beacon{}) world.RegisterItem(Bedrock{}) world.RegisterItem(BeetrootSeeds{}) + world.RegisterItem(Bell{}) world.RegisterItem(BlastFurnace{}) world.RegisterItem(BlueIce{}) world.RegisterItem(Bone{}) diff --git a/server/entity/item.go b/server/entity/item.go index 3391e8213..b667e5b56 100644 --- a/server/entity/item.go +++ b/server/entity/item.go @@ -23,7 +23,8 @@ func NewItem(i item.Stack, pos mgl64.Vec3) *Ent { func NewItemPickupDelay(i item.Stack, pos mgl64.Vec3, delay time.Duration) *Ent { config := itemConf config.PickupDelay = delay - return Config{Behaviour: config.New(i)}.New(ItemType{}, pos) + behaviour := config.New(i) + return Config{Behaviour: behaviour}.New(ItemType{Behaviour: behaviour}, pos) } var itemConf = ItemBehaviourConfig{ @@ -32,7 +33,9 @@ var itemConf = ItemBehaviourConfig{ } // ItemType is a world.EntityType implementation for Item. -type ItemType struct{} +type ItemType struct { + Behaviour *ItemBehaviour +} func (ItemType) EncodeEntity() string { return "minecraft:item" } func (ItemType) NetworkOffset() float64 { return 0.125 } @@ -64,3 +67,7 @@ func (ItemType) EncodeNBT(e world.Entity) map[string]any { "Item": nbtconv.WriteItem(b.Item(), true), } } + +func (t ItemType) EntityEject(e world.Entity, pos cube.Pos) { + t.Behaviour.EntityEject(e, pos) +} diff --git a/server/entity/item_behaviour.go b/server/entity/item_behaviour.go index 61de607d1..398afff8f 100644 --- a/server/entity/item_behaviour.go +++ b/server/entity/item_behaviour.go @@ -1,6 +1,8 @@ package entity import ( + "github.com/df-mc/dragonfly/server/block" + "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/internal/nbtconv" "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/world" @@ -62,9 +64,26 @@ func (i *ItemBehaviour) Item() item.Stack { return i.i } -// Tick moves the entity, checks if it should be picked up by a nearby collector -// or if it should merge with nearby item entities. +// EntityEject ejects the item entity from the block it is currently on. This is called when items are dropped on bells, +// for example, to make the item entity pop off the bell. +func (i *ItemBehaviour) EntityEject(e world.Entity, pos cube.Pos) { + ent, ok := e.(*Ent) + if !ok { + return + } + delta := e.Position().Sub(pos.Vec3Centre()) + if delta.Len() <= epsilon { + // There is no delta between the item entity and the block, so we can't eject it. + return + } + vel := delta.Normalize().Mul(0.4) + vel[1] = math.Max(0.15, vel[1]) + ent.SetVelocity(vel) +} + +// Tick ticks the entity, performing movement. func (i *ItemBehaviour) Tick(e *Ent) *Movement { + i.checkEntityInsiders(e) return i.passive.Tick(e) } @@ -152,6 +171,34 @@ func (i *ItemBehaviour) collect(e *Ent, collector Collector) { _ = e.Close() } +// checkEntityInsiders checks if the player is colliding with any EntityInsider blocks. +func (i *ItemBehaviour) checkEntityInsiders(e *Ent) { + w := e.World() + box := e.Type().BBox(e).Translate(e.Position()).Grow(-0.0001) + min, max := 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++ { + blockPos := cube.Pos{x, y, z} + b := w.Block(blockPos) + if collide, ok := b.(block.EntityInsider); ok { + collide.EntityInside(blockPos, w, e) + if _, liquid := b.(world.Liquid); liquid { + continue + } + } + + if l, ok := w.Liquid(blockPos); ok { + if collide, ok := l.(block.EntityInsider); ok { + collide.EntityInside(blockPos, w, e) + } + } + } + } + } +} + // Collector represents an entity in the world that is able to collect an item, typically an entity such as // a player or a zombie. type Collector interface { @@ -160,4 +207,6 @@ type Collector interface { // may be picked up. // The count of items collected from the stack n is returned. Collect(stack item.Stack) (n int) + // GameMode returns the gamemode of the collector. + GameMode() world.GameMode } diff --git a/server/entity/projectile.go b/server/entity/projectile.go index d9ecdd7dc..f9a38a45f 100644 --- a/server/entity/projectile.go +++ b/server/entity/projectile.go @@ -300,7 +300,7 @@ func (lt *ProjectileBehaviour) tickMovement(e *Ent) (*Movement, trace.Result) { ) if !mgl64.FloatEqual(end.Sub(pos).LenSqr(), 0) { if hit, ok = trace.Perform(pos, end, w, e.Type().BBox(e).Grow(1.0), lt.ignores(e)); ok { - if _, ok := hit.(trace.BlockResult); ok { + if r, ok := hit.(trace.BlockResult); ok { // Undo the gravity because the velocity as a result of gravity // at the point of collision should be 0. vel[1] = (vel[1] + lt.mc.Gravity) / (1 - lt.mc.Drag) @@ -311,6 +311,10 @@ func (lt *ProjectileBehaviour) tickMovement(e *Ent) (*Movement, trace.Result) { mx, my, mz := hit.Face().Axis().Vec3().Mul(-2).Add(mgl64.Vec3{1, 1, 1}).Elem() vel = mgl64.Vec3{x * mx, y * my, z * mz} + + if h, ok := w.Block(r.BlockPosition()).(block.ProjectileHitter); ok { + h.ProjectileHit(w, e, r.BlockPosition(), r.Face()) + } } else { vel = zeroVec3 } diff --git a/server/session/world.go b/server/session/world.go index 26dcc3b51..e82467743 100644 --- a/server/session/world.go +++ b/server/session/world.go @@ -584,6 +584,8 @@ func (s *Session) playSound(pos mgl64.Vec3, t world.Sound, disableRelative bool) pk.SoundType = packet.SoundEventBlastFurnaceUse case sound.SmokerCrackle: pk.SoundType = packet.SoundEventSmokerUse + case sound.BellRing: + pk.SoundType = packet.SoundEventBell case sound.UseSpyglass: pk.SoundType = packet.SoundEventUseSpyglass case sound.StopUsingSpyglass: @@ -1054,6 +1056,28 @@ func (s *Session) ViewBlockAction(pos cube.Pos, a world.BlockAction) { Position: vec64To32(pos.Vec3()), EventData: int32(65535 / (t.BreakTime.Seconds() * 20)), }) + case block.BellRing: + d := int32(0) + switch t.Face.Direction() { + case cube.West: + d = 1 + case cube.North: + d = 2 + case cube.East: + d = 3 + } + s.writePacket(&packet.BlockActorData{ + Position: blockPos, + NBTData: map[string]any{ + "Direction": d, + "Ringing": uint8(1), + + "id": "Bell", + "x": blockPos.X(), + "y": blockPos.Y(), + "z": blockPos.Z(), + }, + }) } } diff --git a/server/world/sound/block.go b/server/world/sound/block.go index 9fc06dde1..d3a18df98 100644 --- a/server/world/sound/block.go +++ b/server/world/sound/block.go @@ -161,6 +161,9 @@ type BlastFurnaceCrackle struct{ sound } // SmokerCrackle is a sound played every one to five seconds from a smoker. type SmokerCrackle struct{ sound } +// BellRing is a sound played when a bell is rung. +type BellRing struct{ sound } + // ComposterEmpty is a sound played when a composter has been emptied. type ComposterEmpty struct{ sound }