@@ -9,14 +9,16 @@ import (
9
9
"time"
10
10
)
11
11
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
+ }
15
16
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.
18
20
func (t ticker ) tickLoop (w * World ) {
19
- tc := time .NewTicker (time . Second / 20 )
21
+ tc := time .NewTicker (t . interval )
20
22
defer tc .Stop ()
21
23
for {
22
24
select {
@@ -30,75 +32,56 @@ func (t ticker) tickLoop(w *World) {
30
32
}
31
33
}
32
34
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.
34
37
func (t ticker ) tick (tx * Tx ) {
35
38
viewers , loaders := tx .World ().allViewers ()
39
+ w := tx .World ()
36
40
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 ] {
39
43
// Vanilla will set the spawn position's Y value to max to indicate that
40
44
// 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
42
46
}
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 ()
45
50
return
46
51
}
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 ++
51
56
}
52
- if tx . World () .set .WeatherCycle {
53
- tx . World () .advanceWeather ()
57
+ if w .set .WeatherCycle {
58
+ w .advanceWeather ()
54
59
}
55
60
}
56
61
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 ()
59
64
60
65
if tick % 20 == 0 {
61
66
for _ , viewer := range viewers {
62
- if tx . World () .Dimension ().TimeCycle () {
67
+ if w .Dimension ().TimeCycle () {
63
68
viewer .ViewTime (tim )
64
69
}
65
- if tx . World () .Dimension ().WeatherCycle () {
70
+ if w .Dimension ().WeatherCycle () {
66
71
viewer .ViewWeather (rain , thunder )
67
72
}
68
73
}
69
74
}
70
75
if thunder {
71
- tx . World () .tickLightning (tx )
76
+ w .tickLightning (tx )
72
77
}
73
78
74
79
t .tickEntities (tx , tick )
80
+ w .scheduledUpdates .tick (tx , tick )
75
81
t .tickBlocksRandomly (tx , loaders , tick )
76
- t .tickScheduledBlocks (tx , tick )
77
82
t .performNeighbourUpdates (tx )
78
83
}
79
84
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
-
102
85
// performNeighbourUpdates performs all block updates that came as a result of a neighbouring block being changed.
103
86
func (t ticker ) performNeighbourUpdates (tx * Tx ) {
104
87
updates := slices .Clone (tx .World ().neighbourUpdates )
@@ -269,3 +252,75 @@ func (g *randUint4) uint4(r *rand.Rand) uint8 {
269
252
g .n --
270
253
return uint8 (val )
271
254
}
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
+ }
0 commit comments