Skip to content

Implement shulker boxes #956

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 32 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 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
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
3 changes: 2 additions & 1 deletion cmd/blockhash/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,8 @@ func (b *hashBuilder) ftype(structName, s string, expr ast.Expr, directives map[
case "CoralType", "SkullType":
return "uint64(" + s + ".Uint8())", 3
case "AnvilType", "SandstoneType", "PrismarineType", "StoneBricksType", "NetherBricksType", "FroglightType",
"WallConnectionType", "BlackstoneType", "DeepslateType", "TallGrassType", "CopperType", "OxidationType":
"WallConnectionType", "BlackstoneType", "DeepslateType", "TallGrassType", "CopperType", "OxidationType",
"ShulkerBoxType":
return "uint64(" + s + ".Uint8())", 2
case "OreType", "FireType", "DoubleTallGrassType":
return "uint64(" + s + ".Uint8())", 1
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.

36 changes: 36 additions & 0 deletions server/block/model/shulker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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. It depends on the opening/closing progress of the shulker block.
type Shulker struct {
// Facing is the face that the shulker faces.
Facing cube.Face
// Progress is the opening/closing progress of the shulker.
Progress int32
}

// BBox returns a BBox that depends on the opening/closing progress of the shulker.
func (s Shulker) BBox(cube.Pos, world.BlockSource) []cube.BBox {
peak := physicalPeak(s.Progress)
// Adds peak to the top and subtracts peak from the bottom. (according to BDS)
bbox := full
bbox.ExtendTowards(s.Facing, peak).ExtendTowards(s.Facing.Opposite(), -peak)

return []cube.BBox{bbox}
}

// physicalPeak returns the peak of which the shulker reaches in its current progress
func physicalPeak(progress int32) float64 {
fp := float64(progress) / 10.0
openness := 1.0 - fp
return (1.0 - openness*openness*openness) * 0.5
}

// 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 @@ -210,6 +210,7 @@ func init() {
registerAll(allCopperDoors())
registerAll(allCopperGrates())
registerAll(allCopperTrapdoors())
registerAll(allShulkerBoxes())
}

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

for _, t := range ShulkerBoxTypes() {
world.RegisterItem(ShulkerBox{Type: t})
}
}

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

import (
"fmt"
"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"
"math/rand/v2"
"strings"
"sync"
"sync/atomic"
)

const (
StateClosed = iota
StateOpening
StateOpened
StateClosing
)

// ShulkerBox is a dye-able block that stores items. Unlike other blocks, it keeps its contents when broken.
type ShulkerBox struct {
transparent
sourceWaterDisplacer
// Type is the type of shulker box of the block.
Type ShulkerBoxType
// 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 openness of the shulker box opening or closing. It is a float between 0 and 1.
progress *atomic.Int32
// animationStatus is the current openness state of the shulker box (whether its 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 {
// A shulker box inventory can't store shulker boxes, this is mostly handled by the client.
if _, ok := after.Item().(ShulkerBox); !ok {
viewer.ViewSlotChange(slot, after)
}
}
})

return s
}

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

// WithName returns the shulker box after applying a specific name to the block.
func (s ShulkerBox) WithName(a ...any) world.Item {
s.CustomName = strings.TrimSuffix(fmt.Sprintln(a...), "\n")
return s
}

// AddViewer adds a viewer to the shulker box, so that it is updated whenever the inventory of the shulker box is changed.
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{}{}
}

// RemoveViewer removes a viewer from the shulker box, so that slot updates in the inventory are no longer sent to it.
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)
}
}

// Inventory returns the inventory of the shulker box.
func (s ShulkerBox) Inventory(*world.Tx, cube.Pos) *inventory.Inventory {
return s.inventory
}

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

return false
}

// UseOnBlock ...
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
}

if s.inventory == nil {
typ, customName := s.Type, s.CustomName
//noinspection GoAssignmentToReceiver
s = NewShulkerBox()
s.Type, s.CustomName = typ, customName
}

s.Facing = face
place(tx, pos, s, user, ctx)
return placed(ctx)
}

// open opens the shulker box, displaying the animation and playing a sound.
func (s ShulkerBox) open(tx *world.Tx, pos cube.Pos) {
for _, v := range tx.Viewers(pos.Vec3()) {
v.ViewBlockAction(pos, OpenAction{})
}
s.animationStatus.Store(StateOpening)
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) {
for _, v := range tx.Viewers(pos.Vec3()) {
v.ViewBlockAction(pos, CloseAction{})
}
s.animationStatus.Store(StateClosing)
tx.ScheduleBlockUpdate(pos, s, 0)
}

// ScheduledTick ...
func (s ShulkerBox) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) {
switch s.animationStatus.Load() {
case StateClosed:
s.progress.Store(0)
case StateOpening:
s.progress.Add(1)
if s.progress.Load() >= 10 {
s.progress.Store(10)
s.animationStatus.Store(StateOpened)
}
tx.ScheduleBlockUpdate(pos, s, 0)
case StateOpened:
s.progress.Store(10)
case StateClosing:
s.progress.Add(-1)
if s.progress.Load() <= 0 {
tx.PlaySound(pos.Vec3Centre(), sound.ShulkerBoxClose{})
s.progress.Store(0)
s.animationStatus.Store(StateClosed)
}
tx.ScheduleBlockUpdate(pos, s, 0)
}
}

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

// MaxCount always returns 1.
func (s ShulkerBox) MaxCount() int {
return 1
}

// EncodeBlock ...
func (s ShulkerBox) EncodeBlock() (name string, properties map[string]any) {
return "minecraft:" + s.Type.String(), nil
}

// EncodeItem ...
func (s ShulkerBox) EncodeItem() (id string, meta int16) {
return "minecraft:" + s.Type.String(), 0
}

// DecodeNBT ...
func (s ShulkerBox) DecodeNBT(data map[string]any) any {
typ := s.Type
//noinspection GoAssignmentToReceiver
s = NewShulkerBox()
s.Type = typ
nbtconv.InvFromNBT(s.inventory, nbtconv.Slice(data, "Items"))
s.Facing = cube.Face(nbtconv.Uint8(data, "facing"))
s.CustomName = nbtconv.String(data, "CustomName")
return s
}

// EncodeNBT ..
func (s ShulkerBox) EncodeNBT() map[string]any {
if s.inventory == nil {
typ, facing, customName := s.Type, s.Facing, s.CustomName
//noinspection GoAssignmentToReceiver
s = NewShulkerBox()
s.Type, s.Facing, s.CustomName = typ, facing, customName
}

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 ...e
func allShulkerBoxes() (shulkerboxes []world.Block) {
for _, t := range ShulkerBoxTypes() {
shulkerboxes = append(shulkerboxes, ShulkerBox{Type: t})
}

return
}
Loading