diff --git a/go.mod b/go.mod
index 9afcbf709..381b9dfcd 100644
--- a/go.mod
+++ b/go.mod
@@ -6,11 +6,11 @@ require (
github.com/brentp/intintmap v0.0.0-20251106190759-56907b1f8479
github.com/cespare/xxhash/v2 v2.3.0
github.com/df-mc/goleveldb v1.1.9
- github.com/df-mc/worldupgrader v1.0.20
+ github.com/df-mc/worldupgrader v1.0.21
github.com/go-gl/mathgl v1.2.0
github.com/google/uuid v1.6.0
github.com/pelletier/go-toml v1.9.5
- github.com/sandertv/gophertunnel v1.56.2
+ github.com/sandertv/gophertunnel v1.57.0
github.com/segmentio/fasthash v1.0.3
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329
golang.org/x/mod v0.22.0
diff --git a/go.sum b/go.sum
index 17f22d869..4bc90dae7 100644
--- a/go.sum
+++ b/go.sum
@@ -12,8 +12,8 @@ github.com/df-mc/goleveldb v1.1.9 h1:ihdosZyy5jkQKrxucTQmN90jq/2lUwQnJZjIYIC/9YU
github.com/df-mc/goleveldb v1.1.9/go.mod h1:+NHCup03Sci5q84APIA21z3iPZCuk6m6ABtg4nANCSk=
github.com/df-mc/jsonc v1.0.5 h1:O7oh07kbS5AYY+l2Fji6l4h0iHcdjKbxCtK5VlZlLMU=
github.com/df-mc/jsonc v1.0.5/go.mod h1:+Q++JuCE9IKiP8v7sWImdf/RjQX0nfXyfX6PdfTTmc4=
-github.com/df-mc/worldupgrader v1.0.20 h1:wfJyG3bFeaM/HXy7TCiO4HKVw3Mf3N4gPFmgxMHsKnc=
-github.com/df-mc/worldupgrader v1.0.20/go.mod h1:tsSOLTRm9mpG7VHvYpAjjZrkRHWmSbKZAm9bOLNnlDk=
+github.com/df-mc/worldupgrader v1.0.21 h1:Qr4/QB8ek7En0vkTuRXYq4FrZM0HHSOXsJOL7Ko4Cjg=
+github.com/df-mc/worldupgrader v1.0.21/go.mod h1:tsSOLTRm9mpG7VHvYpAjjZrkRHWmSbKZAm9bOLNnlDk=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-gl/mathgl v1.2.0 h1:v2eOj/y1B2afDxF6URV1qCYmo1KW08lAMtTbOn3KXCY=
github.com/go-gl/mathgl v1.2.0/go.mod h1:pf9+b5J3LFP7iZ4XXaVzZrCle0Q/vNpB/vDe5+3ulRE=
@@ -41,8 +41,8 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/sandertv/go-raknet v1.15.1-0.20260112202637-beca0b10c217 h1:UZQq2253Q+7co/C9Et62RYPBggzz+L+2yqGlvQhSNM8=
github.com/sandertv/go-raknet v1.15.1-0.20260112202637-beca0b10c217/go.mod h1:/yysjwfCXm2+2OY8mBazLzcxJ3irnylKCyG3FLgUPVU=
-github.com/sandertv/gophertunnel v1.56.2 h1:eFc58AkMQo43ntR0Wmvz8GRFSdOgABKVDn52GMbIYag=
-github.com/sandertv/gophertunnel v1.56.2/go.mod h1:F8+ZPbzxJ0LqunXEaDjqeyUgHVB0rI5ZU+PHnptXGfI=
+github.com/sandertv/gophertunnel v1.57.0 h1:UkgVg1xLCsOSm79rP09WmodGSHgA8M7+l4quL01cIL8=
+github.com/sandertv/gophertunnel v1.57.0/go.mod h1:W4VnrX9AIPIVXNDMEIKMIRj1T80EdOgdqXpGbQpyAbE=
github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM=
github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY=
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 h1:9kj3STMvgqy3YA4VQXBrN7925ICMxD5wzMRcgA30588=
diff --git a/server/block/bamboo_block.go b/server/block/bamboo_block.go
new file mode 100644
index 000000000..c03a1a03f
--- /dev/null
+++ b/server/block/bamboo_block.go
@@ -0,0 +1,78 @@
+package block
+
+import (
+ "github.com/df-mc/dragonfly/server/block/cube"
+ "github.com/df-mc/dragonfly/server/item"
+ "github.com/df-mc/dragonfly/server/world"
+ "github.com/go-gl/mathgl/mgl64"
+ "time"
+)
+
+// BambooBlock is a rotatable flammable block made from bamboo.
+type BambooBlock struct {
+ solid
+ bass
+
+ // Axis is the axis which the bamboo block faces.
+ Axis cube.Axis
+ // Stripped specifies if the bamboo block is stripped.
+ Stripped bool
+}
+
+// FlammabilityInfo ...
+func (BambooBlock) FlammabilityInfo() FlammabilityInfo {
+ return newFlammabilityInfo(5, 5, true)
+}
+
+// BreakInfo ...
+func (b BambooBlock) BreakInfo() BreakInfo {
+ return newBreakInfo(2.0, alwaysHarvestable, axeEffective, oneOf(b))
+}
+
+// FuelInfo ...
+func (BambooBlock) FuelInfo() item.FuelInfo {
+ return newFuelInfo(time.Second * 15)
+}
+
+// UseOnBlock ...
+func (b BambooBlock) 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, b)
+ if !used {
+ return
+ }
+ b.Axis = face.Axis()
+
+ place(tx, pos, b, user, ctx)
+ return placed(ctx)
+}
+
+// Strip ...
+func (b BambooBlock) Strip() (world.Block, world.Sound, bool) {
+ return BambooBlock{Axis: b.Axis, Stripped: true}, nil, !b.Stripped
+}
+
+// EncodeItem ...
+func (b BambooBlock) EncodeItem() (name string, meta int16) {
+ if b.Stripped {
+ return "minecraft:stripped_bamboo_block", 0
+ }
+ return "minecraft:bamboo_block", 0
+}
+
+// EncodeBlock ...
+func (b BambooBlock) EncodeBlock() (name string, properties map[string]any) {
+ meta := map[string]any{"pillar_axis": b.Axis.String()}
+ if b.Stripped {
+ return "minecraft:stripped_bamboo_block", meta
+ }
+ return "minecraft:bamboo_block", meta
+}
+
+// allBambooBlocks ...
+func allBambooBlocks() (blocks []world.Block) {
+ for _, axis := range cube.Axes() {
+ blocks = append(blocks, BambooBlock{Axis: axis})
+ blocks = append(blocks, BambooBlock{Axis: axis, Stripped: true})
+ }
+ return
+}
diff --git a/server/block/bamboo_mosaic.go b/server/block/bamboo_mosaic.go
new file mode 100644
index 000000000..5bd89fda2
--- /dev/null
+++ b/server/block/bamboo_mosaic.go
@@ -0,0 +1,42 @@
+package block
+
+import (
+ "github.com/df-mc/dragonfly/server/item"
+ "time"
+)
+
+// BambooMosaic is a decorative bamboo plank variant.
+type BambooMosaic struct {
+ solid
+ bass
+}
+
+// FlammabilityInfo ...
+func (BambooMosaic) FlammabilityInfo() FlammabilityInfo {
+ return newFlammabilityInfo(5, 20, true)
+}
+
+// BreakInfo ...
+func (b BambooMosaic) BreakInfo() BreakInfo {
+ return newBreakInfo(2, alwaysHarvestable, axeEffective, oneOf(b)).withBlastResistance(15)
+}
+
+// RepairsWoodTools ...
+func (BambooMosaic) RepairsWoodTools() bool {
+ return true
+}
+
+// FuelInfo ...
+func (BambooMosaic) FuelInfo() item.FuelInfo {
+ return newFuelInfo(time.Second * 15)
+}
+
+// EncodeItem ...
+func (BambooMosaic) EncodeItem() (name string, meta int16) {
+ return "minecraft:bamboo_mosaic", 0
+}
+
+// EncodeBlock ...
+func (BambooMosaic) EncodeBlock() (string, map[string]any) {
+ return "minecraft:bamboo_mosaic", nil
+}
diff --git a/server/block/bed.go b/server/block/bed.go
index 503d51125..96a953039 100644
--- a/server/block/bed.go
+++ b/server/block/bed.go
@@ -135,8 +135,8 @@ func (b Bed) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, u item.User, _ *i
return false
}
- previousSpawn := w.PlayerSpawn(s.UUID())
- if previousSpawn != headPos {
+ previousSpawn, ok := w.PlayerSpawnPoint(s.UUID())
+ if !ok || previousSpawn.Pos != headPos || previousSpawn.Dim != world.Overworld {
w.SetPlayerSpawn(s.UUID(), headPos)
s.Messaget(chat.MessageRespawnPointSet)
}
diff --git a/server/block/beetroot_seeds.go b/server/block/beetroot_seeds.go
index b0f6516df..f6f13ddf8 100644
--- a/server/block/beetroot_seeds.go
+++ b/server/block/beetroot_seeds.go
@@ -21,16 +21,16 @@ func (BeetrootSeeds) SameCrop(c Crop) bool {
}
// BoneMeal ...
-func (b BeetrootSeeds) BoneMeal(pos cube.Pos, tx *world.Tx) bool {
+func (b BeetrootSeeds) BoneMeal(pos cube.Pos, tx *world.Tx) item.BoneMealResult {
if b.Growth == 7 {
- return false
+ return item.BoneMealResultNone
}
if rand.Float64() < 0.75 {
b.Growth++
tx.SetBlock(pos, b, nil)
- return true
+ return item.BoneMealResultSmall
}
- return false
+ return item.BoneMealResultNone
}
// UseOnBlock ...
diff --git a/server/block/carrot.go b/server/block/carrot.go
index c7d559e4a..834fa35aa 100644
--- a/server/block/carrot.go
+++ b/server/block/carrot.go
@@ -38,13 +38,13 @@ func (c Carrot) Consume(_ *world.Tx, co item.Consumer) item.Stack {
}
// BoneMeal ...
-func (c Carrot) BoneMeal(pos cube.Pos, tx *world.Tx) bool {
+func (c Carrot) BoneMeal(pos cube.Pos, tx *world.Tx) item.BoneMealResult {
if c.Growth == 7 {
- return false
+ return item.BoneMealResultNone
}
c.Growth = min(c.Growth+rand.IntN(4)+2, 7)
tx.SetBlock(pos, c, nil)
- return true
+ return item.BoneMealResultSmall
}
// UseOnBlock ...
diff --git a/server/block/cocoa_bean.go b/server/block/cocoa_bean.go
index bc2a4048b..bf19b0716 100644
--- a/server/block/cocoa_bean.go
+++ b/server/block/cocoa_bean.go
@@ -1,12 +1,13 @@
package block
import (
+ "math/rand/v2"
+
"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/go-gl/mathgl/mgl64"
- "math/rand/v2"
)
// CocoaBean is a crop block found in jungle biomes.
@@ -20,13 +21,13 @@ type CocoaBean struct {
}
// BoneMeal ...
-func (c CocoaBean) BoneMeal(pos cube.Pos, tx *world.Tx) bool {
+func (c CocoaBean) BoneMeal(pos cube.Pos, tx *world.Tx) item.BoneMealResult {
if c.Age == 2 {
- return false
+ return item.BoneMealResultNone
}
c.Age++
tx.SetBlock(pos, c, nil)
- return true
+ return item.BoneMealResultSmall
}
// HasLiquidDrops ...
diff --git a/server/block/cube/trace/bbox.go b/server/block/cube/trace/bbox.go
index 249bef5bc..aef3bbf27 100644
--- a/server/block/cube/trace/bbox.go
+++ b/server/block/cube/trace/bbox.go
@@ -99,6 +99,41 @@ func BBoxIntercept(bb cube.BBox, start, end mgl64.Vec3) (result BBoxResult, ok b
return BBoxResult{bb: bb, pos: *vec, face: f}, true
}
+// BBoxIntersects checks if the line segment from start to end intersects the BBox.
+// Unlike BBoxIntercept, it only reports whether an intersection exists and does not
+// calculate the closest hit position or face.
+func BBoxIntersects(bb cube.BBox, start, end mgl64.Vec3) bool {
+ min, max := bb.Min(), bb.Max()
+ dir := end.Sub(start)
+ tMin, tMax := 0.0, 1.0
+
+ for axis := range 3 {
+ if mgl64.FloatEqual(dir[axis], 0) {
+ if start[axis] < min[axis] || start[axis] > max[axis] {
+ return false
+ }
+ continue
+ }
+
+ inv := 1 / dir[axis]
+ t1 := (min[axis] - start[axis]) * inv
+ t2 := (max[axis] - start[axis]) * inv
+ if t1 > t2 {
+ t1, t2 = t2, t1
+ }
+ if t1 > tMin {
+ tMin = t1
+ }
+ if t2 < tMax {
+ tMax = t2
+ }
+ if tMin > tMax {
+ return false
+ }
+ }
+ return true
+}
+
// vec3OnLineWithX returns an mgl64.Vec3 on the line between mgl64.Vec3 a and b with an X value passed. If no such vec3
// could be found, the bool returned is false.
func vec3OnLineWithX(a, b mgl64.Vec3, x float64) *mgl64.Vec3 {
diff --git a/server/block/cube/trace/block.go b/server/block/cube/trace/block.go
index e9a0f2ce3..3139151eb 100644
--- a/server/block/cube/trace/block.go
+++ b/server/block/cube/trace/block.go
@@ -2,6 +2,7 @@ package trace
import (
"github.com/df-mc/dragonfly/server/block/cube"
+ "github.com/df-mc/dragonfly/server/block/model"
"github.com/df-mc/dragonfly/server/world"
"github.com/go-gl/mathgl/mgl64"
"math"
@@ -70,3 +71,23 @@ func BlockIntercept(pos cube.Pos, src world.BlockSource, b world.Block, start, e
return BlockResult{bb: hit.BBox(), pos: hit.Position(), face: hit.Face(), blockPos: pos}, true
}
+
+// BlockIntersects checks if the line segment from start to end intersects the block model of b at pos. Unlike
+// BlockIntercept, it only reports whether an intersection exists and does not calculate the closest hit position, face,
+// or bounding box.
+func BlockIntersects(pos cube.Pos, src world.BlockSource, b world.Block, start, end mgl64.Vec3) bool {
+ m := b.Model()
+ switch m.(type) {
+ case model.Empty:
+ return false
+ case model.Solid:
+ return BBoxIntersects(cube.Box(0, 0, 0, 1, 1, 1).Translate(pos.Vec3()), start, end)
+ }
+
+ for _, bb := range m.BBox(pos, src) {
+ if BBoxIntersects(bb.Translate(pos.Vec3()), start, end) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/server/block/double_flower.go b/server/block/double_flower.go
index c95748493..00dbc7d28 100644
--- a/server/block/double_flower.go
+++ b/server/block/double_flower.go
@@ -24,9 +24,9 @@ func (d DoubleFlower) FlammabilityInfo() FlammabilityInfo {
}
// BoneMeal ...
-func (d DoubleFlower) BoneMeal(pos cube.Pos, tx *world.Tx) bool {
+func (d DoubleFlower) BoneMeal(pos cube.Pos, tx *world.Tx) item.BoneMealResult {
dropItem(tx, item.NewStack(d, 1), pos.Vec3Centre())
- return true
+ return item.BoneMealResultSmall
}
// NeighbourUpdateTick ...
diff --git a/server/block/fern.go b/server/block/fern.go
index 4317feb93..88479a50d 100644
--- a/server/block/fern.go
+++ b/server/block/fern.go
@@ -25,14 +25,14 @@ func (g Fern) BreakInfo() BreakInfo {
}
// BoneMeal attempts to affect the block using a bone meal item.
-func (g Fern) BoneMeal(pos cube.Pos, tx *world.Tx) bool {
+func (g Fern) BoneMeal(pos cube.Pos, tx *world.Tx) item.BoneMealResult {
upper := DoubleTallGrass{Type: FernDoubleTallGrass(), UpperPart: true}
if replaceableWith(tx, pos.Side(cube.FaceUp), upper) {
tx.SetBlock(pos, DoubleTallGrass{Type: FernDoubleTallGrass()}, nil)
tx.SetBlock(pos.Side(cube.FaceUp), upper, nil)
- return true
+ return item.BoneMealResultSmall
}
- return false
+ return item.BoneMealResultNone
}
// CompostChance ...
diff --git a/server/block/flower.go b/server/block/flower.go
index a294444e6..3cccb320d 100644
--- a/server/block/flower.go
+++ b/server/block/flower.go
@@ -1,13 +1,14 @@
package block
import (
+ "math/rand/v2"
+ "time"
+
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/entity/effect"
"github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/world"
"github.com/go-gl/mathgl/mgl64"
- "math/rand/v2"
- "time"
)
// Flower is a non-solid plant that occur in a variety of shapes and colours. They are primarily used for decoration
@@ -32,7 +33,8 @@ func (f Flower) EntityInside(_ cube.Pos, _ *world.Tx, e world.Entity) {
}
// BoneMeal ...
-func (f Flower) BoneMeal(pos cube.Pos, tx *world.Tx) (success bool) {
+func (f Flower) BoneMeal(pos cube.Pos, tx *world.Tx) (result item.BoneMealResult) {
+ result = item.BoneMealResultNone
if f.Type == WitherRose() {
return
}
@@ -54,7 +56,7 @@ func (f Flower) BoneMeal(pos cube.Pos, tx *world.Tx) (success bool) {
}
}
tx.SetBlock(p, Flower{Type: flowerType}, nil)
- success = true
+ result = item.BoneMealResultArea
}
return
}
diff --git a/server/block/grass.go b/server/block/grass.go
index 10dd0e1aa..dade87ce7 100644
--- a/server/block/grass.go
+++ b/server/block/grass.go
@@ -1,9 +1,11 @@
package block
import (
+ "math/rand/v2"
+
"github.com/df-mc/dragonfly/server/block/cube"
+ "github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/world"
- "math/rand/v2"
)
// Grass blocks generate abundantly across the surface of the world.
@@ -78,18 +80,19 @@ func (g Grass) RandomTick(pos cube.Pos, tx *world.Tx, r *rand.Rand) {
}
// BoneMeal ...
-func (g Grass) BoneMeal(pos cube.Pos, tx *world.Tx) bool {
- for i := 0; i < 14; i++ {
+func (g Grass) BoneMeal(pos cube.Pos, tx *world.Tx) (result item.BoneMealResult) {
+ result = item.BoneMealResultNone
+ for range 14 {
c := pos.Add(cube.Pos{rand.IntN(6) - 3, 0, rand.IntN(6) - 3})
above := c.Side(cube.FaceUp)
_, air := tx.Block(above).(Air)
_, grass := tx.Block(c).(Grass)
if air && grass {
tx.SetBlock(above, plantSelection[rand.IntN(len(plantSelection))], nil)
+ result = item.BoneMealResultArea
}
}
-
- return false
+ return
}
// BreakInfo ...
diff --git a/server/block/hash.go b/server/block/hash.go
index 684d9f010..38d2128db 100644
--- a/server/block/hash.go
+++ b/server/block/hash.go
@@ -10,6 +10,8 @@ const (
hashAncientDebris
hashAndesite
hashAnvil
+ hashBambooBlock
+ hashBambooMosaic
hashBanner
hashBarrel
hashBarrier
@@ -166,6 +168,7 @@ const (
hashReinforcedDeepslate
hashResin
hashResinBricks
+ hashRespawnAnchor
hashSand
hashSandstone
hashSeaLantern
@@ -240,6 +243,14 @@ func (a Anvil) Hash() (uint64, uint64) {
return hashAnvil, uint64(a.Type.Uint8()) | uint64(a.Facing)<<2
}
+func (b BambooBlock) Hash() (uint64, uint64) {
+ return hashBambooBlock, uint64(b.Axis) | uint64(boolByte(b.Stripped))<<2
+}
+
+func (BambooMosaic) Hash() (uint64, uint64) {
+ return hashBambooMosaic, 0
+}
+
func (b Banner) Hash() (uint64, uint64) {
return hashBanner, uint64(b.Attach.Uint8())
}
@@ -864,6 +875,10 @@ func (r ResinBricks) Hash() (uint64, uint64) {
return hashResinBricks, uint64(boolByte(r.Chiseled))
}
+func (r RespawnAnchor) Hash() (uint64, uint64) {
+ return hashRespawnAnchor, uint64(r.Charges)
+}
+
func (s Sand) Hash() (uint64, uint64) {
return hashSand, uint64(boolByte(s.Red))
}
diff --git a/server/block/kelp.go b/server/block/kelp.go
index 27c03af13..7140bbfb5 100644
--- a/server/block/kelp.go
+++ b/server/block/kelp.go
@@ -1,11 +1,12 @@
package block
import (
+ "math/rand/v2"
+
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/world"
"github.com/go-gl/mathgl/mgl64"
- "math/rand/v2"
)
// Kelp is an underwater block which can grow on top of solids underwater.
@@ -24,7 +25,7 @@ func (k Kelp) SmeltInfo() item.SmeltInfo {
}
// BoneMeal ...
-func (k Kelp) BoneMeal(pos cube.Pos, tx *world.Tx) bool {
+func (k Kelp) BoneMeal(pos cube.Pos, tx *world.Tx) item.BoneMealResult {
for y := pos.Y(); y <= tx.Range()[1]; y++ {
currentPos := cube.Pos{pos.X(), y, pos.Z()}
block := tx.Block(currentPos)
@@ -36,11 +37,11 @@ func (k Kelp) BoneMeal(pos cube.Pos, tx *world.Tx) bool {
}
if water, ok := block.(Water); ok && water.Depth == 8 {
tx.SetBlock(currentPos, Kelp{Age: k.Age + 1}, nil)
- return true
+ return item.BoneMealResultSmall
}
break
}
- return false
+ return item.BoneMealResultNone
}
// BreakInfo ...
diff --git a/server/block/log.go b/server/block/log.go
index 9303dd9f9..61cd441fa 100644
--- a/server/block/log.go
+++ b/server/block/log.go
@@ -106,6 +106,9 @@ func (l Log) EncodeBlock() (name string, properties map[string]any) {
// allLogs returns a list of all possible log states.
func allLogs() (logs []world.Block) {
for _, w := range WoodTypes() {
+ if w == BambooWood() {
+ continue
+ }
for axis := cube.Axis(0); axis < 3; axis++ {
logs = append(logs, Log{Axis: axis, Stripped: true, Wood: w})
logs = append(logs, Log{Axis: axis, Stripped: false, Wood: w})
diff --git a/server/block/melon_seeds.go b/server/block/melon_seeds.go
index 68bbaa2d7..2fcfeece4 100644
--- a/server/block/melon_seeds.go
+++ b/server/block/melon_seeds.go
@@ -1,11 +1,12 @@
package block
import (
+ "math/rand/v2"
+
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/world"
"github.com/go-gl/mathgl/mgl64"
- "math/rand/v2"
)
// MelonSeeds grow melon blocks.
@@ -62,13 +63,13 @@ func (m MelonSeeds) RandomTick(pos cube.Pos, tx *world.Tx, r *rand.Rand) {
}
// BoneMeal ...
-func (m MelonSeeds) BoneMeal(pos cube.Pos, tx *world.Tx) bool {
+func (m MelonSeeds) BoneMeal(pos cube.Pos, tx *world.Tx) item.BoneMealResult {
if m.Growth == 7 {
- return false
+ return item.BoneMealResultNone
}
m.Growth = min(m.Growth+rand.IntN(4)+2, 7)
tx.SetBlock(pos, m, nil)
- return true
+ return item.BoneMealResultSmall
}
// UseOnBlock ...
diff --git a/server/block/pink_petals.go b/server/block/pink_petals.go
index 1df913167..3093836c0 100644
--- a/server/block/pink_petals.go
+++ b/server/block/pink_petals.go
@@ -21,14 +21,14 @@ type PinkPetals struct {
}
// BoneMeal ...
-func (p PinkPetals) BoneMeal(pos cube.Pos, tx *world.Tx) bool {
+func (p PinkPetals) BoneMeal(pos cube.Pos, tx *world.Tx) item.BoneMealResult {
if p.AdditionalCount < 3 {
p.AdditionalCount++
tx.SetBlock(pos, p, nil)
- return true
+ return item.BoneMealResultSmall
}
dropItem(tx, item.NewStack(p, 1), pos.Vec3Centre())
- return true
+ return item.BoneMealResultSmall
}
// UseOnBlock ...
diff --git a/server/block/potato.go b/server/block/potato.go
index ecb9e3b5f..266c36a1d 100644
--- a/server/block/potato.go
+++ b/server/block/potato.go
@@ -43,13 +43,13 @@ func (p Potato) Consume(_ *world.Tx, c item.Consumer) item.Stack {
}
// BoneMeal ...
-func (p Potato) BoneMeal(pos cube.Pos, tx *world.Tx) bool {
+func (p Potato) BoneMeal(pos cube.Pos, tx *world.Tx) item.BoneMealResult {
if p.Growth == 7 {
- return false
+ return item.BoneMealResultNone
}
p.Growth = min(p.Growth+rand.IntN(4)+2, 7)
tx.SetBlock(pos, p, nil)
- return true
+ return item.BoneMealResultSmall
}
// UseOnBlock ...
diff --git a/server/block/pumpkin_seeds.go b/server/block/pumpkin_seeds.go
index 5a62c6507..e14be3a13 100644
--- a/server/block/pumpkin_seeds.go
+++ b/server/block/pumpkin_seeds.go
@@ -1,11 +1,12 @@
package block
import (
+ "math/rand/v2"
+
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/world"
"github.com/go-gl/mathgl/mgl64"
- "math/rand/v2"
)
// PumpkinSeeds grow pumpkin blocks.
@@ -62,13 +63,13 @@ func (p PumpkinSeeds) RandomTick(pos cube.Pos, tx *world.Tx, r *rand.Rand) {
}
// BoneMeal ...
-func (p PumpkinSeeds) BoneMeal(pos cube.Pos, tx *world.Tx) bool {
+func (p PumpkinSeeds) BoneMeal(pos cube.Pos, tx *world.Tx) item.BoneMealResult {
if p.Growth == 7 {
- return false
+ return item.BoneMealResultNone
}
p.Growth = min(p.Growth+rand.IntN(4)+2, 7)
tx.SetBlock(pos, p, nil)
- return true
+ return item.BoneMealResultSmall
}
// UseOnBlock ...
diff --git a/server/block/register.go b/server/block/register.go
index 116c454e6..fff70973e 100644
--- a/server/block/register.go
+++ b/server/block/register.go
@@ -14,6 +14,7 @@ func init() {
world.RegisterBlock(AncientDebris{})
world.RegisterBlock(Andesite{Polished: true})
world.RegisterBlock(Andesite{})
+ world.RegisterBlock(BambooMosaic{})
world.RegisterBlock(Barrier{})
world.RegisterBlock(Beacon{})
world.RegisterBlock(Bedrock{InfiniteBurning: true})
@@ -138,6 +139,7 @@ func init() {
}
registerAll(allAnvils())
+ registerAll(allBambooBlocks())
registerAll(allBanners())
registerAll(allBarrels())
registerAll(allBasalt())
@@ -203,6 +205,7 @@ func init() {
registerAll(allQuartz())
registerAll(allRedstoneTorches())
registerAll(allRedstoneWires())
+ registerAll(allRespawnAnchors())
registerAll(allSandstones())
registerAll(allSeaPickles())
registerAll(allSigns())
@@ -243,6 +246,9 @@ func init() {
world.RegisterItem(AncientDebris{})
world.RegisterItem(Andesite{Polished: true})
world.RegisterItem(Andesite{})
+ world.RegisterItem(BambooBlock{})
+ world.RegisterItem(BambooBlock{Stripped: true})
+ world.RegisterItem(BambooMosaic{})
world.RegisterItem(Barrel{})
world.RegisterItem(Barrier{})
world.RegisterItem(Basalt{Polished: true})
@@ -369,6 +375,7 @@ func init() {
world.RegisterItem(RedstoneTorch{})
world.RegisterItem(RedstoneWire{})
world.RegisterItem(ReinforcedDeepslate{})
+ world.RegisterItem(RespawnAnchor{})
world.RegisterItem(ResinBricks{Chiseled: true})
world.RegisterItem(ResinBricks{})
world.RegisterItem(Resin{})
@@ -440,20 +447,21 @@ func init() {
world.RegisterItem(Wool{Colour: c})
}
for _, w := range WoodTypes() {
- if w != WarpedWood() && w != CrimsonWood() {
- t, _ := w.Leaves()
+ if t, ok := w.Leaves(); ok {
world.RegisterItem(Leaves{Type: t, Persistent: true})
}
- world.RegisterItem(Log{Wood: w, Stripped: true})
- world.RegisterItem(Log{Wood: w})
+ if w != BambooWood() {
+ world.RegisterItem(Log{Wood: w, Stripped: true})
+ world.RegisterItem(Log{Wood: w})
+ world.RegisterItem(Wood{Wood: w, Stripped: true})
+ world.RegisterItem(Wood{Wood: w})
+ }
world.RegisterItem(Planks{Wood: w})
world.RegisterItem(Sign{Wood: w})
world.RegisterItem(WoodDoor{Wood: w})
world.RegisterItem(WoodFenceGate{Wood: w})
world.RegisterItem(WoodFence{Wood: w})
world.RegisterItem(WoodTrapdoor{Wood: w})
- world.RegisterItem(Wood{Wood: w, Stripped: true})
- world.RegisterItem(Wood{Wood: w})
}
world.RegisterItem(Leaves{Type: AzaleaLeaves(), Persistent: true})
world.RegisterItem(Leaves{Type: FloweringAzaleaLeaves(), Persistent: true})
diff --git a/server/block/respawn_anchor.go b/server/block/respawn_anchor.go
new file mode 100644
index 000000000..a44fd25f1
--- /dev/null
+++ b/server/block/respawn_anchor.go
@@ -0,0 +1,156 @@
+package block
+
+import (
+ "github.com/df-mc/dragonfly/server/block/cube"
+ "github.com/df-mc/dragonfly/server/item"
+ "github.com/df-mc/dragonfly/server/player/chat"
+ "github.com/df-mc/dragonfly/server/world"
+ "github.com/df-mc/dragonfly/server/world/sound"
+ "github.com/google/uuid"
+)
+
+// RespawnAnchor is a block that allows players to set their respawn point in the Nether after charging it with
+// glowstone.
+type RespawnAnchor struct {
+ solid
+ bassDrum
+
+ // Charges is the amount of glowstone charges stored in the respawn anchor, in the range 0-4.
+ Charges int
+}
+
+// respawnAnchorSpawnOffsets holds the vanilla respawn search priority around an
+// anchor: per column (cardinals before diagonals) at the anchor's level and the
+// level above it, then the columns one level below, then on top of the anchor.
+var respawnAnchorSpawnOffsets = []cube.Pos{
+ {0, 0, -1}, {0, 1, -1},
+ {-1, 0, 0}, {-1, 1, 0},
+ {0, 0, 1}, {0, 1, 1},
+ {1, 0, 0}, {1, 1, 0},
+ {-1, 0, -1}, {-1, 1, -1},
+ {1, 0, -1}, {1, 1, -1},
+ {-1, 0, 1}, {-1, 1, 1},
+ {1, 0, 1}, {1, 1, 1},
+ {0, -1, -1},
+ {-1, -1, 0},
+ {0, -1, 1},
+ {1, -1, 0},
+ {-1, -1, -1},
+ {1, -1, -1},
+ {-1, -1, 1},
+ {1, -1, 1},
+ {0, 1, 0},
+}
+
+// Activate ...
+func (r RespawnAnchor) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, u item.User, ctx *item.UseContext) bool {
+ held, _ := u.HeldItems()
+ if _, ok := held.Item().(Glowstone); ok && r.Charges < 4 {
+ r.Charges++
+ tx.SetBlock(pos, r, nil)
+ tx.PlaySound(pos.Vec3Centre(), sound.RespawnAnchorCharge{})
+ ctx.SubtractFromCount(1)
+ return true
+ }
+ if r.Charges == 0 {
+ return false
+ }
+
+ user, ok := u.(interface {
+ UUID() uuid.UUID
+ Messaget(t chat.Translation, a ...any)
+ })
+ if !ok {
+ return false
+ }
+
+ if tx.World().Dimension() != world.Nether {
+ tx.SetBlock(pos, nil, nil)
+ ExplosionConfig{Size: 5, SpawnFire: true}.Explode(tx, pos.Vec3Centre())
+ return true
+ }
+
+ if spawn, ok := tx.World().PlayerSpawnPoint(user.UUID()); ok && spawn.Pos == pos && spawn.Dim == world.Nether {
+ return true
+ }
+ tx.World().SetPlayerSpawn(user.UUID(), pos)
+ tx.PlaySound(pos.Vec3Centre(), sound.RespawnAnchorSetSpawn{})
+ user.Messaget(chat.MessageRespawnAnchorRespawnPointSet)
+ return true
+}
+
+// CanRespawnOn ...
+func (r RespawnAnchor) CanRespawnOn() bool {
+ return r.Charges > 0
+}
+
+// SafeSpawn ...
+func (r RespawnAnchor) SafeSpawn(pos cube.Pos, tx *world.Tx) (cube.Pos, bool) {
+ if !r.CanRespawnOn() || tx.World().Dimension() != world.Nether {
+ return cube.Pos{}, false
+ }
+ for _, offset := range respawnAnchorSpawnOffsets {
+ spawn := pos.Add(offset)
+ if respawnAnchorSpawnClear(spawn, tx) {
+ r.Charges--
+ tx.SetBlock(pos, r, nil)
+ tx.PlaySound(pos.Vec3Centre(), sound.RespawnAnchorDeplete{})
+ return spawn, true
+ }
+ }
+ return cube.Pos{}, false
+}
+
+// BreakInfo ...
+func (r RespawnAnchor) BreakInfo() BreakInfo {
+ return newBreakInfo(50, func(t item.Tool) bool {
+ return t.ToolType() == item.TypePickaxe && t.HarvestLevel() >= item.ToolTierDiamond.HarvestLevel
+ }, pickaxeEffective, oneOf(RespawnAnchor{})).withBlastResistance(1200)
+}
+
+// LightEmissionLevel ...
+func (r RespawnAnchor) LightEmissionLevel() uint8 {
+ if r.Charges < 1 || r.Charges > 4 {
+ return 0
+ }
+ return [...]uint8{0, 3, 7, 11, 15}[r.Charges]
+}
+
+// EncodeItem ...
+func (r RespawnAnchor) EncodeItem() (name string, meta int16) {
+ return "minecraft:respawn_anchor", 0
+}
+
+// EncodeBlock ...
+func (r RespawnAnchor) EncodeBlock() (name string, properties map[string]any) {
+ return "minecraft:respawn_anchor", map[string]any{"respawn_anchor_charge": int32(r.Charges)}
+}
+
+// allRespawnAnchors returns all possible respawn anchor block states.
+func allRespawnAnchors() (anchors []world.Block) {
+ for charges := 0; charges <= 4; charges++ {
+ anchors = append(anchors, RespawnAnchor{Charges: charges})
+ }
+ return
+}
+
+func respawnAnchorSpawnClear(pos cube.Pos, tx *world.Tx) bool {
+ if pos.OutOfBounds(tx.Range()) || pos.Side(cube.FaceUp).OutOfBounds(tx.Range()) {
+ return false
+ }
+ below := pos.Side(cube.FaceDown)
+ if below.OutOfBounds(tx.Range()) || !tx.Block(below).Model().FaceSolid(below, cube.FaceUp, tx) {
+ return false
+ }
+
+ occupied := cube.Box(0, 0, 0, 1, 2, 1).Translate(pos.Vec3())
+ for y := 0; y < 2; y++ {
+ blockPos := pos.Add(cube.Pos{0, y})
+ for _, box := range tx.Block(blockPos).Model().BBox(blockPos, tx) {
+ if box.Translate(blockPos.Vec3()).IntersectsWith(occupied) {
+ return false
+ }
+ }
+ }
+ return true
+}
diff --git a/server/block/sea_pickle.go b/server/block/sea_pickle.go
index 3b6acbc87..96df4d893 100644
--- a/server/block/sea_pickle.go
+++ b/server/block/sea_pickle.go
@@ -1,12 +1,13 @@
package block
import (
+ "math"
+ "math/rand/v2"
+
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/world"
"github.com/go-gl/mathgl/mgl64"
- "math"
- "math/rand/v2"
)
// SeaPickle is a small stationary underwater block that emits light, and is typically found in colonies of up to
@@ -41,12 +42,12 @@ func (SeaPickle) canSurvive(pos cube.Pos, tx *world.Tx) bool {
}
// BoneMeal ...
-func (s SeaPickle) BoneMeal(pos cube.Pos, tx *world.Tx) bool {
+func (s SeaPickle) BoneMeal(pos cube.Pos, tx *world.Tx) item.BoneMealResult {
if s.Dead {
- return false
+ return item.BoneMealResultNone
}
if coral, ok := tx.Block(pos.Side(cube.FaceDown)).(CoralBlock); !ok || coral.Dead {
- return false
+ return item.BoneMealResultNone
}
if s.AdditionalCount != 3 {
@@ -74,7 +75,7 @@ func (s SeaPickle) BoneMeal(pos cube.Pos, tx *world.Tx) bool {
}
}
- return true
+ return item.BoneMealResultSmall
}
// UseOnBlock ...
diff --git a/server/block/short_grass.go b/server/block/short_grass.go
index 6f74cb832..edf6f3f70 100644
--- a/server/block/short_grass.go
+++ b/server/block/short_grass.go
@@ -27,14 +27,14 @@ func (g ShortGrass) BreakInfo() BreakInfo {
}
// BoneMeal attempts to affect the block using a bone meal item.
-func (g ShortGrass) BoneMeal(pos cube.Pos, tx *world.Tx) bool {
+func (g ShortGrass) BoneMeal(pos cube.Pos, tx *world.Tx) item.BoneMealResult {
upper := DoubleTallGrass{Type: NormalDoubleTallGrass(), UpperPart: true}
if replaceableWith(tx, pos.Side(cube.FaceUp), upper) {
tx.SetBlock(pos, DoubleTallGrass{Type: NormalDoubleTallGrass()}, nil)
tx.SetBlock(pos.Side(cube.FaceUp), upper, nil)
- return true
+ return item.BoneMealResultSmall
}
- return false
+ return item.BoneMealResultNone
}
// CompostChance ...
diff --git a/server/block/slab.go b/server/block/slab.go
index 13cb1d90f..90c578f95 100644
--- a/server/block/slab.go
+++ b/server/block/slab.go
@@ -63,6 +63,9 @@ func (s Slab) Instrument() sound.Instrument {
if _, ok := s.Block.(Planks); ok {
return sound.Bass()
}
+ if _, ok := s.Block.(BambooMosaic); ok {
+ return sound.Bass()
+ }
return sound.BassDrum()
}
diff --git a/server/block/slab_type.go b/server/block/slab_type.go
index f5e4185bf..476bd6446 100644
--- a/server/block/slab_type.go
+++ b/server/block/slab_type.go
@@ -25,6 +25,8 @@ func encodeSlabBlock(block world.Block, double bool) (id string, suffix string)
} else if block.Type == PolishedBlackstone() {
return "polished_blackstone", suffix
}
+ case BambooMosaic:
+ return "bamboo_mosaic", suffix
case Bricks:
return "brick", suffix
case Cobblestone:
@@ -153,6 +155,7 @@ func SlabBlocks() []world.Block {
b := []world.Block{
Andesite{Polished: true},
Andesite{},
+ BambooMosaic{},
Blackstone{Type: PolishedBlackstone()},
Blackstone{},
Bricks{},
diff --git a/server/block/stairs.go b/server/block/stairs.go
index 665199100..ff0e4827d 100644
--- a/server/block/stairs.go
+++ b/server/block/stairs.go
@@ -55,6 +55,9 @@ func (s Stairs) Instrument() sound.Instrument {
if _, ok := s.Block.(Planks); ok {
return sound.Bass()
}
+ if _, ok := s.Block.(BambooMosaic); ok {
+ return sound.Bass()
+ }
return sound.BassDrum()
}
diff --git a/server/block/stairs_type.go b/server/block/stairs_type.go
index e3b2d9f94..a2b1e8dcc 100644
--- a/server/block/stairs_type.go
+++ b/server/block/stairs_type.go
@@ -18,6 +18,8 @@ func encodeStairsBlock(block world.Block) string {
} else if block.Type == PolishedBlackstone() {
return "polished_blackstone"
}
+ case BambooMosaic:
+ return "bamboo_mosaic"
case Bricks:
return "brick"
case Cobblestone:
@@ -136,6 +138,7 @@ func StairsBlocks() []world.Block {
b := []world.Block{
Andesite{Polished: true},
Andesite{},
+ BambooMosaic{},
Blackstone{Type: PolishedBlackstone()},
Blackstone{},
Bricks{},
diff --git a/server/block/sugar_cane.go b/server/block/sugar_cane.go
index 1c6e21c2a..0f7b467f7 100644
--- a/server/block/sugar_cane.go
+++ b/server/block/sugar_cane.go
@@ -1,11 +1,12 @@
package block
import (
+ "math/rand/v2"
+
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/world"
"github.com/go-gl/mathgl/mgl64"
- "math/rand/v2"
)
// SugarCane is a plant block that generates naturally near water.
@@ -63,7 +64,7 @@ func (c SugarCane) RandomTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) {
}
// BoneMeal ...
-func (c SugarCane) BoneMeal(pos cube.Pos, tx *world.Tx) bool {
+func (c SugarCane) BoneMeal(pos cube.Pos, tx *world.Tx) item.BoneMealResult {
for _, ok := tx.Block(pos.Side(cube.FaceDown)).(SugarCane); ok; _, ok = tx.Block(pos.Side(cube.FaceDown)).(SugarCane) {
pos = pos.Side(cube.FaceDown)
}
@@ -73,9 +74,9 @@ func (c SugarCane) BoneMeal(pos cube.Pos, tx *world.Tx) bool {
tx.SetBlock(pos.Add(cube.Pos{0, y}), SugarCane{}, nil)
}
}
- return true
+ return item.BoneMealResultSmall
}
- return false
+ return item.BoneMealResultNone
}
// canGrowHere implements logic to check if sugar cane can live/grow here.
diff --git a/server/block/wheat_seeds.go b/server/block/wheat_seeds.go
index 4103a306b..549f4ceb7 100644
--- a/server/block/wheat_seeds.go
+++ b/server/block/wheat_seeds.go
@@ -21,13 +21,13 @@ func (WheatSeeds) SameCrop(c Crop) bool {
}
// BoneMeal ...
-func (s WheatSeeds) BoneMeal(pos cube.Pos, tx *world.Tx) bool {
+func (s WheatSeeds) BoneMeal(pos cube.Pos, tx *world.Tx) item.BoneMealResult {
if s.Growth == 7 {
- return false
+ return item.BoneMealResultNone
}
s.Growth = min(s.Growth+rand.IntN(4)+2, 7)
tx.SetBlock(pos, s, nil)
- return true
+ return item.BoneMealResultSmall
}
// UseOnBlock ...
diff --git a/server/block/wood.go b/server/block/wood.go
index 787c8573c..c58a07091 100644
--- a/server/block/wood.go
+++ b/server/block/wood.go
@@ -98,9 +98,13 @@ func (w Wood) EncodeBlock() (name string, properties map[string]any) {
}
}
-// allWood returns a list of all possible wood states.
+// allWood returns all possible Wood block states, excluding bamboo blocks,
+// which are registered separately through allBambooBlocks.
func allWood() (wood []world.Block) {
for _, w := range WoodTypes() {
+ if w == BambooWood() {
+ continue
+ }
for axis := cube.Axis(0); axis < 3; axis++ {
wood = append(wood, Wood{Axis: axis, Stripped: true, Wood: w})
wood = append(wood, Wood{Axis: axis, Stripped: false, Wood: w})
diff --git a/server/block/wood_type.go b/server/block/wood_type.go
index ded6df199..cafd9b064 100644
--- a/server/block/wood_type.go
+++ b/server/block/wood_type.go
@@ -61,9 +61,14 @@ func PaleOakWood() WoodType {
return WoodType{10}
}
+// BambooWood returns bamboo wood material.
+func BambooWood() WoodType {
+ return WoodType{11}
+}
+
// WoodTypes returns a list of all wood types
func WoodTypes() []WoodType {
- return []WoodType{OakWood(), SpruceWood(), BirchWood(), JungleWood(), AcaciaWood(), DarkOakWood(), CrimsonWood(), WarpedWood(), MangroveWood(), CherryWood(), PaleOakWood()}
+ return []WoodType{OakWood(), SpruceWood(), BirchWood(), JungleWood(), AcaciaWood(), DarkOakWood(), CrimsonWood(), WarpedWood(), MangroveWood(), CherryWood(), PaleOakWood(), BambooWood()}
}
type wood uint8
@@ -98,6 +103,8 @@ func (w wood) Name() string {
return "Cherry Wood"
case 10:
return "Pale Oak Wood"
+ case 11:
+ return "Bamboo Wood"
}
panic("unknown wood type")
}
@@ -127,6 +134,8 @@ func (w wood) String() string {
return "cherry"
case 10:
return "pale_oak"
+ case 11:
+ return "bamboo"
}
panic("unknown wood type")
}
diff --git a/server/entity/damageable.go b/server/entity/damageable.go
new file mode 100644
index 000000000..aae01b2e4
--- /dev/null
+++ b/server/entity/damageable.go
@@ -0,0 +1,38 @@
+package entity
+
+import "github.com/df-mc/dragonfly/server/world"
+
+// behaviourDamageable represents a Behaviour that may be hurt directly without
+// implementing Living.
+type behaviourDamageable interface {
+ Hurt(e *Ent, damage float64, src world.DamageSource) (n float64, vulnerable bool)
+}
+
+// HurtEntity hurts an entity if it is either Living or has a Behaviour that may
+// be hurt directly. It returns the damage dealt, whether the entity was
+// vulnerable to the damage, and whether the entity could be damaged.
+func HurtEntity(e world.Entity, damage float64, src world.DamageSource) (n float64, vulnerable, ok bool) {
+ if l, ok := e.(Living); ok {
+ n, vulnerable = l.Hurt(damage, src)
+ return n, vulnerable, true
+ }
+ if ent, ok := e.(*Ent); ok {
+ if d, ok := ent.Behaviour().(behaviourDamageable); ok {
+ n, vulnerable = d.Hurt(ent, damage, src)
+ return n, vulnerable, true
+ }
+ }
+ return 0, false, false
+}
+
+// damageableEntity checks if an entity may be damaged.
+func damageableEntity(e world.Entity) bool {
+ if _, ok := e.(Living); ok {
+ return true
+ }
+ if ent, ok := e.(*Ent); ok {
+ _, ok = ent.Behaviour().(behaviourDamageable)
+ return ok
+ }
+ return false
+}
diff --git a/server/entity/end_crystal.go b/server/entity/end_crystal.go
new file mode 100644
index 000000000..c4539e20c
--- /dev/null
+++ b/server/entity/end_crystal.go
@@ -0,0 +1,52 @@
+package entity
+
+import (
+ "github.com/df-mc/dragonfly/server/block/cube"
+ "github.com/df-mc/dragonfly/server/internal/nbtconv"
+ "github.com/df-mc/dragonfly/server/world"
+)
+
+// NewEndCrystal creates a new End crystal entity.
+func NewEndCrystal(opts world.EntitySpawnOpts) *world.EntityHandle {
+ return opts.New(EndCrystalType, endCrystalBehaviour{})
+}
+
+// EndCrystalType is a world.EntityType implementation for End crystals.
+var EndCrystalType endCrystalType
+
+type endCrystalType struct{}
+
+func (endCrystalType) Open(tx *world.Tx, handle *world.EntityHandle, data *world.EntityData) world.Entity {
+ return Open(tx, handle, data)
+}
+
+func (endCrystalType) EncodeEntity() string {
+ return "minecraft:ender_crystal"
+}
+
+func (endCrystalType) BBox(world.Entity) cube.BBox {
+ return cube.Box(-1, 0, -1, 1, 2, 1)
+}
+
+func (endCrystalType) DecodeNBT(m map[string]any, data *world.EntityData) {
+ b := endCrystalBehaviour{showBase: nbtconv.Bool(m, "ShowBottom")}
+ x, hasX := m["BlockTargetX"].(int32)
+ y, hasY := m["BlockTargetY"].(int32)
+ z, hasZ := m["BlockTargetZ"].(int32)
+ if hasX && hasY && hasZ {
+ b.beamTarget = cube.Pos{int(x), int(y), int(z)}
+ b.hasBeamTarget = true
+ }
+ b.Apply(data)
+}
+
+func (endCrystalType) EncodeNBT(data *world.EntityData) map[string]any {
+ b := data.Data.(endCrystalBehaviour)
+ m := map[string]any{"ShowBottom": boolByte(b.showBase)}
+ if b.hasBeamTarget {
+ m["BlockTargetX"] = int32(b.beamTarget[0])
+ m["BlockTargetY"] = int32(b.beamTarget[1])
+ m["BlockTargetZ"] = int32(b.beamTarget[2])
+ }
+ return m
+}
diff --git a/server/entity/end_crystal_behaviour.go b/server/entity/end_crystal_behaviour.go
new file mode 100644
index 000000000..ce070601f
--- /dev/null
+++ b/server/entity/end_crystal_behaviour.go
@@ -0,0 +1,78 @@
+package entity
+
+import (
+ "github.com/df-mc/dragonfly/server/block"
+ "github.com/df-mc/dragonfly/server/block/cube"
+ "github.com/df-mc/dragonfly/server/world"
+ "github.com/go-gl/mathgl/mgl64"
+)
+
+type endCrystalBehaviour struct {
+ showBase bool
+ beamTarget cube.Pos
+ hasBeamTarget bool
+}
+
+// endCrystalExploder may be implemented by a damage source to control whether
+// it makes End crystals explode.
+type endCrystalExploder interface {
+ ExplodesEndCrystal() bool
+}
+
+func (b endCrystalBehaviour) Apply(data *world.EntityData) {
+ data.Data = b
+}
+
+// Tick continuously generates fire at the End crystal's position while in the
+// End, if the block at that position is air.
+func (endCrystalBehaviour) Tick(e *Ent, tx *world.Tx) *Movement {
+ if tx.World().Dimension() == world.End {
+ block.Fire{}.Start(tx, cube.PosFromVec3(e.Position()))
+ }
+ return nil
+}
+
+// Explode makes the End crystal explode itself when hit by another explosion,
+// causing a chain reaction.
+func (endCrystalBehaviour) Explode(e *Ent, _ mgl64.Vec3, _ float64, _ block.ExplosionConfig) {
+ explodeEndCrystal(e)
+}
+
+// Hurt makes the End crystal explode when damaged by any source, even by
+// damage that deals no health, unless the source is void damage or opts out of
+// exploding End crystals.
+func (endCrystalBehaviour) Hurt(e *Ent, damage float64, src world.DamageSource) (float64, bool) {
+ damage = max(damage, 0)
+ if _, ok := src.(VoidDamageSource); ok {
+ _ = e.Close()
+ return damage, true
+ }
+ if exploder, ok := src.(endCrystalExploder); ok && !exploder.ExplodesEndCrystal() {
+ return damage, false
+ }
+ explodeEndCrystal(e)
+ return damage, true
+}
+
+func (endCrystalBehaviour) Immobile() bool {
+ return true
+}
+
+func (b endCrystalBehaviour) ShowBase() bool {
+ return b.showBase
+}
+
+func (b endCrystalBehaviour) BeamTarget() (cube.Pos, bool) {
+ return b.beamTarget, b.hasBeamTarget
+}
+
+// explodeEndCrystal closes the End crystal and creates a radius-6,
+// non-incendiary explosion at its base, if the crystal was not closed yet.
+func explodeEndCrystal(e *Ent) {
+ if _, ok := e.H().Entity(e.tx); !ok {
+ return
+ }
+ pos := e.Position()
+ _ = e.Close()
+ block.ExplosionConfig{Size: 6}.Explode(e.tx, pos)
+}
diff --git a/server/entity/projectile.go b/server/entity/projectile.go
index 8c8a24707..91f93f478 100644
--- a/server/entity/projectile.go
+++ b/server/entity/projectile.go
@@ -176,11 +176,11 @@ func (lt *ProjectileBehaviour) Tick(e *Ent, tx *world.Tx) *Movement {
switch r := result.(type) {
case trace.EntityResult:
- if l, ok := r.Entity().(Living); ok {
- if lt.conf.Damage >= 0 {
- lt.hitEntity(l, e, vel)
- }
- lt.collidedEntities = append(lt.collidedEntities, l.H())
+ if lt.conf.Damage >= 0 {
+ lt.hitEntity(r.Entity(), e, vel)
+ }
+ if damageableEntity(r.Entity()) {
+ lt.collidedEntities = append(lt.collidedEntities, r.Entity().H())
}
case trace.BlockResult:
bpos := r.BlockPosition()
@@ -268,10 +268,9 @@ func (lt *ProjectileBehaviour) hitBlockSurviving(e *Ent, r trace.BlockResult, m
}
}
-// hitEntity is called when a projectile hits a Living. It deals damage to the
-// entity and knocks it back. Additionally, it applies any potion effects and
-// fire if applicable.
-func (lt *ProjectileBehaviour) hitEntity(l Living, e *Ent, vel mgl64.Vec3) {
+// hitEntity is called when a projectile hits an entity. It deals damage to the
+// entity if possible, and applies Living-specific effects such as knockback.
+func (lt *ProjectileBehaviour) hitEntity(victim world.Entity, e *Ent, vel mgl64.Vec3) {
owner, _ := lt.conf.Owner.Entity(e.tx)
src := ProjectileDamageSource{Projectile: e, Owner: owner}
dmg := math.Ceil(lt.conf.Damage * vel.Len())
@@ -279,7 +278,11 @@ func (lt *ProjectileBehaviour) hitEntity(l Living, e *Ent, vel mgl64.Vec3) {
dmg += rand.Float64() * dmg / 2
}
// TODO: Piercing arrows should bypass shield blocking when shields are implemented.
- if _, vulnerable := l.Hurt(dmg, src); vulnerable {
+ if _, vulnerable, ok := HurtEntity(victim, dmg, src); ok && vulnerable {
+ l, ok := victim.(Living)
+ if !ok {
+ return
+ }
l.KnockBack(l.Position().Sub(vel), 0.45+lt.conf.KnockBackForceAddend, 0.3608+lt.conf.KnockBackHeightAddend)
for _, eff := range lt.conf.Potion.Effects() {
@@ -337,7 +340,7 @@ func (lt *ProjectileBehaviour) tickMovement(e *Ent, tx *world.Tx) (*Movement, tr
}
// ignores returns a function to ignore entities in trace.Perform that are
-// either a spectator, not living, the entity itself, its owner in the first
+// either a spectator, not damageable, the entity itself, its owner in the first
// 5 ticks, or an entity it already collided with.
func (lt *ProjectileBehaviour) ignores(e *Ent) trace.EntityFilter {
return func(seq iter.Seq[world.Entity]) iter.Seq[world.Entity] {
@@ -346,10 +349,10 @@ func (lt *ProjectileBehaviour) ignores(e *Ent) trace.EntityFilter {
g, ok := other.(interface{ GameMode() world.GameMode })
spectator := ok && !g.GameMode().HasCollision()
itself := e.H() == other.H()
- _, living := other.(Living)
+ damageable := damageableEntity(other)
owner := e.data.Age < time.Second/4 && lt.conf.Owner == other.H()
collidedEntity := slices.Contains(lt.collidedEntities, other.H())
- if spectator || itself || !living || owner || collidedEntity {
+ if spectator || itself || !damageable || owner || collidedEntity {
continue
}
if !yield(other) {
diff --git a/server/entity/register.go b/server/entity/register.go
index 7155ec986..b4f444014 100644
--- a/server/entity/register.go
+++ b/server/entity/register.go
@@ -14,6 +14,7 @@ var DefaultRegistry = conf.New([]world.EntityType{
ArrowType,
BottleOfEnchantingType,
EggType,
+ EndCrystalType,
EnderPearlType,
ExperienceOrbType,
FallingBlockType,
@@ -30,6 +31,7 @@ var DefaultRegistry = conf.New([]world.EntityType{
var conf = world.EntityRegistryConfig{
TNT: NewTNT,
Egg: NewEgg,
+ EndCrystal: NewEndCrystal,
Snowball: NewSnowball,
BottleOfEnchanting: NewBottleOfEnchanting,
EnderPearl: NewEnderPearl,
diff --git a/server/item/bone_meal.go b/server/item/bone_meal.go
index ec1151da0..9e7deea18 100644
--- a/server/item/bone_meal.go
+++ b/server/item/bone_meal.go
@@ -7,20 +7,40 @@ import (
"github.com/go-gl/mathgl/mgl64"
)
+// BoneMealResult represents the outcome of a bone meal interaction with a block,
+// determining the intensity of the particle effect displayed.
+type BoneMealResult int
+
+const (
+ // BoneMealResultNone indicates that the bone meal had no effect on the block.
+ BoneMealResultNone BoneMealResult = iota
+ // BoneMealResultSmall indicates a minor growth effect, produces a small particle burst.
+ BoneMealResultSmall
+ // BoneMealResultArea indicates a significant growth effect over an area, produces a large particle burst.
+ BoneMealResultArea
+)
+
// BoneMeal is an item used to force growth in plants & crops.
type BoneMeal struct{}
// BoneMealAffected represents a block that is affected when bone meal is used on it.
type BoneMealAffected interface {
// BoneMeal attempts to affect the block using a bone meal item.
- BoneMeal(pos cube.Pos, tx *world.Tx) bool
+ BoneMeal(pos cube.Pos, tx *world.Tx) BoneMealResult
}
// UseOnBlock ...
func (b BoneMeal) UseOnBlock(pos cube.Pos, _ cube.Face, _ mgl64.Vec3, tx *world.Tx, _ User, ctx *UseContext) bool {
- if bm, ok := tx.Block(pos).(BoneMealAffected); ok && bm.BoneMeal(pos, tx) {
+ if bm, ok := tx.Block(pos).(BoneMealAffected); ok {
+ result := bm.BoneMeal(pos, tx)
+ if result == BoneMealResultNone {
+ return false
+ }
+
ctx.SubtractFromCount(1)
- tx.AddParticle(pos.Vec3(), particle.BoneMeal{})
+ tx.AddParticle(pos.Vec3(), particle.BoneMeal{
+ Area: result == BoneMealResultArea,
+ })
return true
}
return false
diff --git a/server/item/creative/creative_items.nbt b/server/item/creative/creative_items.nbt
index 8ebb5d953..ebb532f65 100644
Binary files a/server/item/creative/creative_items.nbt and b/server/item/creative/creative_items.nbt differ
diff --git a/server/item/crossbow.go b/server/item/crossbow.go
index 915b3e274..ed90384f4 100644
--- a/server/item/crossbow.go
+++ b/server/item/crossbow.go
@@ -135,6 +135,7 @@ func (c Crossbow) ReleaseCharge(releaser Releaser, tx *world.Tx, ctx *UseContext
arrowConf := world.ArrowSpawnConfig{
Damage: 9,
Owner: releaser,
+ Critical: true,
ObtainArrowOnPickup: !creative,
PiercingLevel: pierceLevel,
}
@@ -154,6 +155,12 @@ func (c Crossbow) ReleaseCharge(releaser Releaser, tx *world.Tx, ctx *UseContext
return true
}
+// CanCharge ...
+func (c Crossbow) CanCharge(releaser Releaser, _ *world.Tx, ctx *UseContext) bool {
+ _, found := c.findProjectile(releaser, ctx)
+ return found && !c.Item.Empty()
+}
+
// shoot fires the crossbow's loaded projectiles.
func (c Crossbow) shoot(releaser Releaser, tx *world.Tx, offsetAngle float64, arrowConf world.ArrowSpawnConfig) {
rot := releaser.Rotation()
diff --git a/server/item/end_crystal.go b/server/item/end_crystal.go
new file mode 100644
index 000000000..3a8442702
--- /dev/null
+++ b/server/item/end_crystal.go
@@ -0,0 +1,45 @@
+package item
+
+import (
+ "github.com/df-mc/dragonfly/server/block/cube"
+ "github.com/df-mc/dragonfly/server/world"
+ "github.com/go-gl/mathgl/mgl64"
+)
+
+// EndCrystal is an item that can be placed on obsidian or bedrock to spawn an End crystal entity.
+type EndCrystal struct{}
+
+// UseOnBlock places an End crystal on top of the clicked block if it is
+// obsidian or bedrock, the two blocks above it are air and no entities
+// intersect the space the crystal is placed in. The face clicked is ignored.
+func (e EndCrystal) UseOnBlock(pos cube.Pos, _ cube.Face, _ mgl64.Vec3, tx *world.Tx, _ User, ctx *UseContext) bool {
+ clickedBlock, _ := tx.Block(pos).EncodeBlock()
+ if clickedBlock != "minecraft:obsidian" && clickedBlock != "minecraft:bedrock" {
+ return false
+ }
+
+ above, twoAbove := pos.Side(cube.FaceUp), pos.Side(cube.FaceUp).Side(cube.FaceUp)
+ if above.OutOfBounds(tx.Range()) || twoAbove.OutOfBounds(tx.Range()) {
+ return false
+ }
+ if tx.Block(above) != air() || tx.Block(twoAbove) != air() {
+ return false
+ }
+
+ box := cube.Box(0, 0, 0, 1, 2, 1).Translate(above.Vec3())
+ for entity := range tx.EntitiesWithin(box.Grow(2)) {
+ if entity.H().Type().BBox(entity).Translate(entity.Position()).IntersectsWith(box) {
+ return false
+ }
+ }
+
+ opts := world.EntitySpawnOpts{Position: pos.Vec3().Add(mgl64.Vec3{0.5, 1, 0.5})}
+ tx.AddEntity(tx.World().EntityRegistry().Config().EndCrystal(opts))
+ ctx.SubtractFromCount(1)
+ return true
+}
+
+// EncodeItem ...
+func (EndCrystal) EncodeItem() (name string, meta int16) {
+ return "minecraft:end_crystal", 0
+}
diff --git a/server/item/item.go b/server/item/item.go
index 1562cd373..33a503cd2 100644
--- a/server/item/item.go
+++ b/server/item/item.go
@@ -2,12 +2,13 @@ package item
import (
"encoding/binary"
+ "image/color"
+ "time"
+
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/entity/effect"
"github.com/df-mc/dragonfly/server/world"
"github.com/go-gl/mathgl/mgl64"
- "image/color"
- "time"
)
// MaxCounter represents an item that has a specific max count. By default, each item will be expected to have
@@ -168,6 +169,8 @@ type Chargeable interface {
ContinueCharge(releaser Releaser, tx *world.Tx, ctx *UseContext, duration time.Duration)
// ReleaseCharge is called when an item is being released.
ReleaseCharge(releaser Releaser, tx *world.Tx, ctx *UseContext) bool
+ // CanCharge returns whether the item can currently be charged.
+ CanCharge(releaser Releaser, tx *world.Tx, ctx *UseContext) bool
}
// User represents an entity that is able to use an item in the world, typically entities such as players,
diff --git a/server/item/recipe/crafting_data.nbt b/server/item/recipe/crafting_data.nbt
index 800aab6e3..350fc0c12 100644
Binary files a/server/item/recipe/crafting_data.nbt and b/server/item/recipe/crafting_data.nbt differ
diff --git a/server/item/recipe/potion_data.nbt b/server/item/recipe/potion_data.nbt
index 980737cf3..3b9f707eb 100644
Binary files a/server/item/recipe/potion_data.nbt and b/server/item/recipe/potion_data.nbt differ
diff --git a/server/item/register.go b/server/item/register.go
index 786af8575..73db0d5b4 100644
--- a/server/item/register.go
+++ b/server/item/register.go
@@ -52,6 +52,7 @@ func init() {
world.RegisterItem(Emerald{})
world.RegisterItem(EnchantedApple{})
world.RegisterItem(EnchantedBook{})
+ world.RegisterItem(EndCrystal{})
world.RegisterItem(EnderPearl{})
world.RegisterItem(Feather{})
world.RegisterItem(FermentedSpiderEye{})
diff --git a/server/player/bossbar/colour.go b/server/player/bossbar/colour.go
index 62af54060..56ef6746e 100644
--- a/server/player/bossbar/colour.go
+++ b/server/player/bossbar/colour.go
@@ -33,8 +33,8 @@ func Purple() Colour {
return Colour{colour(5)}
}
-// DarkPurple is the colour for a dark purple boss bar.
-func DarkPurple() Colour {
+// RebeccaPurple is the colour for a rebecca purple boss bar.
+func RebeccaPurple() Colour {
return Colour{colour(6)}
}
diff --git a/server/player/chat/translate.go b/server/player/chat/translate.go
index 5a263e8eb..9e7e67549 100644
--- a/server/player/chat/translate.go
+++ b/server/player/chat/translate.go
@@ -16,10 +16,12 @@ var MessageServerDisconnect = Translate(str("%disconnect.disconnected"), 0, `Dis
var MessageBedTooFar = Translate(str("%tile.bed.tooFar"), 0, `Bed is too far away`).Enc("%v")
var MessageBedObstructed = Translate(str("%tile.bed.obstructed"), 0, `Bed is obstructed`).Enc("%v")
var MessageRespawnPointSet = Translate(str("%tile.bed.respawnSet"), 0, `Respawn point set`).Enc("%v")
+var MessageRespawnAnchorRespawnPointSet = Translate(str("%tile.respawn_anchor.respawnSet"), 0, `Respawn point set`).Enc("%v")
var MessageNoSleep = Translate(str("%tile.bed.noSleep"), 0, `You can only sleep at night and during thunderstorms`).Enc("%v")
var MessageBedIsOccupied = Translate(str("%tile.bed.occupied"), 0, `This bed is occupied`).Enc("%v")
var MessageSleeping = Translate(str("%chat.type.sleeping"), 2, `%v is sleeping in a bed. To skip to dawn, %v more users need to sleep in beds at the same time.`)
var MessageBedNotValid = Translate(str("%tile.bed.notValid"), 0, `Your home bed was missing or obstructed`)
+var MessageRespawnAnchorNotValid = Translate(str("%tile.respawn_anchor.notValid"), 0, `Your respawn anchor was out of charges, missing or obstructed`)
type str string
diff --git a/server/player/conf.go b/server/player/conf.go
index 194156c0d..4e81974ff 100644
--- a/server/player/conf.go
+++ b/server/player/conf.go
@@ -24,6 +24,9 @@ type Config struct {
Name string
Locale language.Tag
GameMode world.GameMode
+ // WorldByDimension returns the default World for a Dimension. If nil, saved respawn points outside the player's
+ // current World cannot be resolved.
+ WorldByDimension func(world.Dimension) *world.World
Position mgl64.Vec3
Rotation cube.Rotation
@@ -70,6 +73,7 @@ func (cfg Config) Apply(data *world.EntityData) {
mc: &entity.MovementComputer{Gravity: 0.08, Drag: 0.02, DragBeforeGravity: true},
heldSlot: &slot,
gameMode: conf.GameMode,
+ worldByDimension: conf.WorldByDimension,
skin: conf.Skin,
enchantSeed: conf.EnchantmentSeed,
s: conf.Session,
diff --git a/server/player/debug/shape.go b/server/player/debug/shape.go
index 75438c2e9..66bcc9b1e 100644
--- a/server/player/debug/shape.go
+++ b/server/player/debug/shape.go
@@ -161,3 +161,95 @@ type Text struct {
// Entity is an optional entity handle to attach the shape to.
Entity *world.EntityHandle
}
+
+// Cylinder represents a hollow cylinder, or frustum, that can be drawn at any point in the world, with a
+// height running up the Y axis. The base and top each have their own radius on the X and Z axes, allowing
+// for tapered and elliptical cylinders. A Cone is the special case of a Cylinder with a zero top radius.
+type Cylinder struct {
+ shape
+
+ // Colour is the colour that will be used for the outline. If empty, it will default to white.
+ Colour color.RGBA
+ // Position is the origin position of the shape in the world.
+ Position mgl64.Vec3
+ // Scale is the rate to scale the shape from its origin point. If zero, it will default to 1.0.
+ Scale float64
+ // BaseRadius is the radius of the cylinder's base along the X and Z axes. If empty, it will default to a
+ // radius of 1.0 on each axis. Differing X and Z radii produce an elliptical cylinder.
+ BaseRadius mgl64.Vec2
+ // TopRadius is the radius of the cylinder's top along the X and Z axes. If empty, it will default to
+ // BaseRadius, producing a straight cylinder. A smaller TopRadius tapers the cylinder into a frustum.
+ TopRadius mgl64.Vec2
+ // Height is the height of the cylinder. If zero, it will default to 1.0.
+ Height float64
+ // Segments is the number of segments that the cylinder will be drawn with. The more segments, the
+ // smoother the cylinder will look. If zero, it will default to 20.
+ Segments int
+ // Entity is an optional entity handle to attach the shape to.
+ Entity *world.EntityHandle
+}
+
+// Pyramid represents a pyramid that can be drawn at any point in the world, with a base on the X and Z axes
+// and a height running up the Y axis to a single apex.
+type Pyramid struct {
+ shape
+
+ // Colour is the colour that will be used for the outline. If empty, it will default to white.
+ Colour color.RGBA
+ // Position is the origin position of the shape in the world.
+ Position mgl64.Vec3
+ // Scale is the rate to scale the shape from its origin point. If zero, it will default to 1.0.
+ Scale float64
+ // Width is the width along the X axis of the pyramid base. If zero, it will default to 1.0.
+ Width float64
+ // Depth is the depth along the Z axis of the pyramid base. If zero, it will default to Width.
+ Depth float64
+ // Height is the height of the pyramid. If zero, it will default to 1.0.
+ Height float64
+ // Entity is an optional entity handle to attach the shape to.
+ Entity *world.EntityHandle
+}
+
+// Ellipsoid represents a hollow ellipsoid that can be drawn at any point in the world, with a radius along
+// each of the X, Y and Z axes.
+type Ellipsoid struct {
+ shape
+
+ // Colour is the colour that will be used for the outline. If empty, it will default to white.
+ Colour color.RGBA
+ // Position is the origin position of the shape in the world.
+ Position mgl64.Vec3
+ // Scale is the rate to scale the shape from its origin point. If zero, it will default to 1.0.
+ Scale float64
+ // Radii are the radii of the ellipsoid along the X, Y and Z axes. If empty, it will default to a radius
+ // of 1.0 on each axis.
+ Radii mgl64.Vec3
+ // SegmentsPerAxis is the number of segments that the ellipsoid will be drawn with per axis. The more
+ // segments, the smoother the ellipsoid will look. If zero, it will default to 20.
+ SegmentsPerAxis int
+ // Entity is an optional entity handle to attach the shape to.
+ Entity *world.EntityHandle
+}
+
+// Cone represents a cone that can be drawn at any point in the world, with a base on the X and Z axes and a
+// height running up the Y axis to a single apex.
+type Cone struct {
+ shape
+
+ // Colour is the colour that will be used for the outline. If empty, it will default to white.
+ Colour color.RGBA
+ // Position is the origin position of the shape in the world.
+ Position mgl64.Vec3
+ // Scale is the rate to scale the shape from its origin point. If zero, it will default to 1.0.
+ Scale float64
+ // Radii are the radii along the X and Z axes of the cone base. If empty, it will default to a radius of
+ // 1.0 on each axis.
+ Radii mgl64.Vec2
+ // Height is the height of the cone. If zero, it will default to 1.0.
+ Height float64
+ // Segments is the number of segments that the cone will be drawn with. The more segments, the smoother
+ // the cone will look. If zero, it will default to 20.
+ Segments int
+ // Entity is an optional entity handle to attach the shape to.
+ Entity *world.EntityHandle
+}
diff --git a/server/player/player.go b/server/player/player.go
index bbccbd06e..9c524755b 100644
--- a/server/player/player.go
+++ b/server/player/player.go
@@ -51,6 +51,8 @@ type playerData struct {
s *session.Session
h Handler
+ worldByDimension func(world.Dimension) *world.World
+
inv, offHand, enderChest, ui *inventory.Inventory
armour *inventory.Armour
heldSlot *uint32
@@ -949,51 +951,100 @@ func (p *Player) respawn(f func(p *Player)) {
return
}
- blockPos, w, spawnObstructed, _ := p.spawnLocation()
+ blockPos, w, resolve, notValid := p.spawnLocation()
pos := blockPos.Vec3Middle()
- if spawnObstructed {
- p.Messaget(chat.MessageBedNotValid)
- }
-
p.addHealth(p.MaxHealth())
p.hunger.Reset()
p.sendFood()
p.Extinguish()
p.ResetFallDistance()
+ prevPos, prevW := pos, w
p.Handler().HandleRespawn(p, &pos, &w)
+ // If a handler changed the respawn position or world, that position is
+ // used as-is instead of resolving one around the respawn block.
+ resolve = resolve && pos == prevPos && w == prevW
+
+ // A missing or obstructed respawn block sends the player to the world
+ // spawn of the overworld instead.
+ fallback := p.tx.World().PortalDestination(p.tx.World().Dimension())
handle := p.tx.RemoveEntity(p)
- w.Exec(func(tx *world.Tx) {
+ finish := func(tx *world.Tx, pos mgl64.Vec3, obstructed bool) {
np := tx.AddEntity(handle).(*Player)
+ if obstructed {
+ np.Messaget(notValid)
+ }
np.Teleport(pos)
np.session().SendRespawn(pos, p)
np.SetVisible()
if f != nil {
f(np)
}
+ }
+ w.Exec(func(tx *world.Tx) {
+ if !resolve {
+ finish(tx, pos, false)
+ return
+ }
+ if spawn, ok := safeSpawnLocation(tx, blockPos); ok {
+ finish(tx, spawn.Vec3Middle(), false)
+ return
+ }
+ if fallback == tx.World() {
+ finish(tx, fallback.Spawn().Vec3Middle(), true)
+ return
+ }
+ fallback.Exec(func(tx *world.Tx) {
+ finish(tx, fallback.Spawn().Vec3Middle(), true)
+ })
})
}
-// spawnLocation designates a players safe spawn location.
-func (p *Player) spawnLocation() (playerSpawn cube.Pos, w *world.World, spawnBlockBroken bool, previousDimension world.Dimension) {
- tx := p.tx
- w = tx.World()
- previousDimension = w.Dimension()
- playerSpawn = w.PlayerSpawn(p.UUID())
- if b, ok := tx.Block(playerSpawn).(block.Bed); ok && b.CanRespawnOn() {
- pos, ok := b.SafeSpawn(playerSpawn, tx)
- if ok {
- return pos, w, false, previousDimension
+type respawnBlock interface {
+ CanRespawnOn() bool
+ SafeSpawn(cube.Pos, *world.Tx) (cube.Pos, bool)
+}
+
+// spawnLocation designates a player's respawn location and world. If resolve
+// is true, pos holds the position of the player's respawn block (a bed or
+// respawn anchor) in w. It must still be resolved to a safe spawn position
+// around the block with safeSpawnLocation in a transaction on w once the
+// player actually respawns, as resolving may consume a respawn anchor charge.
+// notValid is the message sent to the player if the respawn block turns out
+// to be missing or obstructed.
+func (p *Player) spawnLocation() (pos cube.Pos, w *world.World, resolve bool, notValid chat.Translation) {
+ w = p.tx.World()
+ notValid = chat.MessageBedNotValid
+ if spawn, ok := w.PlayerSpawnPoint(p.UUID()); ok {
+ if spawn.Dim == world.Nether {
+ notValid = chat.MessageRespawnAnchorNotValid
+ }
+ spawnWorld := w
+ if p.worldByDimension != nil {
+ spawnWorld = p.worldByDimension(spawn.Dim)
+ } else if spawn.Dim != w.Dimension() {
+ spawnWorld = nil
+ }
+ if spawnWorld != nil {
+ return spawn.Pos, spawnWorld, true, notValid
}
}
// We can use the principle here that returning through a portal of a specific dimension inside that dimension will
// always bring us back to the overworld.
w = w.PortalDestination(w.Dimension())
- worldSpawn := w.Spawn()
- return worldSpawn, w, playerSpawn != worldSpawn, previousDimension
+ return w.Spawn(), w, false, notValid
+}
+
+// safeSpawnLocation checks if playerSpawn points at a valid respawn block in
+// tx and returns the block's safe respawn position.
+func safeSpawnLocation(tx *world.Tx, playerSpawn cube.Pos) (cube.Pos, bool) {
+ if b, ok := tx.Block(playerSpawn).(respawnBlock); ok && b.CanRespawnOn() {
+ return b.SafeSpawn(playerSpawn, tx)
+ }
+ return cube.Pos{}, false
}
// StartSprinting makes a player start sprinting, increasing the speed of the player by 30% and making
@@ -1512,7 +1563,7 @@ func (p *Player) UseItem() {
case item.Chargeable:
useCtx := p.useContext()
if !p.usingItem {
- if !usable.ReleaseCharge(p, p.tx, useCtx) {
+ if !usable.ReleaseCharge(p, p.tx, useCtx) && usable.CanCharge(p, p.tx, useCtx) {
// If the item was not charged yet, start charging.
p.usingSince, p.usingItem = time.Now(), true
}
@@ -1786,7 +1837,15 @@ func (p *Player) AttackEntity(e world.Entity) bool {
p.SwingArm()
if !isLiving {
- return false
+ n, vulnerable, ok := entity.HurtEntity(e, i.AttackDamage(), entity.AttackDamageSource{Attacker: p})
+ if !ok {
+ return false
+ }
+ p.tx.PlaySound(entity.EyePosition(e), sound.Attack{Damage: !mgl64.FloatEqual(n, 0)})
+ if vulnerable {
+ p.Exhaust(0.1)
+ }
+ return true
}
dmg := i.AttackDamage()
diff --git a/server/server.go b/server/server.go
index 926d37f19..94fd92aa3 100644
--- a/server/server.go
+++ b/server/server.go
@@ -541,6 +541,7 @@ func (srv *Server) createPlayer(id uuid.UUID, conn session.Conn, conf player.Con
conf.Locale, _ = language.Parse(strings.Replace(conn.ClientData().LanguageCode, "_", "-", 1))
conf.Skin = srv.parseSkin(conn.ClientData())
conf.Session = s
+ conf.WorldByDimension = srv.dimension
handle := world.EntitySpawnOpts{Position: conf.Position, ID: id}.New(player.Type, conf)
s.SetHandle(handle, conf.Skin)
diff --git a/server/session/enchantment_texts.go b/server/session/enchantment_texts.go
index abe44b56b..853823736 100644
--- a/server/session/enchantment_texts.go
+++ b/server/session/enchantment_texts.go
@@ -4,4 +4,4 @@ package session
// enchantNames are names translated to the 'Standard Galactic Alphabet' client-side. The names generally have no meaning
// on the vanilla server implementation, so we can sneak some easter eggs in here without anyone noticing.
-var enchantNames = []string{"aabstractt", "abimek", "aericio", "aimjel", "akmal fairuz", "alvin0319", "andreashgk", "assassin ghost yt", "atm85", "azvyl", "blackjack200", "cetfu", "cjmustard", "cooldogedev", "cqdetdev", "da pig guy", "daft0175", "dasciam", "deniel world", "didntpot", "driftlgtm", "eminarican", "endermanbugzjfc", "erkam246", "ethaniccc", "fdutch", "flonja", "game parrot", "gewinum", "hashim the arab", "hochbaum", "hydzilla", "im da real ani", "inotflying", "ipad54", "its zodia x", "ivan craft623", "javier leon9966", "just tal develops", "k4ties", "krivey", "manab-pr", "mmm545", "mohamed587100", "myma qc", "natuyasai natuo", "neutronic mc", "nonono697", "nope not dark", "provsalt", "restart fu", "riccskn", "robertdudaa", "royal mcpe", "sallypemdas", "sandertv", "schphe", "sculas", "sergittos", "sqmatheus", "ssaini123456", "studgi", "superomarking", "t14 raptor", "tadhunt", "theaddonn", "thicksunny", "thunder33345", "tristanmorgan", "twisted asylum mc", "unickorn", "unknown ore", "uramnoil", "wqrro", "x natsuri", "x toast-dev", "x4caa", "xd-pro"}
+var enchantNames = []string{"aabstractt", "abimek", "aericio", "aimjel", "akmal fairuz", "alvin0319", "andreashgk", "assassin ghost yt", "atm85", "azvyl", "blackjack200", "cetfu", "cjmustard", "cooldogedev", "cqdetdev", "da pig guy", "daft0175", "dasciam", "deniel world", "didntpot", "driftlgtm", "eminarican", "endermanbugzjfc", "erkam246", "ethaniccc", "fdutch", "flonja", "game parrot", "gewinum", "hashim the arab", "hochbaum", "hydzilla", "im da real ani", "inotflying", "ipad54", "its zodia x", "ivan craft623", "javier leon9966", "just tal develops", "k4ties", "krivey", "manab-pr", "memoxiiii", "mmm545", "mohamed587100", "myma qc", "natuyasai natuo", "neutronic mc", "nonono697", "nope not dark", "provsalt", "restart fu", "riccskn", "robertdudaa", "royal mcpe", "sallypemdas", "sandertv", "schphe", "sculas", "sergittos", "smell-of-curry", "sqmatheus", "ssaini123456", "studgi", "superomarking", "t14 raptor", "tadhunt", "theaddonn", "thicksunny", "thunder33345", "tripple awap", "tristanmorgan", "twisted asylum mc", "unickorn", "unknown ore", "uramnoil", "wqrro", "x natsuri", "x toast-dev", "x4caa", "xd-pro"}
diff --git a/server/session/entity_metadata.go b/server/session/entity_metadata.go
index 427266aac..6f5c1e574 100644
--- a/server/session/entity_metadata.go
+++ b/server/session/entity_metadata.go
@@ -123,6 +123,16 @@ func (s *Session) addSpecificMetadata(e any, m protocol.EntityMetadata) {
if sc, ok := e.(scoreTag); ok {
m[protocol.EntityDataKeyScore] = sc.ScoreTag()
}
+ if c, ok := e.(endCrystal); ok {
+ if c.ShowBase() {
+ m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagShowBottom)
+ } else {
+ m.UnsetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagShowBottom)
+ }
+ if target, ok := c.BeamTarget(); ok {
+ m[protocol.EntityDataKeyBlockTarget] = protocol.BlockPos{int32(target[0]), int32(target[1]), int32(target[2])}
+ }
+ }
if sl, ok := e.(sleeper); ok {
if pos, ok := sl.Sleeping(); ok {
m[protocol.EntityDataKeyBedPosition] = protocol.BlockPos{int32(pos[0]), int32(pos[1]), int32(pos[2])}
@@ -148,7 +158,7 @@ func (s *Session) addSpecificMetadata(e any, m protocol.EntityMetadata) {
}
}
- if l, ok := e.(living); ok && s.ent.UUID() == l.UUID() {
+ if l, ok := e.(living); ok && s.ent != nil && s.ent.UUID() == l.UUID() {
deathPos, deathDimension, died := l.DeathPosition()
if died {
dim, _ := world.DimensionID(deathDimension)
@@ -242,6 +252,11 @@ type scoreTag interface {
ScoreTag() string
}
+type endCrystal interface {
+ ShowBase() bool
+ BeamTarget() (cube.Pos, bool)
+}
+
type splash interface {
Potion() potion.Potion
}
diff --git a/server/session/player.go b/server/session/player.go
index a8642a701..e039bb440 100644
--- a/server/session/player.go
+++ b/server/session/player.go
@@ -1117,6 +1117,14 @@ func (s *Session) shapeAttachedEntityRuntimeID(shape debug.Shape) int64 {
handle = shape.Entity
case *debug.Text:
handle = shape.Entity
+ case *debug.Cylinder:
+ handle = shape.Entity
+ case *debug.Pyramid:
+ handle = shape.Entity
+ case *debug.Ellipsoid:
+ handle = shape.Entity
+ case *debug.Cone:
+ handle = shape.Entity
}
if handle == nil {
return 0
@@ -1138,7 +1146,7 @@ func debugShapeToProtocol(shape debug.Shape, dim world.Dimension, attachedEntity
white := color.RGBA{R: 255, G: 255, B: 255, A: 255}
switch shape := shape.(type) {
case *debug.Arrow:
- ps.Type = protocol.Option(uint8(protocol.PrimitiveShapeArrow))
+ ps.Type = protocol.Option(protocol.PrimitiveShapeArrow)
ps.Colour = protocol.Option(valueOrDefault(shape.Colour, white))
ps.Location = protocol.Option(vec64To32(shape.Position))
ps.ExtraShapeData = &protocol.ArrowShape{
@@ -1148,30 +1156,30 @@ func debugShapeToProtocol(shape debug.Shape, dim world.Dimension, attachedEntity
Segments: protocol.Option(valueOrDefault(uint8(shape.HeadSegments), 4)),
}
case *debug.Box:
- ps.Type = protocol.Option(uint8(protocol.PrimitiveShapeBox))
+ ps.Type = protocol.Option(protocol.PrimitiveShapeBox)
ps.Colour = protocol.Option(valueOrDefault(shape.Colour, white))
ps.Location = protocol.Option(vec64To32(shape.Position))
ps.Scale = protocol.Option(valueOrDefault(float32(shape.Scale), 1))
ps.ExtraShapeData = &protocol.BoxShape{BoxBound: valueOrDefault(vec64To32(shape.Bounds), mgl32.Vec3{1, 1, 1})}
case *debug.Circle:
- ps.Type = protocol.Option(uint8(protocol.PrimitiveShapeCircle))
+ ps.Type = protocol.Option(protocol.PrimitiveShapeCircle)
ps.Colour = protocol.Option(valueOrDefault(shape.Colour, white))
ps.Location = protocol.Option(vec64To32(shape.Position))
ps.Scale = protocol.Option(valueOrDefault(float32(shape.Scale), 1))
ps.ExtraShapeData = &protocol.SphereShape{Segments: valueOrDefault(uint8(shape.Segments), 20)}
case *debug.Line:
- ps.Type = protocol.Option(uint8(protocol.PrimitiveShapeLine))
+ ps.Type = protocol.Option(protocol.PrimitiveShapeLine)
ps.Colour = protocol.Option(valueOrDefault(shape.Colour, white))
ps.Location = protocol.Option(vec64To32(shape.Position))
ps.ExtraShapeData = &protocol.LineShape{LineEndLocation: vec64To32(shape.EndPosition)}
case *debug.Sphere:
- ps.Type = protocol.Option(uint8(protocol.PrimitiveShapeSphere))
+ ps.Type = protocol.Option(protocol.PrimitiveShapeSphere)
ps.Colour = protocol.Option(valueOrDefault(shape.Colour, white))
ps.Location = protocol.Option(vec64To32(shape.Position))
ps.Scale = protocol.Option(valueOrDefault(float32(shape.Scale), 1))
ps.ExtraShapeData = &protocol.SphereShape{Segments: valueOrDefault(uint8(shape.Segments), 20)}
case *debug.Text:
- ps.Type = protocol.Option(uint8(protocol.PrimitiveShapeText))
+ ps.Type = protocol.Option(protocol.PrimitiveShapeText)
ps.Colour = protocol.Option(valueOrDefault(shape.Colour, white))
ps.Location = protocol.Option(vec64To32(shape.Position))
ps.Scale = protocol.Option(valueOrDefault(float32(shape.Scale), 1))
@@ -1192,6 +1200,51 @@ func debugShapeToProtocol(shape debug.Shape, dim world.Dimension, attachedEntity
textData.BackgroundColour = protocol.Option(shape.BackgroundColour)
}
ps.ExtraShapeData = textData
+ case *debug.Cylinder:
+ ps.Type = protocol.Option(protocol.PrimitiveShapeCylinder)
+ ps.Colour = protocol.Option(valueOrDefault(shape.Colour, white))
+ ps.Location = protocol.Option(vec64To32(shape.Position))
+ ps.Scale = protocol.Option(valueOrDefault(float32(shape.Scale), 1))
+ base := valueOrDefault(shape.BaseRadius, mgl64.Vec2{1, 1})
+ top := valueOrDefault(shape.TopRadius, base)
+ ps.ExtraShapeData = &protocol.CylinderShape{
+ RadiusX: mgl32.Vec2{float32(base[0]), float32(top[0])},
+ RadiusZ: mgl32.Vec2{float32(base[1]), float32(top[1])},
+ Height: valueOrDefault(float32(shape.Height), 1),
+ NumSegments: valueOrDefault(uint8(shape.Segments), 20),
+ }
+ case *debug.Pyramid:
+ ps.Type = protocol.Option(protocol.PrimitiveShapePyramid)
+ ps.Colour = protocol.Option(valueOrDefault(shape.Colour, white))
+ ps.Location = protocol.Option(vec64To32(shape.Position))
+ ps.Scale = protocol.Option(valueOrDefault(float32(shape.Scale), 1))
+ pyramid := &protocol.PyramidShape{
+ Width: valueOrDefault(float32(shape.Width), 1),
+ Height: valueOrDefault(float32(shape.Height), 1),
+ }
+ if shape.Depth != 0 {
+ pyramid.Depth = protocol.Option(float32(shape.Depth))
+ }
+ ps.ExtraShapeData = pyramid
+ case *debug.Ellipsoid:
+ ps.Type = protocol.Option(protocol.PrimitiveShapeEllipsoid)
+ ps.Colour = protocol.Option(valueOrDefault(shape.Colour, white))
+ ps.Location = protocol.Option(vec64To32(shape.Position))
+ ps.Scale = protocol.Option(valueOrDefault(float32(shape.Scale), 1))
+ ps.ExtraShapeData = &protocol.EllipsoidShape{
+ Radii: valueOrDefault(vec64To32(shape.Radii), mgl32.Vec3{1, 1, 1}),
+ SegmentsPerAxis: valueOrDefault(uint8(shape.SegmentsPerAxis), 20),
+ }
+ case *debug.Cone:
+ ps.Type = protocol.Option(protocol.PrimitiveShapeCone)
+ ps.Colour = protocol.Option(valueOrDefault(shape.Colour, white))
+ ps.Location = protocol.Option(vec64To32(shape.Position))
+ ps.Scale = protocol.Option(valueOrDefault(float32(shape.Scale), 1))
+ ps.ExtraShapeData = &protocol.ConeShape{
+ Radii: valueOrDefault(vec2To32(shape.Radii), mgl32.Vec2{1, 1}),
+ Height: valueOrDefault(float32(shape.Height), 1),
+ NumSegments: valueOrDefault(uint8(shape.Segments), 20),
+ }
default:
panic(fmt.Sprintf("unknown debug shape type %T", shape))
}
diff --git a/server/session/session.go b/server/session/session.go
index 0bfcc7934..cdd1a69c1 100644
--- a/server/session/session.go
+++ b/server/session/session.go
@@ -144,7 +144,7 @@ type Conn interface {
}
// Nop represents a no-operation session. It does not do anything when sending a packet to it.
-var Nop = &Session{}
+var Nop = &Session{conf: Config{Log: slog.New(slog.DiscardHandler)}}
// selfEntityRuntimeID is the entity runtime (or unique) ID of the controllable that the session holds.
const selfEntityRuntimeID = 1
diff --git a/server/session/text.go b/server/session/text.go
index 4c447b7f5..d8418df4a 100644
--- a/server/session/text.go
+++ b/server/session/text.go
@@ -1,12 +1,13 @@
package session
import (
+ "time"
+
"github.com/df-mc/dragonfly/server/player/chat"
"github.com/df-mc/dragonfly/server/player/scoreboard"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"golang.org/x/text/language"
- "time"
)
// SendMessage ...
@@ -145,7 +146,7 @@ func (s *Session) SendBossBar(text string, colour uint8, healthPercentage float6
EventType: packet.BossEventShow,
BossBarTitle: text,
HealthPercentage: float32(healthPercentage),
- Colour: uint32(colour),
+ Colour: colour,
})
}
diff --git a/server/session/world.go b/server/session/world.go
index 1ecdfb728..9b58f031c 100644
--- a/server/session/world.go
+++ b/server/session/world.go
@@ -384,6 +384,7 @@ func (s *Session) ViewParticle(pos mgl64.Vec3, p world.Particle) {
s.writePacket(&packet.LevelEvent{
EventType: packet.LevelEventParticleCropGrowth,
Position: vec64To32(pos),
+ EventData: int32(boolByte(pa.Area)),
})
case particle.BlockForceField:
s.writePacket(&packet.LevelEvent{
@@ -487,7 +488,7 @@ func (s *Session) ViewParticle(pos mgl64.Vec3, p world.Particle) {
}
// tierToSoundEvent converts an item.ArmourTier to a sound event associated with equipping it.
-func tierToSoundEvent(tier item.ArmourTier) uint32 {
+func tierToSoundEvent(tier item.ArmourTier) string {
switch tier.(type) {
case item.ArmourTierLeather:
return packet.SoundEventEquipLeather
@@ -629,6 +630,12 @@ func (s *Session) playSound(pos mgl64.Vec3, t world.Sound, disableRelative bool)
pk.SoundType = packet.SoundEventSmokerUse
case sound.PotionBrewed:
pk.SoundType = packet.SoundEventPotionBrewed
+ case sound.RespawnAnchorCharge:
+ pk.SoundType = packet.SoundEventRespawnAnchorCharge
+ case sound.RespawnAnchorDeplete:
+ pk.SoundType = packet.SoundEventRespawnAnchorDeplete
+ case sound.RespawnAnchorSetSpawn:
+ pk.SoundType = packet.SoundEventRespawnAnchorSetSpawn
case sound.UseSpyglass:
pk.SoundType = packet.SoundEventUseSpyglass
case sound.StopUsingSpyglass:
@@ -1412,6 +1419,11 @@ func vec64To32(vec3 mgl64.Vec3) mgl32.Vec3 {
return mgl32.Vec3{float32(vec3[0]), float32(vec3[1]), float32(vec3[2])}
}
+// vec2To32 converts a mgl64.Vec2 to a mgl32.Vec2.
+func vec2To32(vec2 mgl64.Vec2) mgl32.Vec2 {
+ return mgl32.Vec2{float32(vec2[0]), float32(vec2[1])}
+}
+
// blockPosFromProtocol ...
func blockPosFromProtocol(pos protocol.BlockPos) cube.Pos {
return cube.Pos{int(pos.X()), int(pos.Y()), int(pos.Z())}
diff --git a/server/world/biome/register.go b/server/world/biome/register.go
index 4db6de511..d5b5aec45 100644
--- a/server/world/biome/register.go
+++ b/server/world/biome/register.go
@@ -80,6 +80,7 @@ func init() {
world.RegisterBiome(SoulSandValley{})
world.RegisterBiome(StonyPeaks{})
world.RegisterBiome(StonyShore{})
+ world.RegisterBiome(SulfurCaves{})
world.RegisterBiome(SunflowerPlains{})
world.RegisterBiome(SwampHills{})
world.RegisterBiome(Swamp{})
diff --git a/server/world/biome/sulfur_caves.go b/server/world/biome/sulfur_caves.go
new file mode 100644
index 000000000..7c32f06aa
--- /dev/null
+++ b/server/world/biome/sulfur_caves.go
@@ -0,0 +1,46 @@
+package biome
+
+import "image/color"
+
+// SulfurCaves ...
+type SulfurCaves struct{}
+
+// Temperature ...
+func (SulfurCaves) Temperature() float64 {
+ return 0.8
+}
+
+// Rainfall ...
+func (SulfurCaves) Rainfall() float64 {
+ return 0.4
+}
+
+// Depth ...
+func (SulfurCaves) Depth() float64 {
+ return 0.1
+}
+
+// Scale ...
+func (SulfurCaves) Scale() float64 {
+ return 0.2
+}
+
+// WaterColour ...
+func (SulfurCaves) WaterColour() color.RGBA {
+ return color.RGBA{R: 0x60, G: 0xb7, B: 0xff, A: 0xa6}
+}
+
+// Tags ...
+func (SulfurCaves) Tags() []string {
+ return []string{"caves", "sulfur_caves", "overworld", "monster"}
+}
+
+// String ...
+func (SulfurCaves) String() string {
+ return "sulfur_caves"
+}
+
+// EncodeBiome ...
+func (SulfurCaves) EncodeBiome() int {
+ return 194
+}
diff --git a/server/world/block_states.nbt b/server/world/block_states.nbt
index 73ccf6fe6..1e4d94632 100644
Binary files a/server/world/block_states.nbt and b/server/world/block_states.nbt differ
diff --git a/server/world/chunk/block_registry.go b/server/world/chunk/block_registry.go
index d5a749dde..24888d2ba 100644
--- a/server/world/chunk/block_registry.go
+++ b/server/world/chunk/block_registry.go
@@ -27,4 +27,6 @@ type BlockRegistry interface {
LiquidBlock(rid uint32) bool
// HashToRuntimeID resolves a "network block hash" to a runtime ID.
HashToRuntimeID(hash uint32) (rid uint32, ok bool)
+ // RuntimeIDToHash resolves a runtime ID to its "network block hash".
+ RuntimeIDToHash(runtimeID uint32) (hash uint32, ok bool)
}
diff --git a/server/world/chunk/chunk.go b/server/world/chunk/chunk.go
index 17ef1f10f..ddee338b5 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: slices.Clone(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) {
@@ -212,14 +232,14 @@ func (chunk *Chunk) SubY(index int16) int16 {
return (index << 4) + int16(chunk.r[0])
}
-// HighestFilledSubChunk returns the index of the highest sub chunk in the chunk
-// that has any blocks in it. 0 is returned if no subchunks have any blocks.
+// HighestFilledSubChunk returns the number of sub chunks up to and including the
+// highest sub chunk in the chunk that has any blocks in it. 0 is returned if no
+// subchunks have any blocks.
func (chunk *Chunk) HighestFilledSubChunk() uint16 {
- highest := uint16(0)
- for highest = uint16(len(chunk.sub) - 1); highest > 0; highest-- {
- if !chunk.sub[highest].Empty() {
- break
+ for i, sub := range slices.Backward(chunk.sub) {
+ if !sub.Empty() {
+ return uint16(i + 1)
}
}
- return highest
+ return 0
}
diff --git a/server/world/chunk/palette.go b/server/world/chunk/palette.go
index 3b3d32b17..f3121f4f5 100644
--- a/server/world/chunk/palette.go
+++ b/server/world/chunk/palette.go
@@ -2,6 +2,7 @@ package chunk
import (
"math"
+ "slices"
)
// paletteSize is the size of a palette. It indicates the amount of bits occupied per value stored.
@@ -23,6 +24,16 @@ 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 &Palette{
+ last: palette.last,
+ lastIndex: palette.lastIndex,
+ size: palette.size,
+ values: slices.Clone(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..ac49c67c4 100644
--- a/server/world/chunk/paletted_storage.go
+++ b/server/world/chunk/paletted_storage.go
@@ -2,6 +2,7 @@ package chunk
import (
"bytes"
+ "slices"
"unsafe"
)
@@ -59,6 +60,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(slices.Clone(storage.indices), storage.palette.Clone())
+}
+
// Palette returns the Palette of the PalettedStorage.
func (storage *PalettedStorage) Palette() *Palette {
return storage.palette
@@ -164,7 +170,7 @@ func (storage *PalettedStorage) resize(newPaletteSize paletteSize) {
// relatively heavy task which should only happen right before the sub chunk holding this PalettedStorage is
// saved to disk. compact also shrinks the palette size if possible.
func (storage *PalettedStorage) compact() {
- if storage.palette == nil || storage.palette.Len() == 0 {
+ if storage.palette.Len() == 0 {
return
}
if storage.palette.Len() == 1 {
diff --git a/server/world/chunk/sub_chunk.go b/server/world/chunk/sub_chunk.go
index 21436cecb..7a06680de 100644
--- a/server/world/chunk/sub_chunk.go
+++ b/server/world/chunk/sub_chunk.go
@@ -1,5 +1,7 @@
package chunk
+import "slices"
+
// SubChunk is a cube of blocks located in a chunk. It has a size of 16x16x16 blocks and forms part of a stack
// that forms a Chunk.
type SubChunk struct {
@@ -29,6 +31,34 @@ 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,
+ storages: make([]*PalettedStorage, len(sub.storages)),
+ blockLight: cloneLight(sub.blockLight),
+ skyLight: cloneLight(sub.skyLight),
+ }
+ for i, storage := range sub.storages {
+ clone.storages[i] = storage.Clone()
+ }
+ return clone
+}
+
+func cloneLight(light []uint8) []uint8 {
+ if len(light) == 0 {
+ return slices.Clone(light)
+ }
+ switch &light[0] {
+ case noLightPtr:
+ return noLight
+ case fullLightPtr:
+ return fullLight
+ default:
+ return slices.Clone(light)
+ }
+}
+
// 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/entity.go b/server/world/entity.go
index 01c6c0ba6..42c061399 100644
--- a/server/world/entity.go
+++ b/server/world/entity.go
@@ -368,6 +368,7 @@ type EntityRegistryConfig struct {
BottleOfEnchanting func(opts EntitySpawnOpts, owner Entity) *EntityHandle
Arrow func(opts EntitySpawnOpts, conf ArrowSpawnConfig) *EntityHandle
Egg func(opts EntitySpawnOpts, owner Entity) *EntityHandle
+ EndCrystal func(opts EntitySpawnOpts) *EntityHandle
EnderPearl func(opts EntitySpawnOpts, owner Entity) *EntityHandle
Firework func(opts EntitySpawnOpts, firework Item, owner Entity, sidewaysVelocityMultiplier, upwardsAcceleration float64, attached bool) *EntityHandle
LingeringPotion func(opts EntitySpawnOpts, t any, owner Entity) *EntityHandle
diff --git a/server/world/mcdb/db.go b/server/world/mcdb/db.go
index 06b97ae7a..046e2643a 100644
--- a/server/world/mcdb/db.go
+++ b/server/world/mcdb/db.go
@@ -66,17 +66,23 @@ type playerData struct {
SelfSignedID string `nbt:"SelfSignedId"`
}
-// LoadPlayerSpawnPosition loads the players spawn position stored in the level.dat from their UUID.
-func (db *DB) LoadPlayerSpawnPosition(id uuid.UUID) (pos cube.Pos, exists bool, err error) {
+// LoadPlayerSpawn loads the players spawn position stored in the level.dat from their UUID.
+func (db *DB) LoadPlayerSpawn(id uuid.UUID) (spawn world.PlayerSpawn, exists bool, err error) {
serverData, _, exists, err := db.loadPlayerData(id)
if !exists || err != nil {
- return cube.Pos{}, exists, err
+ return world.PlayerSpawn{}, exists, err
}
x, y, z := serverData["SpawnX"], serverData["SpawnY"], serverData["SpawnZ"]
if x == nil || y == nil || z == nil {
- return cube.Pos{}, true, fmt.Errorf("error reading spawn fields from server data for player %v", id)
+ return world.PlayerSpawn{}, true, fmt.Errorf("error reading spawn fields from server data for player %v", id)
}
- return cube.Pos{int(x.(int32)), int(y.(int32)), int(z.(int32))}, true, nil
+ spawn = world.PlayerSpawn{Pos: cube.Pos{int(x.(int32)), int(y.(int32)), int(z.(int32))}, Dim: world.Overworld}
+ if dimID, ok := serverData["SpawnDimension"].(int32); ok {
+ if dim, ok := world.DimensionByID(int(dimID)); ok {
+ spawn.Dim = dim
+ }
+ }
+ return spawn, true, nil
}
// loadPlayerData loads the data stored in a LevelDB database for a specific UUID.
@@ -106,8 +112,8 @@ func (db *DB) loadPlayerData(id uuid.UUID) (serverData map[string]interface{}, k
return serverData, d.ServerID, true, nil
}
-// SavePlayerSpawnPosition saves the player spawn position passed to the levelDB database.
-func (db *DB) SavePlayerSpawnPosition(id uuid.UUID, pos cube.Pos) error {
+// SavePlayerSpawn saves the player spawn position passed to the levelDB database.
+func (db *DB) SavePlayerSpawn(id uuid.UUID, spawn world.PlayerSpawn) error {
_, err := db.ldb.Get([]byte("player_"+id.String()), nil)
d := make(map[string]interface{})
k := "player_server_" + id.String()
@@ -123,7 +129,11 @@ func (db *DB) SavePlayerSpawnPosition(id uuid.UUID, pos cube.Pos) error {
} else if d, k, _, err = db.loadPlayerData(id); err != nil {
return err
}
- d["SpawnX"], d["SpawnY"], d["SpawnZ"] = int32(pos.X()), int32(pos.Y()), int32(pos.Z())
+ d["SpawnX"], d["SpawnY"], d["SpawnZ"] = int32(spawn.Pos.X()), int32(spawn.Pos.Y()), int32(spawn.Pos.Z())
+ // An unregistered Dimension resolves to 0 here, so that the spawn falls
+ // back to the Overworld instead of a stale, previously saved dimension.
+ dim, _ := world.DimensionID(spawn.Dim)
+ d["SpawnDimension"] = int32(dim)
data, err := nbt.MarshalEncoding(d, nbt.LittleEndian)
if err != nil {
diff --git a/server/world/mcdb/leveldat/data.go b/server/world/mcdb/leveldat/data.go
index 05fd50744..68f45038c 100644
--- a/server/world/mcdb/leveldat/data.go
+++ b/server/world/mcdb/leveldat/data.go
@@ -74,83 +74,86 @@ type Data struct {
WalkSpeed float32 `nbt:"walkSpeed"`
VerticalFlySpeed float32 `nbt:"verticalFlySpeed"`
} `nbt:"abilities"`
- BonusChestEnabled bool `nbt:"bonusChestEnabled"`
- BonusChestSpawned bool `nbt:"bonusChestSpawned"`
- CommandBlockOutput bool `nbt:"commandblockoutput"`
- CommandBlocksEnabled bool `nbt:"commandblocksenabled"`
- CommandsEnabled bool `nbt:"commandsEnabled"`
- CurrentTick int64 `nbt:"currentTick"`
- DoDayLightCycle bool `nbt:"dodaylightcycle"`
- DoEntityDrops bool `nbt:"doentitydrops"`
- DoFireTick bool `nbt:"dofiretick"`
- DoImmediateRespawn bool `nbt:"doimmediaterespawn"`
- DoInsomnia bool `nbt:"doinsomnia"`
- DoMobLoot bool `nbt:"domobloot"`
- DoMobSpawning bool `nbt:"domobspawning"`
- DoTileDrops bool `nbt:"dotiledrops"`
- DoWeatherCycle bool `nbt:"doweathercycle"`
- DrowningDamage bool `nbt:"drowningdamage"`
- EduLevel bool `nbt:"eduLevel"`
- EducationFeaturesEnabled bool `nbt:"educationFeaturesEnabled"`
- ExperimentalGamePlay bool `nbt:"experimentalgameplay"`
- FallDamage bool `nbt:"falldamage"`
- FireDamage bool `nbt:"firedamage"`
- FunctionCommandLimit int32 `nbt:"functioncommandlimit"`
- HasBeenLoadedInCreative bool `nbt:"hasBeenLoadedInCreative"`
- HasLockedBehaviourPack bool `nbt:"hasLockedBehaviorPack"`
- HasLockedResourcePack bool `nbt:"hasLockedResourcePack"`
- ImmutableWorld bool `nbt:"immutableWorld"`
- IsCreatedInEditor bool `nbt:"isCreatedInEditor"`
- IsExportedFromEditor bool `nbt:"isExportedFromEditor"`
- IsFromLockedTemplate bool `nbt:"isFromLockedTemplate"`
- IsFromWorldTemplate bool `nbt:"isFromWorldTemplate"`
- IsWorldTemplateOptionLocked bool `nbt:"isWorldTemplateOptionLocked"`
- KeepInventory bool `nbt:"keepinventory"`
- LastOpenedWithVersion []int32 `nbt:"lastOpenedWithVersion"`
- LightningLevel float32 `nbt:"lightningLevel"`
- LightningTime int32 `nbt:"lightningTime"`
- MaxCommandChainLength int32 `nbt:"maxcommandchainlength"`
- MobGriefing bool `nbt:"mobgriefing"`
- NaturalRegeneration bool `nbt:"naturalregeneration"`
- PRID string `nbt:"prid"`
- PVP bool `nbt:"pvp"`
- RainLevel float32 `nbt:"rainLevel"`
- RainTime int32 `nbt:"rainTime"`
- RandomTickSpeed int32 `nbt:"randomtickspeed"`
- RequiresCopiedPackRemovalCheck bool `nbt:"requiresCopiedPackRemovalCheck"`
- SendCommandFeedback bool `nbt:"sendcommandfeedback"`
- ServerChunkTickRange int32 `nbt:"serverChunkTickRange"`
- ShowCoordinates bool `nbt:"showcoordinates"`
- ShowDeathMessages bool `nbt:"showdeathmessages"`
- SpawnMobs bool `nbt:"spawnMobs"`
- SpawnRadius int32 `nbt:"spawnradius"`
- StartWithMapEnabled bool `nbt:"startWithMapEnabled"`
- TexturePacksRequired bool `nbt:"texturePacksRequired"`
- TNTExplodes bool `nbt:"tntexplodes"`
- UseMSAGamerTagsOnly bool `nbt:"useMsaGamertagsOnly"`
- WorldStartCount int64 `nbt:"worldStartCount"`
- Experiments map[string]any `nbt:"experiments"`
- FreezeDamage bool `nbt:"freezedamage"`
- WorldPolicies map[string]any `nbt:"world_policies"`
- WorldVersion int32 `nbt:"WorldVersion"`
- RespawnBlocksExplode bool `nbt:"respawnblocksexplode"`
- ShowBorderEffect bool `nbt:"showbordereffect"`
- PermissionsLevel int32 `nbt:"permissionsLevel"`
- PlayerPermissionsLevel int32 `nbt:"playerPermissionsLevel"`
- IsRandomSeedAllowed bool `nbt:"isRandomSeedAllowed"`
- DoLimitedCrafting bool `nbt:"dolimitedcrafting"`
- EditorWorldType int32 `nbt:"editorWorldType"`
- PlayersSleepingPercentage int32 `nbt:"playerssleepingpercentage"`
- RecipesUnlock bool `nbt:"recipesunlock"`
- NaturalGeneration bool `nbt:"naturalgeneration"`
- ProjectilesCanBreakBlocks bool `nbt:"projectilescanbreakblocks"`
- ShowRecipeMessages bool `nbt:"showrecipemessages"`
- IsHardcore bool `nbt:"IsHardcore"`
- ShowDaysPlayed bool `nbt:"showdaysplayed"`
- TNTExplosionDropDecay bool `nbt:"tntexplosiondropdecay"`
- HasUncompleteWorldFileOnDisk bool `nbt:"HasUncompleteWorldFileOnDisk"`
- PlayerHasDied bool `nbt:"PlayerHasDied"`
- UseAllowList bool `nbt:"UseAllowList"`
+ BonusChestEnabled bool `nbt:"bonusChestEnabled"`
+ BonusChestSpawned bool `nbt:"bonusChestSpawned"`
+ CommandBlockOutput bool `nbt:"commandblockoutput"`
+ CommandBlocksEnabled bool `nbt:"commandblocksenabled"`
+ CommandsEnabled bool `nbt:"commandsEnabled"`
+ CurrentTick int64 `nbt:"currentTick"`
+ DoDayLightCycle bool `nbt:"dodaylightcycle"`
+ DoEntityDrops bool `nbt:"doentitydrops"`
+ DoFireTick bool `nbt:"dofiretick"`
+ DoImmediateRespawn bool `nbt:"doimmediaterespawn"`
+ DoInsomnia bool `nbt:"doinsomnia"`
+ DoMobLoot bool `nbt:"domobloot"`
+ DoMobSpawning bool `nbt:"domobspawning"`
+ DoTileDrops bool `nbt:"dotiledrops"`
+ DoWeatherCycle bool `nbt:"doweathercycle"`
+ DrowningDamage bool `nbt:"drowningdamage"`
+ EduLevel bool `nbt:"eduLevel"`
+ EducationFeaturesEnabled bool `nbt:"educationFeaturesEnabled"`
+ ExperimentalGamePlay bool `nbt:"experimentalgameplay"`
+ FallDamage bool `nbt:"falldamage"`
+ FireDamage bool `nbt:"firedamage"`
+ FunctionCommandLimit int32 `nbt:"functioncommandlimit"`
+ HasBeenLoadedInCreative bool `nbt:"hasBeenLoadedInCreative"`
+ HasLockedBehaviourPack bool `nbt:"hasLockedBehaviorPack"`
+ HasLockedResourcePack bool `nbt:"hasLockedResourcePack"`
+ ImmutableWorld bool `nbt:"immutableWorld"`
+ IsCreatedInEditor bool `nbt:"isCreatedInEditor"`
+ IsExportedFromEditor bool `nbt:"isExportedFromEditor"`
+ IsFromLockedTemplate bool `nbt:"isFromLockedTemplate"`
+ IsFromWorldTemplate bool `nbt:"isFromWorldTemplate"`
+ IsWorldTemplateOptionLocked bool `nbt:"isWorldTemplateOptionLocked"`
+ KeepInventory bool `nbt:"keepinventory"`
+ LastOpenedWithVersion []int32 `nbt:"lastOpenedWithVersion"`
+ LightningLevel float32 `nbt:"lightningLevel"`
+ LightningTime int32 `nbt:"lightningTime"`
+ MaxCommandChainLength int32 `nbt:"maxcommandchainlength"`
+ MobGriefing bool `nbt:"mobgriefing"`
+ NaturalRegeneration bool `nbt:"naturalregeneration"`
+ PRID string `nbt:"prid"`
+ PVP bool `nbt:"pvp"`
+ RainLevel float32 `nbt:"rainLevel"`
+ RainTime int32 `nbt:"rainTime"`
+ RandomTickSpeed int32 `nbt:"randomtickspeed"`
+ RequiresCopiedPackRemovalCheck bool `nbt:"requiresCopiedPackRemovalCheck"`
+ SendCommandFeedback bool `nbt:"sendcommandfeedback"`
+ ServerChunkTickRange int32 `nbt:"serverChunkTickRange"`
+ ShowCoordinates bool `nbt:"showcoordinates"`
+ ShowDeathMessages bool `nbt:"showdeathmessages"`
+ SpawnMobs bool `nbt:"spawnMobs"`
+ SpawnRadius int32 `nbt:"spawnradius"`
+ StartWithMapEnabled bool `nbt:"startWithMapEnabled"`
+ TexturePacksRequired bool `nbt:"texturePacksRequired"`
+ TNTExplodes bool `nbt:"tntexplodes"`
+ UseMSAGamerTagsOnly bool `nbt:"useMsaGamertagsOnly"`
+ WorldStartCount int64 `nbt:"worldStartCount"`
+ Experiments map[string]any `nbt:"experiments"`
+ FreezeDamage bool `nbt:"freezedamage"`
+ WorldPolicies map[string]any `nbt:"world_policies"`
+ WorldVersion int32 `nbt:"WorldVersion"`
+ RespawnBlocksExplode bool `nbt:"respawnblocksexplode"`
+ ShowBorderEffect bool `nbt:"showbordereffect"`
+ PermissionsLevel int32 `nbt:"permissionsLevel"`
+ PlayerPermissionsLevel int32 `nbt:"playerPermissionsLevel"`
+ IsRandomSeedAllowed bool `nbt:"isRandomSeedAllowed"`
+ DoLimitedCrafting bool `nbt:"dolimitedcrafting"`
+ EditorWorldType int32 `nbt:"editorWorldType"`
+ PlayersSleepingPercentage int32 `nbt:"playerssleepingpercentage"`
+ RecipesUnlock bool `nbt:"recipesunlock"`
+ NaturalGeneration bool `nbt:"naturalgeneration"`
+ ProjectilesCanBreakBlocks bool `nbt:"projectilescanbreakblocks"`
+ ShowRecipeMessages bool `nbt:"showrecipemessages"`
+ IsHardcore bool `nbt:"IsHardcore"`
+ ShowDaysPlayed bool `nbt:"showdaysplayed"`
+ TNTExplosionDropDecay bool `nbt:"tntexplosiondropdecay"`
+ HasUncompleteWorldFileOnDisk bool `nbt:"HasUncompleteWorldFileOnDisk"`
+ PlayerHasDied bool `nbt:"PlayerHasDied"`
+ UseAllowList bool `nbt:"UseAllowList"`
+ AllowAnonymousBlockDropsInEditorWorlds bool `nbt:"allowAnonymousBlockDropsInEditorWorlds"`
+ PlayerWaypoints int32 `nbt:"playerwaypoints"`
+ ServerEditorConnectionPolicy int32 `nbt:"serverEditorConnectionPolicy"`
}
// FillDefault fills out d with all the default level.dat values.
diff --git a/server/world/mcdb/leveldat/level_dat.go b/server/world/mcdb/leveldat/level_dat.go
index eabdd1910..4e4d63ccb 100644
--- a/server/world/mcdb/leveldat/level_dat.go
+++ b/server/world/mcdb/leveldat/level_dat.go
@@ -4,9 +4,10 @@ import (
"bufio"
"encoding/binary"
"fmt"
- "github.com/sandertv/gophertunnel/minecraft/nbt"
"io"
"os"
+
+ "github.com/sandertv/gophertunnel/minecraft/nbt"
)
// LevelDat implements the encoding and decoding of level.dat files. An empty
@@ -40,7 +41,7 @@ func Read(r io.Reader) (*LevelDat, error) {
return nil, fmt.Errorf("level.dat: read header: %w", err)
}
ldat.data = make([]byte, ldat.hdr.FileLength)
- if n, err := r.Read(ldat.data); err != nil || int32(n) != ldat.hdr.FileLength {
+ if n, err := io.ReadFull(r, ldat.data); err != nil || int32(n) != ldat.hdr.FileLength {
return nil, fmt.Errorf("level.dat: read data: %w", err)
}
return &ldat, nil
diff --git a/server/world/particle/block.go b/server/world/particle/block.go
index 43130e2b3..71133aaa4 100644
--- a/server/world/particle/block.go
+++ b/server/world/particle/block.go
@@ -1,11 +1,12 @@
package particle
import (
+ "image/color"
+
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/world"
"github.com/df-mc/dragonfly/server/world/sound"
"github.com/go-gl/mathgl/mgl64"
- "image/color"
)
// Flame is a particle shown around torches. It can have any colour specified with the Colour field.
@@ -49,7 +50,14 @@ type PunchBlock struct {
type BlockForceField struct{ particle }
// BoneMeal is a particle that shows up on bone meal usage.
-type BoneMeal struct{ particle }
+type BoneMeal struct {
+ particle
+
+ // Area specifies whether the particle effect should be for area. If false,
+ // a small burst is used for minor growth. If true, a large burst is used
+ // for significant growth.
+ Area bool
+}
// Note is a particle that shows up on note block interactions.
type Note struct {
diff --git a/server/world/provider.go b/server/world/provider.go
index 713c38bfb..545614b01 100644
--- a/server/world/provider.go
+++ b/server/world/provider.go
@@ -17,11 +17,11 @@ type Provider interface {
// SaveSettings saves the settings of a World.
SaveSettings(*Settings)
- // LoadPlayerSpawnPosition loads the player spawn point if found, otherwise an error will be returned.
- LoadPlayerSpawnPosition(uuid uuid.UUID) (pos cube.Pos, exists bool, err error)
- // SavePlayerSpawnPosition saves the player spawn point. In vanilla, this can be done with beds in the overworld
- // or respawn anchors in the nether.
- SavePlayerSpawnPosition(uuid uuid.UUID, pos cube.Pos) error
+ // LoadPlayerSpawn loads the player spawn point if found, otherwise an error will be returned.
+ LoadPlayerSpawn(uuid uuid.UUID) (spawn PlayerSpawn, exists bool, err error)
+ // SavePlayerSpawn saves the player spawn point. In vanilla, this can be done with beds in the overworld or
+ // respawn anchors in the nether.
+ SavePlayerSpawn(uuid uuid.UUID, spawn PlayerSpawn) error
// LoadColumn reads a world.Column from the DB at a position and dimension
// in the DB. If no column at that position exists, errors.Is(err,
// leveldb.ErrNotFound) equals true.
@@ -31,6 +31,13 @@ type Provider interface {
StoreColumn(pos ChunkPos, dim Dimension, col *chunk.Column) error
}
+// PlayerSpawn holds a player's respawn position and the Dimension in which it
+// should be resolved.
+type PlayerSpawn struct {
+ Pos cube.Pos
+ Dim Dimension
+}
+
// Compile time check to make sure NopProvider implements Provider.
var _ Provider = (*NopProvider)(nil)
@@ -53,8 +60,8 @@ func (NopProvider) LoadColumn(ChunkPos, Dimension) (*chunk.Column, error) {
return nil, leveldb.ErrNotFound
}
func (NopProvider) StoreColumn(ChunkPos, Dimension, *chunk.Column) error { return nil }
-func (NopProvider) LoadPlayerSpawnPosition(uuid.UUID) (cube.Pos, bool, error) {
- return cube.Pos{}, false, nil
+func (NopProvider) Close() error { return nil }
+func (NopProvider) LoadPlayerSpawn(uuid.UUID) (PlayerSpawn, bool, error) {
+ return PlayerSpawn{}, false, nil
}
-func (NopProvider) SavePlayerSpawnPosition(uuid.UUID, cube.Pos) error { return nil }
-func (NopProvider) Close() error { return nil }
+func (NopProvider) SavePlayerSpawn(uuid.UUID, PlayerSpawn) error { return nil }
diff --git a/server/world/sound/block.go b/server/world/sound/block.go
index fc62f59ab..d06f46013 100644
--- a/server/world/sound/block.go
+++ b/server/world/sound/block.go
@@ -191,6 +191,15 @@ type PowerOn struct{ sound }
// PowerOff is a sound played when a redstone component is powered off.
type PowerOff struct{ sound }
+// RespawnAnchorCharge is a sound played when a respawn anchor is charged using glowstone.
+type RespawnAnchorCharge struct{ sound }
+
+// RespawnAnchorDeplete is a sound played when a respawn anchor charge is consumed by respawning.
+type RespawnAnchorDeplete struct{ sound }
+
+// RespawnAnchorSetSpawn is a sound played when a player sets their spawn point using a respawn anchor.
+type RespawnAnchorSetSpawn struct{ sound }
+
// LecternBookPlace is a sound played when a book is placed in a lectern.
type LecternBookPlace struct{ sound }
diff --git a/server/world/vanilla_items.nbt b/server/world/vanilla_items.nbt
index c5d6a1b2b..52af4d9e1 100644
Binary files a/server/world/vanilla_items.nbt and b/server/world/vanilla_items.nbt differ
diff --git a/server/world/world.go b/server/world/world.go
index 82290e434..60d0b4520 100644
--- a/server/world/world.go
+++ b/server/world/world.go
@@ -844,18 +844,25 @@ func (w *World) SetSpawn(pos cube.Pos) {
// PlayerSpawn returns the spawn position of a player with a UUID in this World.
func (w *World) PlayerSpawn(id uuid.UUID) cube.Pos {
+ spawn, ok := w.PlayerSpawnPoint(id)
+ if !ok {
+ return w.Spawn()
+ }
+ return spawn.Pos
+}
+
+// PlayerSpawnPoint returns the spawn point and Dimension of a player with a
+// UUID in this World.
+func (w *World) PlayerSpawnPoint(id uuid.UUID) (PlayerSpawn, bool) {
if w == nil {
- return cube.Pos{}
+ return PlayerSpawn{}, false
}
- pos, exist, err := w.conf.Provider.LoadPlayerSpawnPosition(id)
+ spawn, exists, err := w.conf.Provider.LoadPlayerSpawn(id)
if err != nil {
w.conf.Log.Error("load player spawn: "+err.Error(), "ID", id)
- return w.Spawn()
- }
- if !exist {
- return w.Spawn()
+ return PlayerSpawn{}, false
}
- return pos
+ return spawn, exists
}
// SetPlayerSpawn sets the spawn position of a player with a UUID in this
@@ -865,7 +872,7 @@ func (w *World) SetPlayerSpawn(id uuid.UUID, pos cube.Pos) {
if w == nil {
return
}
- if err := w.conf.Provider.SavePlayerSpawnPosition(id, pos); err != nil {
+ if err := w.conf.Provider.SavePlayerSpawn(id, PlayerSpawn{Pos: pos, Dim: w.Dimension()}); err != nil {
w.conf.Log.Error("save player spawn: "+err.Error(), "ID", id)
}
}