Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Brewing Stands #916

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e77a34e
implement campfires
xNatsuri Aug 20, 2024
436d7a9
Revert "implement campfires"
xNatsuri Aug 22, 2024
7c38d90
register and send Potion and PotionContainerChange recipes
xNatsuri Aug 22, 2024
75cbcac
Revert "register and send Potion and PotionContainerChange recipes"
xNatsuri Aug 22, 2024
2c0efa3
Merge branch 'df-mc:master' into master
xNatsuri Aug 31, 2024
6709c00
Merge branch 'df-mc:master' into master
xNatsuri Sep 1, 2024
0aa713a
implement brewing stands
xNatsuri Sep 1, 2024
bac251a
remove debug code
xNatsuri Sep 1, 2024
41f96d5
Merge branch 'df-mc:master' into master
xNatsuri Sep 2, 2024
136e2e8
Merge branch 'df-mc:master' into master
xNatsuri Sep 7, 2024
6316bd1
Merge branch 'master' of https://github.com/xNatsuri/dragonfly into b…
xNatsuri Sep 7, 2024
44d918f
update to latest df version
xNatsuri Sep 7, 2024
fdaf4a0
working brewing stands (temp fix to import cycle)
xNatsuri Sep 7, 2024
3fcde10
validate reagents
xNatsuri Sep 7, 2024
4f99f3d
validate reagents
xNatsuri Sep 7, 2024
1be2783
Merge branch 'df-mc:master' into brewing-stand
xNatsuri Nov 16, 2024
91b07ba
server: Latest dragonfly & fix recipe registering
TwistedAsylumMC Nov 16, 2024
958d469
remove comment and finish documentation.
xNatsuri Nov 17, 2024
1ea16ca
recipe/vanilla.go: Fix staticcheck
TwistedAsylumMC Nov 17, 2024
77a3854
Merge branch 'df-mc:master' into brewing-stand
xNatsuri Nov 17, 2024
3875ba7
requested changes
xNatsuri Nov 17, 2024
35e52da
add a check if input is a potion or a splash potion
xNatsuri Nov 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
246 changes: 246 additions & 0 deletions server/block/brewer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
package block

import (
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/item/inventory"
"github.com/df-mc/dragonfly/server/item/recipe"
"github.com/df-mc/dragonfly/server/world"
"github.com/df-mc/dragonfly/server/world/sound"
"sync"
"time"
)

// brewer is a struct that may be embedded by blocks that can brew potions, such as brewing stands.
type brewer struct {
mu sync.Mutex

viewers map[ContainerViewer]struct{}
inventory *inventory.Inventory

duration time.Duration
fuelAmount int32
fuelTotal int32
}

// newBrewer creates a new initialised brewer. The inventory is properly initialised.
func newBrewer() *brewer {
b := &brewer{viewers: make(map[ContainerViewer]struct{})}
b.inventory = inventory.New(5, func(slot int, _, item item.Stack) {
b.mu.Lock()
defer b.mu.Unlock()
for viewer := range b.viewers {
viewer.ViewSlotChange(slot, item)
}
})
return b
}

// InsertItem ...
func (b *brewer) 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 = 0
} else if _, ok := sourceStack.Item().(item.BlazePowder); ok {
slot = 4
} else {
for brewingSlot, brewingStack := range b.inventory.Slots() {
if brewingSlot == 0 || brewingSlot == 4 || !brewingStack.Empty() {
continue
}

_, okPotion := sourceStack.Item().(item.Potion)
_, okSplash := sourceStack.Item().(item.SplashPotion)
if !okPotion && !okSplash {
continue
}

slot = brewingSlot
xNatsuri marked this conversation as resolved.
Show resolved Hide resolved
break
}
}

stack := sourceStack.Grow(-sourceStack.Count() + 1)
it, _ := b.Inventory(w, pos).Item(slot)
if slot == 0 {
if !recipe.ValidBrewingReagent(sourceStack.Item()) {
// This item is not a valid brewing reagent.
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)
}

_ = b.Inventory(w, pos).SetItem(slot, stack)
_ = h.inventory.SetItem(sourceSlot, sourceStack.Grow(-1))
return true

}
return false
}

// ExtractItem ...
func (b *brewer) ExtractItem(h Hopper, pos cube.Pos, w *world.World) bool {
for sourceSlot, sourceStack := range b.inventory.Slots() {
if sourceStack.Empty() {
continue
}

if sourceSlot == 0 || sourceSlot == 4 {
continue
}

_, err := h.inventory.AddItem(sourceStack.Grow(-sourceStack.Count() + 1))
if err != nil {
// The hopper is full.
continue
}

_ = b.Inventory(w, pos).SetItem(sourceSlot, sourceStack.Grow(-1))
return true
}
return false
}

// Duration returns the remaining duration of the brewing process.
func (b *brewer) Duration() time.Duration {
b.mu.Lock()
defer b.mu.Unlock()
return b.duration
}

// Fuel returns the fuel and maximum fuel of the brewer.
func (b *brewer) Fuel() (fuel, maxFuel int32) {
b.mu.Lock()
defer b.mu.Unlock()
return b.fuelAmount, b.fuelTotal
}

// Inventory returns the inventory of the brewer.
func (b *brewer) Inventory(*world.World, cube.Pos) *inventory.Inventory {
return b.inventory
}

// AddViewer adds a viewer to the brewer, so that it is updated whenever the inventory of the brewer is changed.
func (b *brewer) AddViewer(v ContainerViewer, _ *world.World, _ cube.Pos) {
b.mu.Lock()
defer b.mu.Unlock()
b.viewers[v] = struct{}{}
}

// RemoveViewer removes a viewer from the brewer, so that slot updates in the inventory are no longer sent to
// it.
func (b *brewer) RemoveViewer(v ContainerViewer, _ *world.World, _ cube.Pos) {
b.mu.Lock()
defer b.mu.Unlock()
delete(b.viewers, v)
}

// setDuration sets the brew duration of the brewer to the given duration.
func (b *brewer) setDuration(duration time.Duration) {
b.mu.Lock()
defer b.mu.Unlock()
b.duration = duration
}

// setFuel sets the fuel of the brewer to the given fuel and maximum fuel.
func (b *brewer) setFuel(fuel, maxFuel int32) {
b.mu.Lock()
defer b.mu.Unlock()
b.fuelAmount, b.fuelTotal = fuel, maxFuel
}

// tickBrewing ticks the brewer, ensuring the necessary items exist in the brewer, and then processing all inputted
// items for the necessary duration.
func (b *brewer) tickBrewing(block string, pos cube.Pos, w *world.World) {
b.mu.Lock()

// Get each item in the brewer. We don't need to validate errors here since we know the bounds of the brewer.
left, _ := b.inventory.Item(1)
middle, _ := b.inventory.Item(2)
right, _ := b.inventory.Item(3)

// Keep track of our past durations, since if any of them change, we need to be able to tell they did and then
// update the viewers on the change.
prevDuration := b.duration
prevFuelAmount := b.fuelAmount
prevFuelTotal := b.fuelTotal

// If we need fuel, try and burn some.
fuel, _ := b.inventory.Item(4)

if _, ok := fuel.Item().(item.BlazePowder); ok && b.fuelAmount <= 0 {
defer b.inventory.SetItem(4, fuel.Grow(-1))
b.fuelAmount, b.fuelTotal = 20, 20
}

// Now get the ingredient item.
ingredient, _ := b.inventory.Item(0)

// Check each input and see if it is affected by the ingredient.
leftOutput, leftAffected := recipe.Perform(block, left.Item(), ingredient.Item())
middleOutput, middleAffected := recipe.Perform(block, middle.Item(), ingredient.Item())
rightOutput, rightAffected := recipe.Perform(block, right.Item(), ingredient.Item())

// Ensure that we have enough fuel to continue.
if b.fuelAmount > 0 {
// Now make sure that we have at least one potion that is affected by the ingredient.
if leftAffected || middleAffected || rightAffected {
// Tick our duration. If we have no brew duration, set it to the default of twenty seconds.
if b.duration == 0 {
b.duration = time.Second * 20
}
b.duration -= time.Millisecond * 50

// If we have no duration, we are done.
if b.duration <= 0 {
// Create the output items.
if leftAffected {
defer b.inventory.SetItem(1, leftOutput[0])
}
if middleAffected {
defer b.inventory.SetItem(2, middleOutput[0])
}
if rightAffected {
defer b.inventory.SetItem(3, rightOutput[0])
}

// Reduce the ingredient by one.
defer b.inventory.SetItem(0, ingredient.Grow(-1))
w.PlaySound(pos.Vec3Centre(), sound.PotionBrewed{})

// Decrement the fuel, and reset the duration.
b.fuelAmount--
b.duration = 0
}
} else {
// None of the potions are affected by the ingredient, so reset the duration.
b.duration = 0
}
} else {
// We don't have enough fuel, so reset our progress.
b.duration, b.fuelAmount, b.fuelTotal = 0, 0, 0
}

// Update the viewers on the new durations.
for v := range b.viewers {
v.ViewBrewingUpdate(prevDuration, b.duration, prevFuelAmount, b.fuelAmount, prevFuelTotal, b.fuelTotal)
}

b.mu.Unlock()
}
148 changes: 148 additions & 0 deletions server/block/brewing_stand.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
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/go-gl/mathgl/mgl64"
"time"
)

// BrewingStand is a block used for brewing potions, splash potions, and lingering potions. It also serves as a cleric's
// job site block generated in village churches.
type BrewingStand struct {
sourceWaterDisplacer
transparent
*brewer

// LeftSlot is true if the left slot is filled.
LeftSlot bool
// MiddleSlot is true if the middle slot is filled.
MiddleSlot bool
// RightSlot is true if the right slot is filled.
RightSlot bool
}

// NewBrewingStand creates a new initialised brewing stand. The inventory is properly initialised.
func NewBrewingStand() BrewingStand {
return BrewingStand{brewer: newBrewer()}
}

// Model ...
func (b BrewingStand) Model() world.BlockModel {
return model.BrewingStand{}
}

// SideClosed ...
func (b BrewingStand) SideClosed(cube.Pos, cube.Pos, *world.World) bool {
return false
}

// Tick is called to check if the brewing stand should update and start or stop brewing.
func (b BrewingStand) Tick(_ int64, pos cube.Pos, w *world.World) {
// Get each item in the brewing stand. We don't need to validate errors here since we know the bounds of the stand.
left, _ := b.inventory.Item(1)
middle, _ := b.inventory.Item(2)
right, _ := b.inventory.Item(3)

// If any of the slots in the inventory got updated, update the appearance of the brewing stand.
displayLeft, displayMiddle, displayRight := b.LeftSlot, b.MiddleSlot, b.RightSlot
b.LeftSlot, b.MiddleSlot, b.RightSlot = !left.Empty(), !middle.Empty(), !right.Empty()
if b.LeftSlot != displayLeft || b.MiddleSlot != displayMiddle || b.RightSlot != displayRight {
w.SetBlock(pos, b, nil)
}

// Tick brewing.
b.tickBrewing("brewing_stand", pos, w)
}

// Activate ...
func (b BrewingStand) 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 (b BrewingStand) 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, b)
if !used {
return
}

//noinspection GoAssignmentToReceiver
b = NewBrewingStand()
place(w, pos, b, user, ctx)
return placed(ctx)
}

// EncodeNBT ...
func (b BrewingStand) EncodeNBT() map[string]any {
if b.brewer == nil {
//noinspection GoAssignmentToReceiver
b = NewBrewingStand()
}
duration := b.Duration()
fuel, totalFuel := b.Fuel()
return map[string]any{
"id": "BrewingStand",
"Items": nbtconv.InvToNBT(b.inventory),
"CookTime": int16(duration.Milliseconds() / 50),
"FuelTotal": int16(totalFuel),
"FuelAmount": int16(fuel),
}
}

// DecodeNBT ...
func (b BrewingStand) DecodeNBT(data map[string]any) any {
brew := time.Duration(nbtconv.Int16(data, "CookTime")) * time.Millisecond * 50

fuel := int32(nbtconv.Int16(data, "FuelAmount"))
maxFuel := int32(nbtconv.Int16(data, "FuelTotal"))

//noinspection GoAssignmentToReceiver
b = NewBrewingStand()
b.setDuration(brew)
b.setFuel(fuel, maxFuel)
nbtconv.InvFromNBT(b.inventory, nbtconv.Slice(data, "Items"))
return b
}

// BreakInfo ...
func (b BrewingStand) BreakInfo() BreakInfo {
return newBreakInfo(0.5, alwaysHarvestable, pickaxeEffective, 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.Vec3Centre())
}
})
}

// EncodeBlock ...
func (b BrewingStand) EncodeBlock() (string, map[string]any) {
return "minecraft:brewing_stand", map[string]any{
"brewing_stand_slot_a_bit": b.LeftSlot,
"brewing_stand_slot_b_bit": b.MiddleSlot,
"brewing_stand_slot_c_bit": b.RightSlot,
}
}

// EncodeItem ...
func (b BrewingStand) EncodeItem() (name string, meta int16) {
return "minecraft:brewing_stand", 0
}

// allBrewingStands ...
func allBrewingStands() (stands []world.Block) {
for _, left := range []bool{false, true} {
for _, middle := range []bool{false, true} {
for _, right := range []bool{false, true} {
stands = append(stands, BrewingStand{LeftSlot: left, MiddleSlot: middle, RightSlot: right})
}
}
}
return
}
Loading