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
1407 lines
42 KiB
Go
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
|
|
}
|