Files
mc/server/conf.go
T

371 lines
15 KiB
Go
Raw Permalink Normal View History

2026-07-09 08:33:57 +08:00
package server
import (
"fmt"
"log/slog"
"os"
"path/filepath"
"slices"
"time"
_ "unsafe"
"github.com/df-mc/dragonfly/server/block"
"github.com/df-mc/dragonfly/server/entity"
"github.com/df-mc/dragonfly/server/internal/packbuilder"
"github.com/df-mc/dragonfly/server/player"
"github.com/df-mc/dragonfly/server/player/chat"
"github.com/df-mc/dragonfly/server/player/playerdb"
"github.com/df-mc/dragonfly/server/world"
"github.com/df-mc/dragonfly/server/world/biome"
"github.com/df-mc/dragonfly/server/world/generator"
"github.com/df-mc/dragonfly/server/world/mcdb"
"github.com/google/uuid"
"github.com/sandertv/gophertunnel/minecraft"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/sandertv/gophertunnel/minecraft/resource"
)
// Config contains options for starting a Minecraft server.
type Config struct {
// Log is the Logger to use for logging information. If nil, Log is set to
// slog.Default(). Errors reported by the underlying network are only logged
// if Log has at least debug level.
Log *slog.Logger
// Listeners is a list of functions to create a Listener using a Config, one
// for each Listener to be added to the Server. If left empty, no players
// will be able to connect to the Server.
Listeners []func(conf Config) (Listener, error)
// Name is the name of the server. By default, it is shown to users in the
// server list before joining the server and when opening the in-game menu.
Name string
// Resources is a slice of resource packs to use on the server. When joining
// the server, the player will then first be requested to download these
// resource packs.
Resources []*resource.Pack
// ResourcesRequires specifies if the downloading of resource packs is
// required to join the server. If set to true, players will not be able to
// join without first downloading and applying the Resources above.
ResourcesRequired bool
// DisableResourceBuilding specifies if automatic resource pack building for
// custom items should be disabled. Dragonfly, by default, automatically
// produces a resource pack for custom items. If this is not desired (for
// example if a resource pack already exists), this can be set to false.
DisableResourceBuilding bool
// Allower may be used to specify what players can join the server and what
// players cannot. By returning false in the Allow method, for example if
// the player has been banned, will prevent the player from joining.
Allower Allower
// AuthDisabled specifies if XBOX Live authentication should be disabled.
// Note that this should generally only be done for testing purposes or for
// local games. Allowing players to join without authentication is generally
// a security hazard.
AuthDisabled bool
// MuteEmoteChat specifies if the player emote chat should be muted or not.
MuteEmoteChat bool
// MaxPlayers is the maximum amount of players allowed to join the server at
// once.
MaxPlayers int
// MaxChunkRadius is the maximum view distance that each player may have,
// measured in chunks. A chunk radius generally leads to more memory usage.
MaxChunkRadius int
// JoinMessage, QuitMessage and ShutdownMessage are the messages to send for
// when a player joins or quits the server and when the server shuts down,
// kicking all online players. If set, JoinMessage and QuitMessage must have
// exactly 1 argument, which will be replaced with the name of the player
// joining or quitting.
// ShutdownMessage is set to chat.MessageServerDisconnect if empty.
JoinMessage, QuitMessage, ShutdownMessage chat.Translation
// StatusProvider provides the server status shown to players in the server
// list. By default, StatusProvider will show the server name from the Name
// field and the current player count and maximum players.
StatusProvider minecraft.ServerStatusProvider
// Compression is the packet compression used for connections accepted by
// the default listener. If nil, gophertunnel's default compression is used.
Compression packet.Compression
// PlayerProvider is the player.Provider used for storing and loading player
// data. If left as nil, player data will be newly created every time a
// player joins the server and no data will be stored.
PlayerProvider player.Provider
// WorldProvider is the world.Provider used for storing and loading world
// data. If left as nil, world data will be newly created every time and
// chunks will always be newly generated when loaded. The world provider
// will be used for storing/loading the default overworld, nether and end.
WorldProvider world.Provider
// ReadOnlyWorld specifies if the standard worlds should be read only. If
// set to true, the WorldProvider won't be saved to at all.
ReadOnlyWorld bool
// Generator should return a function that specifies the world.Generator to
// use for every world.Dimension (world.Overworld, world.Nether and
// world.End). If left empty, Generator will be set to a flat world for each
// of the dimensions (with netherrack and end stone for nether/end
// respectively).
Generator func(dim world.Dimension) world.Generator
// RandomTickSpeed specifies the rate at which blocks should be ticked in
// the default worlds. Setting this value to -1 or lower will stop random
// ticking altogether, while setting it higher results in faster ticking. If
// left as 0, the RandomTickSpeed will default to a speed of 3 blocks per
// sub chunk per tick (normal ticking speed).
RandomTickSpeed int
// SaveInterval specifies how often a World should be automatically saved to
// disk. This includes chunks, entities and level.dat data. If ReadOnlyWorld
// is set to true, 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
// Entities is a world.EntityRegistry with all entity types registered that
// may be added to the Server's worlds. If no entity types are registered,
// Entities will be set to entity.DefaultRegistry.
Entities world.EntityRegistry
// Blocks is the BlockRegistry template used for newly created worlds. If nil, world.DefaultBlockRegistry is used.
// For a non-default registry, set this to world.NewBlockRegistry(), register blocks on that instance, and ensure
// it is finalized before use.
Blocks world.BlockRegistry
}
// New creates a Server using fields of conf. The Server's worlds are created
// and connections from the Server's listeners may be accepted by calling
// Server.Listen() and Server.Accept() afterwards.
func (conf Config) New() *Server {
if conf.Log == nil {
conf.Log = slog.Default()
}
if len(conf.Listeners) == 0 {
conf.Log.Warn("config: no listeners set, no connections will be accepted")
}
if conf.Name == "" {
conf.Name = "Dragonfly Server"
}
if conf.StatusProvider == nil {
conf.StatusProvider = statusProvider{name: conf.Name}
}
if conf.PlayerProvider == nil {
conf.PlayerProvider = player.NopProvider{}
}
if conf.Allower == nil {
conf.Allower = allower{}
}
if conf.WorldProvider == nil {
conf.WorldProvider = world.NopProvider{}
}
if conf.Generator == nil {
conf.Generator = loadGenerator
}
if conf.MaxChunkRadius == 0 {
conf.MaxChunkRadius = 12
}
if conf.ShutdownMessage.Zero() {
conf.ShutdownMessage = chat.MessageServerDisconnect
}
if len(conf.Entities.Types()) == 0 {
conf.Entities = entity.DefaultRegistry
}
if conf.Blocks == nil {
conf.Blocks = world.DefaultBlockRegistry
}
// Initialize the passed block registry and also initialize the default block registry which
// is used in some vanilla paths.
conf.Blocks.Finalize()
world.DefaultBlockRegistry.Finalize()
if !conf.DisableResourceBuilding {
if pack, ok := packbuilder.BuildResourcePack(conf.Blocks); ok {
conf.Resources = append(conf.Resources, pack)
}
}
// Copy resources so that the slice can't be edited afterward.
conf.Resources = slices.Clone(conf.Resources)
srv := &Server{
conf: conf,
incoming: make(chan incoming),
p: make(map[uuid.UUID]*onlinePlayer),
world: &world.World{}, nether: &world.World{}, end: &world.World{},
}
for _, lf := range conf.Listeners {
l, err := lf(conf)
if err != nil {
conf.Log.Error("create listener: " + err.Error())
}
srv.listeners = append(srv.listeners, l)
}
2026-07-11 00:33:35 +07:00
_ = player.Bans.Load()
2026-07-09 08:33:57 +08:00
creative_registerCreativeItems()
recipe_registerVanilla()
srv.world = srv.createWorld(world.Overworld, &srv.nether, &srv.end)
srv.nether = srv.createWorld(world.Nether, &srv.world, &srv.end)
srv.end = srv.createWorld(world.End, &srv.nether, &srv.world)
return srv
}
// UserConfig is the user configuration for a Dragonfly server. It holds
// settings that affect different aspects of the server, such as its name and
// maximum players. UserConfig may be serialised and can be converted to a
// Config by calling UserConfig.Config().
type UserConfig struct {
// Network holds settings related to network aspects of the server.
Network struct {
// Address is the address on which the server should listen. Players may
// connect to this address in order to join.
Address string
}
Server struct {
// Name is the name of the server as it shows up in the server list.
Name string
// AuthEnabled controls whether players must be connected to Xbox Live
// in order to join the server.
AuthEnabled bool
// DisableJoinQuitMessages specifies if default join and quit messages
// for players should be disabled.
DisableJoinQuitMessages bool
// MuteEmoteChat specifies if the player emote chat should be muted or not.
MuteEmoteChat bool
}
World struct {
// SaveData controls whether a world's data will be saved and loaded.
// If true, the server will use the default LevelDB data provider and if
// false, an empty provider will be used. To use your own provider, turn
// this value to false, as you will still be able to pass your own
// provider.
SaveData bool
// Folder is the folder that the data of the world resides in.
Folder string
}
Players struct {
// MaxCount is the maximum amount of players allowed to join the server
// at the same time. If set to 0, the amount of maximum players will
// grow every time a player joins.
MaxCount int
// MaximumChunkRadius is the maximum chunk radius that players may set
// in their settings. If they try to set it above this number, it will
// be capped and set to the max.
MaximumChunkRadius int
// SaveData controls whether a player's data will be saved and loaded.
// If true, the server will use the default LevelDB data provider and if
// false, an empty provider will be used. To use your own provider, turn
// this value to false, as you will still be able to pass your own
// provider.
SaveData bool
// Folder controls where the player data will be stored by the default
// LevelDB player provider if it is enabled.
Folder string
}
Resources struct {
// AutoBuildPack is if the server should automatically generate a
// resource pack for custom features.
AutoBuildPack bool
// Folder controls the location where resource packs will be loaded
// from.
Folder string
// Required is a boolean to force the client to load the resource pack
// on join. If they do not accept, they'll have to leave the server.
Required bool
}
}
// Config converts a UserConfig to a Config, so that it may be used for creating
// a Server. An error is returned if creating data providers or loading
// resources failed.
func (uc UserConfig) Config(log *slog.Logger) (Config, error) {
var err error
conf := Config{
Log: log,
Name: uc.Server.Name,
ResourcesRequired: uc.Resources.Required,
AuthDisabled: !uc.Server.AuthEnabled,
MuteEmoteChat: uc.Server.MuteEmoteChat,
MaxPlayers: uc.Players.MaxCount,
MaxChunkRadius: uc.Players.MaximumChunkRadius,
DisableResourceBuilding: !uc.Resources.AutoBuildPack,
}
if !uc.Server.DisableJoinQuitMessages {
conf.JoinMessage, conf.QuitMessage = chat.MessageJoin, chat.MessageQuit
}
if uc.World.SaveData {
conf.WorldProvider, err = mcdb.Config{Log: log}.Open(uc.World.Folder)
if err != nil {
return conf, fmt.Errorf("create world provider: %w", err)
}
}
conf.Resources, err = loadResources(uc.Resources.Folder)
if err != nil {
return conf, fmt.Errorf("load resources: %w", err)
}
if uc.Players.SaveData {
conf.PlayerProvider, err = playerdb.NewProvider(uc.Players.Folder)
if err != nil {
return conf, fmt.Errorf("create player provider: %w", err)
}
}
conf.Listeners = append(conf.Listeners, uc.listenerFunc)
return conf, nil
}
// loadResources loads all resource packs found in a directory passed.
func loadResources(dir string) ([]*resource.Pack, error) {
_ = os.MkdirAll(dir, 0777)
resources, err := os.ReadDir(dir)
if err != nil {
return nil, fmt.Errorf("read dir: %w", err)
}
packs := make([]*resource.Pack, len(resources))
for i, entry := range resources {
packs[i], err = resource.ReadPath(filepath.Join(dir, entry.Name()))
if err != nil {
return nil, fmt.Errorf("compile resource (%v): %w", entry.Name(), err)
}
}
return packs, nil
}
// loadGenerator loads a standard world.Generator for a world.Dimension. The
// generators returned are flat generators with grass/dirt, netherrack or end
// stone depending on the dimension passed.
func loadGenerator(dim world.Dimension) world.Generator {
switch dim {
case world.Overworld:
2026-07-11 00:33:35 +07:00
return generator.NewVanillaLightweight(world.DefaultBlockRegistry)
2026-07-09 08:33:57 +08:00
case world.Nether:
return generator.NewFlat(biome.NetherWastes{}, []world.Block{block.Netherrack{}, block.Netherrack{}, block.Netherrack{}, block.Bedrock{}})
case world.End:
return generator.NewFlat(biome.End{}, []world.Block{block.EndStone{}, block.EndStone{}, block.EndStone{}, block.Bedrock{}})
}
panic("should never happen")
}
// DefaultConfig returns a configuration with the default values filled out.
func DefaultConfig() UserConfig {
c := UserConfig{}
c.Network.Address = ":19132"
c.Server.Name = "Dragonfly Server"
c.Server.AuthEnabled = true
c.World.SaveData = true
c.World.Folder = "world"
c.Players.MaximumChunkRadius = 32
c.Players.SaveData = true
c.Players.Folder = "players"
c.Resources.AutoBuildPack = true
c.Resources.Folder = "resources"
c.Resources.Required = false
return c
}
// noinspection ALL
//
//go:linkname creative_registerCreativeItems github.com/df-mc/dragonfly/server/item/creative.registerCreativeItems
func creative_registerCreativeItems()
// noinspection ALL
//
//go:linkname recipe_registerVanilla github.com/df-mc/dragonfly/server/item/recipe.registerVanilla
func recipe_registerVanilla()