github.com/df-mc/dragonfly@v0.9.13/server/server.go (about)

     1  package server
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	_ "embed"
     7  	"encoding/base64"
     8  	"fmt"
     9  	"github.com/df-mc/atomic"
    10  	"github.com/df-mc/dragonfly/server/cmd"
    11  	"github.com/df-mc/dragonfly/server/internal/blockinternal"
    12  	"github.com/df-mc/dragonfly/server/internal/iteminternal"
    13  	"github.com/df-mc/dragonfly/server/internal/sliceutil"
    14  	_ "github.com/df-mc/dragonfly/server/item" // Imported for maintaining correct initialisation order.
    15  	"github.com/df-mc/dragonfly/server/player"
    16  	"github.com/df-mc/dragonfly/server/player/skin"
    17  	"github.com/df-mc/dragonfly/server/session"
    18  	"github.com/df-mc/dragonfly/server/world"
    19  	"github.com/go-gl/mathgl/mgl32"
    20  	"github.com/go-gl/mathgl/mgl64"
    21  	"github.com/google/uuid"
    22  	"github.com/sandertv/gophertunnel/minecraft"
    23  	"github.com/sandertv/gophertunnel/minecraft/nbt"
    24  	"github.com/sandertv/gophertunnel/minecraft/protocol"
    25  	"github.com/sandertv/gophertunnel/minecraft/protocol/login"
    26  	"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
    27  	"github.com/sandertv/gophertunnel/minecraft/text"
    28  	"github.com/sirupsen/logrus"
    29  	"golang.org/x/exp/maps"
    30  	"os"
    31  	"os/exec"
    32  	"os/signal"
    33  	"runtime"
    34  	"strings"
    35  	"sync"
    36  	"syscall"
    37  )
    38  
    39  // Server implements a Dragonfly server. It runs the main server loop and
    40  // handles the connections of players trying to join the server.
    41  type Server struct {
    42  	conf Config
    43  
    44  	once    sync.Once
    45  	started atomic.Bool
    46  
    47  	world, nether, end *world.World
    48  
    49  	customBlocks []protocol.BlockEntry
    50  	customItems  []protocol.ItemComponentEntry
    51  
    52  	listeners []Listener
    53  	incoming  chan *session.Session
    54  
    55  	pmu sync.RWMutex
    56  	// p holds a map of all players currently connected to the server. When they
    57  	// leave, they are removed from the map.
    58  	p map[uuid.UUID]*player.Player
    59  	// pwg is a sync.WaitGroup used to wait for all players to be disconnected
    60  	// before server shutdown, so that their data is saved properly.
    61  	pwg sync.WaitGroup
    62  	// wg is used to wait for all Listeners to be closed and their respective
    63  	// goroutines to be finished.
    64  	wg sync.WaitGroup
    65  }
    66  
    67  // HandleFunc is a function that may be passed to Server.Accept(). It can be
    68  // used to prepare the session of a player before it can do anything.
    69  type HandleFunc func(p *player.Player)
    70  
    71  // New creates a Server using a default Config. The Server's worlds are created
    72  // and connections from the Server's listeners may be accepted by calling
    73  // Server.Listen() and Server.Accept() afterwards.
    74  func New() *Server {
    75  	var conf Config
    76  	return conf.New()
    77  }
    78  
    79  // Listen starts running the server's listeners but does not block, unlike Run.
    80  // Connections will be accepted on a different goroutine until the listeners
    81  // are closed using a call to Close. Once started, players may be accepted
    82  // using Server.Accept().
    83  func (srv *Server) Listen() {
    84  	if !srv.started.CAS(false, true) {
    85  		panic("start server: already started")
    86  	}
    87  
    88  	srv.conf.Log.Infof("Starting Dragonfly for Minecraft v%v...", protocol.CurrentVersion)
    89  	srv.startListening()
    90  	go srv.wait()
    91  }
    92  
    93  // Accept accepts an incoming player into the server. It blocks until a player
    94  // connects to the server. A HandleFunc may be passed which is run immediately
    95  // before a *player.Player is accepted to the Server. This function may be used
    96  // to add a player.Handler to the player and prepare its session. The function
    97  // may be nil if player joining does not need to be handled. Accept returns
    98  // false if the Server is closed using a call to Close.
    99  func (srv *Server) Accept(f HandleFunc) bool {
   100  	s, ok := <-srv.incoming
   101  	if !ok {
   102  		return false
   103  	}
   104  	p := s.Controllable().(*player.Player)
   105  	if f != nil {
   106  		f(p)
   107  	}
   108  
   109  	srv.pmu.Lock()
   110  	srv.p[p.UUID()] = p
   111  	srv.pmu.Unlock()
   112  
   113  	s.Start()
   114  	return true
   115  }
   116  
   117  // World returns the overworld of the server. Players will be spawned in this
   118  // world and this world will be read from and written to when the world is
   119  // edited.
   120  func (srv *Server) World() *world.World {
   121  	return srv.world
   122  }
   123  
   124  // Nether returns the nether world of the server. Players are transported to it
   125  // when entering a nether portal in the world returned by the World method.
   126  func (srv *Server) Nether() *world.World {
   127  	return srv.nether
   128  }
   129  
   130  // End returns the end world of the server. Players are transported to it when
   131  // entering an end portal in the world returned by the World method.
   132  func (srv *Server) End() *world.World {
   133  	return srv.end
   134  }
   135  
   136  // MaxPlayerCount returns the maximum amount of players that are allowed to
   137  // play on the server at the same time. Players trying to join when the server
   138  // is full will be refused to enter. If the config has a maximum player count
   139  // set to 0, MaxPlayerCount will return Server.PlayerCount + 1.
   140  func (srv *Server) MaxPlayerCount() int {
   141  	if srv.conf.MaxPlayers == 0 {
   142  		return len(srv.Players()) + 1
   143  	}
   144  	return srv.conf.MaxPlayers
   145  }
   146  
   147  // Players returns a list of all players currently connected to the server.
   148  // Note that the slice returned is not updated when new players join or leave,
   149  // so it is only valid for as long as no new players join or players leave.
   150  func (srv *Server) Players() []*player.Player {
   151  	srv.pmu.RLock()
   152  	defer srv.pmu.RUnlock()
   153  	return maps.Values(srv.p)
   154  }
   155  
   156  // Player looks for a player on the server with the UUID passed. If found, the
   157  // player is returned and the bool returns holds a true value. If not, the bool
   158  // returned is false and the player is nil.
   159  func (srv *Server) Player(uuid uuid.UUID) (*player.Player, bool) {
   160  	srv.pmu.RLock()
   161  	defer srv.pmu.RUnlock()
   162  	p, ok := srv.p[uuid]
   163  	return p, ok
   164  }
   165  
   166  // PlayerByName looks for a player on the server with the name passed. If
   167  // found, the player is returned and the bool returns holds a true value. If
   168  // not, the bool is false and the player is nil
   169  func (srv *Server) PlayerByName(name string) (*player.Player, bool) {
   170  	return sliceutil.SearchValue(srv.Players(), func(p *player.Player) bool {
   171  		return p.Name() == name
   172  	})
   173  }
   174  
   175  // PlayerByXUID looks for a player on the server with the XUID passed. If found,
   176  // the player is returned and the bool returned is true. If no player with the
   177  // XUID was found, nil and false are returned.
   178  func (srv *Server) PlayerByXUID(xuid string) (*player.Player, bool) {
   179  	return sliceutil.SearchValue(srv.Players(), func(p *player.Player) bool {
   180  		return p.XUID() == xuid
   181  	})
   182  }
   183  
   184  // CloseOnProgramEnd closes the server right before the program ends, so that
   185  // all data of the server are saved properly.
   186  func (srv *Server) CloseOnProgramEnd() {
   187  	c := make(chan os.Signal, 2)
   188  	signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
   189  	go func() {
   190  		<-c
   191  		if err := srv.Close(); err != nil {
   192  			srv.conf.Log.Errorf("close server: %v", err)
   193  		}
   194  	}()
   195  }
   196  
   197  // Close closes the server, making any call to Run/Accept cancel immediately.
   198  func (srv *Server) Close() error {
   199  	if !srv.started.Load() {
   200  		panic("server not yet running")
   201  	}
   202  	srv.once.Do(srv.close)
   203  	return nil
   204  }
   205  
   206  // close stops the server, storing player and world data to disk when
   207  // necessary.
   208  func (srv *Server) close() {
   209  	srv.conf.Log.Infof("Server shutting down...")
   210  	defer srv.conf.Log.Infof("Server stopped.")
   211  
   212  	srv.conf.Log.Debugf("Disconnecting players...")
   213  	for _, p := range srv.Players() {
   214  		p.Disconnect(text.Colourf("<yellow>%v</yellow>", srv.conf.ShutdownMessage))
   215  	}
   216  	srv.pwg.Wait()
   217  
   218  	srv.conf.Log.Debugf("Closing player provider...")
   219  	if err := srv.conf.PlayerProvider.Close(); err != nil {
   220  		srv.conf.Log.Errorf("Error while closing player provider: %v", err)
   221  	}
   222  
   223  	srv.conf.Log.Debugf("Closing worlds...")
   224  	for _, w := range []*world.World{srv.end, srv.nether, srv.world} {
   225  		if err := w.Close(); err != nil {
   226  			srv.conf.Log.Errorf("Error closing %v: %v", w.Dimension(), err)
   227  		}
   228  	}
   229  
   230  	srv.conf.Log.Debugf("Closing listeners...")
   231  	for _, l := range srv.listeners {
   232  		if err := l.Close(); err != nil {
   233  			srv.conf.Log.Errorf("Error closing listener: %v", err)
   234  		}
   235  	}
   236  }
   237  
   238  // listen makes the Server listen for new connections from the Listener passed.
   239  // This may be used to listen for players on different interfaces. Note that
   240  // the maximum player count of additional Listeners added is not enforced
   241  // automatically. The limit must be enforced by the Listener.
   242  func (srv *Server) listen(l Listener) {
   243  	wg := new(sync.WaitGroup)
   244  	ctx, cancel := context.WithCancel(context.Background())
   245  	for {
   246  		c, err := l.Accept()
   247  		if err != nil {
   248  			// Cancel the context so that any call to StartGameContext is
   249  			// cancelled rapidly.
   250  			cancel()
   251  			// First wait until all connections that are being handled are
   252  			// done inserting the player into the channel. Afterwards, when
   253  			// we're sure no more values will be inserted in the players
   254  			// channel, we can return so the player channel can be closed.
   255  			wg.Wait()
   256  			srv.wg.Done()
   257  			return
   258  		}
   259  
   260  		wg.Add(1)
   261  		go func() {
   262  			defer wg.Done()
   263  			if msg, ok := srv.conf.Allower.Allow(c.RemoteAddr(), c.IdentityData(), c.ClientData()); !ok {
   264  				_ = c.WritePacket(&packet.Disconnect{HideDisconnectionScreen: msg == "", Message: msg})
   265  				_ = c.Close()
   266  				return
   267  			}
   268  			srv.finaliseConn(ctx, c, l)
   269  		}()
   270  	}
   271  }
   272  
   273  // startListening starts making the EncodeBlock listener listen, accepting new
   274  // connections from players.
   275  func (srv *Server) startListening() {
   276  	srv.makeBlockEntries()
   277  	srv.makeItemComponents()
   278  
   279  	srv.wg.Add(len(srv.conf.Listeners))
   280  	for _, lf := range srv.conf.Listeners {
   281  		l, err := lf(srv.conf)
   282  		if err != nil {
   283  			srv.conf.Log.Fatalf("create listener: %v", err)
   284  		}
   285  		srv.listeners = append(srv.listeners, l)
   286  		go srv.listen(l)
   287  	}
   288  }
   289  
   290  // makeBlockEntries initializes the server's block components map using the registered custom blocks. It allows block
   291  // components to be created only once at startup.
   292  func (srv *Server) makeBlockEntries() {
   293  	custom := maps.Values(world.CustomBlocks())
   294  	srv.customBlocks = make([]protocol.BlockEntry, len(custom))
   295  
   296  	for i, b := range custom {
   297  		name, _ := b.EncodeBlock()
   298  		srv.customBlocks[i] = protocol.BlockEntry{
   299  			Name:       name,
   300  			Properties: blockinternal.Components(name, b, 10000+int32(i)),
   301  		}
   302  	}
   303  }
   304  
   305  // makeItemComponents initializes the server's item components map using the
   306  // registered custom items. It allows item components to be created only once
   307  // at startup
   308  func (srv *Server) makeItemComponents() {
   309  	custom := world.CustomItems()
   310  	srv.customItems = make([]protocol.ItemComponentEntry, len(custom))
   311  
   312  	for _, it := range custom {
   313  		name, _ := it.EncodeItem()
   314  		srv.customItems = append(srv.customItems, protocol.ItemComponentEntry{
   315  			Name: name,
   316  			Data: iteminternal.Components(it),
   317  		})
   318  	}
   319  }
   320  
   321  // wait awaits the closing of all Listeners added to the Server through a call
   322  // to listen and closed the players channel once that happens.
   323  func (srv *Server) wait() {
   324  	srv.wg.Wait()
   325  	close(srv.incoming)
   326  }
   327  
   328  // finaliseConn finalises the session.Conn passed and subtracts from the
   329  // sync.WaitGroup once done.
   330  func (srv *Server) finaliseConn(ctx context.Context, conn session.Conn, l Listener) {
   331  	id := uuid.MustParse(conn.IdentityData().Identity)
   332  	data := srv.defaultGameData()
   333  
   334  	var playerData *player.Data
   335  	if d, err := srv.conf.PlayerProvider.Load(id, srv.dimension); err == nil {
   336  		if d.World == nil {
   337  			d.World = srv.world
   338  		}
   339  		data.PlayerPosition = vec64To32(d.Position).Add(mgl32.Vec3{0, 1.62})
   340  		dim, _ := world.DimensionID(d.World.Dimension())
   341  		data.Dimension = int32(dim)
   342  		data.Yaw, data.Pitch = float32(d.Yaw), float32(d.Pitch)
   343  
   344  		playerData = &d
   345  	}
   346  
   347  	if err := conn.StartGameContext(ctx, data); err != nil {
   348  		_ = l.Disconnect(conn, "Connection timeout.")
   349  
   350  		srv.conf.Log.Debugf("connection %v failed spawning: %v\n", conn.RemoteAddr(), err)
   351  		return
   352  	}
   353  	_ = conn.WritePacket(&packet.ItemComponent{Items: srv.customItems})
   354  	if p, ok := srv.Player(id); ok {
   355  		p.Disconnect("Logged in from another location.")
   356  	}
   357  	srv.incoming <- srv.createPlayer(id, conn, playerData)
   358  }
   359  
   360  // defaultGameData returns a minecraft.GameData as sent for a new player. It
   361  // may later be modified if the player was saved in the player provider of the
   362  // server.
   363  func (srv *Server) defaultGameData() minecraft.GameData {
   364  	return minecraft.GameData{
   365  		// We set these IDs to 1, because that's how the session will treat them.
   366  		EntityUniqueID:  1,
   367  		EntityRuntimeID: 1,
   368  
   369  		WorldName:       srv.conf.Name,
   370  		BaseGameVersion: protocol.CurrentVersion,
   371  
   372  		Time:       int64(srv.world.Time()),
   373  		Difficulty: 2,
   374  
   375  		PlayerGameMode:    packet.GameTypeCreative,
   376  		PlayerPermissions: packet.PermissionLevelMember,
   377  		PlayerPosition:    vec64To32(srv.world.Spawn().Vec3Centre().Add(mgl64.Vec3{0, 1.62})),
   378  
   379  		Items:        srv.itemEntries(),
   380  		CustomBlocks: srv.customBlocks,
   381  		GameRules:    []protocol.GameRule{{Name: "naturalregeneration", Value: false}},
   382  
   383  		ServerAuthoritativeInventory: true,
   384  		PlayerMovementSettings: protocol.PlayerMovementSettings{
   385  			MovementType:                     protocol.PlayerMovementModeServer,
   386  			ServerAuthoritativeBlockBreaking: true,
   387  		},
   388  	}
   389  }
   390  
   391  // dimension returns a world by a dimension passed.
   392  func (srv *Server) dimension(dimension world.Dimension) *world.World {
   393  	switch dimension {
   394  	default:
   395  		return srv.world
   396  	case world.Nether:
   397  		return srv.nether
   398  	case world.End:
   399  		return srv.end
   400  	}
   401  }
   402  
   403  // checkNetIsolation checks if a loopback exempt is in place to allow the
   404  // hosting device to join the server. This is only relevant on Windows. It will
   405  // never log anything for anything but Windows.
   406  func (srv *Server) checkNetIsolation() {
   407  	if runtime.GOOS != "windows" {
   408  		// Only an issue on Windows.
   409  		return
   410  	}
   411  	data, _ := exec.Command("CheckNetIsolation", "LoopbackExempt", "-s", `-n="microsoft.minecraftuwp_8wekyb3d8bbwe"`).CombinedOutput()
   412  	if bytes.Contains(data, []byte("microsoft.minecraftuwp_8wekyb3d8bbwe")) {
   413  		return
   414  	}
   415  	const loopbackExemptCmd = `CheckNetIsolation LoopbackExempt -a -n="Microsoft.MinecraftUWP_8wekyb3d8bbwe"`
   416  	srv.conf.Log.Infof("You are currently unable to join the server on this machine. Run %v in an admin PowerShell session to resolve.\n", loopbackExemptCmd)
   417  }
   418  
   419  // handleSessionClose handles the closing of a session. It removes the player
   420  // of the session from the server.
   421  func (srv *Server) handleSessionClose(c session.Controllable) {
   422  	srv.pmu.Lock()
   423  	p, ok := srv.p[c.UUID()]
   424  	delete(srv.p, c.UUID())
   425  	srv.pmu.Unlock()
   426  	if !ok {
   427  		// When a player disconnects immediately after a session is started, it might not be added to the players map
   428  		// yet. This is expected, but we need to be careful not to crash when this happens.
   429  		return
   430  	}
   431  
   432  	if err := srv.conf.PlayerProvider.Save(p.UUID(), p.Data()); err != nil {
   433  		srv.conf.Log.Errorf("Error while saving data: %v", err)
   434  	}
   435  	srv.pwg.Done()
   436  }
   437  
   438  // createPlayer creates a new player instance using the UUID and connection
   439  // passed.
   440  func (srv *Server) createPlayer(id uuid.UUID, conn session.Conn, data *player.Data) *session.Session {
   441  	w, gm, pos := srv.world, srv.world.DefaultGameMode(), srv.world.Spawn().Vec3Middle()
   442  	if data != nil {
   443  		w, gm, pos = data.World, data.GameMode, data.Position
   444  	}
   445  	s := session.New(conn, srv.conf.MaxChunkRadius, srv.conf.Log, srv.conf.JoinMessage, srv.conf.QuitMessage)
   446  	p := player.NewWithSession(conn.IdentityData().DisplayName, conn.IdentityData().XUID, id, srv.parseSkin(conn.ClientData()), s, pos, data)
   447  
   448  	s.Spawn(p, pos, w, gm, srv.handleSessionClose)
   449  	srv.pwg.Add(1)
   450  	return s
   451  }
   452  
   453  // createWorld loads a world of the server with a specific dimension, ending
   454  // the program if the world could not be loaded. The layers passed are used to
   455  // create a generator.Flat that is used as generator for the world.
   456  func (srv *Server) createWorld(dim world.Dimension, nether, end **world.World) *world.World {
   457  	logger := srv.conf.Log
   458  	if v, ok := logger.(interface {
   459  		WithField(key string, field any) *logrus.Entry
   460  	}); ok {
   461  		// Add a dimension field to be able to distinguish between the different
   462  		// dimensions in the log. Dimensions implement fmt.Stringer so we can
   463  		// just fmt.Sprint them for a readable name.
   464  		logger = v.WithField("dimension", strings.ToLower(fmt.Sprint(dim)))
   465  	}
   466  	logger.Debugf("Loading world...")
   467  
   468  	conf := world.Config{
   469  		Log:             logger,
   470  		Dim:             dim,
   471  		Provider:        srv.conf.WorldProvider,
   472  		Generator:       srv.conf.Generator(dim),
   473  		RandomTickSpeed: srv.conf.RandomTickSpeed,
   474  		ReadOnly:        srv.conf.ReadOnlyWorld,
   475  		Entities:        srv.conf.Entities,
   476  		PortalDestination: func(dim world.Dimension) *world.World {
   477  			if dim == world.Nether {
   478  				return *nether
   479  			} else if dim == world.End {
   480  				return *end
   481  			}
   482  			return nil
   483  		},
   484  	}
   485  	w := conf.New()
   486  	logger.Infof(`Opened world "%v".`, w.Name())
   487  	return w
   488  }
   489  
   490  // parseSkin parses a skin from the login.ClientData  and returns it.
   491  func (srv *Server) parseSkin(data login.ClientData) skin.Skin {
   492  	// Gophertunnel guarantees the following values are valid data and are of
   493  	// the correct size.
   494  	skinResourcePatch, _ := base64.StdEncoding.DecodeString(data.SkinResourcePatch)
   495  
   496  	playerSkin := skin.New(data.SkinImageWidth, data.SkinImageHeight)
   497  	playerSkin.Persona = data.PersonaSkin
   498  	playerSkin.Pix, _ = base64.StdEncoding.DecodeString(data.SkinData)
   499  	playerSkin.Model, _ = base64.StdEncoding.DecodeString(data.SkinGeometry)
   500  	playerSkin.ModelConfig, _ = skin.DecodeModelConfig(skinResourcePatch)
   501  	playerSkin.PlayFabID = data.PlayFabID
   502  
   503  	playerSkin.Cape = skin.NewCape(data.CapeImageWidth, data.CapeImageHeight)
   504  	playerSkin.Cape.Pix, _ = base64.StdEncoding.DecodeString(data.CapeData)
   505  
   506  	for _, animation := range data.AnimatedImageData {
   507  		var t skin.AnimationType
   508  		switch animation.Type {
   509  		case protocol.SkinAnimationHead:
   510  			t = skin.AnimationHead
   511  		case protocol.SkinAnimationBody32x32:
   512  			t = skin.AnimationBody32x32
   513  		case protocol.SkinAnimationBody128x128:
   514  			t = skin.AnimationBody128x128
   515  		}
   516  
   517  		anim := skin.NewAnimation(animation.ImageWidth, animation.ImageHeight, animation.AnimationExpression, t)
   518  		anim.FrameCount = int(animation.Frames)
   519  		anim.Pix, _ = base64.StdEncoding.DecodeString(animation.Image)
   520  
   521  		playerSkin.Animations = append(playerSkin.Animations, anim)
   522  	}
   523  
   524  	return playerSkin
   525  }
   526  
   527  // registerTargetFunc registers a cmd.TargetFunc to be able to get all players
   528  // connected and all entities in the server's world.
   529  func (srv *Server) registerTargetFunc() {
   530  	cmd.AddTargetFunc(func(src cmd.Source) (entities []cmd.Target, players []cmd.NamedTarget) {
   531  		return sliceutil.Convert[cmd.Target](src.World().Entities()), sliceutil.Convert[cmd.NamedTarget](srv.Players())
   532  	})
   533  }
   534  
   535  // vec64To32 converts a mgl64.Vec3 to a mgl32.Vec3.
   536  func vec64To32(vec3 mgl64.Vec3) mgl32.Vec3 {
   537  	return mgl32.Vec3{float32(vec3[0]), float32(vec3[1]), float32(vec3[2])}
   538  }
   539  
   540  // itemEntries loads a list of all custom item entries of the server, ready to
   541  // be sent in the StartGame packet.
   542  func (srv *Server) itemEntries() []protocol.ItemEntry {
   543  	entries := make([]protocol.ItemEntry, 0, len(itemRuntimeIDs))
   544  
   545  	for name, rid := range itemRuntimeIDs {
   546  		entries = append(entries, protocol.ItemEntry{
   547  			Name:      name,
   548  			RuntimeID: int16(rid),
   549  		})
   550  	}
   551  	for _, it := range world.CustomItems() {
   552  		name, _ := it.EncodeItem()
   553  		rid, _, _ := world.ItemRuntimeID(it)
   554  		entries = append(entries, protocol.ItemEntry{
   555  			Name:           name,
   556  			ComponentBased: true,
   557  			RuntimeID:      int16(rid),
   558  		})
   559  	}
   560  	return entries
   561  }
   562  
   563  // ashyBiome represents a biome that has any form of ash.
   564  type ashyBiome interface {
   565  	// Ash returns the ash and white ash of the biome.
   566  	Ash() (ash float64, whiteAsh float64)
   567  }
   568  
   569  // sporingBiome represents a biome that has blue or red spores.
   570  type sporingBiome interface {
   571  	// Spores returns the blue and red spores of the biome.
   572  	Spores() (blueSpores float64, redSpores float64)
   573  }
   574  
   575  // biomes builds a mapping of all biome definitions of the server, ready to be set in the biomes field of the server
   576  // listener.
   577  func biomes() map[string]any {
   578  	definitions := make(map[string]any)
   579  	for _, b := range world.Biomes() {
   580  		definition := map[string]any{
   581  			"name_hash":   b.String(), // This isn't actually a hash despite what the field name may suggest.
   582  			"temperature": float32(b.Temperature()),
   583  			"downfall":    float32(b.Rainfall()),
   584  			"rain":        b.Rainfall() > 0,
   585  		}
   586  		if a, ok := b.(ashyBiome); ok {
   587  			ash, whiteAsh := a.Ash()
   588  			definition["ash"], definition["white_ash"] = float32(ash), float32(whiteAsh)
   589  		}
   590  		if s, ok := b.(sporingBiome); ok {
   591  			blueSpores, redSpores := s.Spores()
   592  			definition["blue_spores"], definition["red_spores"] = float32(blueSpores), float32(redSpores)
   593  		}
   594  		definitions[b.String()] = definition
   595  	}
   596  	return definitions
   597  }
   598  
   599  var (
   600  	//go:embed world/item_runtime_ids.nbt
   601  	itemRuntimeIDData []byte
   602  	itemRuntimeIDs    = map[string]int32{}
   603  )
   604  
   605  // init reads all item entries from the resource JSON, and sets the according
   606  // values in the runtime ID maps. init also seeds the global `rand` with the
   607  // current time.
   608  func init() {
   609  	_ = nbt.Unmarshal(itemRuntimeIDData, &itemRuntimeIDs)
   610  }