Skip to content

Commit 9cb575e

Browse files
committed
world/tick.go: Made scheduled block updates block type tied (as it is in vanilla) and allow scheduled updates later than the previous update.
I think this resolves df-mc#743, but quite frankly I do not understand how this is an issue that exists in the first place(?) The issue described there should not even be possible.
1 parent b02583e commit 9cb575e

File tree

12 files changed

+143
-92
lines changed

12 files changed

+143
-92
lines changed

server/block/composter.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ func (c Composter) fill(it item.Stack, pos cube.Pos, tx *world.Tx) bool {
123123
tx.SetBlock(pos, c, nil)
124124
tx.PlaySound(pos.Vec3(), sound.ComposterFillLayer{})
125125
if c.Level == 7 {
126-
tx.ScheduleBlockUpdate(pos, time.Second)
126+
tx.ScheduleBlockUpdate(pos, c, time.Second)
127127
}
128128

129129
return true

server/block/coral.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,11 @@ func (c Coral) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) {
6464
if c.Dead {
6565
return
6666
}
67-
tx.ScheduleBlockUpdate(pos, time.Second*5/2)
67+
tx.ScheduleBlockUpdate(pos, c, time.Second*5/2)
6868
}
6969

7070
// ScheduledTick ...
7171
func (c Coral) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) {
72-
if c.Dead {
73-
return
74-
}
75-
7672
adjacentWater := false
7773
pos.Neighbours(func(neighbour cube.Pos) {
7874
if liquid, ok := tx.Liquid(neighbour); ok {

server/block/coral_block.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,11 @@ func (c CoralBlock) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) {
2323
if c.Dead {
2424
return
2525
}
26-
tx.ScheduleBlockUpdate(pos, time.Second*5/2)
26+
tx.ScheduleBlockUpdate(pos, c, time.Second*5/2)
2727
}
2828

2929
// ScheduledTick ...
3030
func (c CoralBlock) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) {
31-
if c.Dead {
32-
return
33-
}
34-
3531
adjacentWater := false
3632
pos.Neighbours(func(neighbour cube.Pos) {
3733
if liquid, ok := tx.Liquid(neighbour); ok {

server/block/fire.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ func (f Fire) tick(pos cube.Pos, tx *world.Tx, r *rand.Rand) {
9797
tx.SetBlock(pos, f, nil)
9898
}
9999

100-
tx.ScheduleBlockUpdate(pos, time.Duration(30+r.Intn(10))*time.Second/20)
100+
tx.ScheduleBlockUpdate(pos, f, time.Duration(30+r.Intn(10))*time.Second/20)
101101

102102
if !infinitelyBurns {
103103
_, waterBelow := tx.Block(pos.Side(cube.FaceDown)).(Water)
@@ -184,8 +184,9 @@ func (f Fire) spread(from, to cube.Pos, tx *world.Tx, r *rand.Rand) {
184184
if tx.World().Handler().HandleFireSpread(ctx, from, to); ctx.Cancelled() {
185185
return
186186
}
187-
tx.SetBlock(to, Fire{Type: f.Type, Age: min(15, f.Age+r.Intn(5)/4)}, nil)
188-
tx.ScheduleBlockUpdate(to, time.Duration(30+r.Intn(10))*time.Second/20)
187+
spread := Fire{Type: f.Type, Age: min(15, f.Age+r.Intn(5)/4)}
188+
tx.SetBlock(to, spread, nil)
189+
tx.ScheduleBlockUpdate(to, spread, time.Duration(30+r.Intn(10))*time.Second/20)
189190
}
190191

191192
// EntityInside ...

server/block/lava.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ func (Lava) LightEmissionLevel() uint8 {
9090
// NeighbourUpdateTick ...
9191
func (l Lava) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) {
9292
if !l.Harden(pos, tx, nil) {
93-
tx.ScheduleBlockUpdate(pos, tx.World().Dimension().LavaSpreadDuration())
93+
tx.ScheduleBlockUpdate(pos, l, tx.World().Dimension().LavaSpreadDuration())
9494
}
9595
}
9696

server/block/water.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,13 @@ func (w Water) ScheduledTick(pos cube.Pos, tx *world.Tx, _ *rand.Rand) {
113113
}
114114

115115
// NeighbourUpdateTick ...
116-
func (Water) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) {
116+
func (w Water) NeighbourUpdateTick(pos, _ cube.Pos, tx *world.Tx) {
117117
if tx.World().Dimension().WaterEvaporates() {
118118
// Particles are spawned client-side.
119119
tx.SetLiquid(pos, nil)
120120
return
121121
}
122-
tx.ScheduleBlockUpdate(pos, time.Second/4)
122+
tx.ScheduleBlockUpdate(pos, w, time.Second/4)
123123
}
124124

125125
// LiquidType ...

server/item/fire_charge.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ func (f FireCharge) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *w
2727
} else if s := pos.Side(face); tx.Block(s) == air() {
2828
ctx.SubtractFromCount(1)
2929
tx.PlaySound(s.Vec3Centre(), sound.FireCharge{})
30-
tx.SetBlock(s, fire(), nil)
31-
tx.ScheduleBlockUpdate(s, time.Duration(30+rand.Intn(10))*time.Second/20)
30+
31+
flame := fire()
32+
tx.SetBlock(s, flame, nil)
33+
tx.ScheduleBlockUpdate(s, flame, time.Duration(30+rand.Intn(10))*time.Second/20)
3234
return true
3335
}
3436
return false

server/item/flint_and_steel.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@ func (f FlintAndSteel) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx
3838
return true
3939
} else if s := pos.Side(face); tx.Block(s) == air() {
4040
tx.PlaySound(s.Vec3Centre(), sound.Ignite{})
41-
tx.SetBlock(s, fire(), nil)
42-
tx.ScheduleBlockUpdate(s, time.Duration(30+rand.Intn(10))*time.Second/20)
41+
42+
flame := fire()
43+
tx.SetBlock(s, flame, nil)
44+
tx.ScheduleBlockUpdate(s, flame, time.Duration(30+rand.Intn(10))*time.Second/20)
4345
return true
4446
}
4547
return false

server/world/conf.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package world
22

33
import (
4-
"github.com/df-mc/dragonfly/server/block/cube"
54
"log/slog"
65
"math/rand"
76
"time"
@@ -81,7 +80,7 @@ func (conf Config) New() *World {
8180
}
8281
s := conf.Provider.Settings()
8382
w := &World{
84-
scheduledUpdates: make(map[cube.Pos]int64),
83+
scheduledUpdates: newScheduledTickQueue(),
8584
entities: make(map[*EntityHandle]ChunkPos),
8685
viewers: make(map[*Loader]Viewer),
8786
chunks: make(map[ChunkPos]*Column),
@@ -93,15 +92,17 @@ func (conf Config) New() *World {
9392
ra: conf.Dim.Range(),
9493
set: s,
9594
}
96-
w.weather, w.ticker = weather{w: w}, ticker{}
95+
w.weather = weather{w: w}
9796
var h Handler = NopHandler{}
9897
w.handler.Store(&h)
9998

10099
w.running.Add(3)
101-
go w.tickLoop(w)
100+
101+
t := ticker{interval: time.Second / 20}
102+
go t.tickLoop(w)
102103
go w.autoSave()
103104
go w.handleTransactions()
104105

105-
<-w.Exec(w.tick)
106+
<-w.Exec(t.tick)
106107
return w
107108
}

server/world/tick.go

Lines changed: 101 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@ import (
99
"time"
1010
)
1111

12-
// ticker implements World ticking methods. World embeds this struct, so any exported methods on ticker are exported
13-
// methods on World.
14-
type ticker struct{}
12+
// ticker implements World ticking methods.
13+
type ticker struct {
14+
interval time.Duration
15+
}
1516

16-
// tickLoop starts ticking the World 20 times every second, updating all entities, blocks and other features such as
17-
// the time and weather of the world, as required.
17+
// tickLoop starts ticking the World 20 times every second, updating all
18+
// entities, blocks and other features such as the time and weather of the
19+
// world, as required.
1820
func (t ticker) tickLoop(w *World) {
19-
tc := time.NewTicker(time.Second / 20)
21+
tc := time.NewTicker(t.interval)
2022
defer tc.Stop()
2123
for {
2224
select {
@@ -30,75 +32,56 @@ func (t ticker) tickLoop(w *World) {
3032
}
3133
}
3234

33-
// tick performs a tick on the World and updates the time, weather, blocks and entities that require updates.
35+
// tick performs a tick on the World and updates the time, weather, blocks and
36+
// entities that require updates.
3437
func (t ticker) tick(tx *Tx) {
3538
viewers, loaders := tx.World().allViewers()
39+
w := tx.World()
3640

37-
tx.World().set.Lock()
38-
if s := tx.World().set.Spawn; s[1] > tx.Range()[1] {
41+
w.set.Lock()
42+
if s := w.set.Spawn; s[1] > tx.Range()[1] {
3943
// Vanilla will set the spawn position's Y value to max to indicate that
4044
// the player should spawn at the highest position in the world.
41-
tx.World().set.Spawn[1] = tx.World().highestObstructingBlock(s[0], s[2]) + 1
45+
w.set.Spawn[1] = w.highestObstructingBlock(s[0], s[2]) + 1
4246
}
43-
if len(viewers) == 0 && tx.World().set.CurrentTick != 0 {
44-
tx.World().set.Unlock()
47+
if len(viewers) == 0 && w.set.CurrentTick != 0 {
48+
// Don't continue ticking if no viewers are in the world.
49+
w.set.Unlock()
4550
return
4651
}
47-
if tx.World().advance {
48-
tx.World().set.CurrentTick++
49-
if tx.World().set.TimeCycle {
50-
tx.World().set.Time++
52+
if w.advance {
53+
w.set.CurrentTick++
54+
if w.set.TimeCycle {
55+
w.set.Time++
5156
}
52-
if tx.World().set.WeatherCycle {
53-
tx.World().advanceWeather()
57+
if w.set.WeatherCycle {
58+
w.advanceWeather()
5459
}
5560
}
5661

57-
rain, thunder, tick, tim := tx.World().set.Raining, tx.World().set.Thundering && tx.World().set.Raining, tx.World().set.CurrentTick, int(tx.World().set.Time)
58-
tx.World().set.Unlock()
62+
rain, thunder, tick, tim := w.set.Raining, w.set.Thundering && w.set.Raining, w.set.CurrentTick, int(w.set.Time)
63+
w.set.Unlock()
5964

6065
if tick%20 == 0 {
6166
for _, viewer := range viewers {
62-
if tx.World().Dimension().TimeCycle() {
67+
if w.Dimension().TimeCycle() {
6368
viewer.ViewTime(tim)
6469
}
65-
if tx.World().Dimension().WeatherCycle() {
70+
if w.Dimension().WeatherCycle() {
6671
viewer.ViewWeather(rain, thunder)
6772
}
6873
}
6974
}
7075
if thunder {
71-
tx.World().tickLightning(tx)
76+
w.tickLightning(tx)
7277
}
7378

7479
t.tickEntities(tx, tick)
80+
w.scheduledUpdates.tick(tx, tick)
7581
t.tickBlocksRandomly(tx, loaders, tick)
76-
t.tickScheduledBlocks(tx, tick)
7782
t.performNeighbourUpdates(tx)
7883
}
7984

80-
// tickScheduledBlocks executes scheduled block updates in chunks that are currently loaded.
81-
func (t ticker) tickScheduledBlocks(tx *Tx, tick int64) {
82-
positions := make([]cube.Pos, 0, len(tx.World().scheduledUpdates)/4)
83-
for pos, scheduledTick := range tx.World().scheduledUpdates {
84-
if scheduledTick <= tick {
85-
positions = append(positions, pos)
86-
delete(tx.World().scheduledUpdates, pos)
87-
}
88-
}
89-
90-
for _, pos := range positions {
91-
if ticker, ok := tx.Block(pos).(ScheduledTicker); ok {
92-
ticker.ScheduledTick(pos, tx, tx.World().r)
93-
}
94-
if liquid, ok := tx.World().additionalLiquid(pos); ok {
95-
if ticker, ok := liquid.(ScheduledTicker); ok {
96-
ticker.ScheduledTick(pos, tx, tx.World().r)
97-
}
98-
}
99-
}
100-
}
101-
10285
// performNeighbourUpdates performs all block updates that came as a result of a neighbouring block being changed.
10386
func (t ticker) performNeighbourUpdates(tx *Tx) {
10487
updates := slices.Clone(tx.World().neighbourUpdates)
@@ -269,3 +252,75 @@ func (g *randUint4) uint4(r *rand.Rand) uint8 {
269252
g.n--
270253
return uint8(val)
271254
}
255+
256+
// scheduledTickQueue implements a queue for scheduled block updates. Scheduled
257+
// block updates are both position and block type specific.
258+
type scheduledTickQueue struct {
259+
ticks []scheduledTick
260+
furthestTicks map[scheduledTickIndex]int64
261+
currentTick int64
262+
}
263+
264+
type scheduledTick struct {
265+
pos cube.Pos
266+
b Block
267+
bhash uint64
268+
t int64
269+
}
270+
271+
type scheduledTickIndex struct {
272+
pos cube.Pos
273+
hash uint64
274+
}
275+
276+
// newScheduledTickQueue creates a queue for scheduled block ticks.
277+
func newScheduledTickQueue() *scheduledTickQueue {
278+
return &scheduledTickQueue{furthestTicks: make(map[scheduledTickIndex]int64)}
279+
}
280+
281+
// tick processes scheduled ticks, calling ScheduledTicker.ScheduledTick for any
282+
// block update that is scheduled for the tick passed, and removing it from the
283+
// queue.
284+
func (queue *scheduledTickQueue) tick(tx *Tx, tick int64) {
285+
queue.currentTick = tick
286+
287+
w := tx.World()
288+
for _, t := range queue.ticks {
289+
if t.t > tick {
290+
continue
291+
}
292+
b := tx.Block(t.pos)
293+
if ticker, ok := tx.Block(t.pos).(ScheduledTicker); ok && BlockHash(b) == t.bhash {
294+
ticker.ScheduledTick(t.pos, tx, w.r)
295+
} else if liquid, ok := tx.World().additionalLiquid(t.pos); ok && BlockHash(b) == t.bhash {
296+
if ticker, ok := liquid.(ScheduledTicker); ok {
297+
ticker.ScheduledTick(t.pos, tx, w.r)
298+
}
299+
}
300+
}
301+
302+
// Clear scheduled ticks that were processed from the queue.
303+
queue.ticks = slices.DeleteFunc(queue.ticks, func(t scheduledTick) bool {
304+
return t.t <= tick
305+
})
306+
maps.DeleteFunc(queue.furthestTicks, func(index scheduledTickIndex, t int64) bool {
307+
return t <= tick
308+
})
309+
}
310+
311+
// schedule schedules a block update at the position passed for the block type
312+
// passed after a specific delay. A block update is only scheduled if no block
313+
// update with the same position and block type is already scheduled at a later
314+
// time than the newly scheduled update.
315+
func (queue *scheduledTickQueue) schedule(pos cube.Pos, b Block, delay time.Duration) {
316+
resTick := queue.currentTick + max(delay.Nanoseconds()/int64(time.Second/20), 1)
317+
index := scheduledTickIndex{pos: pos, hash: BlockHash(b)}
318+
if t, ok := queue.furthestTicks[index]; ok && t >= resTick {
319+
// Already have a tick scheduled for this position that will occur after
320+
// the delay passed. Block updates can only be scheduled if they are
321+
// after any currently scheduled updates.
322+
return
323+
}
324+
queue.furthestTicks[index] = resTick
325+
queue.ticks = append(queue.ticks, scheduledTick{pos: pos, t: resTick, b: b, bhash: index.hash})
326+
}

server/world/tx.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,14 @@ func (tx *Tx) BuildStructure(pos cube.Pos, s Structure) {
7474
tx.World().buildStructure(pos, s)
7575
}
7676

77-
// ScheduleBlockUpdate schedules a block update at the position passed after a
78-
// specific delay. If the block at that position does not handle block updates,
79-
// nothing will happen.
80-
func (tx *Tx) ScheduleBlockUpdate(pos cube.Pos, delay time.Duration) {
81-
tx.World().scheduleBlockUpdate(pos, delay)
77+
// ScheduleBlockUpdate schedules a block update at the position passed for the
78+
// block type passed after a specific delay. If the block at that position does
79+
// not handle block updates, nothing will happen.
80+
// Block updates are both block and position specific. A block update is only
81+
// scheduled if no block update with the same position and block type is
82+
// already scheduled at a later time than the newly scheduled update.
83+
func (tx *Tx) ScheduleBlockUpdate(pos cube.Pos, b Block, delay time.Duration) {
84+
tx.World().scheduleBlockUpdate(pos, b, delay)
8285
}
8386

8487
// HighestLightBlocker gets the Y value of the highest fully light blocking

0 commit comments

Comments
 (0)