diff --git a/cmd/blockhash/main.go b/cmd/blockhash/main.go index 5e3c0cd10..6a0898b53 100644 --- a/cmd/blockhash/main.go +++ b/cmd/blockhash/main.go @@ -5,8 +5,7 @@ import ( "flag" "fmt" "go/ast" - "go/parser" - "go/token" + "golang.org/x/tools/go/packages" "io" "log" "os" @@ -22,8 +21,10 @@ func main() { if len(flag.Args()) != 1 { log.Fatalln("Must pass one package to produce block hashes for.") } - fs := token.NewFileSet() - packages, err := parser.ParseDir(fs, flag.Args()[0], nil, parser.ParseComments) + cfg := &packages.Config{ + Mode: packages.NeedName | packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedFiles, + } + pkgs, err := packages.Load(cfg, flag.Args()[0]) if err != nil { log.Fatalln(err) } @@ -31,15 +32,14 @@ func main() { if err != nil { log.Fatalln(err) } - for _, pkg := range packages { - procPackage(pkg, fs, f) + for _, pkg := range pkgs { + procPackage(pkg, f) } _ = f.Close() } -func procPackage(pkg *ast.Package, fs *token.FileSet, w io.Writer) { +func procPackage(pkg *packages.Package, w io.Writer) { b := &hashBuilder{ - fs: fs, pkg: pkg, fields: make(map[string][]*ast.Field), aliases: make(map[string]string), @@ -66,8 +66,7 @@ var ( ) type hashBuilder struct { - fs *token.FileSet - pkg *ast.Package + pkg *packages.Package fields map[string][]*ast.Field funcs map[string]*ast.FuncDecl aliases map[string]string @@ -151,7 +150,7 @@ func (b *hashBuilder) writeMethods(w io.Writer) { for _, n := range fun.Recv.List[0].Names { recvName = n.Name } - pos := b.fs.Position(fun.Body.Pos()) + pos := b.pkg.Fset.Position(fun.Body.Pos()) f, err := os.Open(pos.Filename) if err != nil { log.Fatalln(err) @@ -277,8 +276,8 @@ func (b *hashBuilder) resolveBlocks() { } } -func (b *hashBuilder) readFuncs(pkg *ast.Package) { - for _, f := range pkg.Files { +func (b *hashBuilder) readFuncs(pkg *packages.Package) { + for _, f := range pkg.Syntax { ast.Inspect(f, b.readFuncDecls) } } @@ -294,8 +293,8 @@ func (b *hashBuilder) readFuncDecls(node ast.Node) bool { return true } -func (b *hashBuilder) readStructFields(pkg *ast.Package) { - for _, f := range pkg.Files { +func (b *hashBuilder) readStructFields(pkg *packages.Package) { + for _, f := range pkg.Syntax { ast.Inspect(f, b.readStructs) } b.resolveEmbedded() diff --git a/go.mod b/go.mod index 4e3e0407b..6de57f6c3 100644 --- a/go.mod +++ b/go.mod @@ -9,16 +9,17 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 github.com/df-mc/atomic v1.10.0 github.com/df-mc/goleveldb v1.1.9 - github.com/df-mc/worldupgrader v1.0.15 + github.com/df-mc/worldupgrader v1.0.16 github.com/go-gl/mathgl v1.1.0 github.com/google/uuid v1.6.0 github.com/pelletier/go-toml v1.9.5 github.com/rogpeppe/go-internal v1.11.0 - github.com/sandertv/gophertunnel v1.39.0 + github.com/sandertv/gophertunnel v1.40.1 github.com/segmentio/fasthash v1.0.3 github.com/sirupsen/logrus v1.9.3 golang.org/x/exp v0.0.0-20230206171751-46f607a40771 golang.org/x/text v0.16.0 + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d ) require ( @@ -29,8 +30,10 @@ require ( github.com/sandertv/go-raknet v1.14.1 // indirect golang.org/x/crypto v0.24.0 // indirect golang.org/x/image v0.17.0 // indirect + golang.org/x/mod v0.17.0 // indirect golang.org/x/net v0.26.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.21.0 // indirect gopkg.in/yaml.v2 v2.3.0 // indirect ) diff --git a/go.sum b/go.sum index 0aa4ad0fa..3ea662d61 100644 --- a/go.sum +++ b/go.sum @@ -9,8 +9,8 @@ github.com/df-mc/atomic v1.10.0 h1:0ZuxBKwR/hxcFGorKiHIp+hY7hgY+XBTzhCYD2NqSEg= github.com/df-mc/atomic v1.10.0/go.mod h1:Gw9rf+rPIbydMjA329Jn4yjd/O2c/qusw3iNp4tFGSc= 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/worldupgrader v1.0.15 h1:kR/nYWQbFvmR5LqPncpBXtXKGyiRBPc9NPkBKlLSAIk= -github.com/df-mc/worldupgrader v1.0.15/go.mod h1:tsSOLTRm9mpG7VHvYpAjjZrkRHWmSbKZAm9bOLNnlDk= +github.com/df-mc/worldupgrader v1.0.16 h1:3n9yvLFNCe8IDJnUEliTGbhDvV1frjtPX/y5zl3Q5EE= +github.com/df-mc/worldupgrader v1.0.16/go.mod h1:tsSOLTRm9mpG7VHvYpAjjZrkRHWmSbKZAm9bOLNnlDk= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/go-gl/mathgl v1.1.0 h1:0lzZ+rntPX3/oGrDzYGdowSLC2ky8Osirvf5uAwfIEA= github.com/go-gl/mathgl v1.1.0/go.mod h1:yhpkQzEiH9yPyxDUGzkmgScbaBVlhC06qodikEM0ZwQ= @@ -20,8 +20,9 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= @@ -44,8 +45,8 @@ github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDN github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/sandertv/go-raknet v1.14.1 h1:V2Gslo+0x4jfj+p0PM48mWxmMbYkxSlgeKy//y3ZrzI= github.com/sandertv/go-raknet v1.14.1/go.mod h1:/yysjwfCXm2+2OY8mBazLzcxJ3irnylKCyG3FLgUPVU= -github.com/sandertv/gophertunnel v1.39.0 h1:Am2NhSQjxscAVihRG/Qz7cfyJJp91RIl/5DpSLbwXp0= -github.com/sandertv/gophertunnel v1.39.0/go.mod h1:uSaX7RbVaCcxsGAx2vyZnkT0M6kZFGCdAqLn0+wuKyY= +github.com/sandertv/gophertunnel v1.40.1 h1:foWxpIEXm/pCqik8r8TmcIlQyG8Vpe98JJwFKurdb/4= +github.com/sandertv/gophertunnel v1.40.1/go.mod h1:uSaX7RbVaCcxsGAx2vyZnkT0M6kZFGCdAqLn0+wuKyY= github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM= github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -69,6 +70,8 @@ golang.org/x/image v0.17.0 h1:nTRVVdajgB8zCMZVsViyzhnMKPwYeroEERRC64JuLco= golang.org/x/image v0.17.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -83,6 +86,8 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -112,6 +117,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= diff --git a/server/block/anvil.go b/server/block/anvil.go index 64da372b0..7a903d083 100644 --- a/server/block/anvil.go +++ b/server/block/anvil.go @@ -81,13 +81,12 @@ func (Anvil) Landed(w *world.World, pos cube.Pos) { // EncodeItem ... func (a Anvil) EncodeItem() (name string, meta int16) { - return "minecraft:anvil", int16(a.Type.Uint8() * 4) + return "minecraft:" + a.Type.String(), 0 } // EncodeBlock ... func (a Anvil) EncodeBlock() (string, map[string]any) { - return "minecraft:anvil", map[string]any{ - "damage": a.Type.String(), + return "minecraft:" + a.Type.String(), map[string]any{ "minecraft:cardinal_direction": a.Facing.String(), } } diff --git a/server/block/anvil_type.go b/server/block/anvil_type.go index 76cbc5420..b42d3d476 100644 --- a/server/block/anvil_type.go +++ b/server/block/anvil_type.go @@ -36,11 +36,11 @@ func (a anvil) Uint8() uint8 { func (a anvil) String() string { switch a { case 0: - return "undamaged" + return "anvil" case 1: - return "slightly_damaged" + return "chipped_anvil" case 2: - return "very_damaged" + return "damaged_anvil" } panic("should never happen") } diff --git a/server/block/barrel.go b/server/block/barrel.go index 749f9a1ec..17847970c 100644 --- a/server/block/barrel.go +++ b/server/block/barrel.go @@ -51,7 +51,7 @@ func NewBarrel() Barrel { } // Inventory returns the inventory of the barrel. The size of the inventory will be 27. -func (b Barrel) Inventory() *inventory.Inventory { +func (b Barrel) Inventory(*world.World, cube.Pos) *inventory.Inventory { return b.inventory } @@ -124,7 +124,11 @@ func (b Barrel) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, w *world. // BreakInfo ... func (b Barrel) BreakInfo() BreakInfo { - return newBreakInfo(2.5, alwaysHarvestable, axeEffective, oneOf(b)) + return newBreakInfo(2.5, alwaysHarvestable, axeEffective, oneOf(b)).withBreakHandler(func(pos cube.Pos, w *world.World, u item.User) { + for _, i := range b.Inventory(w, pos).Clear() { + dropItem(w, i, pos.Vec3()) + } + }) } // FlammabilityInfo ... diff --git a/server/block/blast_furnace.go b/server/block/blast_furnace.go index 46956479d..ffc7104f1 100644 --- a/server/block/blast_furnace.go +++ b/server/block/blast_furnace.go @@ -73,7 +73,11 @@ func (b BlastFurnace) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, w * // BreakInfo ... func (b BlastFurnace) BreakInfo() BreakInfo { xp := b.Experience() - return newBreakInfo(3.5, alwaysHarvestable, pickaxeEffective, oneOf(b)).withXPDropRange(xp, xp) + return newBreakInfo(3.5, alwaysHarvestable, pickaxeEffective, oneOf(b)).withXPDropRange(xp, xp).withBreakHandler(func(pos cube.Pos, w *world.World, u item.User) { + for _, i := range b.Inventory(w, pos).Clear() { + dropItem(w, i, pos.Vec3()) + } + }) } // Activate ... @@ -97,7 +101,7 @@ func (b BlastFurnace) EncodeNBT() map[string]interface{} { "CookTime": int16(cook.Milliseconds() / 50), "BurnDuration": int16(maximum.Milliseconds() / 50), "StoredXPInt": int16(b.Experience()), - "Items": nbtconv.InvToNBT(b.Inventory()), + "Items": nbtconv.InvToNBT(b.inventory), "id": "BlastFurnace", } } @@ -116,7 +120,7 @@ func (b BlastFurnace) DecodeNBT(data map[string]interface{}) interface{} { b.Lit = lit b.setExperience(xp) b.setDurations(remaining, maximum, cook) - nbtconv.InvFromNBT(b.Inventory(), nbtconv.Slice(data, "Items")) + nbtconv.InvFromNBT(b.inventory, nbtconv.Slice(data, "Items")) return b } diff --git a/server/block/chest.go b/server/block/chest.go index 0dc5c07b6..9b3b9cdfd 100644 --- a/server/block/chest.go +++ b/server/block/chest.go @@ -29,6 +29,10 @@ type Chest struct { // include colour codes. CustomName string + paired bool + pairX, pairZ int + pairInv *inventory.Inventory + inventory *inventory.Inventory viewerMu *sync.RWMutex viewers map[ContainerViewer]struct{} @@ -36,24 +40,38 @@ type Chest struct { // NewChest creates a new initialised chest. The inventory is properly initialised. func NewChest() Chest { - m := new(sync.RWMutex) - v := make(map[ContainerViewer]struct{}, 1) - return Chest{ - inventory: inventory.New(27, func(slot int, _, item item.Stack) { - m.RLock() - defer m.RUnlock() - for viewer := range v { - viewer.ViewSlotChange(slot, item) - } - }), - viewerMu: m, - viewers: v, + c := Chest{ + viewerMu: new(sync.RWMutex), + viewers: make(map[ContainerViewer]struct{}, 1), } + + c.inventory = inventory.New(27, func(slot int, _, after item.Stack) { + c.viewerMu.RLock() + defer c.viewerMu.RUnlock() + for viewer := range c.viewers { + viewer.ViewSlotChange(slot, after) + } + }) + return c } // Inventory returns the inventory of the chest. The size of the inventory will be 27 or 54, depending on // whether the chest is single or double. -func (c Chest) Inventory() *inventory.Inventory { +func (c Chest) Inventory(w *world.World, pos cube.Pos) *inventory.Inventory { + if c.paired { + if c.pairInv == nil { + if ch, pair, ok := c.pair(w, pos, c.pairPos(pos)); ok { + c = ch + w.SetBlock(pos, ch, nil) + w.SetBlock(c.pairPos(pos), pair, nil) + } else { + c.paired = false + w.SetBlock(pos, c, nil) + return c.inventory + } + } + return c.pairInv + } return c.inventory } @@ -71,6 +89,9 @@ func (Chest) SideClosed(cube.Pos, cube.Pos, *world.World) bool { // open opens the chest, displaying the animation and playing a sound. func (c Chest) open(w *world.World, pos cube.Pos) { for _, v := range w.Viewers(pos.Vec3()) { + if c.paired { + v.ViewBlockAction(c.pairPos(pos), OpenAction{}) + } v.ViewBlockAction(pos, OpenAction{}) } w.PlaySound(pos.Vec3Centre(), sound.ChestOpen{}) @@ -79,6 +100,9 @@ func (c Chest) open(w *world.World, pos cube.Pos) { // close closes the chest, displaying the animation and playing a sound. func (c Chest) close(w *world.World, pos cube.Pos) { for _, v := range w.Viewers(pos.Vec3()) { + if c.paired { + v.ViewBlockAction(c.pairPos(pos), CloseAction{}) + } v.ViewBlockAction(pos, CloseAction{}) } w.PlaySound(pos.Vec3Centre(), sound.ChestClose{}) @@ -111,6 +135,11 @@ func (c Chest) RemoveViewer(v ContainerViewer, w *world.World, pos cube.Pos) { // Activate ... func (c Chest) Activate(pos cube.Pos, _ cube.Face, w *world.World, u item.User, _ *item.UseContext) bool { if opener, ok := u.(ContainerOpener); ok { + if c.paired { + if d, ok := w.Block(c.pairPos(pos).Side(cube.FaceUp)).(LightDiffuser); !ok || d.LightDiffusionLevel() > 2 { + return false + } + } if d, ok := w.Block(pos.Side(cube.FaceUp)).(LightDiffuser); ok && d.LightDiffusionLevel() <= 2 { opener.OpenBlockContainer(pos) } @@ -129,13 +158,34 @@ func (c Chest) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, w *world.W c = NewChest() c.Facing = user.Rotation().Direction().Opposite() + // Check both sides of the chest to see if it is possible to pair with another chest. + for _, dir := range []cube.Direction{c.Facing.RotateLeft(), c.Facing.RotateRight()} { + if ch, pair, ok := c.pair(w, pos, pos.Side(dir.Face())); ok { + place(w, pos, ch, user, ctx) + w.SetBlock(ch.pairPos(pos), pair, nil) + return placed(ctx) + } + } + place(w, pos, c, user, ctx) return placed(ctx) } // BreakInfo ... func (c Chest) BreakInfo() BreakInfo { - return newBreakInfo(2.5, alwaysHarvestable, axeEffective, oneOf(c)) + return newBreakInfo(2.5, alwaysHarvestable, axeEffective, oneOf(c)).withBreakHandler(func(pos cube.Pos, w *world.World, u item.User) { + if c.paired { + pairPos := c.pairPos(pos) + if _, pair, ok := c.unpair(w, pos); ok { + c.paired = false + w.SetBlock(pairPos, pair, nil) + } + } + + for _, i := range c.Inventory(w, pos).Clear() { + dropItem(w, i, pos.Vec3Centre()) + } + }) } // FuelInfo ... @@ -148,6 +198,89 @@ func (c Chest) FlammabilityInfo() FlammabilityInfo { return newFlammabilityInfo(0, 0, true) } +// Paired returns whether the chest is paired with another chest. +func (c Chest) Paired() bool { + return c.paired +} + +// pair pairs this chest with the given chest position. +func (c Chest) pair(w *world.World, pos, pairPos cube.Pos) (ch, pair Chest, ok bool) { + pair, ok = w.Block(pairPos).(Chest) + if !ok || c.Facing != pair.Facing || pair.paired && (pair.pairX != pos[0] || pair.pairZ != pos[2]) { + return c, pair, false + } + m := new(sync.RWMutex) + v := make(map[ContainerViewer]struct{}) + left, right := c.inventory.Clone(nil), pair.inventory.Clone(nil) + if pos.Side(c.Facing.RotateRight().Face()) == pairPos { + left, right = right, left + } + double := left.Merge(right, func(slot int, _, item item.Stack) { + if slot < 27 { + _ = left.SetItem(slot, item) + } else { + _ = right.SetItem(slot-27, item) + } + m.RLock() + defer m.RUnlock() + for viewer := range v { + viewer.ViewSlotChange(slot, item) + } + }) + + c.inventory, pair.inventory = left, right + if pos.Side(c.Facing.RotateRight().Face()) == pairPos { + c.inventory, pair.inventory = right, left + } + c.pairX, c.pairZ, c.paired = pairPos[0], pairPos[2], true + pair.pairX, pair.pairZ, pair.paired = pos[0], pos[2], true + c.viewerMu, pair.viewerMu = m, m + c.viewers, pair.viewers = v, v + c.pairInv, pair.pairInv = double, double + return c, pair, true +} + +// unpair unpairs this chest from the chest it is currently paired with. +func (c Chest) unpair(w *world.World, pos cube.Pos) (ch, pair Chest, ok bool) { + if !c.paired { + return c, Chest{}, false + } + + pair, ok = w.Block(c.pairPos(pos)).(Chest) + if !ok || c.Facing != pair.Facing || pair.paired && (pair.pairX != pos[0] || pair.pairZ != pos[2]) { + return c, pair, false + } + + if len(c.viewers) != 0 { + c.close(w, pos) + } + + c.inventory = c.inventory.Clone(func(slot int, _, after item.Stack) { + c.viewerMu.RLock() + defer c.viewerMu.RUnlock() + for viewer := range c.viewers { + viewer.ViewSlotChange(slot, after) + } + }) + pair.inventory = pair.inventory.Clone(func(slot int, _, after item.Stack) { + pair.viewerMu.RLock() + defer pair.viewerMu.RUnlock() + for viewer := range pair.viewers { + viewer.ViewSlotChange(slot, after) + } + }) + c.paired, pair.paired = false, false + c.viewerMu, pair.viewerMu = new(sync.RWMutex), new(sync.RWMutex) + c.viewers, pair.viewers = make(map[ContainerViewer]struct{}, 1), make(map[ContainerViewer]struct{}, 1) + c.pairInv, pair.pairInv = nil, nil + return c, pair, true +} + +// pairPos returns the position of the chest that this chest is paired with. +func (c Chest) pairPos(pos cube.Pos) cube.Pos { + return cube.Pos{c.pairX, pos[1], c.pairZ} +} + // DecodeNBT ... func (c Chest) DecodeNBT(data map[string]any) any { facing := c.Facing @@ -155,6 +288,18 @@ func (c Chest) DecodeNBT(data map[string]any) any { c = NewChest() c.Facing = facing c.CustomName = nbtconv.String(data, "CustomName") + + pairX, ok := data["pairx"] + pairZ, ok2 := data["pairz"] + if ok && ok2 { + pairX, ok := pairX.(int32) + pairZ, ok2 := pairZ.(int32) + if ok && ok2 { + c.paired = true + c.pairX, c.pairZ = int(pairX), int(pairZ) + } + } + nbtconv.InvFromNBT(c.inventory, nbtconv.Slice(data, "Items")) return c } @@ -174,6 +319,11 @@ func (c Chest) EncodeNBT() map[string]any { if c.CustomName != "" { m["CustomName"] = c.CustomName } + + if c.paired { + m["pairx"] = int32(c.pairX) + m["pairz"] = int32(c.pairZ) + } return m } diff --git a/server/block/composter.go b/server/block/composter.go index e605b93f6..6491ed9bc 100644 --- a/server/block/composter.go +++ b/server/block/composter.go @@ -22,6 +22,44 @@ type Composter struct { Level int } +// InsertItem ... +func (c Composter) InsertItem(h Hopper, pos cube.Pos, w *world.World) bool { + if c.Level >= 7 || h.Facing != cube.FaceDown { + return false + } + + for sourceSlot, sourceStack := range h.inventory.Slots() { + if sourceStack.Empty() { + continue + } + + if c.fill(sourceStack, pos, w) { + _ = h.inventory.SetItem(sourceSlot, sourceStack.Grow(-1)) + return true + } + } + + return false +} + +// ExtractItem ... +func (c Composter) ExtractItem(h Hopper, pos cube.Pos, w *world.World) bool { + if c.Level == 8 { + _, err := h.inventory.AddItem(item.NewStack(item.BoneMeal{}, 1)) + if err != nil { + // The hopper is full. + return false + } + + c.Level = 0 + w.SetBlock(pos.Side(cube.FaceUp), c, nil) + w.PlaySound(pos.Side(cube.FaceUp).Vec3(), sound.ComposterEmpty{}) + return true + } + + return false +} + // Model ... func (c Composter) Model() world.BlockModel { return model.Composter{Level: c.Level} @@ -63,11 +101,19 @@ func (c Composter) Activate(pos cube.Pos, _ cube.Face, w *world.World, u item.Us return false } it, _ := u.HeldItems() + if c.fill(it, pos, w) { + ctx.SubtractFromCount(1) + return true + } + return false +} + +// Fill fills up the composter. +func (c Composter) fill(it item.Stack, pos cube.Pos, w *world.World) bool { compostable, ok := it.Item().(item.Compostable) if !ok { return false } - ctx.SubtractFromCount(1) w.AddParticle(pos.Vec3(), particle.BoneMeal{}) if rand.Float64() > compostable.CompostChance() { w.PlaySound(pos.Vec3(), sound.ComposterFill{}) @@ -79,6 +125,7 @@ func (c Composter) Activate(pos cube.Pos, _ cube.Face, w *world.World, u item.Us if c.Level == 7 { w.ScheduleBlockUpdate(pos, time.Second) } + return true } diff --git a/server/block/container.go b/server/block/container.go index fc5aff50c..03c1e3a7c 100644 --- a/server/block/container.go +++ b/server/block/container.go @@ -26,5 +26,5 @@ type ContainerOpener interface { type Container interface { AddViewer(v ContainerViewer, w *world.World, pos cube.Pos) RemoveViewer(v ContainerViewer, w *world.World, pos cube.Pos) - Inventory() *inventory.Inventory + Inventory(w *world.World, pos cube.Pos) *inventory.Inventory } diff --git a/server/block/dirt.go b/server/block/dirt.go index 5774066a6..a25deafb3 100644 --- a/server/block/dirt.go +++ b/server/block/dirt.go @@ -46,17 +46,17 @@ func (d Dirt) Shovel() (world.Block, bool) { // EncodeItem ... func (d Dirt) EncodeItem() (name string, meta int16) { if d.Coarse { - meta = 1 + return "minecraft:coarse_dirt", 0 } - return "minecraft:dirt", meta + return "minecraft:dirt", 0 } // EncodeBlock ... func (d Dirt) EncodeBlock() (string, map[string]any) { if d.Coarse { - return "minecraft:dirt", map[string]any{"dirt_type": "coarse"} + return "minecraft:coarse_dirt", nil } - return "minecraft:dirt", map[string]any{"dirt_type": "normal"} + return "minecraft:dirt", nil } // supportsVegetation checks if the vegetation can exist on the block. diff --git a/server/block/explosion.go b/server/block/explosion.go index 2085385a3..b24e4bd1a 100644 --- a/server/block/explosion.go +++ b/server/block/explosion.go @@ -139,6 +139,26 @@ func (c ExplosionConfig) Explode(w *world.World, explosionPos mgl64.Vec3) { dropItem(w, drop, pos.Vec3Centre()) } } + + if container, ok := bl.(Container); ok { + if cb, ok := bl.(Chest); ok { + if cb.Paired() { + pairPos := cb.pairPos(pos) + if _, pair, ok := cb.unpair(w, pos); ok { + cb.paired = false + w.SetBlock(pairPos, pair, nil) + } + } + + for _, i := range cb.Inventory(w, pos).Clear() { + dropItem(w, i, pos.Vec3()) + } + } else { + for _, i := range container.Inventory(w, pos).Clear() { + dropItem(w, i, pos.Vec3()) + } + } + } } } if c.SpawnFire { diff --git a/server/block/flower_type.go b/server/block/flower_type.go index 10c14a237..d67447148 100644 --- a/server/block/flower_type.go +++ b/server/block/flower_type.go @@ -114,7 +114,7 @@ func (f flower) Name() string { func (f flower) String() string { switch f { case 0: - return "yellow_flower" + return "dandelion" case 1: return "poppy" case 2: diff --git a/server/block/furnace.go b/server/block/furnace.go index f7e664e3d..5feb026f3 100644 --- a/server/block/furnace.go +++ b/server/block/furnace.go @@ -72,7 +72,11 @@ func (f Furnace) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, w *world // BreakInfo ... func (f Furnace) BreakInfo() BreakInfo { xp := f.Experience() - return newBreakInfo(3.5, alwaysHarvestable, pickaxeEffective, oneOf(f)).withXPDropRange(xp, xp) + return newBreakInfo(3.5, alwaysHarvestable, pickaxeEffective, oneOf(f)).withXPDropRange(xp, xp).withBreakHandler(func(pos cube.Pos, w *world.World, u item.User) { + for _, i := range f.Inventory(w, pos).Clear() { + dropItem(w, i, pos.Vec3()) + } + }) } // Activate ... @@ -96,7 +100,7 @@ func (f Furnace) EncodeNBT() map[string]interface{} { "CookTime": int16(cook.Milliseconds() / 50), "BurnDuration": int16(maximum.Milliseconds() / 50), "StoredXPInt": int16(f.Experience()), - "Items": nbtconv.InvToNBT(f.Inventory()), + "Items": nbtconv.InvToNBT(f.inventory), "id": "Furnace", } } @@ -115,7 +119,7 @@ func (f Furnace) DecodeNBT(data map[string]interface{}) interface{} { f.Lit = lit f.setExperience(xp) f.setDurations(remaining, maximum, cook) - nbtconv.InvFromNBT(f.Inventory(), nbtconv.Slice(data, "Items")) + nbtconv.InvFromNBT(f.inventory, nbtconv.Slice(data, "Items")) return f } diff --git a/server/block/hopper.go b/server/block/hopper.go new file mode 100644 index 000000000..c62a0e31e --- /dev/null +++ b/server/block/hopper.go @@ -0,0 +1,281 @@ +package block + +import ( + "fmt" + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/model" + "github.com/df-mc/dragonfly/server/internal/nbtconv" + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/item/inventory" + "github.com/df-mc/dragonfly/server/world" + "github.com/go-gl/mathgl/mgl64" + "strings" + "sync" +) + +// Hopper is a low-capacity storage block that can be used to collect item entities directly above it, as well as to +// transfer items into and out of other containers. +type Hopper struct { + transparent + sourceWaterDisplacer + + // Facing is the direction the hopper is facing. + Facing cube.Face + // Powered is whether the hopper is powered or not. If the hopper is powered it will be locked and will stop + // moving items into or out of itself. + Powered bool + // CustomName is the custom name of the hopper. This name is displayed when the hopper is opened, and may include + // colour codes. + CustomName string + + // LastTick is the last world tick that the hopper was ticked. + LastTick int64 + // TransferCooldown is the duration in ticks until the hopper can transfer items again. + TransferCooldown int64 + // CollectCooldown is the duration in ticks until the hopper can collect items again. + CollectCooldown int64 + + inventory *inventory.Inventory + viewerMu *sync.RWMutex + viewers map[ContainerViewer]struct{} +} + +// NewHopper creates a new initialised hopper. The inventory is properly initialised. +func NewHopper() Hopper { + m := new(sync.RWMutex) + v := make(map[ContainerViewer]struct{}, 1) + return Hopper{ + inventory: inventory.New(5, func(slot int, _, item item.Stack) { + m.RLock() + defer m.RUnlock() + for viewer := range v { + viewer.ViewSlotChange(slot, item) + } + }), + viewerMu: m, + viewers: v, + } +} + +// Model ... +func (Hopper) Model() world.BlockModel { + return model.Hopper{} +} + +// SideClosed ... +func (Hopper) SideClosed(cube.Pos, cube.Pos, *world.World) bool { + return false +} + +// BreakInfo ... +func (h Hopper) BreakInfo() BreakInfo { + return newBreakInfo(3, pickaxeHarvestable, pickaxeEffective, oneOf(h)).withBlastResistance(24).withBreakHandler(func(pos cube.Pos, w *world.World, u item.User) { + for _, i := range h.Inventory(w, pos).Clear() { + dropItem(w, i, pos.Vec3()) + } + }) +} + +// Inventory returns the inventory of the hopper. +func (h Hopper) Inventory(*world.World, cube.Pos) *inventory.Inventory { + return h.inventory +} + +// WithName returns the hopper after applying a specific name to the block. +func (h Hopper) WithName(a ...any) world.Item { + h.CustomName = strings.TrimSuffix(fmt.Sprintln(a...), "\n") + return h +} + +// AddViewer adds a viewer to the hopper, so that it is updated whenever the inventory of the hopper is changed. +func (h Hopper) AddViewer(v ContainerViewer, _ *world.World, _ cube.Pos) { + h.viewerMu.Lock() + defer h.viewerMu.Unlock() + h.viewers[v] = struct{}{} +} + +// RemoveViewer removes a viewer from the hopper, so that slot updates in the inventory are no longer sent to it. +func (h Hopper) RemoveViewer(v ContainerViewer, _ *world.World, _ cube.Pos) { + h.viewerMu.Lock() + defer h.viewerMu.Unlock() + delete(h.viewers, v) +} + +// Activate ... +func (Hopper) Activate(pos cube.Pos, _ cube.Face, _ *world.World, u item.User, _ *item.UseContext) bool { + if opener, ok := u.(ContainerOpener); ok { + opener.OpenBlockContainer(pos) + return true + } + return false +} + +// UseOnBlock ... +func (h Hopper) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, w *world.World, user item.User, ctx *item.UseContext) bool { + pos, _, used := firstReplaceable(w, pos, face, h) + if !used { + return false + } + + //noinspection GoAssignmentToReceiver + h = NewHopper() + h.Facing = cube.FaceDown + if h.Facing != face { + h.Facing = face.Opposite() + } + + place(w, pos, h, user, ctx) + return placed(ctx) +} + +// Tick ... +func (h Hopper) Tick(currentTick int64, pos cube.Pos, w *world.World) { + h.TransferCooldown-- + h.CollectCooldown-- + h.LastTick = currentTick + + if !h.Powered && h.TransferCooldown <= 0 { + inserted := h.insertItem(pos, w) + extracted := h.extractItem(pos, w) + if inserted || extracted { + h.TransferCooldown = 8 + } + } + + w.SetBlock(pos, h, nil) +} + +// HopperInsertable represents a block that can have its contents inserted into by a hopper. +type HopperInsertable interface { + // InsertItem handles the insert logic for that block. + InsertItem(h Hopper, pos cube.Pos, w *world.World) bool +} + +// insertItem inserts an item into a block that can receive contents from the hopper. +func (h Hopper) insertItem(pos cube.Pos, w *world.World) bool { + destPos := pos.Side(h.Facing) + dest := w.Block(destPos) + + if e, ok := dest.(HopperInsertable); ok { + return e.InsertItem(h, pos.Side(h.Facing), w) + } + + if container, ok := dest.(Container); ok { + for sourceSlot, sourceStack := range h.inventory.Slots() { + if sourceStack.Empty() { + continue + } + + _, err := container.Inventory(w, pos).AddItem(sourceStack.Grow(-sourceStack.Count() + 1)) + if err != nil { + // The destination is full. + return false + } + + _ = h.inventory.SetItem(sourceSlot, sourceStack.Grow(-1)) + + if hopper, ok := dest.(Hopper); ok { + hopper.TransferCooldown = 8 + w.SetBlock(destPos, hopper, nil) + } + + return true + } + } + return false +} + +// HopperExtractable represents a block that can have its contents extracted by a hopper. +type HopperExtractable interface { + // ExtractItem handles the extract logic for that block. + ExtractItem(h Hopper, pos cube.Pos, w *world.World) bool +} + +// extractItem extracts an item from a container into the hopper. +func (h Hopper) extractItem(pos cube.Pos, w *world.World) bool { + originPos := pos.Side(cube.FaceUp) + origin := w.Block(originPos) + + if e, ok := origin.(HopperExtractable); ok { + return e.ExtractItem(h, pos, w) + } + + if containerOrigin, ok := origin.(Container); ok { + for slot, stack := range containerOrigin.Inventory(w, originPos).Slots() { + if stack.Empty() { + // We don't have any items to extract. + continue + } + + _, err := h.inventory.AddItem(stack.Grow(-stack.Count() + 1)) + if err != nil { + // The hopper is full. + continue + } + + _ = containerOrigin.Inventory(w, originPos).SetItem(slot, stack.Grow(-1)) + + if hopper, ok := origin.(Hopper); ok { + hopper.TransferCooldown = 8 + w.SetBlock(originPos, hopper, nil) + } + + return true + } + } + return false +} + +// EncodeItem ... +func (Hopper) EncodeItem() (name string, meta int16) { + return "minecraft:hopper", 0 +} + +// EncodeBlock ... +func (h Hopper) EncodeBlock() (string, map[string]any) { + return "minecraft:hopper", map[string]any{ + "facing_direction": int32(h.Facing), + "toggle_bit": h.Powered, + } +} + +// EncodeNBT ... +func (h Hopper) EncodeNBT() map[string]any { + if h.inventory == nil { + facing, powered, customName := h.Facing, h.Powered, h.CustomName + //noinspection GoAssignmentToReceiver + h = NewHopper() + h.Facing, h.Powered, h.CustomName = facing, powered, customName + } + m := map[string]any{ + "Items": nbtconv.InvToNBT(h.inventory), + "TransferCooldown": int32(h.TransferCooldown), + "id": "Hopper", + } + if h.CustomName != "" { + m["CustomName"] = h.CustomName + } + return m +} + +// DecodeNBT ... +func (h Hopper) DecodeNBT(data map[string]any) any { + facing, powered := h.Facing, h.Powered + //noinspection GoAssignmentToReceiver + h = NewHopper() + h.Facing = facing + h.Powered = powered + h.CustomName = nbtconv.String(data, "CustomName") + h.TransferCooldown = int64(nbtconv.Int32(data, "TransferCooldown")) + nbtconv.InvFromNBT(h.inventory, nbtconv.Slice(data, "Items")) + return h +} + +// allHoppers ... +func allHoppers() (hoppers []world.Block) { + for _, f := range cube.Faces() { + hoppers = append(hoppers, Hopper{Facing: f}) + hoppers = append(hoppers, Hopper{Facing: f, Powered: true}) + } + return hoppers +} diff --git a/server/block/jukebox.go b/server/block/jukebox.go index 2cdd43b8c..67acb96ae 100644 --- a/server/block/jukebox.go +++ b/server/block/jukebox.go @@ -19,6 +19,35 @@ type Jukebox struct { Item item.Stack } +// InsertItem ... +func (j Jukebox) InsertItem(h Hopper, pos cube.Pos, w *world.World) bool { + if !j.Item.Empty() { + return false + } + + for sourceSlot, sourceStack := range h.inventory.Slots() { + if sourceStack.Empty() { + continue + } + + if m, ok := sourceStack.Item().(item.MusicDisc); ok { + j.Item = sourceStack + w.SetBlock(pos, j, nil) + _ = h.inventory.SetItem(sourceSlot, sourceStack.Grow(-1)) + w.PlaySound(pos.Vec3Centre(), sound.MusicDiscPlay{DiscType: m.DiscType}) + return true + } + } + + return false +} + +// ExtractItem ... +func (j Jukebox) ExtractItem(h Hopper, pos cube.Pos, w *world.World) bool { + //TODO: This functionality requires redstone to be implemented. + return false +} + // FuelInfo ... func (j Jukebox) FuelInfo() item.FuelInfo { return newFuelInfo(time.Second * 15) @@ -32,6 +61,7 @@ func (j Jukebox) BreakInfo() BreakInfo { } return newBreakInfo(0.8, alwaysHarvestable, axeEffective, simpleDrops(d...)).withBreakHandler(func(pos cube.Pos, w *world.World, u item.User) { if _, hasDisc := j.Disc(); hasDisc { + dropItem(w, j.Item, pos.Vec3()) w.PlaySound(pos.Vec3Centre(), sound.MusicDiscEnd{}) } }) diff --git a/server/block/ladder.go b/server/block/ladder.go index 0e62a68ca..48e26076b 100644 --- a/server/block/ladder.go +++ b/server/block/ladder.go @@ -16,13 +16,14 @@ type Ladder struct { transparent sourceWaterDisplacer - // Facing is the side of the block the ladder is currently attached to. - Facing cube.Direction + // Facing is the side of the block the ladder is currently attached to. cube.FaceDown and cube.FaceUp + // do not do anything in game but they are still valid states. + Facing cube.Face } // NeighbourUpdateTick ... func (l Ladder) NeighbourUpdateTick(pos, _ cube.Pos, w *world.World) { - if _, ok := w.Block(pos.Side(l.Facing.Opposite().Face())).(LightDiffuser); ok { + if _, ok := w.Block(pos.Side(l.Facing.Opposite())).(LightDiffuser); ok { w.SetBlock(pos, nil, nil) w.AddParticle(pos.Vec3Centre(), particle.BlockBreak{Block: l}) dropItem(w, item.NewStack(l, 1), pos.Vec3Centre()) @@ -51,7 +52,7 @@ func (l Ladder) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, w *world. return false } } - l.Facing = face.Direction() + l.Facing = face place(w, pos, l, user, ctx) return placed(ctx) @@ -86,7 +87,7 @@ func (l Ladder) EncodeItem() (name string, meta int16) { // EncodeBlock ... func (l Ladder) EncodeBlock() (string, map[string]any) { - return "minecraft:ladder", map[string]any{"facing_direction": int32(l.Facing + 2)} + return "minecraft:ladder", map[string]any{"facing_direction": int32(l.Facing)} } // Model ... @@ -96,8 +97,8 @@ func (l Ladder) Model() world.BlockModel { // allLadders ... func allLadders() (b []world.Block) { - for i := cube.Direction(0); i <= 3; i++ { - b = append(b, Ladder{Facing: i}) + for _, f := range cube.Faces() { + b = append(b, Ladder{Facing: f}) } return } diff --git a/server/block/light.go b/server/block/light.go index a52932b01..49302ed71 100644 --- a/server/block/light.go +++ b/server/block/light.go @@ -3,6 +3,7 @@ package block import ( "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" + "strconv" ) // Light is an invisible block that can produce any light level. @@ -24,7 +25,7 @@ func (Light) SideClosed(cube.Pos, cube.Pos, *world.World) bool { // EncodeItem ... func (l Light) EncodeItem() (name string, meta int16) { - return "minecraft:light_block", int16(l.Level) + return "minecraft:light_block_" + strconv.Itoa(l.Level), 0 } // LightEmissionLevel ... @@ -34,7 +35,7 @@ func (l Light) LightEmissionLevel() uint8 { // EncodeBlock ... func (l Light) EncodeBlock() (name string, properties map[string]any) { - return "minecraft:light_block", map[string]any{"block_light_level": int32(l.Level)} + return "minecraft:light_block_" + strconv.Itoa(l.Level), nil } // allLight returns all possible light blocks. diff --git a/server/block/model/hopper.go b/server/block/model/hopper.go new file mode 100644 index 000000000..f84821545 --- /dev/null +++ b/server/block/model/hopper.go @@ -0,0 +1,23 @@ +package model + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" +) + +// Hopper is a model used by hoppers. +type Hopper struct{} + +// BBox returns a physics.BBox that spans a full block. +func (h Hopper) BBox(cube.Pos, *world.World) []cube.BBox { + bbox := []cube.BBox{full.ExtendTowards(cube.FaceUp, -0.375)} + for _, f := range cube.HorizontalFaces() { + bbox = append(bbox, full.ExtendTowards(f, -0.875)) + } + return bbox +} + +// FaceSolid only returns true for the top face of the hopper. +func (Hopper) FaceSolid(_ cube.Pos, face cube.Face, _ *world.World) bool { + return face == cube.FaceUp +} diff --git a/server/block/model/ladder.go b/server/block/model/ladder.go index 93274019c..cd18b8938 100644 --- a/server/block/model/ladder.go +++ b/server/block/model/ladder.go @@ -8,12 +8,12 @@ import ( // Ladder is the model for a ladder block. type Ladder struct { // Facing is the side opposite to the block the Ladder is currently attached to. - Facing cube.Direction + Facing cube.Face } // BBox returns one physics.BBox that depends on the facing direction of the Ladder. func (l Ladder) BBox(cube.Pos, *world.World) []cube.BBox { - return []cube.BBox{full.ExtendTowards(l.Facing.Face(), -0.8125)} + return []cube.BBox{full.ExtendTowards(l.Facing, -0.8125)} } // FaceSolid always returns false. diff --git a/server/block/prismarine.go b/server/block/prismarine.go index 63d2ad079..ea6da7a05 100644 --- a/server/block/prismarine.go +++ b/server/block/prismarine.go @@ -20,12 +20,12 @@ func (p Prismarine) BreakInfo() BreakInfo { // EncodeItem ... func (p Prismarine) EncodeItem() (id string, meta int16) { - return "minecraft:prismarine", int16(p.Type.Uint8()) + return "minecraft:" + p.Type.String(), 0 } // EncodeBlock ... func (p Prismarine) EncodeBlock() (name string, properties map[string]any) { - return "minecraft:prismarine", map[string]any{"prismarine_block_type": p.Type.String()} + return "minecraft:" + p.Type.String(), nil } // allPrismarine returns a list of all prismarine block variants. diff --git a/server/block/prismarine_type.go b/server/block/prismarine_type.go index a232f8783..b2ff68d2e 100644 --- a/server/block/prismarine_type.go +++ b/server/block/prismarine_type.go @@ -44,11 +44,11 @@ func (s prismarine) Name() string { func (s prismarine) String() string { switch s { case 0: - return "default" + return "prismarine" case 1: - return "dark" + return "dark_prismarine" case 2: - return "bricks" + return "prismarine_bricks" } panic("unknown prismarine type") } diff --git a/server/block/quartz.go b/server/block/quartz.go index a8d469346..6e5f6e92e 100644 --- a/server/block/quartz.go +++ b/server/block/quartz.go @@ -68,37 +68,37 @@ func (q Quartz) SmeltInfo() item.SmeltInfo { // EncodeItem ... func (q Quartz) EncodeItem() (name string, meta int16) { if q.Smooth { - return "minecraft:quartz_block", 3 + return "minecraft:smooth_quartz", 0 } return "minecraft:quartz_block", 0 } // EncodeItem ... func (c ChiseledQuartz) EncodeItem() (name string, meta int16) { - return "minecraft:quartz_block", 1 + return "minecraft:chiseled_quartz_block", 0 } // EncodeItem ... func (q QuartzPillar) EncodeItem() (name string, meta int16) { - return "minecraft:quartz_block", 2 + return "minecraft:quartz_pillar", 0 } // EncodeBlock ... func (q Quartz) EncodeBlock() (name string, properties map[string]any) { if q.Smooth { - return "minecraft:quartz_block", map[string]any{"chisel_type": "smooth", "pillar_axis": "y"} + return "minecraft:smooth_quartz", map[string]any{"pillar_axis": "y"} } - return "minecraft:quartz_block", map[string]any{"chisel_type": "default", "pillar_axis": "y"} + return "minecraft:quartz_block", map[string]any{"pillar_axis": "y"} } // EncodeBlock ... func (ChiseledQuartz) EncodeBlock() (name string, properties map[string]any) { - return "minecraft:quartz_block", map[string]any{"chisel_type": "chiseled", "pillar_axis": "y"} + return "minecraft:chiseled_quartz_block", map[string]any{"pillar_axis": "y"} } // EncodeBlock ... func (q QuartzPillar) EncodeBlock() (name string, properties map[string]any) { - return "minecraft:quartz_block", map[string]any{"pillar_axis": q.Axis.String(), "chisel_type": "lines"} + return "minecraft:quartz_pillar", map[string]any{"pillar_axis": q.Axis.String()} } // allQuartz ... diff --git a/server/block/register.go b/server/block/register.go index de4d60448..5a1db6c0a 100644 --- a/server/block/register.go +++ b/server/block/register.go @@ -149,6 +149,7 @@ func init() { registerAll(allGlazedTerracotta()) registerAll(allGrindstones()) registerAll(allHayBales()) + registerAll(allHoppers()) registerAll(allItemFrames()) registerAll(allKelp()) registerAll(allLadders()) @@ -258,6 +259,7 @@ func init() { world.RegisterItem(Grindstone{}) world.RegisterItem(HayBale{}) world.RegisterItem(Honeycomb{}) + world.RegisterItem(Hopper{}) world.RegisterItem(InvisibleBedrock{}) world.RegisterItem(IronBars{}) world.RegisterItem(Iron{}) diff --git a/server/block/sand.go b/server/block/sand.go index b5dd7c020..c5e9899ae 100644 --- a/server/block/sand.go +++ b/server/block/sand.go @@ -43,7 +43,7 @@ func (Sand) SmeltInfo() item.SmeltInfo { // EncodeItem ... func (s Sand) EncodeItem() (name string, meta int16) { if s.Red { - return "minecraft:sand", 1 + return "minecraft:red_sand", 0 } return "minecraft:sand", 0 } @@ -51,7 +51,7 @@ func (s Sand) EncodeItem() (name string, meta int16) { // EncodeBlock ... func (s Sand) EncodeBlock() (string, map[string]any) { if s.Red { - return "minecraft:sand", map[string]any{"sand_type": "red"} + return "minecraft:red_sand", nil } - return "minecraft:sand", map[string]any{"sand_type": "normal"} + return "minecraft:sand", nil } diff --git a/server/block/sandstone.go b/server/block/sandstone.go index fe134268b..1254cc0a6 100644 --- a/server/block/sandstone.go +++ b/server/block/sandstone.go @@ -33,10 +33,15 @@ func (s Sandstone) EncodeItem() (name string, meta int16) { // EncodeBlock ... func (s Sandstone) EncodeBlock() (string, map[string]any) { + var prefix string + if s.Type != NormalSandstone() { + prefix = s.Type.String() + "_" + } + suffix := "sandstone" if s.Red { - return "minecraft:red_sandstone", map[string]any{"sand_stone_type": s.Type.String()} + suffix = "red_sandstone" } - return "minecraft:sandstone", map[string]any{"sand_stone_type": s.Type.String()} + return "minecraft:" + prefix + suffix, nil } // SmeltInfo ... diff --git a/server/block/sandstone_type.go b/server/block/sandstone_type.go index 489052001..dff52cd3e 100644 --- a/server/block/sandstone_type.go +++ b/server/block/sandstone_type.go @@ -55,7 +55,7 @@ func (s sandstone) String() string { case 1: return "cut" case 2: - return "heiroglyphs" + return "chiseled" case 3: return "smooth" } diff --git a/server/block/slab.go b/server/block/slab.go index 09b11d4c3..25b41a856 100644 --- a/server/block/slab.go +++ b/server/block/slab.go @@ -151,38 +151,20 @@ func (s Slab) Model() world.BlockModel { // EncodeItem ... func (s Slab) EncodeItem() (string, int16) { - id, slabType, meta, _ := encodeSlabBlock(s.Block) - if slabType != "" { - return "minecraft:" + encodeLegacySlabId(slabType), meta - } - return "minecraft:" + id + "_slab", meta + return "minecraft:" + encodeSlabBlock(s.Block) + "_slab", 0 } // EncodeBlock ... func (s Slab) EncodeBlock() (string, map[string]any) { - id, slabType, _, halfFlattened := encodeSlabBlock(s.Block) side := "bottom" if s.Top { side = "top" } - properties := map[string]any{"minecraft:vertical_half": side} - if slabType != "" && !halfFlattened { - properties[slabType] = id - id = encodeLegacySlabId(slabType) - if s.Double { - id = "double_" + id - } - } else if s.Double { - if halfFlattened { - properties[slabType] = id - id = "double_" + encodeLegacySlabId(slabType) - } else { - id = id + "_double_slab" - } - } else { - id = id + "_slab" + suffix := "_slab" + if s.Double { + suffix = "_double_slab" } - return "minecraft:" + id, properties + return "minecraft:" + encodeSlabBlock(s.Block) + suffix, map[string]any{"minecraft:vertical_half": side} } // allSlabs ... diff --git a/server/block/slab_type.go b/server/block/slab_type.go index 5c0603b7f..4f4a87c98 100644 --- a/server/block/slab_type.go +++ b/server/block/slab_type.go @@ -7,135 +7,118 @@ import ( // encodeSlabBlock encodes the provided block in to an identifier and meta value that can be used to encode the slab. // halfFlattened is a temporary hack for a stone_block_slab which has been flattened but double_stone_block_slab // has not. This can be removed in 1.21.10 where they have flattened all slab types. -func encodeSlabBlock(block world.Block) (id, slabType string, meta int16, halfFlattened bool) { +func encodeSlabBlock(block world.Block) (id string) { switch block := block.(type) { // TODO: Copper case Andesite: if block.Polished { - return "polished_andesite", "stone_slab_type_3", 2, false + return "polished_andesite" } - return "andesite", "stone_slab_type_3", 3, false + return "andesite" case Blackstone: if block.Type == NormalBlackstone() { - return "blackstone", "", 0, false + return "blackstone" } else if block.Type == PolishedBlackstone() { - return "polished_blackstone", "", 0, false + return "polished_blackstone" } case Bricks: - return "brick", "stone_slab_type", 4, true + return "brick" case Cobblestone: if block.Mossy { - return "mossy_cobblestone", "stone_slab_type_2", 5, false + return "mossy_cobblestone" } - return "cobblestone", "stone_slab_type", 3, true + return "cobblestone" case Deepslate: if block.Type == CobbledDeepslate() { - return "cobbled_deepslate", "", 0, false + return "cobbled_deepslate" } else if block.Type == PolishedDeepslate() { - return "polished_deepslate", "", 0, false + return "polished_deepslate" } case DeepslateBricks: if !block.Cracked { - return "deepslate_brick", "", 0, false + return "deepslate_brick" } case DeepslateTiles: if !block.Cracked { - return "deepslate_tile", "", 0, false + return "deepslate_tile" } case Diorite: if block.Polished { - return "polished_diorite", "stone_slab_type_3", 5, false + return "polished_diorite" } - return "diorite", "stone_slab_type_3", 4, false + return "diorite" case EndBricks: - return "end_stone_brick", "stone_slab_type_3", 0, false + return "end_stone_brick" case Granite: if block.Polished { - return "polished_granite", "stone_slab_type_3", 7, false + return "polished_granite" } - return "granite", "stone_slab_type_3", 6, false + return "granite" case MudBricks: - return "mud_brick", "", 0, false + return "mud_brick" case NetherBricks: if block.Type == RedNetherBricks() { - return "nether_brick", "stone_slab_type", 7, true + return "nether_brick" } - return "red_nether_brick", "stone_slab_type_2", 7, false + return "red_nether_brick" case Planks: - return block.Wood.String(), "", 0, false + return block.Wood.String() case PolishedBlackstoneBrick: if !block.Cracked { - return "polished_blackstone_brick", "", 0, false + return "polished_blackstone_brick" } case Prismarine: switch block.Type { case NormalPrismarine(): - return "prismarine_rough", "stone_slab_type_2", 2, false + return "prismarine" case DarkPrismarine(): - return "prismarine_dark", "stone_slab_type_2", 3, false + return "dark_prismarine" case BrickPrismarine(): - return "prismarine_brick", "stone_slab_type_2", 4, false + return "prismarine_brick" } panic("invalid prismarine type") case Purpur: - return "purpur", "stone_slab_type_2", 1, false + return "purpur" case Quartz: if block.Smooth { - return "smooth_quartz", "stone_slab_type_4", 1, false + return "smooth_quartz" } - return "quartz", "stone_slab_type", 6, true + return "quartz" case Sandstone: switch block.Type { case NormalSandstone(): if block.Red { - return "red_sandstone", "stone_slab_type_2", 0, false + return "red_sandstone" } - return "sandstone", "stone_slab_type", 1, true + return "sandstone" case CutSandstone(): if block.Red { - return "cut_red_sandstone", "stone_slab_type_4", 4, false + return "cut_red_sandstone" } - return "cut_sandstone", "stone_slab_type_4", 3, false + return "cut_sandstone" case SmoothSandstone(): if block.Red { - return "smooth_red_sandstone", "stone_slab_type_3", 1, false + return "smooth_red_sandstone" } - return "smooth_sandstone", "stone_slab_type_2", 6, false + return "smooth_sandstone" } panic("invalid sandstone type") case Stone: if block.Smooth { - return "smooth_stone", "stone_slab_type", 0, true + return "smooth_stone" } - return "stone", "stone_slab_type_4", 2, false + return "normal_stone" case StoneBricks: if block.Type == MossyStoneBricks() { - return "mossy_stone_brick", "stone_slab_type_4", 0, false + return "mossy_stone_brick" } - return "stone_brick", "stone_slab_type", 5, true + return "stone_brick" case Tuff: - return "tuff", "", 0, false + return "tuff" } panic("invalid block used for slab") } -// encodeLegacySlabId encodes a legacy slab type to its identifier. -func encodeLegacySlabId(slabType string) string { - switch slabType { - case "wood_type": - return "wooden_slab" - case "stone_slab_type": - return "stone_block_slab" - case "stone_slab_type_2": - return "stone_block_slab2" - case "stone_slab_type_3": - return "stone_block_slab3" - case "stone_slab_type_4": - return "stone_block_slab4" - } - panic("invalid slab type") -} - // SlabBlocks returns a list of all possible blocks for a slab. func SlabBlocks() []world.Block { b := []world.Block{ diff --git a/server/block/smelter.go b/server/block/smelter.go index 26b1a6fe2..6a61986ab 100644 --- a/server/block/smelter.go +++ b/server/block/smelter.go @@ -38,6 +38,80 @@ func newSmelter() *smelter { return s } +// InsertItem ... +func (s *smelter) InsertItem(h Hopper, pos cube.Pos, w *world.World) bool { + for sourceSlot, sourceStack := range h.inventory.Slots() { + var slot int + + if sourceStack.Empty() { + continue + } + + if h.Facing != cube.FaceDown { + slot = 1 + } else { + slot = 0 + } + + stack := sourceStack.Grow(-sourceStack.Count() + 1) + it, _ := s.Inventory(w, pos).Item(slot) + if slot == 1 { + if _, ok := sourceStack.Item().(item.Fuel); !ok { + // The item is not fuel. + continue + } + } + if !sourceStack.Comparable(it) { + // The items are not the same. + continue + } + if it.Count() == it.MaxCount() { + // The item has the maximum count that the stack is able to hold. + continue + } + if !it.Empty() { + stack = it.Grow(1) + } + + _ = s.Inventory(w, pos).SetItem(slot, stack) + _ = h.inventory.SetItem(sourceSlot, sourceStack.Grow(-1)) + return true + } + + return false +} + +// ExtractItem ... +func (s *smelter) ExtractItem(h Hopper, pos cube.Pos, w *world.World) bool { + for sourceSlot, sourceStack := range s.inventory.Slots() { + if sourceStack.Empty() { + continue + } + + if sourceSlot == 0 { + continue + } + + if sourceSlot == 1 { + fuel, ok := sourceStack.Item().(item.Fuel) + if ok && fuel.FuelInfo().Duration.Seconds() != 0 { + continue + } + } + + _, err := h.inventory.AddItem(sourceStack.Grow(-sourceStack.Count() + 1)) + if err != nil { + // The hopper is full. + continue + } + + _ = s.Inventory(w, pos).SetItem(sourceSlot, sourceStack.Grow(-1)) + return true + } + + return false +} + // Durations returns the remaining, maximum, and cook durations of the smelter. func (s *smelter) Durations() (remaining time.Duration, max time.Duration, cook time.Duration) { s.mu.Lock() @@ -62,7 +136,7 @@ func (s *smelter) ResetExperience() int { } // Inventory returns the inventory of the furnace. -func (s *smelter) Inventory() *inventory.Inventory { +func (s *smelter) Inventory(*world.World, cube.Pos) *inventory.Inventory { return s.inventory } diff --git a/server/block/smoker.go b/server/block/smoker.go index 94c2f8e8d..a43a206fd 100644 --- a/server/block/smoker.go +++ b/server/block/smoker.go @@ -73,7 +73,11 @@ func (s Smoker) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, w *world. // BreakInfo ... func (s Smoker) BreakInfo() BreakInfo { xp := s.Experience() - return newBreakInfo(3.5, alwaysHarvestable, pickaxeEffective, oneOf(s)).withXPDropRange(xp, xp) + return newBreakInfo(3.5, alwaysHarvestable, pickaxeEffective, oneOf(s)).withXPDropRange(xp, xp).withBreakHandler(func(pos cube.Pos, w *world.World, u item.User) { + for _, i := range s.Inventory(w, pos).Clear() { + dropItem(w, i, pos.Vec3()) + } + }) } // Activate ... @@ -97,7 +101,7 @@ func (s Smoker) EncodeNBT() map[string]interface{} { "CookTime": int16(cook.Milliseconds() / 50), "BurnDuration": int16(maximum.Milliseconds() / 50), "StoredXPInt": int16(s.Experience()), - "Items": nbtconv.InvToNBT(s.Inventory()), + "Items": nbtconv.InvToNBT(s.inventory), "id": "Smoker", } } @@ -116,7 +120,7 @@ func (s Smoker) DecodeNBT(data map[string]interface{}) interface{} { s.Lit = lit s.setExperience(xp) s.setDurations(remaining, maximum, cook) - nbtconv.InvFromNBT(s.Inventory(), nbtconv.Slice(data, "Items")) + nbtconv.InvFromNBT(s.inventory, nbtconv.Slice(data, "Items")) return s } diff --git a/server/block/stone_bricks.go b/server/block/stone_bricks.go index 65ff6d581..0e043ce5e 100644 --- a/server/block/stone_bricks.go +++ b/server/block/stone_bricks.go @@ -30,12 +30,12 @@ func (s StoneBricks) SmeltInfo() item.SmeltInfo { // EncodeItem ... func (s StoneBricks) EncodeItem() (name string, meta int16) { - return "minecraft:stonebrick", int16(s.Type.Uint8()) + return "minecraft:" + s.Type.String(), 0 } // EncodeBlock ... func (s StoneBricks) EncodeBlock() (string, map[string]any) { - return "minecraft:stonebrick", map[string]any{"stone_brick_type": s.Type.String()} + return "minecraft:" + s.Type.String(), nil } // allStoneBricks returns a list of all stoneBricks block variants. diff --git a/server/block/stone_bricks_type.go b/server/block/stone_bricks_type.go index 9a2c50f64..a831fe553 100644 --- a/server/block/stone_bricks_type.go +++ b/server/block/stone_bricks_type.go @@ -51,13 +51,13 @@ func (s stoneBricks) Name() string { func (s stoneBricks) String() string { switch s { case 0: - return "default" + return "stone_bricks" case 1: - return "mossy" + return "mossy_stone_bricks" case 2: - return "cracked" + return "cracked_stone_bricks" case 3: - return "chiseled" + return "chiseled_stone_bricks" } panic("unknown stone bricks type") } diff --git a/server/cmd/output.go b/server/cmd/output.go index c7e09295a..cc8be47b2 100644 --- a/server/cmd/output.go +++ b/server/cmd/output.go @@ -1,6 +1,9 @@ package cmd -import "fmt" +import ( + "errors" + "fmt" +) // Output holds the output of a command execution. It holds success messages and error messages, which the // source of a command execution gets sent. @@ -16,7 +19,7 @@ func (o *Output) Errorf(format string, a ...any) { // Error formats an error message and adds it to the command output. func (o *Output) Error(a ...any) { - o.errors = append(o.errors, fmt.Errorf(fmt.Sprint(a...))) + o.errors = append(o.errors, errors.New(fmt.Sprint(a...))) } // Printf formats a (success) message and adds it to the command output. diff --git a/server/entity/item_behaviour.go b/server/entity/item_behaviour.go index 61de607d1..f1246c17c 100644 --- a/server/entity/item_behaviour.go +++ b/server/entity/item_behaviour.go @@ -1,6 +1,8 @@ package entity import ( + "github.com/df-mc/dragonfly/server/block" + "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/internal/nbtconv" "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/world" @@ -65,6 +67,26 @@ func (i *ItemBehaviour) Item() item.Stack { // Tick moves the entity, checks if it should be picked up by a nearby collector // or if it should merge with nearby item entities. func (i *ItemBehaviour) Tick(e *Ent) *Movement { + w := e.World() + pos := cube.PosFromVec3(e.Position()) + blockPos := pos.Side(cube.FaceDown) + + bl, ok := w.Block(blockPos).(block.Hopper) + if ok && !bl.Powered && bl.CollectCooldown <= 0 { + addedCount, err := bl.Inventory(w, blockPos).AddItem(i.i) + if err != nil { + if addedCount == 0 { + return i.passive.Tick(e) + } + + // This is only reached if part of the item stack was collected into the hopper. + w.AddEntity(NewItem(i.Item().Grow(-addedCount), pos.Vec3Centre())) + } + + _ = e.Close() + bl.CollectCooldown = 8 + w.SetBlock(blockPos, bl, nil) + } return i.passive.Tick(e) } diff --git a/server/item/creative/creative_items.nbt b/server/item/creative/creative_items.nbt index b223f798d..692d5d66e 100644 Binary files a/server/item/creative/creative_items.nbt and b/server/item/creative/creative_items.nbt differ diff --git a/server/item/inventory/inventory.go b/server/item/inventory/inventory.go index 005324d1e..64246f1dc 100644 --- a/server/item/inventory/inventory.go +++ b/server/item/inventory/inventory.go @@ -42,6 +42,13 @@ func New(size int, f func(slot int, before, after item.Stack)) *Inventory { return &Inventory{h: NopHandler{}, slots: make([]item.Stack, size), f: f, canAdd: func(s item.Stack, slot int) bool { return true }} } +func (inv *Inventory) Clone(f func(slot int, before, after item.Stack)) *Inventory { + if f == nil { + f = func(slot int, before, after item.Stack) {} + } + return &Inventory{h: NopHandler{}, slots: inv.Slots(), f: f, canAdd: func(s item.Stack, slot int) bool { return true }} +} + // Item attempts to obtain an item from a specific slot in the inventory. If an item was present in that slot, // the item is returned and the error is nil. If no item was present in the slot, a Stack with air as its item // and a count of 0 is returned. Stack.Empty() may be called to check if this is the case. @@ -265,6 +272,18 @@ func (inv *Inventory) ContainsItemFunc(n int, comparable func(stack item.Stack) return n <= 0 } +// Merge merges two inventories into one. The function passed is called for every slot change in the new inventory. +func (inv *Inventory) Merge(inv2 *Inventory, f func(int, item.Stack, item.Stack)) *Inventory { + inv.mu.RLock() + defer inv.mu.RUnlock() + inv2.mu.RLock() + defer inv2.mu.RUnlock() + + n := New(len(inv.slots)+len(inv2.slots), f) + n.slots = append(inv.slots, inv2.slots...) + return n +} + // Empty checks if the inventory is fully empty: It iterates over the inventory and makes sure every stack in // it is empty. func (inv *Inventory) Empty() bool { diff --git a/server/item/recipe/crafting_data.nbt b/server/item/recipe/crafting_data.nbt index 88d0bb46d..a0c2e71d6 100644 Binary files a/server/item/recipe/crafting_data.nbt and b/server/item/recipe/crafting_data.nbt differ diff --git a/server/player/diagnostics/diagnostics.go b/server/player/diagnostics/diagnostics.go new file mode 100644 index 000000000..1d0d7e62d --- /dev/null +++ b/server/player/diagnostics/diagnostics.go @@ -0,0 +1,29 @@ +package diagnostics + +// Diagnostics represents the latest diagnostics data of the client. +type Diagnostics struct { + // AverageFramesPerSecond is the average amount of frames per second that the client has been + // running at. + AverageFramesPerSecond float64 + // AverageServerSimTickTime is the average time that the server spends simulating a single tick + // in milliseconds. + AverageServerSimTickTime float64 + // AverageClientSimTickTime is the average time that the client spends simulating a single tick + // in milliseconds. + AverageClientSimTickTime float64 + // AverageBeginFrameTime is the average time that the client spends beginning a frame in + // milliseconds. + AverageBeginFrameTime float64 + // AverageInputTime is the average time that the client spends processing input in milliseconds. + AverageInputTime float64 + // AverageRenderTime is the average time that the client spends rendering in milliseconds. + AverageRenderTime float64 + // AverageEndFrameTime is the average time that the client spends ending a frame in milliseconds. + AverageEndFrameTime float64 + // AverageRemainderTimePercent is the average percentage of time that the client spends on + // tasks that are not accounted for. + AverageRemainderTimePercent float64 + // AverageUnaccountedTimePercent is the average percentage of time that the client spends on + // unaccounted tasks. + AverageUnaccountedTimePercent float64 +} diff --git a/server/player/handler.go b/server/player/handler.go index d2baa2320..981bfc9f6 100644 --- a/server/player/handler.go +++ b/server/player/handler.go @@ -5,6 +5,7 @@ import ( "github.com/df-mc/dragonfly/server/cmd" "github.com/df-mc/dragonfly/server/event" "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/player/diagnostics" "github.com/df-mc/dragonfly/server/player/skin" "github.com/df-mc/dragonfly/server/world" "github.com/go-gl/mathgl/mgl64" @@ -55,6 +56,10 @@ type Handler interface { // HandleSkinChange handles the player changing their skin. ctx.Cancel() may be called to cancel the skin // change. HandleSkinChange(ctx *event.Context, skin *skin.Skin) + // HandleFireExtinguish handles the player extinguishing a fire at a specific position. ctx.Cancel() may + // be called to cancel the fire being extinguished. + // cube.Pos can be used to see where was the fire extinguished, may be used to cancel this on specific positions. + HandleFireExtinguish(ctx *event.Context, pos cube.Pos) // HandleStartBreak handles the player starting to break a block at the position passed. ctx.Cancel() may // be called to stop the player from breaking the block completely. HandleStartBreak(ctx *event.Context, pos cube.Pos) @@ -131,6 +136,10 @@ type Handler interface { // HandleQuit handles the closing of a player. It is always called when the player is disconnected, // regardless of the reason. HandleQuit() + // HandleDiagnostics handles the latest diagnostics data that the player has sent to the server. This is + // not sent by every client however, only those with the "Creator > Enable Client Diagnostics" setting + // enabled. + HandleDiagnostics(d diagnostics.Diagnostics) } // NopHandler implements the Handler interface but does not execute any code when an event is called. The @@ -152,6 +161,7 @@ func (NopHandler) HandleCommandExecution(*event.Context, cmd.Command, []string) func (NopHandler) HandleTransfer(*event.Context, *net.UDPAddr) {} func (NopHandler) HandleChat(*event.Context, *string) {} func (NopHandler) HandleSkinChange(*event.Context, *skin.Skin) {} +func (NopHandler) HandleFireExtinguish(*event.Context, cube.Pos) {} func (NopHandler) HandleStartBreak(*event.Context, cube.Pos) {} func (NopHandler) HandleBlockBreak(*event.Context, cube.Pos, *[]item.Stack, *int) {} func (NopHandler) HandleBlockPlace(*event.Context, cube.Pos, world.Block) {} @@ -173,3 +183,4 @@ func (NopHandler) HandleFoodLoss(*event.Context, int, *int) func (NopHandler) HandleDeath(world.DamageSource, *bool) {} func (NopHandler) HandleRespawn(*mgl64.Vec3, **world.World) {} func (NopHandler) HandleQuit() {} +func (NopHandler) HandleDiagnostics(d diagnostics.Diagnostics) {} diff --git a/server/player/hunger.go b/server/player/hunger.go index da94a8c47..c6471b475 100644 --- a/server/player/hunger.go +++ b/server/player/hunger.go @@ -30,13 +30,14 @@ func (m *hungerManager) Food() int { // SetFood sets the food level of a player. The level passed must be in a range of 0-20. If the level passed // is negative, the food level will be set to 0. If the level exceeds 20, the food level will be set to 20. func (m *hungerManager) SetFood(level int) { + m.mu.Lock() + defer m.mu.Unlock() + if level < 0 { level = 0 } else if level > 20 { level = 20 } - m.mu.Lock() - defer m.mu.Unlock() m.foodLevel = level } @@ -58,11 +59,12 @@ func (m *hungerManager) AddFood(points int) { // using newHungerManager. func (m *hungerManager) Reset() { m.mu.Lock() + defer m.mu.Unlock() + m.foodLevel = 20 m.saturationLevel = 5 m.exhaustionLevel = 0 m.foodTick = 0 - m.mu.Unlock() } // exhaust exhausts the player by the amount of points passed. If the total exhaustion level exceeds 4, a @@ -87,6 +89,7 @@ func (m *hungerManager) exhaust(points float64) { // saturation will never exceed the total food value. func (m *hungerManager) saturate(food int, saturation float64) { m.mu.Lock() + defer m.mu.Unlock() level := m.foodLevel + food if level < 0 { @@ -106,7 +109,6 @@ func (m *hungerManager) saturate(food int, saturation float64) { sat = float64(m.foodLevel) } m.saturationLevel = sat - m.mu.Unlock() } // desaturate removes one saturation point from the player. If the saturation level of the player is already diff --git a/server/player/player.go b/server/player/player.go index c6c114aec..3718a3eaa 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -2,6 +2,7 @@ package player import ( "fmt" + "github.com/df-mc/dragonfly/server/player/diagnostics" "math" "math/rand" "net" @@ -1602,7 +1603,13 @@ func (p *Player) StartBreaking(pos cube.Pos, face cube.Face) { return } if _, ok := w.Block(pos.Side(face)).(block.Fire); ok { - // TODO: Add a way to cancel fire extinguishing. This is currently not possible to handle. + ctx := event.C() + if p.Handler().HandleFireExtinguish(ctx, pos); ctx.Cancelled() { + // Resend the block because on client side that was extinguished + p.resendBlocks(pos, w, face) + return + } + w.SetBlock(pos.Side(face), nil, nil) w.PlaySound(pos.Vec3(), sound.FireExtinguish{}) return @@ -1847,17 +1854,7 @@ func (p *Player) drops(held item.Stack, b world.Block) []item.Stack { t = item.ToolNone{} } var drops []item.Stack - if container, ok := b.(block.Container); ok { - // If the block is a container, it should drop its inventory contents regardless whether the - // player is in creative mode or not. - drops = container.Inventory().Items() - if breakable, ok := b.(block.Breakable); ok && !p.GameMode().CreativeInventory() { - if breakable.BreakInfo().Harvestable(t) { - drops = append(drops, breakable.BreakInfo().Drops(t, held.Enchantments())...) - } - } - container.Inventory().Clear() - } else if breakable, ok := b.(block.Breakable); ok && !p.GameMode().CreativeInventory() { + if breakable, ok := b.(block.Breakable); ok && !p.GameMode().CreativeInventory() { if breakable.BreakInfo().Harvestable(t) { drops = breakable.BreakInfo().Drops(t, held.Enchantments()) } @@ -2052,6 +2049,11 @@ func (p *Player) Rotation() cube.Rotation { return cube.Rotation{p.yaw.Load(), p.pitch.Load()} } +// ChangingDimension returns whether the player is currently changing dimension or not. +func (p *Player) ChangingDimension() bool { + return p.session().ChangingDimension() +} + // Collect makes the player collect the item stack passed, adding it to the inventory. The amount of items that could // be added is returned. func (p *Player) Collect(s item.Stack) int { @@ -2719,6 +2721,11 @@ func (p *Player) PunchAir() { p.World().PlaySound(p.Position(), sound.Attack{}) } +// UpdateDiagnostics updates the diagnostics of the player. +func (p *Player) UpdateDiagnostics(d diagnostics.Diagnostics) { + p.Handler().HandleDiagnostics(d) +} + // damageItem damages the item stack passed with the damage passed and returns the new stack. If the item // broke, a breaking sound is played. // If the player is not survival, the original stack is returned. diff --git a/server/session/controllable.go b/server/session/controllable.go index f14344e6c..bb144aa27 100644 --- a/server/session/controllable.go +++ b/server/session/controllable.go @@ -7,6 +7,7 @@ import ( "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/item/inventory" "github.com/df-mc/dragonfly/server/player/chat" + "github.com/df-mc/dragonfly/server/player/diagnostics" "github.com/df-mc/dragonfly/server/player/form" "github.com/df-mc/dragonfly/server/player/skin" "github.com/df-mc/dragonfly/server/world" @@ -99,4 +100,6 @@ type Controllable interface { // entity looks in the world. Skin() skin.Skin SetSkin(skin.Skin) + + UpdateDiagnostics(diagnostics.Diagnostics) } diff --git a/server/session/enchantment_texts.go b/server/session/enchantment_texts.go index 7d46a17fb..cf3e3e0af 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{"abimek", "aericio", "aimjel", "alvin0319", "andreas hgk", "atm85", "blackjack200", "cetfu", "da pig guy", "daft0175", "dasciam", "deniel world", "didntpot", "eminarican", "endermanbugzjfc", "erkam246", "flonja", "hashim the arab", "hochbaum", "hyper flare mc", "im da real ani", "its zodia x", "ivan craft623", "javier leon9966", "just tal develops", "liatoast", "mmm545", "mohamed587100", "myma qc", "natuyasai natuo", "neutronic mc", "nonono697", "provsalt", "restart fu", "riccskn", "robertdudaa", "royal mcpe", "sallypemdas", "sandertv", "sculas", "sqmatheus", "ssaini123456", "t14 raptor", "tadhunt", "thunder33345", "tristanmorgan", "twisted asylum mc", "unickorn", "unknown ore", "uramnoil", "wqrro", "x natsuri", "x4caa", "xd-pro"} +var enchantNames = []string{"aabstractt", "abimek", "aericio", "aimjel", "alvin0319", "andreas hgk", "atm85", "blackjack200", "cetfu", "cjmustard1452", "cqdetdev", "da pig guy", "daft0175", "dasciam", "deniel world", "didntpot", "eminarican", "endermanbugzjfc", "erkam246", "flonja", "gewinum", "hashim the arab", "hochbaum", "hyper flare mc", "im da real ani", "ipad54", "its zodia x", "ivan craft623", "javier leon9966", "just tal develops", "liatoast", "mmm545", "mohamed587100", "myma qc", "natuyasai natuo", "neutronic mc", "nonono697", "provsalt", "restart fu", "riccskn", "robertdudaa", "royal mcpe", "sallypemdas", "sandertv", "sculas", "sqmatheus", "ssaini123456", "t14 raptor", "tadhunt", "thunder33345", "tristanmorgan", "twisted asylum mc", "unickorn", "unknown ore", "uramnoil", "wqrro", "x natsuri", "x4caa", "xd-pro"} diff --git a/server/session/entity_metadata.go b/server/session/entity_metadata.go index f75783d85..421d8a455 100644 --- a/server/session/entity_metadata.go +++ b/server/session/entity_metadata.go @@ -141,22 +141,22 @@ func (s *Session) addSpecificMetadata(e any, m protocol.EntityMetadata) { m[protocol.EntityDataKeyCustomDisplay] = tip + 1 } } - if eff, ok := e.(effectBearer); ok && len(eff.Effects()) > 0 { - visibleEffects := make([]effect.Effect, 0, len(eff.Effects())) + if eff, ok := e.(effectBearer); ok { + var packedEffects int64 + for _, ef := range eff.Effects() { if !ef.ParticlesHidden() { - visibleEffects = append(visibleEffects, ef) - } - } - if len(visibleEffects) > 0 { - colour, am := effect.ResultingColour(visibleEffects) - m[protocol.EntityDataKeyEffectColor] = nbtconv.Int32FromRGBA(colour) - if am { - m[protocol.EntityDataKeyEffectAmbience] = byte(1) - } else { - m[protocol.EntityDataKeyEffectAmbience] = byte(0) + id, found := effect.ID(ef.Type()) + if !found { + continue + } + packedEffects = (packedEffects << 7) | int64(id<<1) + if ef.Ambient() { + packedEffects |= 1 + } } } + m[protocol.EntityDataKeyVisibleMobEffects] = packedEffects } if v, ok := e.(variable); ok { m[protocol.EntityDataKeyVariant] = v.Variant() diff --git a/server/session/handler_anvil.go b/server/session/handler_anvil.go index 737f7f48f..36780689d 100644 --- a/server/session/handler_anvil.go +++ b/server/session/handler_anvil.go @@ -35,15 +35,15 @@ func (h *ItemStackRequestHandler) handleCraftRecipeOptional(a *protocol.CraftRec } input, _ := h.itemInSlot(protocol.StackRequestSlotInfo{ - ContainerID: protocol.ContainerAnvilInput, - Slot: anvilInputSlot, + Container: protocol.FullContainerName{ContainerID: protocol.ContainerAnvilInput}, + Slot: anvilInputSlot, }, s) if input.Empty() { return fmt.Errorf("no item in input input slot") } material, _ := h.itemInSlot(protocol.StackRequestSlotInfo{ - ContainerID: protocol.ContainerAnvilMaterial, - Slot: anvilMaterialSlot, + Container: protocol.FullContainerName{ContainerID: protocol.ContainerAnvilMaterial}, + Slot: anvilMaterialSlot, }, s) result := input @@ -150,18 +150,18 @@ func (h *ItemStackRequestHandler) handleCraftRecipeOptional(a *protocol.CraftRec } h.setItemInSlot(protocol.StackRequestSlotInfo{ - ContainerID: protocol.ContainerAnvilInput, - Slot: anvilInputSlot, + Container: protocol.FullContainerName{ContainerID: protocol.ContainerAnvilInput}, + Slot: anvilInputSlot, }, item.Stack{}, s) if repairCount > 0 { h.setItemInSlot(protocol.StackRequestSlotInfo{ - ContainerID: protocol.ContainerAnvilMaterial, - Slot: anvilMaterialSlot, + Container: protocol.FullContainerName{ContainerID: protocol.ContainerAnvilMaterial}, + Slot: anvilMaterialSlot, }, material.Grow(-repairCount), s) } else { h.setItemInSlot(protocol.StackRequestSlotInfo{ - ContainerID: protocol.ContainerAnvilMaterial, - Slot: anvilMaterialSlot, + Container: protocol.FullContainerName{ContainerID: protocol.ContainerAnvilMaterial}, + Slot: anvilMaterialSlot, }, item.Stack{}, s) } return h.createResults(s, result) diff --git a/server/session/handler_beacon.go b/server/session/handler_beacon.go index 12f580e64..264ddf9f1 100644 --- a/server/session/handler_beacon.go +++ b/server/session/handler_beacon.go @@ -15,8 +15,8 @@ const beaconInputSlot = 0x1b // for those effects. func (h *ItemStackRequestHandler) handleBeaconPayment(a *protocol.BeaconPaymentStackRequestAction, s *Session) error { slot := protocol.StackRequestSlotInfo{ - ContainerID: protocol.ContainerBeaconPayment, - Slot: beaconInputSlot, + Container: protocol.FullContainerName{ContainerID: protocol.ContainerBeaconPayment}, + Slot: beaconInputSlot, } // First check if there actually is a beacon opened. if !s.containerOpened.Load() { diff --git a/server/session/handler_crafting.go b/server/session/handler_crafting.go index fdddee50b..a45ea956f 100644 --- a/server/session/handler_crafting.go +++ b/server/session/handler_crafting.go @@ -49,8 +49,8 @@ func (h *ItemStackRequestHandler) handleCraft(a *protocol.CraftRecipeStackReques processed, consumed[slot-offset] = true, true st := has.Grow(-expected.Count()) h.setItemInSlot(protocol.StackRequestSlotInfo{ - ContainerID: protocol.ContainerCraftingInput, - Slot: byte(slot), + Container: protocol.FullContainerName{ContainerID: protocol.ContainerCraftingInput}, + Slot: byte(slot), }, st, s) break } @@ -120,8 +120,8 @@ func (h *ItemStackRequestHandler) handleAutoCraft(a *protocol.AutoCraftRecipeSta expected, has = grow(expected, -removal), has.Grow(-removal) h.setItemInSlot(protocol.StackRequestSlotInfo{ - ContainerID: id, - Slot: byte(slot), + Container: protocol.FullContainerName{ContainerID: id}, + Slot: byte(slot), }, has, s) if expected.Empty() { // Consumed this item, so go to the next one. diff --git a/server/session/handler_enchanting.go b/server/session/handler_enchanting.go index 7c2166d74..01d1ac21b 100644 --- a/server/session/handler_enchanting.go +++ b/server/session/handler_enchanting.go @@ -74,8 +74,8 @@ func (h *ItemStackRequestHandler) handleEnchant(a *protocol.CraftRecipeStackRequ // Deduct the experience and Lapis Lazuli. s.c.SetExperienceLevel(s.c.ExperienceLevel() - cost) h.setItemInSlot(protocol.StackRequestSlotInfo{ - ContainerID: protocol.ContainerEnchantingMaterial, - Slot: enchantingLapisSlot, + Container: protocol.FullContainerName{ContainerID: protocol.ContainerEnchantingMaterial}, + Slot: enchantingLapisSlot, }, lapis.Grow(-cost), s) } @@ -85,8 +85,8 @@ func (h *ItemStackRequestHandler) handleEnchant(a *protocol.CraftRecipeStackRequ // Clear the existing input item, and apply the new item into the crafting result slot of the UI. The client will // automatically move the item into the input slot. h.setItemInSlot(protocol.StackRequestSlotInfo{ - ContainerID: protocol.ContainerEnchantingInput, - Slot: enchantingInputSlot, + Container: protocol.FullContainerName{ContainerID: protocol.ContainerEnchantingInput}, + Slot: enchantingInputSlot, }, item.Stack{}, s) return h.createResults(s, input.WithEnchantments(enchants...)) diff --git a/server/session/handler_grindstone.go b/server/session/handler_grindstone.go index fe1584dc3..c32821916 100644 --- a/server/session/handler_grindstone.go +++ b/server/session/handler_grindstone.go @@ -31,12 +31,12 @@ func (h *ItemStackRequestHandler) handleGrindstoneCraft(s *Session) error { // Next, get both input items and ensure they are comparable. firstInput, _ := h.itemInSlot(protocol.StackRequestSlotInfo{ - ContainerID: protocol.ContainerGrindstoneInput, - Slot: grindstoneFirstInputSlot, + Container: protocol.FullContainerName{ContainerID: protocol.ContainerGrindstoneInput}, + Slot: grindstoneFirstInputSlot, }, s) secondInput, _ := h.itemInSlot(protocol.StackRequestSlotInfo{ - ContainerID: protocol.ContainerGrindstoneAdditional, - Slot: grindstoneSecondInputSlot, + Container: protocol.FullContainerName{ContainerID: protocol.ContainerGrindstoneAdditional}, + Slot: grindstoneSecondInputSlot, }, s) if firstInput.Empty() && secondInput.Empty() { return fmt.Errorf("input item(s) are empty") @@ -65,12 +65,12 @@ func (h *ItemStackRequestHandler) handleGrindstoneCraft(s *Session) error { } h.setItemInSlot(protocol.StackRequestSlotInfo{ - ContainerID: protocol.ContainerGrindstoneInput, - Slot: grindstoneFirstInputSlot, + Container: protocol.FullContainerName{ContainerID: protocol.ContainerGrindstoneInput}, + Slot: grindstoneFirstInputSlot, }, item.Stack{}, s) h.setItemInSlot(protocol.StackRequestSlotInfo{ - ContainerID: protocol.ContainerGrindstoneAdditional, - Slot: grindstoneSecondInputSlot, + Container: protocol.FullContainerName{ContainerID: protocol.ContainerGrindstoneAdditional}, + Slot: grindstoneSecondInputSlot, }, item.Stack{}, s) return h.createResults(s, stripPossibleEnchantments(resultStack)) } diff --git a/server/session/handler_item_stack_request.go b/server/session/handler_item_stack_request.go index 79b40e447..aa665fbf1 100644 --- a/server/session/handler_item_stack_request.go +++ b/server/session/handler_item_stack_request.go @@ -161,8 +161,8 @@ func (h *ItemStackRequestHandler) handleTransfer(from, to protocol.StackRequestS dest = i.Grow(-math.MaxInt32) } - invA, _ := s.invByID(int32(from.ContainerID)) - invB, _ := s.invByID(int32(to.ContainerID)) + invA, _ := s.invByID(int32(from.Container.ContainerID)) + invB, _ := s.invByID(int32(to.Container.ContainerID)) ctx := event.C() _ = call(ctx, int(from.Slot), i.Grow(int(count)-i.Count()), invA.Handler().HandleTake) @@ -185,8 +185,8 @@ func (h *ItemStackRequestHandler) handleSwap(a *protocol.SwapStackRequestAction, i, _ := h.itemInSlot(a.Source, s) dest, _ := h.itemInSlot(a.Destination, s) - invA, _ := s.invByID(int32(a.Source.ContainerID)) - invB, _ := s.invByID(int32(a.Destination.ContainerID)) + invA, _ := s.invByID(int32(a.Source.Container.ContainerID)) + invB, _ := s.invByID(int32(a.Destination.Container.ContainerID)) ctx := event.C() _ = call(ctx, int(a.Source.Slot), i, invA.Handler().HandleTake) @@ -249,7 +249,7 @@ func (h *ItemStackRequestHandler) handleDrop(a *protocol.DropStackRequestAction, return fmt.Errorf("client attempted to drop %v items, but only %v present", a.Count, i.Count()) } - inv, _ := s.invByID(int32(a.Source.ContainerID)) + inv, _ := s.invByID(int32(a.Source.Container.ContainerID)) if err := call(event.C(), int(a.Source.Slot), i.Grow(int(a.Count)-i.Count()), inv.Handler().HandleDrop); err != nil { return err } @@ -263,7 +263,7 @@ func (h *ItemStackRequestHandler) handleDrop(a *protocol.DropStackRequestAction, // by Mojang to deal with the durability changes client-side. func (h *ItemStackRequestHandler) handleMineBlock(a *protocol.MineBlockStackRequestAction, s *Session) error { slot := protocol.StackRequestSlotInfo{ - ContainerID: protocol.ContainerInventory, + Container: protocol.FullContainerName{ContainerID: protocol.ContainerInventory}, Slot: byte(a.HotbarSlot), StackNetworkID: a.StackNetworkID, } @@ -293,8 +293,8 @@ func (h *ItemStackRequestHandler) handleCreate(a *protocol.CreateStackRequestAct h.pendingResults[slot] = item.Stack{} h.setItemInSlot(protocol.StackRequestSlotInfo{ - ContainerID: protocol.ContainerCreatedOutput, - Slot: craftingResult, + Container: protocol.FullContainerName{ContainerID: protocol.ContainerCreatedOutput}, + Slot: craftingResult, }, res, s) return nil } @@ -330,7 +330,7 @@ func (h *ItemStackRequestHandler) verifySlot(slot protocol.StackRequestSlotInfo, if len(h.responseChanges) > 256 { return fmt.Errorf("too many unacknowledged request slot changes") } - inv, _ := s.invByID(int32(slot.ContainerID)) + inv, _ := s.invByID(int32(slot.Container.ContainerID)) i, err := h.itemInSlot(slot, s) if err != nil { @@ -361,11 +361,11 @@ func (h *ItemStackRequestHandler) resolveID(inv *inventory.Inventory, slot proto } changes, ok := containerChanges[inv] if !ok { - return 0, fmt.Errorf("slot pointed to stack request %v with container %v, but that container was not changed in the request", slot.StackNetworkID, slot.ContainerID) + return 0, fmt.Errorf("slot pointed to stack request %v with container %v, but that container was not changed in the request", slot.StackNetworkID, slot.Container.ContainerID) } actual, ok := changes[slot.Slot] if !ok { - return 0, fmt.Errorf("slot pointed to stack request %v with container %v and slot %v, but that slot was not changed in the request", slot.StackNetworkID, slot.ContainerID, slot.Slot) + return 0, fmt.Errorf("slot pointed to stack request %v with container %v and slot %v, but that slot was not changed in the request", slot.StackNetworkID, slot.Container.ContainerID, slot.Slot) } return actual.id, nil } @@ -374,9 +374,9 @@ func (h *ItemStackRequestHandler) resolveID(inv *inventory.Inventory, slot proto // info passed from the client has the right stack network ID in any of the stored slots. If this is the case, // that entry is removed, so that the maps are cleaned up eventually. func (h *ItemStackRequestHandler) tryAcknowledgeChanges(s *Session, slot protocol.StackRequestSlotInfo) error { - inv, ok := s.invByID(int32(slot.ContainerID)) + inv, ok := s.invByID(int32(slot.Container.ContainerID)) if !ok { - return fmt.Errorf("could not find container with id %v", slot.ContainerID) + return fmt.Errorf("could not find container with id %v", slot.Container.ContainerID) } for requestID, containerChanges := range h.responseChanges { @@ -399,9 +399,9 @@ func (h *ItemStackRequestHandler) tryAcknowledgeChanges(s *Session, slot protoco // itemInSlot looks for the item in the slot as indicated by the slot info passed. func (h *ItemStackRequestHandler) itemInSlot(slot protocol.StackRequestSlotInfo, s *Session) (item.Stack, error) { - inv, ok := s.invByID(int32(slot.ContainerID)) + inv, ok := s.invByID(int32(slot.Container.ContainerID)) if !ok { - return item.Stack{}, fmt.Errorf("unable to find container with ID %v", slot.ContainerID) + return item.Stack{}, fmt.Errorf("unable to find container with ID %v", slot.Container.ContainerID) } sl := int(slot.Slot) @@ -418,7 +418,7 @@ func (h *ItemStackRequestHandler) itemInSlot(slot protocol.StackRequestSlotInfo, // setItemInSlot sets an item stack in the slot of a container present in the slot info. func (h *ItemStackRequestHandler) setItemInSlot(slot protocol.StackRequestSlotInfo, i item.Stack, s *Session) { - inv, _ := s.invByID(int32(slot.ContainerID)) + inv, _ := s.invByID(int32(slot.Container.ContainerID)) sl := int(slot.Slot) if inv == s.offHand { @@ -436,10 +436,10 @@ func (h *ItemStackRequestHandler) setItemInSlot(slot protocol.StackRequestSlotIn DurabilityCorrection: int32(i.MaxDurability() - i.Durability()), } - if h.changes[slot.ContainerID] == nil { - h.changes[slot.ContainerID] = map[byte]changeInfo{} + if h.changes[slot.Container.ContainerID] == nil { + h.changes[slot.Container.ContainerID] = map[byte]changeInfo{} } - h.changes[slot.ContainerID][slot.Slot] = changeInfo{ + h.changes[slot.Container.ContainerID][slot.Slot] = changeInfo{ after: respSlot, before: before, } @@ -465,8 +465,8 @@ func (h *ItemStackRequestHandler) resolve(id int32, s *Session) { slots = append(slots, slot.after) } info = append(info, protocol.StackResponseContainerInfo{ - ContainerID: container, - SlotInfo: slots, + Container: protocol.FullContainerName{ContainerID: container}, + SlotInfo: slots, }) } s.writePacket(&packet.ItemStackResponse{Responses: []protocol.ItemStackResponse{{ diff --git a/server/session/handler_loom.go b/server/session/handler_loom.go index 782d46806..299b112ca 100644 --- a/server/session/handler_loom.go +++ b/server/session/handler_loom.go @@ -28,8 +28,8 @@ func (h *ItemStackRequestHandler) handleLoomCraft(a *protocol.CraftLoomRecipeSta // Next, check if the input slot has a valid banner item. input, _ := h.itemInSlot(protocol.StackRequestSlotInfo{ - ContainerID: protocol.ContainerLoomInput, - Slot: loomInputSlot, + Container: protocol.FullContainerName{ContainerID: protocol.ContainerLoomInput}, + Slot: loomInputSlot, }, s) if input.Empty() { return fmt.Errorf("input item is empty") @@ -44,8 +44,8 @@ func (h *ItemStackRequestHandler) handleLoomCraft(a *protocol.CraftLoomRecipeSta // Do the same with the input dye. dye, _ := h.itemInSlot(protocol.StackRequestSlotInfo{ - ContainerID: protocol.ContainerLoomDye, - Slot: loomDyeSlot, + Container: protocol.FullContainerName{ContainerID: protocol.ContainerLoomDye}, + Slot: loomDyeSlot, }, s) if dye.Empty() { return fmt.Errorf("dye item is empty") @@ -62,8 +62,8 @@ func (h *ItemStackRequestHandler) handleLoomCraft(a *protocol.CraftLoomRecipeSta // Some banner patterns have equivalent banner pattern items that are required to craft the pattern. If the expected // pattern has a pattern item, check if the player input the correct pattern item. pattern, _ := h.itemInSlot(protocol.StackRequestSlotInfo{ - ContainerID: protocol.ContainerLoomMaterial, - Slot: loomPatternSlot, + Container: protocol.FullContainerName{ContainerID: protocol.ContainerLoomMaterial}, + Slot: loomPatternSlot, }, s) if expectedPatternItem, hasPatternItem := expectedPattern.Item(); hasPatternItem { if pattern.Empty() { @@ -84,12 +84,12 @@ func (h *ItemStackRequestHandler) handleLoomCraft(a *protocol.CraftLoomRecipeSta Colour: d.Colour, }) h.setItemInSlot(protocol.StackRequestSlotInfo{ - ContainerID: protocol.ContainerLoomInput, - Slot: loomInputSlot, + Container: protocol.FullContainerName{ContainerID: protocol.ContainerLoomInput}, + Slot: loomInputSlot, }, input.Grow(-1), s) h.setItemInSlot(protocol.StackRequestSlotInfo{ - ContainerID: protocol.ContainerLoomDye, - Slot: loomDyeSlot, + Container: protocol.FullContainerName{ContainerID: protocol.ContainerLoomDye}, + Slot: loomDyeSlot, }, dye.Grow(-1), s) return h.createResults(s, duplicateStack(input, b)) } diff --git a/server/session/handler_server_bound_diagnostics.go b/server/session/handler_server_bound_diagnostics.go new file mode 100644 index 000000000..a156ce8a1 --- /dev/null +++ b/server/session/handler_server_bound_diagnostics.go @@ -0,0 +1,26 @@ +package session + +import ( + "github.com/df-mc/dragonfly/server/player/diagnostics" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" +) + +// ServerBoundDiagnosticsHandler handles diagnostic updates from the client. +type ServerBoundDiagnosticsHandler struct{} + +// Handle ... +func (h *ServerBoundDiagnosticsHandler) Handle(p packet.Packet, s *Session) error { + pk := p.(*packet.ServerBoundDiagnostics) + s.c.UpdateDiagnostics(diagnostics.Diagnostics{ + AverageFramesPerSecond: float64(pk.AverageFramesPerSecond), + AverageServerSimTickTime: float64(pk.AverageServerSimTickTime), + AverageClientSimTickTime: float64(pk.AverageClientSimTickTime), + AverageBeginFrameTime: float64(pk.AverageBeginFrameTime), + AverageInputTime: float64(pk.AverageInputTime), + AverageRenderTime: float64(pk.AverageRenderTime), + AverageEndFrameTime: float64(pk.AverageEndFrameTime), + AverageRemainderTimePercent: float64(pk.AverageRemainderTimePercent), + AverageUnaccountedTimePercent: float64(pk.AverageUnaccountedTimePercent), + }) + return nil +} diff --git a/server/session/handler_server_bound_loading_screen.go b/server/session/handler_server_bound_loading_screen.go new file mode 100644 index 000000000..66a1bd1b8 --- /dev/null +++ b/server/session/handler_server_bound_loading_screen.go @@ -0,0 +1,29 @@ +package session + +import ( + "fmt" + "github.com/df-mc/atomic" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" +) + +// ServerBoundLoadingScreenHandler handles loading screen updates from the clients. It is used to ensure that +// the server knows when the client is loading a screen, and when it is done loading it. +type ServerBoundLoadingScreenHandler struct { + currentID atomic.Uint32 + expectedID atomic.Uint32 +} + +// Handle ... +func (h *ServerBoundLoadingScreenHandler) Handle(p packet.Packet, s *Session) error { + pk := p.(*packet.ServerBoundLoadingScreen) + v, ok := pk.LoadingScreenID.Value() + if !ok || h.expectedID.Load() == 0 { + return nil + } else if v != h.expectedID.Load() { + return fmt.Errorf("expected loading screen ID %d, got %d", h.expectedID.Load(), v) + } else if pk.Type == packet.LoadingScreenTypeEnd { + s.changingDimension.Store(false) + h.expectedID.Store(0) + } + return nil +} diff --git a/server/session/handler_smithing.go b/server/session/handler_smithing.go index 4f921125b..3c82df355 100644 --- a/server/session/handler_smithing.go +++ b/server/session/handler_smithing.go @@ -35,22 +35,22 @@ func (h *ItemStackRequestHandler) handleSmithing(a *protocol.CraftRecipeStackReq // Check if the input item and material item match what the recipe requires. expectedInputs := craft.Input() input, _ := h.itemInSlot(protocol.StackRequestSlotInfo{ - ContainerID: protocol.ContainerSmithingTableInput, - Slot: smithingInputSlot, + Container: protocol.FullContainerName{ContainerID: protocol.ContainerSmithingTableInput}, + Slot: smithingInputSlot, }, s) if !matchingStacks(input, expectedInputs[0]) { return fmt.Errorf("input item is not the same as expected input") } material, _ := h.itemInSlot(protocol.StackRequestSlotInfo{ - ContainerID: protocol.ContainerSmithingTableMaterial, - Slot: smithingMaterialSlot, + Container: protocol.FullContainerName{ContainerID: protocol.ContainerSmithingTableMaterial}, + Slot: smithingMaterialSlot, }, s) if !matchingStacks(material, expectedInputs[1]) { return fmt.Errorf("material item is not the same as expected material") } template, _ := h.itemInSlot(protocol.StackRequestSlotInfo{ - ContainerID: protocol.ContainerSmithingTableTemplate, - Slot: smithingTemplateSlot, + Container: protocol.FullContainerName{ContainerID: protocol.ContainerSmithingTableTemplate}, + Slot: smithingTemplateSlot, }, s) if !matchingStacks(template, expectedInputs[2]) { return fmt.Errorf("template item is not the same as expected template") @@ -58,16 +58,16 @@ func (h *ItemStackRequestHandler) handleSmithing(a *protocol.CraftRecipeStackReq // Create the output using the input stack as reference and the recipe's output item type. h.setItemInSlot(protocol.StackRequestSlotInfo{ - ContainerID: protocol.ContainerSmithingTableInput, - Slot: smithingInputSlot, + Container: protocol.FullContainerName{ContainerID: protocol.ContainerSmithingTableInput}, + Slot: smithingInputSlot, }, input.Grow(-1), s) h.setItemInSlot(protocol.StackRequestSlotInfo{ - ContainerID: protocol.ContainerSmithingTableMaterial, - Slot: smithingMaterialSlot, + Container: protocol.FullContainerName{ContainerID: protocol.ContainerSmithingTableMaterial}, + Slot: smithingMaterialSlot, }, material.Grow(-1), s) h.setItemInSlot(protocol.StackRequestSlotInfo{ - ContainerID: protocol.ContainerSmithingTableTemplate, - Slot: smithingTemplateSlot, + Container: protocol.FullContainerName{ContainerID: protocol.ContainerSmithingTableTemplate}, + Slot: smithingTemplateSlot, }, template.Grow(-1), s) if _, ok = craft.(recipe.SmithingTrim); ok { diff --git a/server/session/handler_stonecutter.go b/server/session/handler_stonecutter.go index a928476b1..28eed9d1a 100644 --- a/server/session/handler_stonecutter.go +++ b/server/session/handler_stonecutter.go @@ -24,8 +24,8 @@ func (h *ItemStackRequestHandler) handleStonecutting(a *protocol.CraftRecipeStac expectedInputs := craft.Input() input, _ := h.itemInSlot(protocol.StackRequestSlotInfo{ - ContainerID: protocol.ContainerStonecutterInput, - Slot: stonecutterInputSlot, + Container: protocol.FullContainerName{ContainerID: protocol.ContainerStonecutterInput}, + Slot: stonecutterInputSlot, }, s) if !matchingStacks(input, expectedInputs[0]) { return fmt.Errorf("input item is not the same as expected input") @@ -33,8 +33,8 @@ func (h *ItemStackRequestHandler) handleStonecutting(a *protocol.CraftRecipeStac output := craft.Output() h.setItemInSlot(protocol.StackRequestSlotInfo{ - ContainerID: protocol.ContainerStonecutterInput, - Slot: stonecutterInputSlot, + Container: protocol.FullContainerName{ContainerID: protocol.ContainerStonecutterInput}, + Slot: stonecutterInputSlot, }, input.Grow(-1), s) return h.createResults(s, output...) } diff --git a/server/session/player.go b/server/session/player.go index 82724fa3a..c1e98b1d2 100644 --- a/server/session/player.go +++ b/server/session/player.go @@ -28,21 +28,33 @@ import ( // StopShowingEntity stops showing a world.Entity to the Session. It will be completely invisible until a call to // StartShowingEntity is made. func (s *Session) StopShowingEntity(e world.Entity) { - s.HideEntity(e) s.entityMutex.Lock() - s.hiddenEntities[e] = struct{}{} + _, ok := s.hiddenEntities[e] + if !ok { + s.hiddenEntities[e] = struct{}{} + } s.entityMutex.Unlock() + + if !ok { + s.HideEntity(e) + } } // StartShowingEntity starts showing a world.Entity to the Session that was previously hidden using StopShowingEntity. func (s *Session) StartShowingEntity(e world.Entity) { s.entityMutex.Lock() - delete(s.hiddenEntities, e) + _, ok := s.hiddenEntities[e] + if ok { + delete(s.hiddenEntities, e) + } s.entityMutex.Unlock() - s.ViewEntity(e) - s.ViewEntityState(e) - s.ViewEntityItems(e) - s.ViewEntityArmour(e) + + if ok { + s.ViewEntity(e) + s.ViewEntityState(e) + s.ViewEntityItems(e) + s.ViewEntityArmour(e) + } } // closeCurrentContainer closes the container the player might currently have open. @@ -228,12 +240,7 @@ func (s *Session) invByID(id int32) (*inventory.Inventory, bool) { return s.armour.Inventory(), true case protocol.ContainerLevelEntity: if s.containerOpened.Load() { - b := s.c.World().Block(s.openedPos.Load()) - if _, chest := b.(block.Chest); chest { - return s.openedWindow.Load(), true - } else if _, enderChest := b.(block.EnderChest); enderChest { - return s.openedWindow.Load(), true - } + return s.openedWindow.Load(), true } case protocol.ContainerBarrel: if s.containerOpened.Load() { diff --git a/server/session/session.go b/server/session/session.go index 926cabcd9..ce24c2e67 100644 --- a/server/session/session.go +++ b/server/session/session.go @@ -71,6 +71,7 @@ type Session struct { openedWindow atomic.Value[*inventory.Inventory] openedPos atomic.Value[cube.Pos] swingingArm atomic.Bool + changingDimension atomic.Bool recipes map[uint32]recipe.Recipe @@ -399,7 +400,16 @@ func (s *Session) handleWorldSwitch(w *world.World) { // changeDimension changes the dimension of the client. If silent is set to true, the portal noise will be stopped // immediately. func (s *Session) changeDimension(dim int32, silent bool) { - s.writePacket(&packet.ChangeDimension{Dimension: dim, Position: vec64To32(s.c.Position().Add(entityOffset(s.c)))}) + s.changingDimension.Store(true) + h := s.handlers[packet.IDServerBoundLoadingScreen].(*ServerBoundLoadingScreenHandler) + id := h.currentID.Add(1) + h.expectedID.Store(id) + + s.writePacket(&packet.ChangeDimension{ + Dimension: dim, + Position: vec64To32(s.c.Position().Add(entityOffset(s.c))), + LoadingScreenID: protocol.Option(id), + }) s.writePacket(&packet.StopSound{StopAll: silent}) s.writePacket(&packet.PlayStatus{Status: packet.PlayStatusPlayerSpawn}) @@ -411,6 +421,11 @@ func (s *Session) changeDimension(dim int32, silent bool) { }) } +// ChangingDimension returns whether the session is currently changing dimension or not. +func (s *Session) ChangingDimension() bool { + return s.changingDimension.Load() +} + // handlePacket handles an incoming packet, processing it accordingly. If the packet had invalid data or was // otherwise not valid in its context, an error is returned. func (s *Session) handlePacket(pk packet.Packet) error { @@ -463,6 +478,8 @@ func (s *Session) registerHandlers() { packet.IDSubChunkRequest: &SubChunkRequestHandler{}, packet.IDText: &TextHandler{}, packet.IDTickSync: nil, + packet.IDServerBoundLoadingScreen: &ServerBoundLoadingScreenHandler{}, + packet.IDServerBoundDiagnostics: &ServerBoundDiagnosticsHandler{}, } } diff --git a/server/session/world.go b/server/session/world.go index c36505afd..1018c45b7 100644 --- a/server/session/world.go +++ b/server/session/world.go @@ -117,6 +117,8 @@ func (s *Session) ViewEntity(e world.Entity) { s.writePacket(&packet.PlayerList{ActionType: packet.PlayerListActionRemove, Entries: []protocol.PlayerListEntry{{ UUID: v.UUID(), }}}) + } else { + s.ViewSkin(e) } return case *entity.Ent: @@ -1012,7 +1014,7 @@ func (s *Session) openNormalContainer(b block.Container, pos cube.Pos) { nextID := s.nextWindowID() s.containerOpened.Store(true) - s.openedWindow.Store(b.Inventory()) + s.openedWindow.Store(b.Inventory(s.c.World(), pos)) s.openedPos.Store(pos) var containerType byte @@ -1023,6 +1025,8 @@ func (s *Session) openNormalContainer(b block.Container, pos cube.Pos) { containerType = protocol.ContainerTypeBlastFurnace case block.Smoker: containerType = protocol.ContainerTypeSmoker + case block.Hopper: + containerType = protocol.ContainerTypeHopper } s.writePacket(&packet.ContainerOpen{ @@ -1031,7 +1035,7 @@ func (s *Session) openNormalContainer(b block.Container, pos cube.Pos) { ContainerPosition: protocol.BlockPos{int32(pos[0]), int32(pos[1]), int32(pos[2])}, ContainerEntityUniqueID: -1, }) - s.sendInv(b.Inventory(), uint32(nextID)) + s.sendInv(b.Inventory(s.c.World(), pos), uint32(nextID)) } // ViewSlotChange ... diff --git a/server/world/block_states.nbt b/server/world/block_states.nbt index d4ba1bd5d..f32ccb2e0 100644 Binary files a/server/world/block_states.nbt and b/server/world/block_states.nbt differ diff --git a/server/world/chunk/decode.go b/server/world/chunk/decode.go index 0582a1dfc..2ed10e2e9 100644 --- a/server/world/chunk/decode.go +++ b/server/world/chunk/decode.go @@ -161,6 +161,9 @@ func decodePalettedStorage(buf *bytes.Buffer, e Encoding, pe paletteEncoding) (* } size := paletteSize(blockSize) + if size > 32 { + return nil, fmt.Errorf("cannot read paletted storage (size=%v) %T: size too large", blockSize, pe) + } uint32Count := size.uint32s() uint32s := make([]uint32, uint32Count) diff --git a/server/world/item_runtime_ids.nbt b/server/world/item_runtime_ids.nbt index 51ae9a8f9..62149ff1d 100644 Binary files a/server/world/item_runtime_ids.nbt and b/server/world/item_runtime_ids.nbt differ diff --git a/server/world/mcdb/leveldat/data.go b/server/world/mcdb/leveldat/data.go index c7ca9e3ab..b40f3d202 100644 --- a/server/world/mcdb/leveldat/data.go +++ b/server/world/mcdb/leveldat/data.go @@ -145,6 +145,7 @@ type Data struct { IsHardcore bool `nbt:"IsHardcore"` ShowDaysPlayed bool `nbt:"showdaysplayed"` TNTExplosionDropDecay bool `nbt:"tntexplosiondropdecay"` + HasUncompleteWorldFileOnDisk bool `nbt:"HasUncompleteWorldFileOnDisk"` } // FillDefault fills out d with all the default level.dat values. diff --git a/server/world/world.go b/server/world/world.go index e925e5ed3..c987aa9e6 100644 --- a/server/world/world.go +++ b/server/world/world.go @@ -2,11 +2,14 @@ package world import ( "errors" - "github.com/df-mc/goleveldb/leveldb" "math/rand" "sync" "time" + "github.com/df-mc/goleveldb/leveldb" + + "slices" + "github.com/df-mc/atomic" "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/event" @@ -15,7 +18,6 @@ import ( "github.com/go-gl/mathgl/mgl64" "github.com/google/uuid" "golang.org/x/exp/maps" - "slices" ) // World implements a Minecraft world. It manages all aspects of what players can see, such as blocks, @@ -1016,6 +1018,19 @@ func (w *World) PortalDestination(dim Dimension) *World { return w } +// Save saves the World to the provider. +func (w *World) Save() { + w.conf.Log.Debugf("Saving chunks in memory to disk...") + + w.chunkMu.Lock() + toSave := maps.Clone(w.chunks) + w.chunkMu.Unlock() + + for pos, c := range toSave { + w.saveChunk(pos, c, false) + } +} + // Close closes the world and saves all chunks currently loaded. func (w *World) Close() error { if w == nil { @@ -1043,7 +1058,7 @@ func (w *World) close() { w.chunkMu.Unlock() for pos, c := range toSave { - w.saveChunk(pos, c) + w.saveChunk(pos, c, true) } w.set.ref.Dec() @@ -1313,7 +1328,7 @@ func (w *World) spreadLight(pos ChunkPos) { // saveChunk is called when a chunk is removed from the cache. We first compact the chunk, then we write it to // the provider. -func (w *World) saveChunk(pos ChunkPos, c *Column) { +func (w *World) saveChunk(pos ChunkPos, c *Column, closeEntities bool) { c.Lock() if !w.conf.ReadOnly && c.modified { c.Compact() @@ -1321,12 +1336,16 @@ func (w *World) saveChunk(pos ChunkPos, c *Column) { w.conf.Log.Errorf("save chunk: %v", err) } } - ent := c.Entities - c.Entities = nil - c.Unlock() + if closeEntities { + ent := c.Entities + c.Entities = nil + c.Unlock() - for _, e := range ent { - _ = e.Close() + for _, e := range ent { + _ = e.Close() + } + } else { + c.Unlock() } } @@ -1356,7 +1375,7 @@ func (w *World) chunkCacheJanitor() { w.chunkMu.Unlock() for pos, c := range chunksToRemove { - w.saveChunk(pos, c) + w.saveChunk(pos, c, true) delete(chunksToRemove, pos) } case <-w.closing: