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  }