From ce3b980b4a6bf91fcc477e1a39aa3411ada14d41 Mon Sep 17 00:00:00 2001 From: Jon <86489758+xNatsuri@users.noreply.github.com> Date: Sun, 1 Sep 2024 03:02:19 -0700 Subject: [PATCH] dragonfly/server: Implement hoppers (#914) --- server/block/campfire.go | 283 ++++++++++++++++++++++++++++ server/block/hash.go | 6 + server/block/item_frame.go | 2 +- server/block/model/campfire.go | 19 ++ server/block/register.go | 2 + server/entity/splashable.go | 29 +++ server/item/recipe/furnace_data.nbt | Bin 0 -> 32647 bytes server/item/recipe/item.go | 88 ++++++--- server/item/recipe/recipe.go | 14 ++ server/item/recipe/vanilla.go | 32 +++- server/session/player.go | 6 + server/session/world.go | 4 +- server/world/sound/block.go | 7 +- 13 files changed, 456 insertions(+), 36 deletions(-) create mode 100644 server/block/campfire.go create mode 100644 server/block/model/campfire.go create mode 100644 server/item/recipe/furnace_data.nbt diff --git a/server/block/campfire.go b/server/block/campfire.go new file mode 100644 index 000000000..93e37c7ca --- /dev/null +++ b/server/block/campfire.go @@ -0,0 +1,283 @@ +package block + +import ( + "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/world" + "github.com/df-mc/dragonfly/server/world/sound" + "github.com/go-gl/mathgl/mgl64" + "math/rand" + "strconv" + "time" +) + +// Campfire is a block that can be used to cook food, pacify bees, act as a spread-proof light source, smoke signal or +// damaging trap block. +type Campfire struct { + transparent + bass + sourceWaterDisplacer + + // Items represents the items in the campfire that are being cooked. + Items [4]CampfireItem + // Facing represents the direction that the campfire is facing. + Facing cube.Direction + // Extinguished is true if the campfire was extinguished by a water source. + Extinguished bool + // Type represents the type of Campfire, currently there are Normal and Soul campfires. + Type FireType +} + +// CampfireItem holds data about the items in the campfire. +type CampfireItem struct { + // Item is a specific item being cooked on top of the campfire. + Item item.Stack + // Time is the countdown of ticks until the food item is cooked (when 0). + Time time.Duration +} + +// Model ... +func (Campfire) Model() world.BlockModel { + return model.Campfire{} +} + +// SideClosed ... +func (Campfire) SideClosed(cube.Pos, cube.Pos, *world.World) bool { + return false +} + +// BreakInfo ... +func (c Campfire) BreakInfo() BreakInfo { + return newBreakInfo(2, alwaysHarvestable, axeEffective, func(t item.Tool, enchantments []item.Enchantment) []item.Stack { + var drops []item.Stack + if hasSilkTouch(enchantments) { + drops = append(drops, item.NewStack(c, 1)) + } else { + switch c.Type { + case NormalFire(): + drops = append(drops, item.NewStack(item.Charcoal{}, 2)) + case SoulFire(): + drops = append(drops, item.NewStack(SoulSoil{}, 1)) + } + } + for _, v := range c.Items { + if !v.Item.Empty() { + drops = append(drops, v.Item) + } + } + return drops + }) +} + +// LightEmissionLevel ... +func (c Campfire) LightEmissionLevel() uint8 { + if c.Extinguished { + return 0 + } + return c.Type.LightLevel() +} + +// Ignite ... +func (c Campfire) Ignite(pos cube.Pos, w *world.World, _ world.Entity) bool { + w.PlaySound(pos.Vec3(), sound.Ignite{}) + if !c.Extinguished { + return false + } + if _, ok := w.Liquid(pos); ok { + return false + } + + c.Extinguished = false + w.SetBlock(pos, c, nil) + return true +} + +// Splash ... +func (c Campfire) Splash(w *world.World, pos cube.Pos) { + if c.Extinguished { + return + } + + c.extinguish(pos, w) +} + +// extinguish extinguishes the campfire. +func (c Campfire) extinguish(pos cube.Pos, w *world.World) { + w.PlaySound(pos.Vec3Centre(), sound.FireExtinguish{}) + c.Extinguished = true + + for i := range c.Items { + c.Items[i].Time = time.Second * 30 + } + + w.SetBlock(pos, c, nil) +} + +// Activate ... +func (c Campfire) Activate(pos cube.Pos, _ cube.Face, w *world.World, u item.User, ctx *item.UseContext) bool { + held, _ := u.HeldItems() + if held.Empty() { + return false + } + + if _, ok := held.Item().(item.Shovel); ok && !c.Extinguished { + c.extinguish(pos, w) + ctx.DamageItem(1) + return true + } + + rawFood, ok := held.Item().(item.Smeltable) + if !ok || !rawFood.SmeltInfo().Food { + return false + } + + for i, it := range c.Items { + if it.Item.Empty() { + c.Items[i] = CampfireItem{ + Item: held.Grow(-held.Count() + 1), + Time: time.Second * 30, + } + + ctx.SubtractFromCount(1) + + w.PlaySound(pos.Vec3Centre(), sound.ItemAdd{}) + w.SetBlock(pos, c, nil) + return true + } + } + return false +} + +// UseOnBlock ... +func (c Campfire) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, w *world.World, user item.User, ctx *item.UseContext) (used bool) { + pos, _, used = firstReplaceable(w, pos, face, c) + if !used { + return + } + if _, ok := w.Block(pos.Side(cube.FaceDown)).(Campfire); ok { + return false + } + c.Facing = user.Rotation().Direction().Opposite() + place(w, pos, c, user, ctx) + return placed(ctx) +} + +// Tick is called to cook the items within the campfire. +func (c Campfire) Tick(_ int64, pos cube.Pos, w *world.World) { + if c.Extinguished { + // Extinguished, do nothing. + return + } + if rand.Float64() <= 0.016 { // Every three or so seconds. + w.PlaySound(pos.Vec3Centre(), sound.CampfireCrackle{}) + } + + updated := false + for i, it := range c.Items { + if it.Item.Empty() { + continue + } + + updated = true + if it.Time > 0 { + c.Items[i].Time = it.Time - time.Millisecond*50 + continue + } + + if food, ok := it.Item.Item().(item.Smeltable); ok { + dropItem(w, food.SmeltInfo().Product, pos.Vec3Middle()) + } + c.Items[i].Item = item.Stack{} + } + if updated { + w.SetBlock(pos, c, nil) + } +} + +// NeighbourUpdateTick ... +func (c Campfire) NeighbourUpdateTick(pos, _ cube.Pos, w *world.World) { + _, ok := w.Liquid(pos) + liquid, okTwo := w.Liquid(pos.Side(cube.FaceUp)) + if (ok || (okTwo && liquid.LiquidType() == "water")) && !c.Extinguished { + c.extinguish(pos, w) + } +} + +// EntityInside ... +func (c Campfire) EntityInside(pos cube.Pos, w *world.World, e world.Entity) { + if flammable, ok := e.(flammableEntity); ok { + if flammable.OnFireDuration() > 0 && c.Extinguished { + c.Extinguished = false + w.PlaySound(pos.Vec3(), sound.Ignite{}) + w.SetBlock(pos, c, nil) + } + if !c.Extinguished { + if l, ok := e.(livingEntity); ok && !l.AttackImmune() { + l.Hurt(c.Type.Damage(), FireDamageSource{}) + } + } + } +} + +// EncodeNBT ... +func (c Campfire) EncodeNBT() map[string]any { + m := map[string]any{"id": "Campfire"} + for i, v := range c.Items { + id := strconv.Itoa(i + 1) + if !v.Item.Empty() { + m["Item"+id] = nbtconv.WriteItem(v.Item, true) + m["ItemTime"+id] = uint8(v.Time.Milliseconds() / 50) + } + } + return m +} + +// DecodeNBT ... +func (c Campfire) DecodeNBT(data map[string]any) any { + for i := 0; i < 4; i++ { + id := strconv.Itoa(i + 1) + c.Items[i] = CampfireItem{ + Item: nbtconv.MapItem(data, "Item"+id), + Time: time.Duration(nbtconv.Int16(data, "ItemTime"+id)) * time.Millisecond * 50, + } + } + return c +} + +// EncodeItem ... +func (c Campfire) EncodeItem() (name string, meta int16) { + switch c.Type { + case NormalFire(): + return "minecraft:campfire", 0 + case SoulFire(): + return "minecraft:soul_campfire", 0 + } + panic("invalid fire type") +} + +// EncodeBlock ... +func (c Campfire) EncodeBlock() (name string, properties map[string]any) { + switch c.Type { + case NormalFire(): + name = "minecraft:campfire" + case SoulFire(): + name = "minecraft:soul_campfire" + } + return name, map[string]any{ + "minecraft:cardinal_direction": c.Facing.String(), + "extinguished": c.Extinguished, + } +} + +// allCampfires ... +func allCampfires() (campfires []world.Block) { + for _, d := range cube.Directions() { + for _, f := range FireTypes() { + campfires = append(campfires, Campfire{Facing: d, Type: f, Extinguished: true}) + campfires = append(campfires, Campfire{Facing: d, Type: f}) + } + } + return campfires +} diff --git a/server/block/hash.go b/server/block/hash.go index 00b8a5e8a..983e243ff 100644 --- a/server/block/hash.go +++ b/server/block/hash.go @@ -24,6 +24,7 @@ const ( hashCactus hashCake hashCalcite + hashCampfire hashCarpet hashCarrot hashChain @@ -289,6 +290,11 @@ func (Calcite) Hash() uint64 { return hashCalcite } +// Hash ... +func (c Campfire) Hash() uint64 { + return hashCampfire | uint64(c.Facing)<<8 | uint64(boolByte(c.Extinguished))<<10 | uint64(c.Type.Uint8())<<11 +} + // Hash ... func (c Carpet) Hash() uint64 { return hashCarpet | uint64(c.Colour.Uint8())<<8 diff --git a/server/block/item_frame.go b/server/block/item_frame.go index 4cbf49b35..aef2a71d5 100644 --- a/server/block/item_frame.go +++ b/server/block/item_frame.go @@ -41,7 +41,7 @@ func (i ItemFrame) Activate(pos cube.Pos, _ cube.Face, w *world.World, u item.Us i.Item = held.Grow(-held.Count() + 1) // TODO: When maps are implemented, check the item is a map, and if so, display the large version of the frame. ctx.SubtractFromCount(1) - w.PlaySound(pos.Vec3Centre(), sound.ItemFrameAdd{}) + w.PlaySound(pos.Vec3Centre(), sound.ItemAdd{}) } else { return true } diff --git a/server/block/model/campfire.go b/server/block/model/campfire.go new file mode 100644 index 000000000..8c4f816e3 --- /dev/null +++ b/server/block/model/campfire.go @@ -0,0 +1,19 @@ +package model + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" +) + +// Campfire is the model used by campfires. +type Campfire struct{} + +// BBox returns a flat BBox with a height of 0.4375. +func (Campfire) BBox(cube.Pos, *world.World) []cube.BBox { + return []cube.BBox{cube.Box(0, 0, 0, 1, 0.4375, 1)} +} + +// FaceSolid returns true if the face is down. +func (Campfire) FaceSolid(_ cube.Pos, face cube.Face, _ *world.World) bool { + return face == cube.FaceDown +} diff --git a/server/block/register.go b/server/block/register.go index 5a1db6c0a..2ce11330e 100644 --- a/server/block/register.go +++ b/server/block/register.go @@ -124,6 +124,7 @@ func init() { registerAll(allBoneBlock()) registerAll(allCactus()) registerAll(allCake()) + registerAll(allCampfires()) registerAll(allCarpet()) registerAll(allCarrots()) registerAll(allChains()) @@ -394,6 +395,7 @@ func init() { for _, f := range FireTypes() { world.RegisterItem(Lantern{Type: f}) world.RegisterItem(Torch{Type: f}) + world.RegisterItem(Campfire{Type: f}) } for _, f := range FlowerTypes() { world.RegisterItem(Flower{Type: f}) diff --git a/server/entity/splashable.go b/server/entity/splashable.go index a843ee1a2..9e5ae8840 100644 --- a/server/entity/splashable.go +++ b/server/entity/splashable.go @@ -10,6 +10,20 @@ import ( "time" ) +// SplashableBlock is a block that can be splashed with a splash bottle. +type SplashableBlock interface { + world.Block + // Splash is called when a water bottle splashes onto a block. + Splash(w *world.World, pos cube.Pos) +} + +// SplashableEntity is an entity that can be splashed with a splash bottle. +type SplashableEntity interface { + world.Entity + // Splash is called when a water bottle splashes onto an entity. + Splash(w *world.World, pos mgl64.Vec3) +} + // potionSplash returns a function that creates a potion splash with a specific // duration multiplier and potion type. func potionSplash(durMul float64, pot potion.Potion, linger bool) func(e *Ent, res trace.Result) { @@ -66,10 +80,25 @@ func potionSplash(durMul float64, pot potion.Potion, linger bool) func(e *Ent, r if h := blockPos.Side(f); w.Block(h) == fire() { w.SetBlock(h, nil, nil) } + + if b, ok := w.Block(blockPos.Side(f)).(SplashableBlock); ok { + b.Splash(w, blockPos.Side(f)) + } + } + + resultPos := result.BlockPosition() + if b, ok := w.Block(resultPos).(SplashableBlock); ok { + b.Splash(w, resultPos) } case trace.EntityResult: // TODO: Damage endermen, blazes, striders and snow golems when implemented and rehydrate axolotls. } + + for _, otherE := range w.EntitiesWithin(box.GrowVec3(mgl64.Vec3{8.25, 4.25, 8.25}), ignores) { + if splashE, ok := otherE.(SplashableEntity); ok { + splashE.Splash(w, otherE.Position()) + } + } } if linger { w.AddEntity(NewAreaEffectCloud(pos, pot)) diff --git a/server/item/recipe/furnace_data.nbt b/server/item/recipe/furnace_data.nbt new file mode 100644 index 0000000000000000000000000000000000000000..01703c41f48643d64aed8085284689ec11de570d GIT binary patch literal 32647 zcmc&-Tb87_5zU$E?rE1#+cSQ@J$@X2c?I4KM@dqdxJbZ2PEE}kybynREnbmr@esNQ zDI^Mr{8#8+U4=rSkjVVr;ql)OkMGmGyfq)+<>H$B=$ht9T#2XVZ&h-NYLTBF-d&SM z{P%wk5AWmRmN%at9zPVfW;FSsHaTaau0QXaeRgCkjnRW(@kLZ|Au=t%HiPl#zna1CEOLri#HnzL>9M(w zlpaz(N8Uxqzu)q6me_<0(x_iNwt+#%hpm0mgSWeozqNSeF|FduN*(EbHpg|j*hKnvYYjbm$G)nfXyYONbK&2KiB$E+q6pzi>9s~ zH`mPtw7X_M)tdSCVtdmdH80RHkk7s(5p)i;kQ!+ZDLHcU98Np zDqaX>W*V676|<>ygfe{x%aw81Dg2F;`K5O3V|Gj2F4GI+ZmxLIxZ3y+TaN=@O}eWlHmjzEAWlf`i-W173U;xgxi@cQjzA0>P)9U z%|Qd*Da2C2Zln+_o}v=@*&Br@1H~PxCFrECrFBb1HF?mEHj3`J6xP}Y|B zQOZhXcd4L;8((A{^|inJ-n)ityKLpe2eAj()I$I$vD*Q65JDMX;J^p-qWy-}Mx5TSH|bBxxdeRmil$`7e#VLLe9EOx8r$I}ll{OIj6qh`)d?oyq39>`R2w1rkBMmNHSMb%-#qDfc$=Il?VV zm+i%@6eum=a$Re2tkU?o9>asGo)}LMpt>W!CWxQg7y4t8(|4go!Iv2UhUyS#6sCgS zVL;hk^J&s7@s0?d0nA6#^6-)_x#pnw+_$76;%`{oD=0HYT6G1yuv)^sT>Xi^}Y z+$;ZOr9iDYaL_zyPwqI%O@3EUhon$K|m>2B9v!35!R6)t#u->+a0H z2o`mrR>xan6ukZtd7R3N4jDV;FufsaHfyOoQPy(2`HQE#VZz`e)Sdd*})YVG)Bpufnov{SHjgf%u^=;v+Uqd^BF;k}Db z5Pj=JM5W9yGH(F{h;~=mr`JSJYlwG4dNWp@+97ikf8Z!PVoxrm_Ldxy%#pzGp#jJE zZSZi%wD;)YFgA`}-`nhsZW}z@hU^UwpW=2~sEVDFuVu|fDi|3Xnh|~A{4>Pc4neQW zKHSjR;qi?ByHcxTe3!~Fm~ikf3J>Gs6TR`x-J@q{v5_lfoJW%XnRj5ZE9V*-Gr zyocxb7#qUyJu+1`cq%Ue68lyz?BBz)!3S!#~q7LV1L@Qpf;IEe3#ZcQJP|VQZ*Fr{@=Q^!@-hBAGMY@6v>U12JfdA+gx^ zp2r^H+2E<^hRjUe5{1;nA)+X=F$n273|VK41SlOKe#l=*X>Q)S47h*)<=Nt+D&&H3 zCSFJVRBSbZI21Bu3_m5gESa@-T5d<#W72xfdVv#tU&d@N0erN<%X_1)wl`X7_6wb* z3rta1p%5NIs6ru{YYwQO$7qd^;PXR0cq)p+1E9X3hJogeA8GPgtr-YR?mZrHu)$N2 z16IX9ZlY?w3yGf)ZhE6PE(j)YYW(2L!}z;j2va^h$q*fFl?yJ)qahv+Wr-f%zM5UF z!=QuG3CL1jE^U?G`NgIseJa0}T32ENkw+bPJ69_fRZLOF7J#!`@^5A^Av zwL9~RM95IyRx`JvjLTHHx3NjDMg>2WiBz9;}^%cJO1 z!HL--S71Z8`vDs~jOqVTjK{h z!OkokUI3eWcMV@(ds^qdJrSDls6; zGH{^OfP-?!gj)$dd`bm4@%HQErKr5e5@sa76uf&XA3mkR zd^VQpu!leF$AD4-F5~3gQJP;xnnjl+lmD6I$h|k+P}$%q4d9|jYao9;rEbdhM=dyX zV5LB50bdqqEwbc%mfuL%97-?}pmf0NwsBn?xb z1=)W4zyeKOJg^XO+&RG8{`Q5v{C$DUd*c8fX-*$=yJp<3JCp({@e;LLt?#Y_9Io4$ z*Zgo38$7k(3=(Y!S)f4|aW>RoKX^(5nAFw}`k&D8sQ{sJgP)INr9f!`^^nfNFGW=+ zk*Kak