Files
mc/server/world/tick.go
T
TarnaWijaya 26ed99fda6
Build and deploy / Build (push) Has been cancelled
Build and deploy / Update Contributors (push) Has been cancelled
Build and deploy / Deploy (push) Has been cancelled
up3
2026-07-09 08:33:57 +08:00

384 lines
12 KiB
Go

package world
import (
"maps"
"math/rand/v2"
"slices"
"time"
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/internal/sliceutil"
)
// ticker implements World ticking methods.
type ticker struct {
interval time.Duration
}
// tickLoop starts ticking the World 20 times every second, updating all
// entities, blocks and other features such as the time and weather of the
// world, as required.
func (t ticker) tickLoop(w *World) {
tc := time.NewTicker(t.interval)
defer tc.Stop()
for {
select {
case <-tc.C:
<-w.Exec(t.tick)
case <-w.closing:
// World is being closed: Stop ticking and get rid of a task.
w.running.Done()
return
}
}
}
// AdvanceTick advances the World by a single tick. It is generally only useful
// for Worlds created with Config.Synchronous set: other Worlds tick
// automatically 20 times per second. Synchronous Worlds tick loaded chunks
// even when no viewers are present.
func (w *World) AdvanceTick() {
<-w.Exec(ticker{}.tick)
}
// tick performs a tick on the World and updates the time, weather, blocks and
// entities that require updates.
func (t ticker) tick(tx *Tx) {
viewers, loaders := tx.World().allViewers()
w := tx.World()
w.set.Lock()
if s := w.set.Spawn; s[1] > tx.Range()[1] && w.Dimension() == Overworld {
// Vanilla will set the spawn position's Y value to max to indicate that
// the player should spawn at the highest position in the world.
w.set.Spawn[1] = w.highestObstructingBlock(s[0], s[2]) + 1
}
if len(viewers) == 0 && w.set.CurrentTick != 0 && !w.conf.Synchronous {
// Don't continue ticking if no viewers are in the world. Synchronous
// worlds only tick on explicit AdvanceTick calls, so they always tick.
w.set.Unlock()
return
}
if w.advance {
w.set.CurrentTick++
if w.set.TimeCycle {
w.set.Time++
}
if w.set.WeatherCycle {
w.advanceWeather()
}
}
rain, thunder, tick, tim, cycle := w.set.Raining, w.set.Thundering && w.set.Raining, w.set.CurrentTick, int(w.set.Time), w.set.TimeCycle
tryAdvanceDay := false
if tx.w.set.RequiredSleepTicks > 0 {
tx.w.set.RequiredSleepTicks--
tryAdvanceDay = tx.w.set.RequiredSleepTicks <= 0
}
w.set.Unlock()
if tryAdvanceDay {
t.tryAdvanceDay(tx, cycle)
}
if tick%20 == 0 {
for _, viewer := range viewers {
if w.Dimension().TimeCycle() && cycle {
viewer.ViewTime(tim)
}
if w.Dimension().WeatherCycle() {
viewer.ViewWeather(rain, thunder)
}
}
}
if thunder {
w.tickLightning(tx)
}
t.tickEntities(tx, tick)
w.scheduledUpdates.tick(tx, tick)
t.tickBlocksRandomly(tx, loaders, tick)
t.performNeighbourUpdates(tx)
}
// performNeighbourUpdates performs all block updates that came as a result of a neighbouring block being changed.
func (t ticker) performNeighbourUpdates(tx *Tx) {
updates := slices.Clone(tx.World().neighbourUpdates)
clear(tx.World().neighbourUpdates)
tx.World().neighbourUpdates = tx.World().neighbourUpdates[:0]
for _, update := range updates {
pos, changedNeighbour := update.pos, update.neighbour
if ticker, ok := tx.Block(pos).(NeighbourUpdateTicker); ok {
ticker.NeighbourUpdateTick(pos, changedNeighbour, tx)
}
if liquid, ok := tx.World().additionalLiquid(pos); ok {
if ticker, ok := liquid.(NeighbourUpdateTicker); ok {
ticker.NeighbourUpdateTick(pos, changedNeighbour, tx)
}
}
}
}
// tickBlocksRandomly executes random block ticks in loaded chunks within range of loaders.
func (t ticker) tickBlocksRandomly(tx *Tx, loaders []*Loader, tick int64) {
var (
r = int32(tx.World().tickRange())
g randUint4
blockEntities []cube.Pos
randomBlocks []cube.Pos
)
if r == 0 {
// NOP if the simulation distance is 0.
return
}
loaded := make([]ChunkPos, 0, len(loaders))
if tx.World().conf.Synchronous {
loaded = slices.Collect(maps.Keys(tx.World().chunks))
} else {
for _, loader := range loaders {
loader.mu.RLock()
pos := loader.pos
loader.mu.RUnlock()
loaded = append(loaded, pos)
}
}
for pos, c := range tx.World().chunks {
if !t.anyWithinDistance(pos, loaded, r) {
// No loaders in this chunk that are within the simulation distance, so proceed to the next.
continue
}
blockEntities = append(blockEntities, slices.Collect(maps.Keys(c.BlockEntities))...)
cx, cz := int(pos[0]<<4), int(pos[1]<<4)
// We generate up to j random positions for every sub chunk.
for j := 0; j < tx.World().conf.RandomTickSpeed; j++ {
x, y, z := g.uint4(tx.World().r), g.uint4(tx.World().r), g.uint4(tx.World().r)
for i, sub := range c.Sub() {
if sub.Empty() {
// SubChunk is empty, so skip it right away.
continue
}
// Generally we would want to make sure the block has its block entities, but provided blocks
// with block entities are generally ticked already, we are safe to assume that blocks
// implementing the RandomTicker don't rely on additional block entity data.
if rid := sub.Layers()[0].At(x, y, z); tx.World().conf.Blocks.RandomTickBlock(rid) {
subY := (i + (tx.Range().Min() >> 4)) << 4
randomBlocks = append(randomBlocks, cube.Pos{cx + int(x), subY + int(y), cz + int(z)})
// Only generate new coordinates if a tickable block was actually found. If not, we can just re-use
// the coordinates for the next sub chunk.
x, y, z = g.uint4(tx.World().r), g.uint4(tx.World().r), g.uint4(tx.World().r)
}
}
}
}
for _, pos := range randomBlocks {
if rb, ok := tx.Block(pos).(RandomTicker); ok {
rb.RandomTick(pos, tx, tx.World().r)
}
}
for _, pos := range blockEntities {
if tb, ok := tx.Block(pos).(TickerBlock); ok {
tb.Tick(tick, pos, tx)
}
}
}
// anyWithinDistance checks if any of the ChunkPos loaded are within the distance r of the ChunkPos pos.
func (t ticker) anyWithinDistance(pos ChunkPos, loaded []ChunkPos, r int32) bool {
for _, chunkPos := range loaded {
xDiff, zDiff := chunkPos[0]-pos[0], chunkPos[1]-pos[1]
if (xDiff*xDiff)+(zDiff*zDiff) <= r*r {
// The chunk was within the simulation distance of at least one viewer, so we can proceed to
// ticking the block.
return true
}
}
return false
}
// tickEntities ticks all entities in the world, making sure they are still located in the correct chunks and
// updating where necessary.
func (t ticker) tickEntities(tx *Tx, tick int64) {
for handle, lastPos := range tx.World().entities {
e := handle.mustEntity(tx)
chunkPos := chunkPosFromVec3(handle.data.Pos)
c, ok := tx.World().chunks[chunkPos]
if !ok {
continue
}
if lastPos != chunkPos {
// The entity was stored using an outdated chunk position. We update it and make sure it is ready
// for loaders to view it.
tx.World().entities[handle] = chunkPos
c.Entities = append(c.Entities, handle)
var viewers []Viewer
// When changing an entity's world, then teleporting it immediately, we could end up in a situation
// where the old chunk of the entity was not loaded. In this case, it should be safe simply to ignore
// the loaders from the old chunk. We can assume they never saw the entity in the first place.
if old, ok := tx.World().chunks[lastPos]; ok {
old.Entities = sliceutil.DeleteVal(old.Entities, handle)
viewers = old.viewers
}
for _, viewer := range viewers {
if slices.Index(c.viewers, viewer) == -1 {
// First we hide the entity from all loaders that were previously viewing it, but no
// longer are.
viewer.HideEntity(e)
}
}
for _, viewer := range c.viewers {
if slices.Index(viewers, viewer) == -1 {
// Then we show the entity to all loaders that are now viewing the entity in the new
// chunk.
showEntity(e, viewer)
}
}
}
if tx.World().conf.Synchronous || len(c.viewers) > 0 {
if te, ok := e.(TickerEntity); ok {
te.Tick(tx, tick)
}
}
}
}
// randUint4 is a structure used to generate random uint4s.
type randUint4 struct {
x uint64
n uint8
}
// uint4 returns a random uint4.
func (g *randUint4) uint4(r *rand.Rand) uint8 {
if g.n == 0 {
g.x = r.Uint64()
g.n = 16
}
val := g.x & 0b1111
g.x >>= 4
g.n--
return uint8(val)
}
// scheduledTickQueue implements a queue for scheduled block updates. Scheduled
// block updates are both position and block type specific.
type scheduledTickQueue struct {
ticks []scheduledTick
furthestTicks map[scheduledTickIndex]int64
currentTick int64
}
type scheduledTick struct {
pos cube.Pos
b Block
bhash uint64
t int64
}
type scheduledTickIndex struct {
pos cube.Pos
hash uint64
}
// newScheduledTickQueue creates a queue for scheduled block ticks.
func newScheduledTickQueue(tick int64) *scheduledTickQueue {
return &scheduledTickQueue{furthestTicks: make(map[scheduledTickIndex]int64), currentTick: tick}
}
// tick processes scheduled ticks, calling ScheduledTicker.ScheduledTick for any
// block update that is scheduled for the tick passed, and removing it from the
// queue.
func (queue *scheduledTickQueue) tick(tx *Tx, tick int64) {
queue.currentTick = tick
w := tx.World()
for _, t := range queue.ticks {
if t.t > tick {
continue
}
b := tx.Block(t.pos)
if ticker, ok := b.(ScheduledTicker); ok && w.conf.Blocks.BlockHash(b) == t.bhash {
ticker.ScheduledTick(t.pos, tx, w.r)
} else if liquid, ok := tx.World().additionalLiquid(t.pos); ok && w.conf.Blocks.BlockHash(liquid) == t.bhash {
if ticker, ok := liquid.(ScheduledTicker); ok {
ticker.ScheduledTick(t.pos, tx, w.r)
}
}
}
// Clear scheduled ticks that were processed from the queue.
queue.ticks = slices.DeleteFunc(queue.ticks, func(t scheduledTick) bool {
return t.t <= tick
})
maps.DeleteFunc(queue.furthestTicks, func(index scheduledTickIndex, t int64) bool {
return t <= tick
})
}
// schedule schedules a block update at the position passed for the block type
// passed after a specific delay. A block update is only scheduled if no block
// update with the same position and block type is already scheduled at a later
// time than the newly scheduled update.
func (queue *scheduledTickQueue) schedule(br BlockRegistry, pos cube.Pos, b Block, delay time.Duration) {
resTick := queue.currentTick + int64(max(delay/(time.Second/20), 1))
index := scheduledTickIndex{pos: pos, hash: br.BlockHash(b)}
if t, ok := queue.furthestTicks[index]; ok && t >= resTick {
// Already have a tick scheduled for this position that will occur after
// the delay passed. Block updates can only be scheduled if they are
// after any currently scheduled updates.
return
}
queue.furthestTicks[index] = resTick
queue.ticks = append(queue.ticks, scheduledTick{pos: pos, t: resTick, b: b, bhash: index.hash})
}
// fromChunk returns all scheduled ticks positioned within a ChunkPos.
func (queue *scheduledTickQueue) fromChunk(pos ChunkPos) []scheduledTick {
m := make([]scheduledTick, 0, 8)
for _, t := range queue.ticks {
if pos == chunkPosFromBlockPos(t.pos) {
m = append(m, t)
}
}
return m
}
// removeChunk removes all scheduled ticks positioned within a ChunkPos.
func (queue *scheduledTickQueue) removeChunk(pos ChunkPos) {
queue.ticks = slices.DeleteFunc(queue.ticks, func(tick scheduledTick) bool {
return chunkPosFromBlockPos(tick.pos) == pos
})
}
// add adds a slice of scheduled ticks to the queue. It assumes no duplicate
// ticks are present in the slice.
func (queue *scheduledTickQueue) add(ticks []scheduledTick) {
queue.ticks = append(queue.ticks, ticks...)
for _, t := range ticks {
index := scheduledTickIndex{pos: t.pos, hash: t.bhash}
if existing, ok := queue.furthestTicks[index]; ok {
// Make sure we find the furthest tick for each of the ticks added.
// Some ticks may have the same block and position, in which case we
// need to set the furthest tick.
queue.furthestTicks[index] = max(existing, t.t)
}
}
}