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) } _ = player.Bans.Load() 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: return generator.NewVanillaLightweight(world.DefaultBlockRegistry) 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()