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 }