diff --git a/cmd/blockhash/main.go b/cmd/blockhash/main.go index 3f86174cd..da380d14c 100644 --- a/cmd/blockhash/main.go +++ b/cmd/blockhash/main.go @@ -244,6 +244,8 @@ func (b *hashBuilder) ftype(structName, s string, expr ast.Expr, directives map[ return "uint64(" + s + ".FaceUint8())", 3 } return "uint64(" + s + ".Uint8())", 5 + case "HangingAttachment": + return "uint64(" + s + ".Uint8())", 5 case "GrindstoneAttachment": return "uint64(" + s + ".Uint8())", 2 case "WoodType", "LeavesType", "FlowerType", "DoubleFlowerType", "Colour": diff --git a/server/block/hanging_attachment.go b/server/block/hanging_attachment.go new file mode 100644 index 000000000..84017c402 --- /dev/null +++ b/server/block/hanging_attachment.go @@ -0,0 +1,47 @@ +package block + +import "github.com/df-mc/dragonfly/server/block/cube" + +// HangingAttachment describes how a hanging sign is attached. It may be mounted to the side of a block, hang +// underneath a block by chains, or be attached underneath a block with the 16-direction ceiling variant. +type HangingAttachment struct { + ceiling bool + attached bool + facing cube.Direction + o cube.Orientation +} + +// WallHangingAttachment returns a HangingAttachment for a hanging sign mounted to the side of a block. +func WallHangingAttachment(facing cube.Direction) HangingAttachment { + return HangingAttachment{facing: facing} +} + +// CeilingHangingAttachment returns a HangingAttachment for a hanging sign hanging underneath a block. +func CeilingHangingAttachment(facing cube.Direction) HangingAttachment { + return HangingAttachment{ceiling: true, facing: facing} +} + +// AttachedCeilingHangingAttachment returns a HangingAttachment for a hanging sign attached underneath a block using +// the 16-direction ceiling variant. +func AttachedCeilingHangingAttachment(o cube.Orientation) HangingAttachment { + return HangingAttachment{ceiling: true, attached: true, o: o} +} + +// Uint8 returns the HangingAttachment as a uint8. +func (a HangingAttachment) Uint8() uint8 { + if !a.ceiling { + return uint8(a.facing) + } + if !a.attached { + return 4 | uint8(a.facing) + } + return 8 | uint8(a.o) +} + +// Rotation returns the rotation of the HangingAttachment. +func (a HangingAttachment) Rotation() cube.Rotation { + if a.attached { + return cube.Rotation{a.o.Yaw()} + } + return WallAttachment(a.facing).Rotation() +} diff --git a/server/block/hanging_sign.go b/server/block/hanging_sign.go new file mode 100644 index 000000000..83a9f81ef --- /dev/null +++ b/server/block/hanging_sign.go @@ -0,0 +1,238 @@ +package block + +import ( + "time" + + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/world" + "github.com/df-mc/dragonfly/server/world/sound" + "github.com/go-gl/mathgl/mgl64" +) + +// HangingSign is a decorative sign block that may be attached to the side of a block or hung from its underside. +type HangingSign struct { + transparent + empty + bass + sourceWaterDisplacer + + // Wood is the type of wood of the hanging sign. + Wood WoodType + // Attach describes how the hanging sign is mounted. + Attach HangingAttachment + // Waxed specifies if the sign can no longer be edited. + Waxed bool + // Front is the text on the front side of the sign. + Front SignText + // Back is the text on the back side of the sign. + Back SignText +} + +// SideClosed reports that no face of a hanging sign fully closes off an adjacent block face. +func (h HangingSign) SideClosed(cube.Pos, cube.Pos, *world.Tx) bool { + return false +} + +// MaxCount returns the maximum number of hanging signs that may be stacked in one inventory slot. +func (h HangingSign) MaxCount() int { + return 16 +} + +// FlammabilityInfo returns the flammability properties of the hanging sign. +func (h HangingSign) FlammabilityInfo() FlammabilityInfo { + return newFlammabilityInfo(0, 0, true) +} + +// FuelInfo returns the furnace fuel properties of the hanging sign. +func (h HangingSign) FuelInfo() item.FuelInfo { + if !h.Wood.Flammable() { + return item.FuelInfo{} + } + return newFuelInfo(time.Second * 10) +} + +// EncodeItem encodes the hanging sign item name. +func (h HangingSign) EncodeItem() (name string, meta int16) { + return "minecraft:" + h.Wood.String() + "_hanging_sign", 0 +} + +// BreakInfo returns the breaking properties of the hanging sign. +func (h HangingSign) BreakInfo() BreakInfo { + return newBreakInfo(1, alwaysHarvestable, axeEffective, oneOf(HangingSign{Wood: h.Wood})) +} + +// Dye dyes the HangingSign, changing its base colour to that of the colour passed. +func (h HangingSign) Dye(pos cube.Pos, userPos mgl64.Vec3, c item.Colour) (world.Block, bool) { + if h.EditingFrontSide(pos, userPos) { + if h.Front.BaseColour == c.SignRGBA() { + return h, false + } + h.Front.BaseColour = c.SignRGBA() + } else { + if h.Back.BaseColour == c.SignRGBA() { + return h, false + } + h.Back.BaseColour = c.SignRGBA() + } + return h, true +} + +// Ink inks the sign either glowing or non-glowing. +func (h HangingSign) Ink(pos cube.Pos, userPos mgl64.Vec3, glowing bool) (world.Block, bool) { + if h.EditingFrontSide(pos, userPos) { + if h.Front.Glowing == glowing { + return h, false + } + h.Front.Glowing = glowing + } else { + if h.Back.Glowing == glowing { + return h, false + } + h.Back.Glowing = glowing + } + return h, true +} + +// Wax waxes a sign to prevent it from further editing. +func (h HangingSign) Wax(cube.Pos, mgl64.Vec3) (world.Block, bool) { + if h.Waxed { + return h, false + } + h.Waxed = true + return h, true +} + +// Activate opens the sign editor when the hanging sign is editable or plays the waxed interaction sound otherwise. +func (h HangingSign) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, u item.User, _ *item.UseContext) bool { + if editor, ok := u.(SignEditor); ok && !h.Waxed { + editor.OpenSign(pos, h.EditingFrontSide(pos, u.Position())) + } else if h.Waxed { + tx.PlaySound(pos.Vec3(), sound.WaxedSignFailedInteraction{}) + } + return true +} + +// EditingFrontSide returns if the user is editing the front side of the sign based on their position relative to the sign. +func (h HangingSign) EditingFrontSide(pos cube.Pos, userPos mgl64.Vec3) bool { + return userPos.Sub(pos.Vec3Centre()).Dot(h.Attach.Rotation().Vec3()) > 0 +} + +// UseOnBlock places the hanging sign either on the side of a block or underneath a supporting block. +func (h HangingSign) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) (used bool) { + pos, face, used = firstReplaceable(tx, pos, face, h) + if !used || face == cube.FaceUp { + return false + } + switch face { + case cube.FaceDown: + supportPos := pos.Side(cube.FaceUp) + support := tx.Block(supportPos) + if !supportsCeilingHangingSign(support, supportPos, tx) { + return false + } + + rotation := user.Rotation() + if supportsAttachedCeilingHangingSign(support) || sneaking(user) { + h.Attach = AttachedCeilingHangingAttachment(rotation.Orientation().Opposite()) + } else { + h.Attach = CeilingHangingAttachment(rotation.Direction().Opposite()) + } + default: + h.Attach = WallHangingAttachment(face.Opposite().Direction().RotateRight()) + } + + place(tx, pos, h, user, ctx) + if editor, ok := user.(SignEditor); ok { + editor.OpenSign(pos, true) + } + return placed(ctx) +} + +// NeighbourUpdateTick breaks the hanging sign when the block supporting it is no longer valid. +func (h HangingSign) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) { + if h.Attach.ceiling { + supportPos := pos.Side(cube.FaceUp) + if !supportsCeilingHangingSign(tx.Block(supportPos), supportPos, tx) { + breakBlock(h, pos, tx) + } + return + } + + supportFace := h.Attach.facing.RotateLeft().Face() + supportPos := pos.Side(supportFace) + if !tx.Block(supportPos).Model().FaceSolid(supportPos, supportFace.Opposite(), tx) { + breakBlock(h, pos, tx) + } +} + +// EncodeBlock encodes the Bedrock block state of the hanging sign. +func (h HangingSign) EncodeBlock() (name string, properties map[string]any) { + var facing, ground int32 + if h.Attach.attached { + ground = int32(h.Attach.o) + } else { + facing = int32(h.Attach.facing.Face()) + } + return "minecraft:" + h.Wood.String() + "_hanging_sign", map[string]any{ + "attached_bit": boolByte(h.Attach.attached), + "facing_direction": facing, + "ground_sign_direction": ground, + "hanging": boolByte(h.Attach.ceiling), + } +} + +// DecodeNBT decodes block actor data for the hanging sign using the same text format as regular signs. +func (h HangingSign) DecodeNBT(data map[string]any) any { + s := Sign{Front: h.Front, Back: h.Back, Waxed: h.Waxed} + s = s.DecodeNBT(data).(Sign) + h.Front, h.Back, h.Waxed = s.Front, s.Back, s.Waxed + return h +} + +// EncodeNBT encodes block actor data for the hanging sign. +func (h HangingSign) EncodeNBT() map[string]any { + nbt := Sign{Front: h.Front, Back: h.Back, Waxed: h.Waxed}.EncodeNBT() + nbt["id"] = "HangingSign" + return nbt +} + +// supportsCeilingHangingSign reports whether the block above can support a ceiling-hanging sign. +func supportsCeilingHangingSign(b world.Block, pos cube.Pos, tx *world.Tx) bool { + if supportsAttachedCeilingHangingSign(b) { + return true + } + return b.Model().FaceSolid(pos, cube.FaceDown, tx) +} + +// supportsAttachedCeilingHangingSign reports whether the block above supports the attached chain variant. +func supportsAttachedCeilingHangingSign(b world.Block) bool { + switch c := b.(type) { + case IronChain: + return c.Axis == cube.Y + case CopperChain: + return c.Axis == cube.Y + default: + return false + } +} + +// sneaking reports whether the user is currently sneaking. +func sneaking(u item.User) bool { + s, ok := u.(interface{ Sneaking() bool }) + return ok && s.Sneaking() +} + +// allHangingSigns returns all registered hanging sign permutations. +func allHangingSigns() (signs []world.Block) { + for _, w := range WoodTypes() { + for _, d := range cube.Directions() { + signs = append(signs, HangingSign{Wood: w, Attach: WallHangingAttachment(d)}) + signs = append(signs, HangingSign{Wood: w, Attach: CeilingHangingAttachment(d)}) + } + for o := cube.Orientation(0); o <= 15; o++ { + signs = append(signs, HangingSign{Wood: w, Attach: AttachedCeilingHangingAttachment(o)}) + } + } + return +} diff --git a/server/block/hash.go b/server/block/hash.go index f884eb870..7a71da0fb 100644 --- a/server/block/hash.go +++ b/server/block/hash.go @@ -93,6 +93,7 @@ const ( hashGrass hashGravel hashGrindstone + hashHangingSign hashHayBale hashHoneycomb hashHopper @@ -561,6 +562,10 @@ func (g Grindstone) Hash() (uint64, uint64) { return hashGrindstone, uint64(g.Attach.Uint8()) | uint64(g.Facing)<<2 } +func (h HangingSign) Hash() (uint64, uint64) { + return hashHangingSign, uint64(h.Wood.Uint8()) | uint64(h.Attach.Uint8())<<4 +} + func (h HayBale) Hash() (uint64, uint64) { return hashHayBale, uint64(h.Axis) } diff --git a/server/block/register.go b/server/block/register.go index 8ff3f72cb..3ecac1634 100644 --- a/server/block/register.go +++ b/server/block/register.go @@ -163,6 +163,7 @@ func init() { registerAll(allFurnaces()) registerAll(allGlazedTerracotta()) registerAll(allGrindstones()) + registerAll(allHangingSigns()) registerAll(allHayBales()) registerAll(allHoppers()) registerAll(allItemFrames()) @@ -418,6 +419,7 @@ func init() { } world.RegisterItem(Log{Wood: w, Stripped: true}) world.RegisterItem(Log{Wood: w}) + world.RegisterItem(HangingSign{Wood: w}) world.RegisterItem(Planks{Wood: w}) world.RegisterItem(Sign{Wood: w}) world.RegisterItem(WoodDoor{Wood: w}) diff --git a/server/player/player.go b/server/player/player.go index 7383b0de9..40fbddb1b 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -2914,34 +2914,49 @@ 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 { - sign, ok := p.tx.Block(pos).(block.Sign) - if !ok { + switch sign := p.tx.Block(pos).(type) { + case block.Sign: + return p.editSign(pos, frontText, backText, sign.Front, sign.Back, sign.Waxed, func(front, back block.SignText) world.Block { + sign.Front = front + sign.Back = back + return sign + }) + case block.HangingSign: + return p.editSign(pos, frontText, backText, sign.Front, sign.Back, sign.Waxed, func(front, back block.SignText) world.Block { + sign.Front = front + sign.Back = back + return sign + }) + default: return fmt.Errorf("edit sign: no sign at position %v", pos) } +} - if sign.Waxed { +// editSign applies validated sign text changes to either a regular sign or a hanging sign. +func (p *Player) editSign(pos cube.Pos, frontText, backText string, front, back block.SignText, waxed bool, update func(front, back block.SignText) world.Block) error { + if waxed { return nil - } else if frontText == sign.Front.Text && backText == sign.Back.Text { + } else if frontText == front.Text && backText == back.Text { return nil } ctx := event.C(p) - if frontText != sign.Front.Text { - if p.Handler().HandleSignEdit(ctx, pos, true, sign.Front.Text, frontText); ctx.Cancelled() { + if frontText != front.Text { + if p.Handler().HandleSignEdit(ctx, pos, true, front.Text, frontText); ctx.Cancelled() { p.resendNearbyBlock(pos) return nil } - sign.Front.Text = frontText - sign.Front.Owner = p.XUID() + front.Text = frontText + front.Owner = p.XUID() } else { - if p.Handler().HandleSignEdit(ctx, pos, false, sign.Back.Text, backText); ctx.Cancelled() { + if p.Handler().HandleSignEdit(ctx, pos, false, back.Text, backText); ctx.Cancelled() { p.resendNearbyBlock(pos) return nil } - sign.Back.Text = backText - sign.Back.Owner = p.XUID() + back.Text = backText + back.Owner = p.XUID() } - p.tx.SetBlock(pos, sign, nil) + p.tx.SetBlock(pos, update(front, back), nil) return nil } diff --git a/server/session/handler_block_actor_data.go b/server/session/handler_block_actor_data.go index ae0ea70e4..c8b4782c2 100644 --- a/server/session/handler_block_actor_data.go +++ b/server/session/handler_block_actor_data.go @@ -26,18 +26,28 @@ func (b BlockActorDataHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, } switch id { case "Sign": - return b.handleSign(pk, pos, s, tx, c) + return b.handleSign(pk, pos, s, tx, c, false) + case "HangingSign": + return b.handleSign(pk, pos, s, tx, c, true) } return fmt.Errorf("unhandled block actor data ID %v", id) } return fmt.Errorf("block actor data without 'id' tag: %v", pk.NBTData) } -// handleSign handles the BlockActorData packet sent when editing a sign. -func (b BlockActorDataHandler) handleSign(pk *packet.BlockActorData, pos cube.Pos, s *Session, tx *world.Tx, co Controllable) error { - if _, ok := tx.Block(pos).(block.Sign); !ok { - s.conf.Log.Debug("no sign at position of sign block actor data", "pos", pos.String()) - return nil +// handleSign handles the BlockActorData packet sent when editing a sign and validates the expected sign variant. +func (b BlockActorDataHandler) handleSign(pk *packet.BlockActorData, pos cube.Pos, s *Session, tx *world.Tx, co Controllable, hanging bool) error { + bl := tx.Block(pos) + if hanging { + if _, ok := bl.(block.HangingSign); !ok { + s.conf.Log.Debug("no hanging sign at position of sign block actor data", "pos", pos.String()) + return nil + } + } else { + if _, ok := bl.(block.Sign); !ok { + s.conf.Log.Debug("no sign at position of sign block actor data", "pos", pos.String()) + return nil + } } frontText, err := b.textFromNBTData(pk.NBTData, true)