Files
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

1407 lines
42 KiB
Go

package world
import (
"encoding/binary"
"errors"
"fmt"
"iter"
"maps"
"math/rand/v2"
"slices"
"sync"
"sync/atomic"
"time"
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/event"
"github.com/df-mc/dragonfly/server/internal/sliceutil"
"github.com/df-mc/dragonfly/server/world/chunk"
"github.com/df-mc/dragonfly/server/world/redstone"
"github.com/df-mc/goleveldb/leveldb"
"github.com/go-gl/mathgl/mgl64"
"github.com/google/uuid"
)
// World implements a Minecraft world. It manages all aspects of what players
// can see, such as blocks, entities and particles. World generally provides a
// synchronised state: All entities, blocks and players usually operate in this
// world, so World ensures that all its methods will always be safe for
// simultaneous calls. A nil *World is safe to use but not functional.
type World struct {
conf Config
ra cube.Range
queue chan transaction
queueClosing chan struct{}
queueing sync.WaitGroup
// advance is a bool that specifies if this World should advance the current
// tick, time and weather saved in the Settings struct held by the World.
advance bool
o sync.Once
set *Settings
handler atomic.Pointer[Handler]
weather
closing chan struct{}
running sync.WaitGroup
// chunks holds a cache of chunks currently loaded. These chunks are cleared
// from this map after some time of not being used.
chunks map[ChunkPos]*Column
// entities holds a map of entities currently loaded and the last ChunkPos
// that the Entity was in. These are tracked so that a call to RemoveEntity
// can find the correct Entity.
entities map[*EntityHandle]ChunkPos
r *rand.Rand
// scheduledUpdates is a map of tick time values indexed by the block
// position at which an update is scheduled. If the current tick exceeds the
// tick value passed, the block update will be performed and the entry will
// be removed from the map.
scheduledUpdates *scheduledTickQueue
neighbourUpdates []neighbourUpdate
redstone redstone.State
viewerMu sync.Mutex
viewers map[*Loader]Viewer
}
// transaction is a type that may be added to the transaction queue of a World.
// Its Run method is called when the transaction is taken out of the queue.
type transaction interface {
Run(w *World)
}
// New creates a new initialised world. The world may be used right away, but
// it will not be saved or loaded from files until it has been given a
// different provider than the default. (NopProvider) By default, the name of
// the world will be 'World'.
func New() *World {
var conf Config
return conf.New()
}
// Name returns the display name of the world. Generally, this name is
// displayed at the top of the player list in the pause screen in-game. If a
// provider is set, the name will be updated according to the name that it
// provides.
func (w *World) Name() string {
w.set.Lock()
defer w.set.Unlock()
return w.set.Name
}
// Dimension returns the Dimension assigned to the World in world.New. The sky
// colour and behaviour of a variety of world features differ based on the
// Dimension.
func (w *World) Dimension() Dimension {
return w.conf.Dim
}
// Range returns the range in blocks of the World (min and max). It is
// equivalent to calling World.Dimension().Range().
func (w *World) Range() cube.Range {
return w.ra
}
// BlockRegistry returns the BlockRegistry used by the World.
func (w *World) BlockRegistry() BlockRegistry {
return w.conf.Blocks
}
// ExecFunc is a function that performs a synchronised transaction on a World.
type ExecFunc func(tx *Tx)
// Exec performs a synchronised transaction f on a World. Exec returns a channel
// that is closed once the transaction is complete. For Worlds created with
// Config.Synchronous set, the transaction is executed on the calling goroutine
// and the channel returned is closed when Exec returns. Awaiting a nested Exec
// from within a transaction deadlocks on non-synchronous Worlds.
func (w *World) Exec(f ExecFunc) <-chan struct{} {
c := make(chan struct{})
ntx := normalTransaction{c: c, f: f}
if w.conf.Synchronous {
ntx.Run(w)
return c
}
w.queue <- ntx
return c
}
func (w *World) weakExec(invalid *atomic.Bool, cond *sync.Cond, f ExecFunc) <-chan bool {
c := make(chan bool, 1)
if w.conf.Synchronous {
valid := !invalid.Load()
if valid {
// As in weakTransaction.Run, f must not run under cond.L: it may
// relock it, e.g. through RemoveEntity.
cond.L.Unlock()
tx := &Tx{w: w}
f(tx)
tx.close()
cond.L.Lock()
}
c <- valid
return c
}
w.queue <- weakTransaction{c: c, f: f, invalid: invalid, cond: cond}
return c
}
// handleTransactions continuously reads transactions from the queue and runs
// them.
func (w *World) handleTransactions() {
for {
select {
case tx := <-w.queue:
tx.Run(w)
case <-w.queueClosing:
w.queueing.Done()
return
}
}
}
// EntityRegistry returns the EntityRegistry that was passed to the World's
// Config upon construction.
func (w *World) EntityRegistry() EntityRegistry {
return w.conf.Entities
}
// block reads a block from the position passed. If a chunk is not yet loaded
// at that position, the chunk is loaded, or generated if it could not be found
// in the world save, and the block returned.
func (w *World) block(pos cube.Pos) Block {
return w.blockInChunk(w.chunk(chunkPosFromBlockPos(pos)), pos)
}
// blockInChunk reads a block from a chunk at the position passed. The block
// is assumed to be within the chunk passed.
func (w *World) blockInChunk(c *Column, pos cube.Pos) Block {
if pos.OutOfBounds(w.ra) {
// Fast way out.
return w.conf.Blocks.Air()
}
rid := c.Block(uint8(pos[0]), int16(pos[1]), uint8(pos[2]), 0)
if w.conf.Blocks.NBTBlock(rid) {
// The block was also a block entity, so we look it up in the block entity map.
if b, ok := c.BlockEntities[pos]; ok {
return b
}
// Despite being a block with NBT, the block didn't actually have any
// stored NBT yet. We add it here and update the block.
nbtB := w.conf.Blocks.BlockByRuntimeIDOrAir(rid).(NBTer).DecodeNBT(map[string]any{}).(Block)
c.BlockEntities[pos] = nbtB
for _, v := range c.viewers {
v.ViewBlockUpdate(pos, nbtB, 0)
}
return nbtB
}
return w.conf.Blocks.BlockByRuntimeIDOrAir(rid)
}
// biome reads the Biome at the position passed. If a chunk is not yet loaded
// at that position, the chunk is loaded, or generated if it could not be found
// in the world save, and the Biome returned.
func (w *World) biome(pos cube.Pos) Biome {
if pos.OutOfBounds(w.Range()) {
// Fast way out.
return ocean()
}
id := int(w.chunk(chunkPosFromBlockPos(pos)).Biome(uint8(pos[0]), int16(pos[1]), uint8(pos[2])))
b, ok := BiomeByID(id)
if !ok {
w.conf.Log.Error("biome not found by ID", "ID", id)
}
return b
}
// HighestLightBlocker gets the Y value of the highest fully light blocking
// block at the x and z values passed in the World.
func (w *World) HighestLightBlocker(x, z int) int {
return int(w.chunk(ChunkPos{int32(x >> 4), int32(z >> 4)}).HighestLightBlocker(uint8(x), uint8(z)))
}
// highestBlock looks up the highest non-air block in the World at a specific x
// and z The y value of the highest block is returned, or 0 if no blocks were
// present in the column.
func (w *World) highestBlock(x, z int) int {
return int(w.chunk(ChunkPos{int32(x >> 4), int32(z >> 4)}).HighestBlock(uint8(x), uint8(z)))
}
// highestObstructingBlock returns the highest block in the World at a given x
// and z that has at least a solid top or bottom face.
func (w *World) highestObstructingBlock(x, z int) int {
yHigh := w.highestBlock(x, z)
src := worldSource{w: w}
for y := yHigh; y >= w.Range()[0]; y-- {
pos := cube.Pos{x, y, z}
m := w.block(pos).Model()
if m.FaceSolid(pos, cube.FaceUp, src) || m.FaceSolid(pos, cube.FaceDown, src) {
return y
}
}
return w.Range()[0]
}
// SetOpts holds several parameters that may be set to disable updates in the
// World of different kinds as a result of a call to SetBlock.
type SetOpts struct {
// DisableBlockUpdates makes SetBlock not update any neighbouring blocks as
// a result of the SetBlock call.
DisableBlockUpdates bool
// DisableLiquidDisplacement disables the displacement of liquid blocks to
// the second layer (or back to the first layer, if it already was on the
// second layer). Disabling this is not widely recommended unless
// performance is very important, or where it is known no liquid can be
// present anyway.
DisableLiquidDisplacement bool
}
// setBlock writes a block to the position passed. If a chunk is not yet loaded
// at that position, the chunk is first loaded or generated if it could not be
// found in the world save. setBlock panics if the block passed has not yet
// been registered using RegisterBlock(). Nil may be passed as the block to set
// the block to air.
//
// A SetOpts struct may be passed to additionally modify behaviour of setBlock,
// specifically to improve performance under specific circumstances. Nil should
// be passed where performance is not essential, to make sure the world is
// updated adequately.
//
// setBlock should be avoided in situations where performance is critical when
// needing to set a lot of blocks to the world. BuildStructure may be used
// instead.
func (w *World) setBlock(pos cube.Pos, b Block, opts *SetOpts) {
if pos.OutOfBounds(w.Range()) {
// Fast way out.
return
}
if opts == nil {
opts = &SetOpts{}
}
x, y, z := uint8(pos[0]), int16(pos[1]), uint8(pos[2])
c := w.chunk(chunkPosFromBlockPos(pos))
rid := w.conf.Blocks.BlockRuntimeID(b)
var before uint32
if rid != w.conf.Blocks.AirRuntimeID() && !opts.DisableLiquidDisplacement {
before = c.Block(x, y, z, 0)
}
c.modified = true
c.SetBlock(x, y, z, 0, rid)
if w.conf.Blocks.NBTBlock(rid) {
c.BlockEntities[pos] = b
} else {
delete(c.BlockEntities, pos)
}
viewers := slices.Clone(c.viewers)
if !opts.DisableLiquidDisplacement {
var secondLayer Block
airRID := w.conf.Blocks.AirRuntimeID()
if rid == airRID {
if li := c.Block(x, y, z, 1); li != airRID {
c.SetBlock(x, y, z, 0, li)
c.SetBlock(x, y, z, 1, airRID)
secondLayer = w.conf.Blocks.Air()
b = w.conf.Blocks.BlockByRuntimeIDOrAir(li)
}
} else if w.conf.Blocks.LiquidDisplacingBlock(rid) {
if w.conf.Blocks.LiquidBlock(before) {
l := w.conf.Blocks.BlockByRuntimeIDOrAir(before)
if b.(LiquidDisplacer).CanDisplace(l.(Liquid)) {
c.SetBlock(x, y, z, 1, before)
secondLayer = l
}
}
} else if li := c.Block(x, y, z, 1); li != airRID {
c.SetBlock(x, y, z, 1, airRID)
secondLayer = w.conf.Blocks.Air()
}
if secondLayer != nil {
for _, viewer := range viewers {
viewer.ViewBlockUpdate(pos, secondLayer, 1)
}
}
}
for _, viewer := range viewers {
viewer.ViewBlockUpdate(pos, b, 0)
}
if !opts.DisableBlockUpdates {
w.doBlockUpdatesAround(pos)
}
}
// setBiome sets the Biome at the position passed. If a chunk is not yet loaded
// at that position, the chunk is first loaded or generated if it could not be
// found in the world save.
func (w *World) setBiome(pos cube.Pos, b Biome) {
if pos.OutOfBounds(w.Range()) {
// Fast way out.
return
}
c := w.chunk(chunkPosFromBlockPos(pos))
c.modified = true
c.SetBiome(uint8(pos[0]), int16(pos[1]), uint8(pos[2]), uint32(b.EncodeBiome()))
}
// buildStructure builds a Structure passed at a specific position in the
// world. Unlike setBlock, it takes a Structure implementation, which provides
// blocks to be placed at a specific location. buildStructure is specifically
// optimised to be able to process a large batch of chunks simultaneously and
// will do so within much less time than separate setBlock calls would. The
// method operates on a per-chunk basis, setting all blocks within a single
// chunk part of the Structure before moving on to the next chunk.
func (w *World) buildStructure(pos cube.Pos, s Structure) {
dim := s.Dimensions()
width, height, length := dim[0], dim[1], dim[2]
maxX, maxY, maxZ := pos[0]+width, pos[1]+height, pos[2]+length
f := func(x, y, z int) Block {
return w.block(cube.Pos{pos[0] + x, pos[1] + y, pos[2] + z})
}
// We approach this on a per-chunk basis, so that we can keep only one chunk
// in memory at a time while not needing to acquire a new chunk lock for
// every block. This also allows us not to send block updates, but instead
// send a single chunk update once.
for chunkX := pos[0] >> 4; chunkX <= maxX>>4; chunkX++ {
for chunkZ := pos[2] >> 4; chunkZ <= maxZ>>4; chunkZ++ {
chunkPos := ChunkPos{int32(chunkX), int32(chunkZ)}
c := w.chunk(chunkPos)
baseX, baseZ := chunkX<<4, chunkZ<<4
for i, sub := range c.Sub() {
baseY := (i + (w.Range()[0] >> 4)) << 4
if baseY>>4 < pos[1]>>4 {
continue
} else if baseY >= maxY {
break
}
for localY := 0; localY < 16; localY++ {
yOffset := baseY + localY
if yOffset > w.Range()[1] || yOffset >= maxY {
// We've hit the height limit for blocks.
break
} else if yOffset < w.Range()[0] || yOffset < pos[1] {
// We've got a block below the minimum, but other blocks might still reach above
// it, so don't break but continue.
continue
}
for localX := 0; localX < 16; localX++ {
xOffset := baseX + localX
if xOffset < pos[0] || xOffset >= maxX {
continue
}
for localZ := 0; localZ < 16; localZ++ {
zOffset := baseZ + localZ
if zOffset < pos[2] || zOffset >= maxZ {
continue
}
b, liq := s.At(xOffset-pos[0], yOffset-pos[1], zOffset-pos[2], f)
if b != nil {
rid := w.conf.Blocks.BlockRuntimeID(b)
sub.SetBlock(uint8(xOffset), uint8(yOffset), uint8(zOffset), 0, rid)
nbtPos := cube.Pos{xOffset, yOffset, zOffset}
if w.conf.Blocks.NBTBlock(rid) {
c.BlockEntities[nbtPos] = b
} else {
delete(c.BlockEntities, nbtPos)
}
}
if liq != nil {
sub.SetBlock(uint8(xOffset), uint8(yOffset), uint8(zOffset), 1, w.conf.Blocks.BlockRuntimeID(liq))
} else if len(sub.Layers()) > 1 {
sub.SetBlock(uint8(xOffset), uint8(yOffset), uint8(zOffset), 1, w.conf.Blocks.AirRuntimeID())
}
}
}
}
}
c.SetBlock(0, 0, 0, 0, c.Block(0, 0, 0, 0)) // Make sure the heightmap is recalculated.
c.modified = true
// After setting all blocks of the structure within a single chunk,
// we show the new chunk to all viewers once.
for _, viewer := range c.viewers {
viewer.ViewChunk(chunkPos, w.Dimension(), c.BlockEntities, c.Chunk)
}
}
}
}
// liquid attempts to return a Liquid block at the position passed. This
// Liquid may be in the foreground or in any other layer. If found, the Liquid
// is returned. If not, the bool returned is false.
func (w *World) liquid(pos cube.Pos) (Liquid, bool) {
if pos.OutOfBounds(w.Range()) {
// Fast way out.
return nil, false
}
c := w.chunk(chunkPosFromBlockPos(pos))
x, y, z := uint8(pos[0]), int16(pos[1]), uint8(pos[2])
id := c.Block(x, y, z, 0)
b, ok := w.conf.Blocks.BlockByRuntimeID(id)
if !ok {
w.conf.Log.Error("Liquid: no block with runtime ID", "ID", id)
return nil, false
}
if liq, ok := b.(Liquid); ok {
return liq, true
}
id = c.Block(x, y, z, 1)
b, ok = w.conf.Blocks.BlockByRuntimeID(id)
if !ok {
w.conf.Log.Error("Liquid: no block with runtime ID", "ID", id)
return nil, false
}
liq, ok := b.(Liquid)
return liq, ok
}
// setLiquid sets a Liquid at a specific position in the World. Unlike
// setBlock, setLiquid will not necessarily overwrite any existing blocks. It
// will instead be in the same position as a block currently there, unless
// there already is a Liquid at that position, in which case it will be
// overwritten. If nil is passed for the Liquid, any Liquid currently present
// will be removed.
func (w *World) setLiquid(pos cube.Pos, b Liquid) {
if pos.OutOfBounds(w.Range()) {
// Fast way out.
return
}
chunkPos := chunkPosFromBlockPos(pos)
c := w.chunk(chunkPos)
if b == nil {
w.removeLiquids(c, pos)
w.doBlockUpdatesAround(pos)
return
}
x, y, z := uint8(pos[0]), int16(pos[1]), uint8(pos[2])
if !replaceable(w, c, pos, b) {
if displacer, ok := w.blockInChunk(c, pos).(LiquidDisplacer); !ok || !displacer.CanDisplace(b) {
return
}
}
rid := w.conf.Blocks.BlockRuntimeID(b)
if w.removeLiquids(c, pos) {
c.SetBlock(x, y, z, 0, rid)
for _, v := range c.viewers {
v.ViewBlockUpdate(pos, b, 0)
}
} else {
c.SetBlock(x, y, z, 1, rid)
for _, v := range c.viewers {
v.ViewBlockUpdate(pos, b, 1)
}
}
c.modified = true
w.doBlockUpdatesAround(pos)
}
// removeLiquids removes any liquid blocks that may be present at a specific
// block position in the chunk passed. The bool returned specifies if no blocks
// were left on the foreground layer.
func (w *World) removeLiquids(c *Column, pos cube.Pos) bool {
x, y, z := uint8(pos[0]), int16(pos[1]), uint8(pos[2])
air := w.conf.Blocks.Air()
noneLeft := false
if noLeft, changed := w.removeLiquidOnLayer(c.Chunk, x, y, z, 0); noLeft {
if changed {
for _, v := range c.viewers {
v.ViewBlockUpdate(pos, air, 0)
}
}
noneLeft = true
}
if _, changed := w.removeLiquidOnLayer(c.Chunk, x, y, z, 1); changed {
for _, v := range c.viewers {
v.ViewBlockUpdate(pos, air, 1)
}
}
return noneLeft
}
// removeLiquidOnLayer removes a liquid block from a specific layer in the
// chunk passed, returning true if successful.
func (w *World) removeLiquidOnLayer(c *chunk.Chunk, x uint8, y int16, z, layer uint8) (bool, bool) {
id := c.Block(x, y, z, layer)
airRID := w.conf.Blocks.AirRuntimeID()
b, ok := w.conf.Blocks.BlockByRuntimeID(id)
if !ok {
w.conf.Log.Error("removeLiquidOnLayer: no block with runtime ID", "ID", id)
return false, false
}
if _, ok := b.(Liquid); ok {
c.SetBlock(x, y, z, layer, airRID)
return true, true
}
return id == airRID, false
}
// additionalLiquid checks if the block at a position has additional liquid on
// another layer and returns the liquid if so.
func (w *World) additionalLiquid(pos cube.Pos) (Liquid, bool) {
if pos.OutOfBounds(w.Range()) {
// Fast way out.
return nil, false
}
c := w.chunk(chunkPosFromBlockPos(pos))
id := c.Block(uint8(pos[0]), int16(pos[1]), uint8(pos[2]), 1)
b, ok := w.conf.Blocks.BlockByRuntimeID(id)
if !ok {
w.conf.Log.Error("additionalLiquid: no block with runtime ID", "ID", id)
return nil, false
}
liq, ok := b.(Liquid)
return liq, ok
}
// light returns the light level at the position passed. This is the highest of
// the sky and block light. The light value returned is a value in the range
// 0-15, where 0 means there is no light present, whereas 15 means the block is
// fully lit.
func (w *World) light(pos cube.Pos) uint8 {
if pos[1] < w.ra[0] {
// Fast way out.
return 0
}
if pos[1] > w.ra[1] {
// Above the rest of the world, so full skylight.
return 15
}
return w.chunk(chunkPosFromBlockPos(pos)).Light(uint8(pos[0]), int16(pos[1]), uint8(pos[2]))
}
// skyLight returns the skylight level at the position passed. This light level
// is not influenced by blocks that emit light, such as torches. The light
// value, similarly to light, is a value in the range 0-15, where 0 means no
// light is present.
func (w *World) skyLight(pos cube.Pos) uint8 {
if pos[1] < w.ra[0] {
// Fast way out.
return 0
}
if pos[1] > w.ra[1] {
// Above the rest of the world, so full skylight.
return 15
}
return w.chunk(chunkPosFromBlockPos(pos)).SkyLight(uint8(pos[0]), int16(pos[1]), uint8(pos[2]))
}
// Time returns the current time of the world. The time is incremented every
// 1/20th of a second, unless World.StopTime() is called.
func (w *World) Time() int {
if w == nil {
return 0
}
w.set.Lock()
defer w.set.Unlock()
return int(w.set.Time)
}
// SetTime sets the new time of the world. SetTime will always work, regardless
// of whether the time is stopped or not.
func (w *World) SetTime(new int) {
if w == nil {
return
}
w.set.Lock()
w.set.Time = int64(new)
w.set.Unlock()
viewers, _ := w.allViewers()
for _, viewer := range viewers {
viewer.ViewTime(new)
}
}
// StopTime stops the time in the world. When called, the time will no longer
// cycle and the world will remain at the time when StopTime is called. The
// time may be restarted by calling World.StartTime().
func (w *World) StopTime() {
w.enableTimeCycle(false)
}
// StartTime restarts the time in the world. When called, the time will start
// cycling again and the day/night cycle will continue. The time may be stopped
// again by calling World.StopTime().
func (w *World) StartTime() {
w.enableTimeCycle(true)
}
// TimeCycle returns whether time cycle is enabled.
func (w *World) TimeCycle() bool {
if w == nil {
return false
}
w.set.Lock()
defer w.set.Unlock()
return w.set.TimeCycle
}
// enableTimeCycle enables or disables the time cycling of the World.
func (w *World) enableTimeCycle(v bool) {
if w == nil {
return
}
w.set.Lock()
defer w.set.Unlock()
w.set.TimeCycle = v
viewers, _ := w.allViewers()
for _, viewer := range viewers {
viewer.ViewTimeCycle(v)
}
}
// temperature returns the temperature in the World at a specific position.
// Higher altitudes and different biomes influence the temperature returned.
func (w *World) temperature(pos cube.Pos) float64 {
const (
tempDrop = 1.0 / 600
seaLevel = 64
)
diff := max(pos[1]-seaLevel, 0)
return w.biome(pos).Temperature() - float64(diff)*tempDrop
}
// addParticle spawns a Particle at a given position in the World. Viewers that
// are viewing the chunk will be shown the particle.
func (w *World) addParticle(pos mgl64.Vec3, p Particle) {
p.Spawn(w, pos)
for _, viewer := range w.viewersOf(pos) {
viewer.ViewParticle(pos, p)
}
}
// playSound plays a sound at a specific position in the World. Viewers of that
// position will be able to hear the sound if they are close enough.
func (w *World) playSound(tx *Tx, pos mgl64.Vec3, s Sound) {
ctx := event.C(tx)
if w.Handler().HandleSound(ctx, s, pos); ctx.Cancelled() {
return
}
s.Play(w, pos)
for _, viewer := range w.viewersOf(pos) {
viewer.ViewSound(pos, s)
}
}
// addEntity adds an EntityHandle to a World. The Entity will be visible to all
// viewers of the World that have the chunk at the EntityHandle's position. If
// the chunk that the EntityHandle is in is not yet loaded, it will first be
// loaded. addEntity panics if the EntityHandle is already in a world.
// addEntity returns the Entity created by the EntityHandle.
func (w *World) addEntity(tx *Tx, handle *EntityHandle) Entity {
handle.setAndUnlockWorld(w)
pos := chunkPosFromVec3(handle.data.Pos)
w.entities[handle] = pos
c := w.chunk(pos)
c.Entities, c.modified = append(c.Entities, handle), true
e := handle.mustEntity(tx)
for _, v := range c.viewers {
// Show the entity to all viewers in the chunk of the entity.
showEntity(e, v)
}
w.Handler().HandleEntitySpawn(tx, e)
return e
}
// removeEntity removes an Entity from the World that is currently present in
// it. Any viewers of the Entity will no longer be able to see it.
// removeEntity returns the EntityHandle of the Entity. After removing an Entity
// from the World, the Entity is no longer usable.
func (w *World) removeEntity(e Entity, tx *Tx) *EntityHandle {
handle := e.H()
pos, found := w.entities[handle]
if !found {
// The entity currently isn't in this world.
return nil
}
w.Handler().HandleEntityDespawn(tx, e)
c := w.chunk(pos)
c.Entities, c.modified = sliceutil.DeleteVal(c.Entities, handle), true
w.removeEntityFromViewLayers(e)
for _, v := range c.viewers {
v.HideEntity(e)
}
delete(w.entities, handle)
handle.unsetAndLockWorld()
return handle
}
// removeEntityFromViewLayers removes stale overrides for despawned entities. Entities that own a ViewLayer,
// such as players, are skipped because they may be removed temporarily when respawning or changing worlds.
func (w *World) removeEntityFromViewLayers(e Entity) {
if _, ok := e.(viewLayerViewer); ok {
return
}
viewers, _ := w.allViewers()
for _, viewer := range viewers {
v, ok := viewer.(viewLayerViewer)
if !ok || v.ViewLayer() == nil {
continue
}
v.ViewLayer().remove(e)
}
}
// entitiesWithin returns an iterator that yields all entities contained within
// the cube.BBox passed.
func (w *World) entitiesWithin(tx *Tx, box cube.BBox) iter.Seq[Entity] {
return func(yield func(Entity) bool) {
minPos, maxPos := chunkPosFromVec3(box.Min()), chunkPosFromVec3(box.Max())
for x := minPos[0]; x <= maxPos[0]; x++ {
for z := minPos[1]; z <= maxPos[1]; z++ {
c, ok := w.chunks[ChunkPos{x, z}]
if !ok {
// The chunk wasn't loaded, so there are no entities here.
continue
}
for _, handle := range slices.Clone(c.Entities) {
if !box.Vec3Within(handle.data.Pos) {
continue
}
ent, ok := handle.Entity(tx)
if ok && !yield(ent) {
return
}
}
}
}
}
}
// allEntities returns an iterator that yields all entities in the World.
func (w *World) allEntities(tx *Tx) iter.Seq[Entity] {
return func(yield func(Entity) bool) {
for e := range w.entities {
if ent := e.mustEntity(tx); !yield(ent) {
return
}
}
}
}
// allPlayers returns an iterator that yields all player entities in the World.
func (w *World) allPlayers(tx *Tx) iter.Seq[Entity] {
return func(yield func(Entity) bool) {
for e := range w.entities {
if e.t.EncodeEntity() == "minecraft:player" {
if ent := e.mustEntity(tx); !yield(ent) {
return
}
}
}
}
}
// Spawn returns the spawn of the world. Every new player will by default spawn
// on this position in the world when joining.
func (w *World) Spawn() cube.Pos {
if w == nil {
return cube.Pos{}
}
if w.Dimension() == End {
return cube.Pos{100, 50}
} else if w.Dimension() == Nether {
return cube.Pos{}
}
w.set.Lock()
defer w.set.Unlock()
return w.set.Spawn
}
// SetSpawn sets the spawn of the world to a different position. The player
// will be spawned in the centre of this position when newly joining.
func (w *World) SetSpawn(pos cube.Pos) {
if w == nil {
return
}
// nether has no spawn point and end spawn point is always 100 50 0.
if w.Dimension() == Nether || w.Dimension() == End {
return
}
w.set.Lock()
w.set.Spawn = pos
w.set.Unlock()
viewers, _ := w.allViewers()
for _, viewer := range viewers {
viewer.ViewWorldSpawn(pos)
}
}
// PlayerSpawn returns the spawn position of a player with a UUID in this World.
func (w *World) PlayerSpawn(id uuid.UUID) cube.Pos {
if w == nil {
return cube.Pos{}
}
pos, exist, err := w.conf.Provider.LoadPlayerSpawnPosition(id)
if err != nil {
w.conf.Log.Error("load player spawn: "+err.Error(), "ID", id)
return w.Spawn()
}
if !exist {
return w.Spawn()
}
return pos
}
// SetPlayerSpawn sets the spawn position of a player with a UUID in this
// World. If the player has a spawn in the world, the player will be teleported
// to this location on respawn.
func (w *World) SetPlayerSpawn(id uuid.UUID, pos cube.Pos) {
if w == nil {
return
}
if err := w.conf.Provider.SavePlayerSpawnPosition(id, pos); err != nil {
w.conf.Log.Error("save player spawn: "+err.Error(), "ID", id)
}
}
// SetRequiredSleepDuration sets the duration of time players in the world must sleep for, in order to advance to the
// next day.
func (w *World) SetRequiredSleepDuration(duration time.Duration) {
if w == nil {
return
}
w.set.Lock()
defer w.set.Unlock()
w.set.RequiredSleepTicks = duration.Milliseconds() / 50
}
// DefaultGameMode returns the default game mode of the world. When players
// join, they are given this game mode. The default game mode may be changed
// using SetDefaultGameMode().
func (w *World) DefaultGameMode() GameMode {
if w == nil {
return GameModeSurvival
}
w.set.Lock()
defer w.set.Unlock()
return w.set.DefaultGameMode
}
// SetTickRange sets the range in chunks around each Viewer that will have the
// chunks (their blocks and entities) ticked when the World is ticked.
func (w *World) SetTickRange(v int) {
if w == nil {
return
}
w.set.Lock()
defer w.set.Unlock()
w.set.TickRange = int32(v)
}
// tickRange returns the tick range around each Viewer.
func (w *World) tickRange() int {
w.set.Lock()
defer w.set.Unlock()
return int(w.set.TickRange)
}
// SetDefaultGameMode changes the default game mode of the world. When players
// join, they are then given that game mode.
func (w *World) SetDefaultGameMode(mode GameMode) {
if w == nil {
return
}
w.set.Lock()
defer w.set.Unlock()
w.set.DefaultGameMode = mode
}
// Difficulty returns the difficulty of the world. Properties of mobs in the
// world and the player's hunger will depend on this difficulty.
func (w *World) Difficulty() Difficulty {
if w == nil {
return DifficultyNormal
}
w.set.Lock()
defer w.set.Unlock()
return w.set.Difficulty
}
// SetDifficulty changes the difficulty of a world.
func (w *World) SetDifficulty(d Difficulty) {
if w == nil {
return
}
w.set.Lock()
defer w.set.Unlock()
w.set.Difficulty = d
}
// scheduleBlockUpdate schedules a block update at the position passed for the
// block type passed after a specific delay. If the block at that position does
// not handle block updates, nothing will happen.
// Block updates are both block and position specific. 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 (w *World) scheduleBlockUpdate(pos cube.Pos, b Block, delay time.Duration) {
if pos.OutOfBounds(w.Range()) {
return
}
w.scheduledUpdates.schedule(w.conf.Blocks, pos, b, delay)
}
// doBlockUpdatesAround schedules block updates directly around and on the
// position passed.
func (w *World) doBlockUpdatesAround(pos cube.Pos) {
if w == nil || pos.OutOfBounds(w.Range()) {
return
}
changed := pos
w.updateNeighbour(pos, changed)
pos.Neighbours(func(pos cube.Pos) {
w.updateNeighbour(pos, changed)
}, w.Range())
}
// neighbourUpdate represents a position that needs to be updated because of a
// neighbour that changed.
type neighbourUpdate struct {
pos, neighbour cube.Pos
}
// updateNeighbour ticks the position passed as a result of the neighbour
// passed being updated.
func (w *World) updateNeighbour(pos, changedNeighbour cube.Pos) {
w.neighbourUpdates = append(w.neighbourUpdates, neighbourUpdate{pos: pos, neighbour: changedNeighbour})
}
// Handle changes the current Handler of the world. As a result, events called
// by the world will call the methods of the Handler passed. Handle sets the
// world's Handler to NopHandler if nil is passed.
func (w *World) Handle(h Handler) {
if w == nil {
return
}
if h == nil {
h = NopHandler{}
}
w.handler.Store(&h)
}
// viewersOf returns all viewers viewing the position passed.
func (w *World) viewersOf(pos mgl64.Vec3) []Viewer {
c, ok := w.chunks[chunkPosFromVec3(pos)]
if !ok {
return nil
}
return c.viewers
}
// PortalDestination returns the destination World for a portal of a specific
// Dimension. If no destination World could be found, the current World is
// returned. Calling PortalDestination(Nether) on an Overworld World returns
// Nether, while calling PortalDestination(Nether) on a Nether World will
// return the Overworld, for instance.
func (w *World) PortalDestination(dim Dimension) *World {
if w.conf.PortalDestination == nil {
return w
}
if res := w.conf.PortalDestination(dim); res != nil {
return res
}
return w
}
// Save saves the World to the provider.
func (w *World) Save() {
<-w.Exec(w.save(w.saveChunk))
}
// save saves all loaded chunks to the World's provider.
func (w *World) save(f func(*Tx, ChunkPos, *Column)) ExecFunc {
return func(tx *Tx) {
if w.conf.ReadOnly {
return
}
w.conf.Log.Debug("Saving chunks in memory to disk...")
for pos, c := range w.chunks {
f(tx, pos, c)
}
w.conf.Log.Debug("Updating level.dat values...")
w.conf.Provider.SaveSettings(w.set)
}
}
// saveChunk saves a chunk and its entities to disk after compacting the chunk.
func (w *World) saveChunk(_ *Tx, pos ChunkPos, c *Column) {
if !w.conf.ReadOnly && c.modified {
c.Compact()
if err := w.conf.Provider.StoreColumn(pos, w.conf.Dim, w.columnTo(c, pos)); err != nil {
w.conf.Log.Error("save chunk: "+err.Error(), "X", pos[0], "Z", pos[1])
}
}
}
// closeChunk saves a chunk and its entities to disk after compacting the chunk.
// Afterwards, scheduled updates from that chunk are removed and all entities
// in it are closed.
func (w *World) closeChunk(tx *Tx, pos ChunkPos, c *Column) {
w.saveChunk(tx, pos, c)
w.scheduledUpdates.removeChunk(pos)
// Note: We close c.Entities here because some entities may remove
// themselves from the world in their Close method, which can lead to
// unexpected conditions.
for _, e := range slices.Clone(c.Entities) {
_ = e.mustEntity(tx).Close()
}
clear(c.Entities)
delete(w.chunks, pos)
}
// Close closes the world and saves all chunks currently loaded.
func (w *World) Close() error {
w.o.Do(w.close)
return nil
}
// close stops the World from ticking, saves all chunks to the Provider and
// updates the world's settings.
func (w *World) close() {
<-w.Exec(func(tx *Tx) {
// Let user code run anything that needs to be finished before closing.
w.Handler().HandleClose(tx)
w.Handle(NopHandler{})
w.save(w.closeChunk)(tx)
})
close(w.closing)
w.running.Wait()
close(w.queueClosing)
w.queueing.Wait()
if w.set.ref.Add(-1); !w.advance {
return
}
w.conf.Log.Debug("Closing provider...")
if err := w.conf.Provider.Close(); err != nil {
w.conf.Log.Error("close world provider: " + err.Error())
}
}
// allViewers returns all viewers and loaders, regardless of where in the world
// they are viewing.
func (w *World) allViewers() ([]Viewer, []*Loader) {
w.viewerMu.Lock()
defer w.viewerMu.Unlock()
viewers, loaders := make([]Viewer, 0, len(w.viewers)), make([]*Loader, 0, len(w.viewers))
for k, v := range w.viewers {
viewers = append(viewers, v)
loaders = append(loaders, k)
}
return viewers, loaders
}
// addWorldViewer adds a viewer to the world. Should only be used while the
// viewer isn't viewing any chunks.
func (w *World) addWorldViewer(l *Loader) {
w.viewerMu.Lock()
w.viewers[l] = l.viewer
w.viewerMu.Unlock()
l.viewer.ViewTime(w.Time())
l.viewer.ViewTimeCycle(w.TimeCycle())
w.set.Lock()
raining, thundering := w.set.Raining, w.set.Raining && w.set.Thundering
w.set.Unlock()
l.viewer.ViewWeather(raining, thundering)
l.viewer.ViewWorldSpawn(w.Spawn())
}
// addViewer adds a viewer to the World at a given position. Any events that
// happen in the chunk at that position, such as block and entity changes, will
// be sent to the viewer.
func (w *World) addViewer(tx *Tx, c *Column, loader *Loader) {
c.viewers = append(c.viewers, loader.viewer)
c.loaders = append(c.loaders, loader)
for _, entity := range c.Entities {
showEntity(entity.mustEntity(tx), loader.viewer)
}
}
// removeViewer removes a viewer from a chunk position. All entities will be
// hidden from the viewer and no more calls will be made when events in the
// chunk happen.
func (w *World) removeViewer(tx *Tx, pos ChunkPos, loader *Loader) {
if w == nil {
return
}
c, ok := w.chunks[pos]
if !ok {
return
}
if i := slices.Index(c.loaders, loader); i != -1 {
c.viewers = slices.Delete(c.viewers, i, i+1)
c.loaders = slices.Delete(c.loaders, i, i+1)
}
// Hide all entities in the chunk from the viewer.
for _, entity := range c.Entities {
loader.viewer.HideEntity(entity.mustEntity(tx))
}
}
// Handler returns the Handler of the world.
func (w *World) Handler() Handler {
if w == nil {
return NopHandler{}
}
return *w.handler.Load()
}
// showEntity shows an Entity to a viewer of the world. It makes sure
// everything of the Entity, including the items held, is shown.
func showEntity(e Entity, viewer Viewer) {
viewer.ViewEntity(e)
viewer.ViewEntityItems(e)
viewer.ViewEntityArmour(e)
}
// chunk reads a chunk from the position passed. If a chunk at that position is
// not yet loaded, the chunk is loaded from the provider, or generated if it
// did not yet exist. Additionally, chunks newly loaded have the light in them
// calculated before they are returned.
func (w *World) chunk(pos ChunkPos) *Column {
c, ok := w.chunks[pos]
if ok {
return c
}
c, err := w.loadChunk(pos)
chunk.LightArea([]*chunk.Chunk{c.Chunk}, int(pos[0]), int(pos[1])).Fill()
if err != nil {
w.conf.Log.Error("load chunk: "+err.Error(), "X", pos[0], "Z", pos[1])
return c
}
w.calculateLight(pos)
return c
}
// loadChunk attempts to load a chunk from the provider, or generates a chunk
// if one doesn't currently exist.
func (w *World) loadChunk(pos ChunkPos) (*Column, error) {
column, err := w.conf.Provider.LoadColumn(pos, w.conf.Dim)
switch {
case err == nil:
col := w.columnFrom(column, pos)
w.chunks[pos] = col
for _, e := range col.Entities {
w.entities[e] = pos
e.w = w
}
return col, nil
case errors.Is(err, leveldb.ErrNotFound):
// The provider doesn't have a chunk saved at this position, so we generate a new one.
col := newColumn(chunk.New(w.conf.Blocks, w.Range()))
w.chunks[pos] = col
w.conf.Generator.GenerateChunk(pos, col.Chunk)
return col, nil
default:
return newColumn(chunk.New(w.conf.Blocks, w.Range())), err
}
}
// calculateLight calculates the light in the chunk passed and spreads the
// light of any surrounding neighbours if they have all chunks loaded around it
// as a result of the one passed.
func (w *World) calculateLight(centre ChunkPos) {
for x := int32(-1); x <= 1; x++ {
for z := int32(-1); z <= 1; z++ {
// For all the neighbours of this chunk, if they exist, check if all
// neighbours of that chunk now exist because of this one.
pos := ChunkPos{centre[0] + x, centre[1] + z}
if _, ok := w.chunks[pos]; ok {
// Attempt to spread the light of all neighbours into the
// surrounding ones.
w.spreadLight(pos)
}
}
}
}
// spreadLight spreads the light from the chunk passed at the position passed
// to all neighbours if each of them is loaded.
func (w *World) spreadLight(pos ChunkPos) {
c := make([]*chunk.Chunk, 0, 9)
for z := int32(-1); z <= 1; z++ {
for x := int32(-1); x <= 1; x++ {
neighbour, ok := w.chunks[ChunkPos{pos[0] + x, pos[1] + z}]
if !ok {
// Not all surrounding chunks existed: Stop spreading light.
return
}
c = append(c, neighbour.Chunk)
}
}
// All chunks surrounding the current one are present, so we can spread.
chunk.LightArea(c, int(pos[0])-1, int(pos[1])-1).Spread()
}
// autoSave runs until the world is running, saving and removing chunks that
// are no longer in use.
func (w *World) autoSave() {
save := &time.Ticker{C: make(<-chan time.Time)}
if w.conf.SaveInterval > 0 {
save = time.NewTicker(w.conf.SaveInterval)
defer save.Stop()
}
closeUnused := time.NewTicker(w.conf.ChunkUnloadInterval)
defer closeUnused.Stop()
for {
select {
case <-closeUnused.C:
<-w.Exec(w.closeUnusedChunks)
case <-save.C:
w.Save()
case <-w.closing:
w.running.Done()
return
}
}
}
// closeUnusedChunk closes all chunks currently not in use by any viewer.
func (w *World) closeUnusedChunks(tx *Tx) {
for pos, c := range w.chunks {
if len(c.viewers) == 0 {
w.closeChunk(tx, pos, c)
}
}
}
// Column represents the data of a chunk including the (block) entities and
// viewers and loaders.
type Column struct {
modified bool
*chunk.Chunk
Entities []*EntityHandle
BlockEntities map[cube.Pos]Block
viewers []Viewer
loaders []*Loader
}
// newColumn returns a new Column wrapper around the chunk.Chunk passed.
func newColumn(c *chunk.Chunk) *Column {
return &Column{Chunk: c, BlockEntities: map[cube.Pos]Block{}}
}
// columnTo converts a Column to a chunk.Column so that it can be written to
// a provider.
func (w *World) columnTo(col *Column, pos ChunkPos) *chunk.Column {
scheduled := w.scheduledUpdates.fromChunk(pos)
c := &chunk.Column{
Chunk: col.Chunk,
Entities: make([]chunk.Entity, 0, len(col.Entities)),
BlockEntities: make([]chunk.BlockEntity, 0, len(col.BlockEntities)),
ScheduledBlocks: make([]chunk.ScheduledBlockUpdate, 0, len(scheduled)),
Tick: w.scheduledUpdates.currentTick,
}
for _, e := range col.Entities {
data := e.encodeNBT()
maps.Copy(data, e.t.EncodeNBT(&e.data))
data["identifier"] = e.t.EncodeEntity()
c.Entities = append(c.Entities, chunk.Entity{ID: int64(binary.LittleEndian.Uint64(e.id[8:])), Data: data})
}
for pos, be := range col.BlockEntities {
c.BlockEntities = append(c.BlockEntities, chunk.BlockEntity{Pos: pos, Data: be.(NBTer).EncodeNBT()})
}
for _, t := range scheduled {
c.ScheduledBlocks = append(c.ScheduledBlocks, chunk.ScheduledBlockUpdate{Pos: t.pos, Block: w.conf.Blocks.BlockRuntimeID(t.b), Tick: t.t})
}
return c
}
// columnFrom converts a chunk.Column to a Column after reading it from a
// provider.
func (w *World) columnFrom(c *chunk.Column, _ ChunkPos) *Column {
col := &Column{
Chunk: c.Chunk,
Entities: make([]*EntityHandle, 0, len(c.Entities)),
BlockEntities: make(map[cube.Pos]Block, len(c.BlockEntities)),
}
for _, e := range c.Entities {
eid, ok := e.Data["identifier"].(string)
if !ok {
w.conf.Log.Error("read column: entity without identifier field", "ID", e.ID)
continue
}
t, ok := w.conf.Entities.Lookup(eid)
if !ok {
w.conf.Log.Error("read column: unknown entity type", "ID", e.ID, "type", eid)
continue
}
col.Entities = append(col.Entities, entityFromData(t, e.ID, e.Data))
}
for _, be := range c.BlockEntities {
rid := c.Chunk.Block(uint8(be.Pos[0]), int16(be.Pos[1]), uint8(be.Pos[2]), 0)
b, ok := w.conf.Blocks.BlockByRuntimeID(rid)
if !ok {
w.conf.Log.Error("read column: no block with runtime ID", "ID", rid)
continue
}
nb, ok := b.(NBTer)
if !ok {
w.conf.Log.Error("read column: block with nbt does not implement NBTer", "block", fmt.Sprintf("%#v", b))
continue
}
col.BlockEntities[be.Pos] = nb.DecodeNBT(be.Data).(Block)
}
scheduled, savedTick := make([]scheduledTick, 0, len(c.ScheduledBlocks)), c.Tick
for _, t := range c.ScheduledBlocks {
bl := w.conf.Blocks.BlockByRuntimeIDOrAir(t.Block)
scheduled = append(scheduled, scheduledTick{
pos: t.Pos,
b: bl,
bhash: w.conf.Blocks.BlockHash(bl),
t: w.scheduledUpdates.currentTick + (t.Tick - savedTick),
})
}
w.scheduledUpdates.add(scheduled)
return col
}