up3
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

This commit is contained in:
2026-07-09 08:33:57 +08:00
commit 26ed99fda6
845 changed files with 75419 additions and 0 deletions
+160
View File
@@ -0,0 +1,160 @@
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
}