github.com/df-mc/dragonfly@v0.9.13/server/world/tick.go (about) 1 package world 2 3 import ( 4 "github.com/df-mc/dragonfly/server/block/cube" 5 "github.com/df-mc/dragonfly/server/internal/sliceutil" 6 "golang.org/x/exp/maps" 7 "math/rand" 8 "slices" 9 "time" 10 ) 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{ w *World } 15 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. 18 func (t ticker) tickLoop() { 19 tc := time.NewTicker(time.Second / 20) 20 defer tc.Stop() 21 22 t.w.running.Add(1) 23 for { 24 select { 25 case <-tc.C: 26 t.tick() 27 case <-t.w.closing: 28 // World is being closed: Stop ticking and get rid of a task. 29 t.w.running.Done() 30 return 31 } 32 } 33 } 34 35 // tick performs a tick on the World and updates the time, weather, blocks and entities that require updates. 36 func (t ticker) tick() { 37 viewers, loaders := t.w.allViewers() 38 39 t.w.set.Lock() 40 if len(viewers) == 0 && t.w.set.CurrentTick != 0 { 41 t.w.set.Unlock() 42 return 43 } 44 if t.w.advance { 45 t.w.set.CurrentTick++ 46 if t.w.set.TimeCycle { 47 t.w.set.Time++ 48 } 49 if t.w.set.WeatherCycle { 50 t.w.advanceWeather() 51 } 52 } 53 54 rain, thunder, tick, tim := t.w.set.Raining, t.w.set.Thundering && t.w.set.Raining, t.w.set.CurrentTick, int(t.w.set.Time) 55 t.w.set.Unlock() 56 57 if tick%20 == 0 { 58 for _, viewer := range viewers { 59 if t.w.conf.Dim.TimeCycle() { 60 viewer.ViewTime(tim) 61 } 62 if t.w.conf.Dim.WeatherCycle() { 63 viewer.ViewWeather(rain, thunder) 64 } 65 } 66 } 67 if thunder { 68 t.w.tickLightning() 69 } 70 71 t.tickEntities(tick) 72 t.tickBlocksRandomly(loaders, tick) 73 t.tickScheduledBlocks(tick) 74 t.performNeighbourUpdates() 75 } 76 77 // tickScheduledBlocks executes scheduled block updates in chunks that are currently loaded. 78 func (t ticker) tickScheduledBlocks(tick int64) { 79 t.w.updateMu.Lock() 80 positions := make([]cube.Pos, 0, len(t.w.scheduledUpdates)/4) 81 for pos, scheduledTick := range t.w.scheduledUpdates { 82 if scheduledTick <= tick { 83 positions = append(positions, pos) 84 delete(t.w.scheduledUpdates, pos) 85 } 86 } 87 t.w.updateMu.Unlock() 88 89 for _, pos := range positions { 90 if ticker, ok := t.w.Block(pos).(ScheduledTicker); ok { 91 ticker.ScheduledTick(pos, t.w, t.w.r) 92 } 93 if liquid, ok := t.w.additionalLiquid(pos); ok { 94 if ticker, ok := liquid.(ScheduledTicker); ok { 95 ticker.ScheduledTick(pos, t.w, t.w.r) 96 } 97 } 98 } 99 } 100 101 // performNeighbourUpdates performs all block updates that came as a result of a neighbouring block being changed. 102 func (t ticker) performNeighbourUpdates() { 103 t.w.updateMu.Lock() 104 positions := slices.Clone(t.w.neighbourUpdates) 105 t.w.neighbourUpdates = t.w.neighbourUpdates[:0] 106 t.w.updateMu.Unlock() 107 108 for _, update := range positions { 109 pos, changedNeighbour := update.pos, update.neighbour 110 if ticker, ok := t.w.Block(pos).(NeighbourUpdateTicker); ok { 111 ticker.NeighbourUpdateTick(pos, changedNeighbour, t.w) 112 } 113 if liquid, ok := t.w.additionalLiquid(pos); ok { 114 if ticker, ok := liquid.(NeighbourUpdateTicker); ok { 115 ticker.NeighbourUpdateTick(pos, changedNeighbour, t.w) 116 } 117 } 118 } 119 } 120 121 // tickBlocksRandomly executes random block ticks in each sub chunk in the world that has at least one viewer 122 // registered from the viewers passed. 123 func (t ticker) tickBlocksRandomly(loaders []*Loader, tick int64) { 124 var ( 125 r = int32(t.w.tickRange()) 126 g randUint4 127 blockEntities []cube.Pos 128 randomBlocks []cube.Pos 129 ) 130 if r == 0 { 131 // NOP if the simulation distance is 0. 132 return 133 } 134 135 loaded := make([]ChunkPos, 0, len(loaders)) 136 for _, loader := range loaders { 137 loader.mu.RLock() 138 pos := loader.pos 139 loader.mu.RUnlock() 140 141 loaded = append(loaded, pos) 142 } 143 144 t.w.chunkMu.Lock() 145 for pos, c := range t.w.chunks { 146 if !t.anyWithinDistance(pos, loaded, r) { 147 // No loaders in this chunk that are within the simulation distance, so proceed to the next. 148 continue 149 } 150 c.Lock() 151 blockEntities = append(blockEntities, maps.Keys(c.BlockEntities)...) 152 153 cx, cz := int(pos[0]<<4), int(pos[1]<<4) 154 155 // We generate up to j random positions for every sub chunk. 156 for j := 0; j < t.w.conf.RandomTickSpeed; j++ { 157 x, y, z := g.uint4(t.w.r), g.uint4(t.w.r), g.uint4(t.w.r) 158 159 for i, sub := range c.Sub() { 160 if sub.Empty() { 161 // SubChunk is empty, so skip it right away. 162 continue 163 } 164 // Generally we would want to make sure the block has its block entities, but provided blocks 165 // with block entities are generally ticked already, we are safe to assume that blocks 166 // implementing the RandomTicker don't rely on additional block entity data. 167 if rid := sub.Layers()[0].At(x, y, z); randomTickBlocks[rid] { 168 subY := (i + (t.w.Range().Min() >> 4)) << 4 169 randomBlocks = append(randomBlocks, cube.Pos{cx + int(x), subY + int(y), cz + int(z)}) 170 171 // Only generate new coordinates if a tickable block was actually found. If not, we can just re-use 172 // the coordinates for the next sub chunk. 173 x, y, z = g.uint4(t.w.r), g.uint4(t.w.r), g.uint4(t.w.r) 174 } 175 } 176 } 177 c.Unlock() 178 } 179 t.w.chunkMu.Unlock() 180 181 for _, pos := range randomBlocks { 182 if rb, ok := t.w.Block(pos).(RandomTicker); ok { 183 rb.RandomTick(pos, t.w, t.w.r) 184 } 185 } 186 for _, pos := range blockEntities { 187 if tb, ok := t.w.Block(pos).(TickerBlock); ok { 188 tb.Tick(tick, pos, t.w) 189 } 190 } 191 } 192 193 // anyWithinDistance checks if any of the ChunkPos loaded are within the distance r of the ChunkPos pos. 194 func (t ticker) anyWithinDistance(pos ChunkPos, loaded []ChunkPos, r int32) bool { 195 for _, chunkPos := range loaded { 196 xDiff, zDiff := chunkPos[0]-pos[0], chunkPos[1]-pos[1] 197 if (xDiff*xDiff)+(zDiff*zDiff) <= r*r { 198 // The chunk was within the simulation distance of at least one viewer, so we can proceed to 199 // ticking the block. 200 return true 201 } 202 } 203 return false 204 } 205 206 // tickEntities ticks all entities in the world, making sure they are still located in the correct chunks and 207 // updating where necessary. 208 func (t ticker) tickEntities(tick int64) { 209 type entityToMove struct { 210 e Entity 211 after *Column 212 viewersBefore []Viewer 213 } 214 var ( 215 entitiesToMove []entityToMove 216 entitiesToTick []TickerEntity 217 ) 218 219 t.w.chunkMu.Lock() 220 t.w.entityMu.Lock() 221 for e, lastPos := range t.w.entities { 222 chunkPos := chunkPosFromVec3(e.Position()) 223 224 c, ok := t.w.chunks[chunkPos] 225 if !ok { 226 continue 227 } 228 229 c.Lock() 230 v := len(c.viewers) 231 c.Unlock() 232 233 if v > 0 { 234 if ticker, ok := e.(TickerEntity); ok { 235 entitiesToTick = append(entitiesToTick, ticker) 236 } 237 } 238 239 if lastPos != chunkPos { 240 // The entity was stored using an outdated chunk position. We update it and make sure it is ready 241 // for loaders to view it. 242 t.w.entities[e] = chunkPos 243 var viewers []Viewer 244 245 // When changing an entity's world, then teleporting it immediately, we could end up in a situation 246 // where the old chunk of the entity was not loaded. In this case, it should be safe simply to ignore 247 // the loaders from the old chunk. We can assume they never saw the entity in the first place. 248 if old, ok := t.w.chunks[lastPos]; ok { 249 old.Lock() 250 old.Entities = sliceutil.DeleteVal(old.Entities, e) 251 viewers = slices.Clone(old.viewers) 252 old.Unlock() 253 } 254 entitiesToMove = append(entitiesToMove, entityToMove{e: e, viewersBefore: viewers, after: c}) 255 } 256 } 257 t.w.entityMu.Unlock() 258 t.w.chunkMu.Unlock() 259 260 for _, move := range entitiesToMove { 261 move.after.Lock() 262 move.after.Entities = append(move.after.Entities, move.e) 263 viewersAfter := move.after.viewers 264 move.after.Unlock() 265 266 for _, viewer := range move.viewersBefore { 267 if sliceutil.Index(viewersAfter, viewer) == -1 { 268 // First we hide the entity from all loaders that were previously viewing it, but no 269 // longer are. 270 viewer.HideEntity(move.e) 271 } 272 } 273 for _, viewer := range viewersAfter { 274 if sliceutil.Index(move.viewersBefore, viewer) == -1 { 275 // Then we show the entity to all loaders that are now viewing the entity in the new 276 // chunk. 277 showEntity(move.e, viewer) 278 } 279 } 280 } 281 for _, ticker := range entitiesToTick { 282 // Make sure the entity is still in world and has not been closed. 283 if ticker.World() == t.w { 284 // We gather entities to ticker and ticker them later, so that the lock on the entity mutex is no longer 285 // active. 286 ticker.Tick(t.w, tick) 287 } 288 } 289 } 290 291 // randUint4 is a structure used to generate random uint4s. 292 type randUint4 struct { 293 x uint64 294 n uint8 295 } 296 297 // uint4 returns a random uint4. 298 func (g *randUint4) uint4(r *rand.Rand) uint8 { 299 if g.n == 0 { 300 g.x = r.Uint64() 301 g.n = 16 302 } 303 val := g.x & 0b1111 304 305 g.x >>= 4 306 g.n-- 307 return uint8(val) 308 }