161 lines
6.1 KiB
Go
161 lines
6.1 KiB
Go
|
|
package world
|
||
|
|
|
||
|
|
import (
|
||
|
|
"log/slog"
|
||
|
|
"math/rand/v2"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
type blockRegistrySetter interface {
|
||
|
|
// SetBlockRegistry updates the registry used by the provider to encode and decode blocks.
|
||
|
|
// Config.New calls it with Config.Blocks after applying the default registry and finalizing it.
|
||
|
|
SetBlockRegistry(BlockRegistry)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Config may be used to create a new World. It holds a variety of fields that
|
||
|
|
// influence the World.
|
||
|
|
type Config struct {
|
||
|
|
// Log is the Logger that will be used to log errors and debug messages to.
|
||
|
|
// If set to nil, slog.Default() is set.
|
||
|
|
Log *slog.Logger
|
||
|
|
// Dim is the Dimension of the World. If set to nil, the World will use
|
||
|
|
// Overworld as its dimension. The dimension set here influences, among
|
||
|
|
// others, the sky colour, weather/time and liquid behaviour in that World.
|
||
|
|
Dim Dimension
|
||
|
|
// PortalDestination is a function that returns the destination World for a
|
||
|
|
// portal of a specific Dimension type. If set to nil, no portals will
|
||
|
|
// function. If the function returns a nil world for a Dimension, only
|
||
|
|
// portals of that specific Dimension type will not function.
|
||
|
|
PortalDestination func(dim Dimension) *World
|
||
|
|
// Provider is the Provider implementation used to read and write World
|
||
|
|
// data. If set to nil, the Provider used will be NopProvider, which does
|
||
|
|
// not store any data to disk.
|
||
|
|
Provider Provider
|
||
|
|
// Generator is the Generator implementation used to generate new areas of
|
||
|
|
// the World. If set to nil, the Generator used will be NopGenerator, which
|
||
|
|
// generates completely empty chunks.
|
||
|
|
Generator Generator
|
||
|
|
// ReadOnly specifies if the World should be read-only, meaning no new data
|
||
|
|
// will be written to the Provider.
|
||
|
|
ReadOnly bool
|
||
|
|
// SaveInterval specifies how often a World should be automatically saved to
|
||
|
|
// disk. This includes chunks, entities and level.dat data. If ReadOnly is
|
||
|
|
// set to false, changing SaveInterval will have no effect.
|
||
|
|
// By default, SaveInterval is set to 10 minutes. Setting SaveInterval to
|
||
|
|
// a negative number disables automatic saving entirely.
|
||
|
|
SaveInterval time.Duration
|
||
|
|
// ChunkUnloadInterval specifies how often unused chunks should be unloaded
|
||
|
|
// from memory when no longer in use. By default, this is set to 2 minutes.
|
||
|
|
// ChunkUnloadInterval should not be used to prevent chunks from unloading
|
||
|
|
// altogether. This should be done using a Loader with a custom Viewer.
|
||
|
|
ChunkUnloadInterval time.Duration
|
||
|
|
// RandomTickSpeed specifies the rate at which blocks should be ticked in
|
||
|
|
// the World. By default, each sub chunk has 3 blocks randomly ticked per
|
||
|
|
// sub chunk, so the default value is 3. Setting this value to -1 or lower
|
||
|
|
// will stop random ticking altogether, while setting it higher results in
|
||
|
|
// faster ticking.
|
||
|
|
RandomTickSpeed int
|
||
|
|
// RandSource is the rand.Source used for generation of random numbers in a
|
||
|
|
// World, such as when selecting blocks to tick or when deciding where to
|
||
|
|
// strike lightning. If set to nil, RandSource defaults to a `rand.PCG`
|
||
|
|
// source seeded with `time.Now().UnixNano()`. PCG is significantly faster
|
||
|
|
// than `rand.ChaCha8` on 64-bit systems at the expense of poorer
|
||
|
|
// statistical distribution, which is acceptable here.
|
||
|
|
// See https://go.dev/blog/chacha8rand.
|
||
|
|
RandSource rand.Source
|
||
|
|
// Entities is an EntityRegistry with all Entity types registered that may
|
||
|
|
// be added to the World.
|
||
|
|
Entities EntityRegistry
|
||
|
|
|
||
|
|
// Blocks is the BlockRegistry used by the World.
|
||
|
|
// If left nil, DefaultBlockRegistry is used. For a non-default registry,
|
||
|
|
// use NewBlockRegistry(), register blocks/states, and call Finalize().
|
||
|
|
Blocks BlockRegistry
|
||
|
|
|
||
|
|
// Synchronous makes the World run without any background goroutines.
|
||
|
|
// Transactions from World.Exec run on the calling goroutine, the World is
|
||
|
|
// not saved or unloaded automatically, and time only passes on explicit
|
||
|
|
// World.AdvanceTick calls. This makes Synchronous Worlds deterministic and
|
||
|
|
// well suited to unit tests that need a World to interact with.
|
||
|
|
// A Synchronous World must be driven from one goroutine. Exec and
|
||
|
|
// AdvanceTick are not safe to call concurrently, including from delayed
|
||
|
|
// item or death callbacks.
|
||
|
|
Synchronous bool
|
||
|
|
}
|
||
|
|
|
||
|
|
// New creates a new World using the Config conf. The World returned will start
|
||
|
|
// ticking as soon as a viewer is added to it and is otherwise ready for use.
|
||
|
|
func (conf Config) New() *World {
|
||
|
|
if conf.Log == nil {
|
||
|
|
conf.Log = slog.Default()
|
||
|
|
}
|
||
|
|
if conf.Dim == nil {
|
||
|
|
conf.Dim = Overworld
|
||
|
|
}
|
||
|
|
if conf.SaveInterval == 0 {
|
||
|
|
conf.SaveInterval = time.Minute * 10
|
||
|
|
}
|
||
|
|
if conf.ChunkUnloadInterval <= 0 {
|
||
|
|
conf.ChunkUnloadInterval = time.Minute * 2
|
||
|
|
}
|
||
|
|
if conf.Generator == nil {
|
||
|
|
conf.Generator = NopGenerator{}
|
||
|
|
}
|
||
|
|
if conf.Provider == nil {
|
||
|
|
// If no provider is set, use the default settings and the default spawn position from the generator.
|
||
|
|
s := defaultSettings()
|
||
|
|
s.Spawn = conf.Generator.DefaultSpawn(conf.Dim)
|
||
|
|
conf.Provider = NopProvider{Set: s}
|
||
|
|
}
|
||
|
|
if conf.RandomTickSpeed == 0 {
|
||
|
|
conf.RandomTickSpeed = 3
|
||
|
|
}
|
||
|
|
if conf.Blocks == nil {
|
||
|
|
conf.Blocks = DefaultBlockRegistry
|
||
|
|
}
|
||
|
|
|
||
|
|
// Initialize the passed block registry and also initialize the default block registry which
|
||
|
|
// is used in some vanilla paths.
|
||
|
|
conf.Blocks.Finalize()
|
||
|
|
DefaultBlockRegistry.Finalize()
|
||
|
|
if provider, ok := conf.Provider.(blockRegistrySetter); ok {
|
||
|
|
provider.SetBlockRegistry(conf.Blocks)
|
||
|
|
}
|
||
|
|
|
||
|
|
if conf.RandSource == nil {
|
||
|
|
t := uint64(time.Now().UnixNano())
|
||
|
|
conf.RandSource = rand.NewPCG(t, t)
|
||
|
|
}
|
||
|
|
s := conf.Provider.Settings()
|
||
|
|
w := &World{
|
||
|
|
scheduledUpdates: newScheduledTickQueue(s.CurrentTick),
|
||
|
|
entities: make(map[*EntityHandle]ChunkPos),
|
||
|
|
viewers: make(map[*Loader]Viewer),
|
||
|
|
chunks: make(map[ChunkPos]*Column),
|
||
|
|
queueClosing: make(chan struct{}),
|
||
|
|
closing: make(chan struct{}),
|
||
|
|
queue: make(chan transaction, 128),
|
||
|
|
r: rand.New(conf.RandSource),
|
||
|
|
advance: s.ref.Add(1) == 1,
|
||
|
|
conf: conf,
|
||
|
|
ra: conf.Dim.Range(),
|
||
|
|
set: s,
|
||
|
|
}
|
||
|
|
w.weather = weather{w: w}
|
||
|
|
var h Handler = NopHandler{}
|
||
|
|
w.handler.Store(&h)
|
||
|
|
|
||
|
|
t := ticker{interval: time.Second / 20}
|
||
|
|
if !conf.Synchronous {
|
||
|
|
w.queueing.Add(1)
|
||
|
|
w.running.Add(2)
|
||
|
|
|
||
|
|
go t.tickLoop(w)
|
||
|
|
go w.autoSave()
|
||
|
|
go w.handleTransactions()
|
||
|
|
}
|
||
|
|
|
||
|
|
<-w.Exec(t.tick)
|
||
|
|
return w
|
||
|
|
}
|