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 }