Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
809039a
Implement shulker boxes (no behaviour)
mmm545 Dec 4, 2024
d93e10e
Forgot BreakInfo
mmm545 Dec 4, 2024
180fd74
Oops
mmm545 Dec 4, 2024
dc2f99b
Implement most of shulker box functionality
mmm545 Dec 5, 2024
58f7dbb
Oops
mmm545 Dec 5, 2024
5bba5e7
Ignore inspection
mmm545 Dec 5, 2024
d955b44
Implement custom names
mmm545 Dec 5, 2024
c42eb4d
Fixed items disappearing after breaking shulker box
mmm545 Dec 10, 2024
60313b9
Make shulker box unstackable
mmm545 Dec 11, 2024
bd0063d
Gotta learn how to type good code
mmm545 Dec 11, 2024
8e3c84e
Fixed inability to place colored shulker boxes
mmm545 Dec 11, 2024
6e2f173
Add docs
mmm545 Dec 11, 2024
fd50d99
Refactore allShulkerBox to allShulkerBoxes
mmm545 Dec 11, 2024
7655008
Fixed doc typo
mmm545 Dec 11, 2024
12091d7
Fixed the sound and closing action being out of sync
mmm545 Dec 11, 2024
1f7c0ba
Add missing doc for ScheduledTick
mmm545 Dec 11, 2024
3018fb6
Fixed doc typo for CustomName field
mmm545 Dec 13, 2024
6c6dd6b
Validate that the shulker box actaully exists
mmm545 Dec 22, 2024
828690d
Make shulker boxes transparent and water loggable
mmm545 Dec 22, 2024
da2d235
Merge branch 'feature/shulker-box' of https://github.com/mmm545/drago…
mmm545 Dec 22, 2024
cb436a9
Remove unnecessary withBlastResistance()
mmm545 Dec 22, 2024
5209327
Merge branch 'master' into feature/shulker-box
mmm545 Dec 22, 2024
87b507a
Update tx.ScheduleBlockUpdate()
mmm545 Dec 22, 2024
e2c8418
Add ShulkerBox model, fix conflict
Feb 27, 2025
152a97f
Merge branch 'df-mc:master' into feature/shulker-box
Superomarking Feb 27, 2025
4b2f6e7
Re-added sounds
Feb 27, 2025
e479142
Fix bounding box
Feb 27, 2025
689fa9b
Merge branch 'master' into feature/shulker-box
Superomarking Mar 4, 2025
09d3f5c
Merge branch 'df-mc:master' into feature/shulker-box
Superomarking Mar 16, 2025
27c31c7
fix doc
Mar 17, 2025
06f2d86
Merge remote-tracking branch 'origin/feature/shulker-box' into featur…
Mar 17, 2025
e6a30f2
Merge branch 'df-mc:master' into feature/shulker-box
Superomarking Apr 7, 2025
9cb1de1
Merge branch 'df-mc:master' into feature/shulker-box
Superomarking Jun 24, 2025
795c06f
Merge remote-tracking branch 'upstream' into feature/shulker-box
didntpot Oct 2, 2025
c13e884
Merge remote-tracking branch 'upstream' into feature/shulker-box
didntpot Dec 24, 2025
80ad7bf
feature/shulker-box: Implement entity pushing, slight clean-up.
didntpot Dec 24, 2025
5939ca6
Merge branch 'master' into feature/shulker-box
HashimTheArab May 1, 2026
602c4c9
simplify code and fix things
HashimTheArab May 1, 2026
5a9dcfb
code improvements
HashimTheArab May 1, 2026
27a9cad
feat: add optionalcolour abstraction
HashimTheArab May 1, 2026
f57051a
rename var
HashimTheArab May 1, 2026
2943189
Merge branch 'master' into feature/shulker-box
HashimTheArab May 5, 2026
d1db73e
Merge branch 'master' into feature/shulker-box
HashimTheArab May 7, 2026
3652b9e
Update server/block/shulker_box.go
HashimTheArab May 7, 2026
a47e8e3
address review comments
HashimTheArab May 8, 2026
92f32b8
docs
HashimTheArab May 8, 2026
20e2a6f
rename forcemove to displace and add handler
HashimTheArab May 8, 2026
1e29aa1
various changes
HashimTheArab May 8, 2026
86fc952
fmt
HashimTheArab May 8, 2026
402ceea
movement change
HashimTheArab May 8, 2026
5e67ecd
Merge branch 'master' into feature/shulker-box
didntpot May 8, 2026
9201f30
revert move thing
HashimTheArab May 8, 2026
4d6646a
simplify code
HashimTheArab May 8, 2026
f84e5cb
fix shit code
HashimTheArab May 8, 2026
5b7a40b
simplify code (verified working)
HashimTheArab May 8, 2026
fd06799
fix -0
HashimTheArab May 8, 2026
f29b359
fix: proper inventory validation check
HashimTheArab May 8, 2026
2cdbb6d
remove cube face offset
HashimTheArab May 8, 2026
daab568
fix incorrect bbox
HashimTheArab May 10, 2026
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
2 changes: 2 additions & 0 deletions cmd/blockhash/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 "OptionalColour":
return "uint64(" + s + ".Uint8())", 5
case "GrindstoneAttachment":
return "uint64(" + s + ".Uint8())", 2
case "WoodType", "LeavesType", "FlowerType", "DoubleFlowerType", "Colour":
Expand Down
5 changes: 5 additions & 0 deletions server/block/hash.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions server/block/model/shulker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package model

import (
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/world"
)

// Shulker is the model of a shulker box. The bounding box grows along the
// facing axis as the lid opens.
type Shulker struct {
// Facing is the direction that the lid opens towards.
Facing cube.Face
// Progress is the lid animation progress, ranging from 0 (closed) to 10 (fully open).
Progress int32
}

// BBox returns a single bounding box that extends outward along Facing as the
// lid opens.
func (s Shulker) BBox(cube.Pos, world.BlockSource) []cube.BBox {
peak := ShulkerPhysicalPeak(s.Progress)
return []cube.BBox{full.ExtendTowards(s.Facing, peak)}
}

// ShulkerPhysicalPeak returns the lid extension along the facing axis for a
// given Progress in [0, 10]. The curve eases out cubically so the lid moves
// quickly and settles.
func ShulkerPhysicalPeak(progress int32) float64 {
t := float64(progress) / 10.0
return (1.0 - (1.0-t)*(1.0-t)*(1.0-t)) * 0.5
}
Comment thread
HashimTheArab marked this conversation as resolved.

// FaceSolid always returns false.
func (Shulker) FaceSolid(cube.Pos, cube.Face, world.BlockSource) bool {
return false
}
5 changes: 5 additions & 0 deletions server/block/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ func init() {
registerAll(allCopperLanterns())
registerAll(allCopperTorches())
registerAll(allCopperTrapdoors())
registerAll(allShulkerBoxes())
}

func init() {
Expand Down Expand Up @@ -518,6 +519,10 @@ func init() {
world.RegisterItem(Copper{Type: c, Oxidation: o, Waxed: true})
}
}

for _, c := range item.OptionalColours() {
world.RegisterItem(ShulkerBox{Colour: c})
}
}

func registerAll(blocks []world.Block) {
Expand Down
294 changes: 294 additions & 0 deletions server/block/shulker_box.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
package block

import (
"fmt"
"math/rand/v2"
"strings"
"sync"
"sync/atomic"

"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/block/model"
"github.com/df-mc/dragonfly/server/internal/nbtconv"
"github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/item/inventory"
"github.com/df-mc/dragonfly/server/world"
"github.com/df-mc/dragonfly/server/world/sound"
"github.com/go-gl/mathgl/mgl64"
)

const (
shulkerStateClosed int32 = iota
shulkerStateOpening
shulkerStateOpened
shulkerStateClosing
)

// shulkerLidTicks is the number of scheduled ticks between fully closed and fully open.
const shulkerLidTicks int32 = 10

// ShulkerBox is a dye-able block that stores items. Unlike other blocks, it keeps its contents when broken.
type ShulkerBox struct {
transparent
sourceWaterDisplacer

// Colour is the colour of the shulker box. A zero OptionalColour represents
// the undyed variant (minecraft:undyed_shulker_box).
Colour item.OptionalColour
// Facing is the direction that the shulker box is facing.
Facing cube.Face
// CustomName is the custom name of the shulker box. This name is displayed when the shulker box is opened, and may
// include colour codes.
CustomName string

inventory *inventory.Inventory
viewerMu *sync.RWMutex
viewers map[ContainerViewer]struct{}
// progress is the lid opening progress in [0, 10].
progress *atomic.Int32
// animationStatus is the current openness state of the shulker box (whether it's opened, closing, etc.).
animationStatus *atomic.Int32
}

// NewShulkerBox creates a new initialised shulker box. The inventory is properly initialised.
func NewShulkerBox() ShulkerBox {
s := ShulkerBox{
viewerMu: new(sync.RWMutex),
viewers: make(map[ContainerViewer]struct{}, 1),
progress: new(atomic.Int32),
animationStatus: new(atomic.Int32),
}

s.inventory = inventory.New(27, func(slot int, _, after item.Stack) {
s.viewerMu.RLock()
defer s.viewerMu.RUnlock()
for viewer := range s.viewers {
viewer.ViewSlotChange(slot, after)
}
Comment thread
HashimTheArab marked this conversation as resolved.
})
Comment thread
HashimTheArab marked this conversation as resolved.
s.inventory.SlotValidatorFunc(canStoreInShulkerBox)

return s
}

// canStoreInShulkerBox rejects nested shulker boxes.
func canStoreInShulkerBox(s item.Stack, _ int) bool {
if s.Empty() {
return true
}
_, nested := s.Item().(ShulkerBox)
return !nested
}

func (s ShulkerBox) Model() world.BlockModel {
return model.Shulker{Facing: s.Facing, Progress: s.progress.Load()}
}

func (s ShulkerBox) WithName(a ...any) world.Item {
s.CustomName = strings.TrimSuffix(fmt.Sprintln(a...), "\n")
return s
}

func (s ShulkerBox) AddViewer(v ContainerViewer, tx *world.Tx, pos cube.Pos) {
s.viewerMu.Lock()
defer s.viewerMu.Unlock()
if len(s.viewers) == 0 {
s.open(tx, pos)
}
s.viewers[v] = struct{}{}
}

func (s ShulkerBox) RemoveViewer(v ContainerViewer, tx *world.Tx, pos cube.Pos) {
s.viewerMu.Lock()
defer s.viewerMu.Unlock()
if len(s.viewers) == 0 {
return
}
delete(s.viewers, v)
if len(s.viewers) == 0 {
s.close(tx, pos)
}
}

func (s ShulkerBox) Inventory(*world.Tx, cube.Pos) *inventory.Inventory {
return s.inventory
}

func (s ShulkerBox) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, u item.User, _ *item.UseContext) bool {
opener, ok := u.(ContainerOpener)
if !ok {
return false
}
if d, ok := tx.Block(pos.Side(s.Facing)).(LightDiffuser); ok && d.LightDiffusionLevel() <= 2 {
opener.OpenBlockContainer(pos, tx)
}
return true
}

func (s ShulkerBox) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) (used bool) {
pos, _, used = firstReplaceable(tx, pos, face, s)
if !used {
return
}
s = s.initialised()
s.Facing = face
place(tx, pos, s, user, ctx)
return placed(ctx)
}

// initialised lazily populates runtime fields on values created via struct
// literal (e.g. those returned from allShulkerBoxes).
func (s ShulkerBox) initialised() ShulkerBox {
if s.inventory != nil {
return s
}
n := NewShulkerBox()
n.Colour, n.Facing, n.CustomName = s.Colour, s.Facing, s.CustomName
return n
}

// open opens the shulker box, displaying the animation and playing a sound.
func (s ShulkerBox) open(tx *world.Tx, pos cube.Pos) {
s.animationStatus.Store(shulkerStateOpening)
for _, v := range tx.Viewers(pos.Vec3()) {
v.ViewBlockAction(pos, OpenAction{})
}
tx.PlaySound(pos.Vec3Centre(), sound.ShulkerBoxOpen{})
tx.ScheduleBlockUpdate(pos, s, 0)
}

// close closes the shulker box, displaying the animation and playing a sound.
func (s ShulkerBox) close(tx *world.Tx, pos cube.Pos) {
s.animationStatus.Store(shulkerStateClosing)
for _, v := range tx.Viewers(pos.Vec3()) {
v.ViewBlockAction(pos, CloseAction{})
}
tx.ScheduleBlockUpdate(pos, s, 0)
}

func (s ShulkerBox) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) {
switch s.animationStatus.Load() {
case shulkerStateClosed:
s.progress.Store(0)
case shulkerStateOpening:
s.progress.Add(1)
s.pushEntities(pos, tx)
if s.progress.Load() >= shulkerLidTicks {
s.progress.Store(shulkerLidTicks)
s.animationStatus.Store(shulkerStateOpened)
}
tx.ScheduleBlockUpdate(pos, s, 0)
case shulkerStateOpened:
s.progress.Store(shulkerLidTicks)
case shulkerStateClosing:
s.progress.Add(-1)
if s.progress.Load() <= 0 {
tx.PlaySound(pos.Vec3Centre(), sound.ShulkerBoxClose{})
s.progress.Store(0)
s.animationStatus.Store(shulkerStateClosed)
}
tx.ScheduleBlockUpdate(pos, s, 0)
}
}

// pushEntities pushes all entities touching the shulker box lid during opening.
func (s ShulkerBox) pushEntities(pos cube.Pos, tx *world.Tx) {
shulkerBBoxes := s.Model().BBox(pos, tx)
if len(shulkerBBoxes) == 0 {
return
}
searchBox := shulkerBBoxes[0].Translate(pos.Vec3()).Grow(0.35)
for e := range tx.EntitiesWithin(searchBox) {
s.push(pos, tx, e)
}
}

// push pushes entities when the shulker box lid is opening.
func (s ShulkerBox) push(pos cube.Pos, tx *world.Tx, e world.Entity) {
if s.animationStatus.Load() != shulkerStateOpening {
return
}
mover, ok := e.(interface {
Displace(deltaPos mgl64.Vec3, deltaYaw, deltaPitch float64)
})
if !ok {
return
}
Comment thread
HashimTheArab marked this conversation as resolved.
shulkerBBoxes := s.Model().BBox(pos, tx)
if len(shulkerBBoxes) == 0 {
return
}
shulkerBBox := shulkerBBoxes[0].Translate(pos.Vec3())
entityBBox := e.H().Type().BBox(e).Translate(e.Position())
if !shulkerBBox.IntersectsWith(entityBBox) {
return
}

// Move the entity out along the lid's facing axis by the penetration depth
// between the shulker lid box and the entity box.
var delta mgl64.Vec3
switch s.Facing {
case cube.FaceUp:
delta[1] = shulkerBBox.Max().Y() - entityBBox.Min().Y()
case cube.FaceEast:
delta[0] = shulkerBBox.Max().X() - entityBBox.Min().X()
case cube.FaceWest:
delta[0] = shulkerBBox.Min().X() - entityBBox.Max().X()
case cube.FaceSouth:
delta[2] = shulkerBBox.Max().Z() - entityBBox.Min().Z()
case cube.FaceNorth:
delta[2] = shulkerBBox.Min().Z() - entityBBox.Max().Z()
}
if delta != (mgl64.Vec3{}) {
mover.Displace(delta, 0, 0)
}
}

func (s ShulkerBox) BreakInfo() BreakInfo {
return newBreakInfo(2, alwaysHarvestable, pickaxeEffective, oneOf(s))
}

func (s ShulkerBox) MaxCount() int {
return 1
}

func (s ShulkerBox) EncodeBlock() (name string, properties map[string]any) {
if c, ok := s.Colour.Colour(); ok {
return "minecraft:" + c.String() + "_shulker_box", nil
}
return "minecraft:undyed_shulker_box", nil
}

func (s ShulkerBox) EncodeItem() (id string, meta int16) {
name, _ := s.EncodeBlock()
return name, 0
}

func (s ShulkerBox) DecodeNBT(data map[string]any) any {
s = s.initialised()
nbtconv.InvFromNBT(s.inventory, nbtconv.Slice(data, "Items"))
s.Facing = cube.Face(nbtconv.Uint8(data, "facing"))
s.CustomName = nbtconv.String(data, "CustomName")
return s
}

func (s ShulkerBox) EncodeNBT() map[string]any {
s = s.initialised()
m := map[string]any{
"Items": nbtconv.InvToNBT(s.inventory),
"id": "ShulkerBox",
"facing": uint8(s.Facing),
}
if s.CustomName != "" {
m["CustomName"] = s.CustomName
}
return m
}

// allShulkerBoxes returns one shulker box per item.OptionalColour, including the undyed variant.
func allShulkerBoxes() (boxes []world.Block) {
for _, c := range item.OptionalColours() {
boxes = append(boxes, ShulkerBox{Colour: c})
}
return boxes
}
4 changes: 2 additions & 2 deletions server/internal/nbtconv/item.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ func InvFromNBT(inv *inventory.Inventory, items []any) {
}

// InvToNBT encodes an inventory to a data slice which may be encoded as NBT.
func InvToNBT(inv *inventory.Inventory) []map[string]any {
var items []map[string]any
func InvToNBT(inv *inventory.Inventory) []any {
var items []any
for index, i := range inv.Slots() {
if i.Empty() {
continue
Expand Down
Loading