Files
hytale-server/docs/10-configuration.md

13 KiB

Configuration System

The Hytale server provides a robust configuration system for both server settings and plugin configurations. This guide covers how to use and extend the configuration system.

Server Configuration

HytaleServerConfig

The main server configuration is stored in HytaleServerConfig:

HytaleServerConfig config = HytaleServer.get().getConfig();

// Access configuration values
String serverName = config.getServerName();
String motd = config.getMotd();
int maxPlayers = config.getMaxPlayers();
int maxViewRadius = config.getMaxViewRadius();

Server Configuration Structure

HytaleServerConfig {
    // Basic settings
    serverName: String           // Server display name
    motd: String                 // Message of the day
    password: String             // Server password (optional)
    maxPlayers: int              // Maximum player count
    maxViewRadius: int           // Maximum view distance
    localCompressionEnabled: boolean  // Enable packet compression
    
    // Nested configurations
    Defaults {
        world: String            // Default world name
        gameMode: GameMode       // Default game mode
    }
    
    ConnectionTimeouts {
        initialTimeout: Duration
        authTimeout: Duration
        playTimeout: Duration
        joinTimeouts: Map<String, Duration>
    }
    
    RateLimitConfig {
        enabled: boolean
        packetsPerSecond: int
        burstCapacity: int
    }
    
    // Dynamic configurations
    modules: Map<String, Module>
    logLevels: Map<String, Level>
    modConfig: Map<PluginIdentifier, ModConfig>
    
    // Storage providers
    playerStorageProvider: PlayerStorageProvider
    authCredentialStoreConfig: BsonDocument
}

Plugin Configuration

Defining Plugin Config

Create a configuration class with a Codec:

public class MyPluginConfig {
    
    // Define the codec for serialization
    public static final Codec<MyPluginConfig> CODEC = BuilderCodec.of(MyPluginConfig::new)
        .with("enabled", Codec.BOOLEAN, c -> c.enabled, true)
        .with("maxItems", Codec.INTEGER, c -> c.maxItems, 100)
        .with("welcomeMessage", Codec.STRING, c -> c.welcomeMessage, "Welcome!")
        .with("debugMode", Codec.BOOLEAN, c -> c.debugMode, false)
        .with("spawnLocation", Vector3d.CODEC, c -> c.spawnLocation, new Vector3d(0, 64, 0))
        .build();
    
    // Configuration fields (final for immutability)
    public final boolean enabled;
    public final int maxItems;
    public final String welcomeMessage;
    public final boolean debugMode;
    public final Vector3d spawnLocation;
    
    // Private constructor used by codec
    private MyPluginConfig(boolean enabled, int maxItems, String welcomeMessage, 
                           boolean debugMode, Vector3d spawnLocation) {
        this.enabled = enabled;
        this.maxItems = maxItems;
        this.welcomeMessage = welcomeMessage;
        this.debugMode = debugMode;
        this.spawnLocation = spawnLocation;
    }
}

Using Plugin Config

public class MyPlugin extends JavaPlugin {
    
    private Config<MyPluginConfig> config;
    
    public MyPlugin(JavaPluginInit init) {
        super(init);
        // Initialize config BEFORE setup() is called
        this.config = withConfig(MyPluginConfig.CODEC);
    }
    
    @Override
    protected void setup() {
        MyPluginConfig cfg = config.get();
        
        if (!cfg.enabled) {
            getLogger().info("Plugin is disabled in config");
            return;
        }
        
        getLogger().info("Max items: " + cfg.maxItems);
        getLogger().info("Debug mode: " + cfg.debugMode);
    }
    
    @Override
    protected void start() {
        MyPluginConfig cfg = config.get();
        // Use config values
    }
    
    // Provide access to config for other classes
    public MyPluginConfig getPluginConfig() {
        return config.get();
    }
}

Configuration Storage

Plugin configurations are stored in the server's main config under the plugin identifier:

{
    "modConfig": {
        "com.example:MyPlugin": {
            "enabled": true,
            "maxItems": 100,
            "welcomeMessage": "Welcome!",
            "debugMode": false,
            "spawnLocation": {
                "x": 0.0,
                "y": 64.0,
                "z": 0.0
            }
        }
    }
}

Nested Configurations

Defining Nested Config

public class AdvancedConfig {
    
    public static final Codec<AdvancedConfig> CODEC = BuilderCodec.of(AdvancedConfig::new)
        .with("general", GeneralConfig.CODEC, c -> c.general, new GeneralConfig())
        .with("features", FeaturesConfig.CODEC, c -> c.features, new FeaturesConfig())
        .with("limits", LimitsConfig.CODEC, c -> c.limits, new LimitsConfig())
        .build();
    
    public final GeneralConfig general;
    public final FeaturesConfig features;
    public final LimitsConfig limits;
    
    private AdvancedConfig(GeneralConfig general, FeaturesConfig features, LimitsConfig limits) {
        this.general = general;
        this.features = features;
        this.limits = limits;
    }
}

public class GeneralConfig {
    public static final Codec<GeneralConfig> CODEC = BuilderCodec.of(GeneralConfig::new)
        .with("name", Codec.STRING, c -> c.name, "Default")
        .with("enabled", Codec.BOOLEAN, c -> c.enabled, true)
        .build();
    
    public final String name;
    public final boolean enabled;
    
    public GeneralConfig() {
        this("Default", true);
    }
    
    private GeneralConfig(String name, boolean enabled) {
        this.name = name;
        this.enabled = enabled;
    }
}

Lists and Maps in Config

List Configuration

public class ListConfig {
    
    public static final Codec<ListConfig> CODEC = BuilderCodec.of(ListConfig::new)
        .with("allowedPlayers", Codec.STRING.listOf(), c -> c.allowedPlayers, List.of())
        .with("spawnPoints", Vector3d.CODEC.listOf(), c -> c.spawnPoints, List.of())
        .build();
    
    public final List<String> allowedPlayers;
    public final List<Vector3d> spawnPoints;
    
    private ListConfig(List<String> allowedPlayers, List<Vector3d> spawnPoints) {
        this.allowedPlayers = List.copyOf(allowedPlayers);
        this.spawnPoints = List.copyOf(spawnPoints);
    }
}

Map Configuration

public class MapConfig {
    
    public static final Codec<MapConfig> CODEC = BuilderCodec.of(MapConfig::new)
        .with("playerHomes", Codec.mapOf(Codec.STRING, Vector3d.CODEC), c -> c.playerHomes, Map.of())
        .with("warpPoints", Codec.mapOf(Codec.STRING, WarpConfig.CODEC), c -> c.warpPoints, Map.of())
        .build();
    
    public final Map<String, Vector3d> playerHomes;
    public final Map<String, WarpConfig> warpPoints;
    
    private MapConfig(Map<String, Vector3d> playerHomes, Map<String, WarpConfig> warpPoints) {
        this.playerHomes = Map.copyOf(playerHomes);
        this.warpPoints = Map.copyOf(warpPoints);
    }
}

Optional Configuration Values

Using Optional

public class OptionalConfig {
    
    public static final Codec<OptionalConfig> CODEC = BuilderCodec.of(OptionalConfig::new)
        .with("requiredValue", Codec.STRING, c -> c.requiredValue)
        .withOptional("optionalValue", Codec.STRING, c -> c.optionalValue)
        .build();
    
    public final String requiredValue;
    public final Optional<String> optionalValue;
    
    private OptionalConfig(String requiredValue, Optional<String> optionalValue) {
        this.requiredValue = requiredValue;
        this.optionalValue = optionalValue;
    }
}

Nullable Values with Defaults

.with("value", Codec.STRING, c -> c.value, "default")  // Default if not present
.withOptional("value", Codec.STRING, c -> c.value)     // Optional, may be absent

Configuration Validation

Custom Validation

public class ValidatedConfig {
    
    public static final Codec<ValidatedConfig> CODEC = BuilderCodec.of(ValidatedConfig::new)
        .with("port", Codec.INTEGER.validate(p -> p > 0 && p < 65536, "Port must be 1-65535"), 
              c -> c.port, 25565)
        .with("name", Codec.STRING.validate(s -> !s.isBlank(), "Name cannot be blank"),
              c -> c.name, "Server")
        .build();
    
    public final int port;
    public final String name;
    
    private ValidatedConfig(int port, String name) {
        this.port = port;
        this.name = name;
    }
}

Range Validation

.with("level", Codec.INTEGER.clamp(1, 100), c -> c.level, 1)

Enum Configuration

public enum Difficulty {
    EASY, NORMAL, HARD, HARDCORE
}

public class GameConfig {
    
    public static final Codec<GameConfig> CODEC = BuilderCodec.of(GameConfig::new)
        .with("difficulty", Codec.enumCodec(Difficulty.class), c -> c.difficulty, Difficulty.NORMAL)
        .build();
    
    public final Difficulty difficulty;
    
    private GameConfig(Difficulty difficulty) {
        this.difficulty = difficulty;
    }
}

Runtime Configuration Access

Reading Config Values

// In your plugin
MyPluginConfig cfg = config.get();

// Use values
if (cfg.enabled) {
    doSomething(cfg.maxItems);
}

Checking Config Changes

Configuration is typically loaded at startup. For dynamic updates:

// Store reference to check later
private MyPluginConfig lastConfig;

@Override
protected void start() {
    lastConfig = config.get();
}

// Check for changes (if config reload is supported)
public void checkConfigUpdate() {
    MyPluginConfig current = config.get();
    if (!current.equals(lastConfig)) {
        onConfigChanged(lastConfig, current);
        lastConfig = current;
    }
}

Per-Module Configuration

Module Config Pattern

// Server modules can have their own config sections
Map<String, Module> modules = serverConfig.getModules();

// Get specific module config
Module myModule = modules.get("myModule");
if (myModule != null && myModule.isEnabled()) {
    // Module-specific logic
}

Best Practices

  1. Use immutable configs - Make config fields final and use List.copyOf(), Map.copyOf()
  2. Provide sensible defaults - Every config field should have a reasonable default
  3. Validate early - Use codec validation to catch invalid values at load time
  4. Document config options - Create documentation for all config options
  5. Use nested configs - Organize related settings into nested config classes
  6. Keep configs simple - Don't over-complicate with too many options
  7. Type safety - Use enums for fixed choices, proper types for values
  8. Test default configs - Ensure your plugin works with all-default configuration
  9. Handle missing configs - Gracefully handle when config file doesn't exist
  10. Version your configs - Consider config versioning for migrations

Configuration Example

Complete example of a well-structured plugin configuration:

public class ShopPluginConfig {
    
    public static final Codec<ShopPluginConfig> CODEC = BuilderCodec.of(ShopPluginConfig::new)
        .with("enabled", Codec.BOOLEAN, c -> c.enabled, true)
        .with("currency", CurrencyConfig.CODEC, c -> c.currency, new CurrencyConfig())
        .with("shops", ShopsConfig.CODEC, c -> c.shops, new ShopsConfig())
        .with("debug", Codec.BOOLEAN, c -> c.debug, false)
        .build();
    
    public final boolean enabled;
    public final CurrencyConfig currency;
    public final ShopsConfig shops;
    public final boolean debug;
    
    private ShopPluginConfig(boolean enabled, CurrencyConfig currency, 
                             ShopsConfig shops, boolean debug) {
        this.enabled = enabled;
        this.currency = currency;
        this.shops = shops;
        this.debug = debug;
    }
}

public class CurrencyConfig {
    public static final Codec<CurrencyConfig> CODEC = BuilderCodec.of(CurrencyConfig::new)
        .with("name", Codec.STRING, c -> c.name, "Gold")
        .with("symbol", Codec.STRING, c -> c.symbol, "G")
        .with("startingAmount", Codec.INTEGER.clamp(0, 1000000), c -> c.startingAmount, 100)
        .build();
    
    public final String name;
    public final String symbol;
    public final int startingAmount;
    
    public CurrencyConfig() { this("Gold", "G", 100); }
    
    private CurrencyConfig(String name, String symbol, int startingAmount) {
        this.name = name;
        this.symbol = symbol;
        this.startingAmount = startingAmount;
    }
}

JSON representation:

{
    "enabled": true,
    "currency": {
        "name": "Gold",
        "symbol": "G",
        "startingAmount": 100
    },
    "shops": {
        "maxItems": 54,
        "allowPlayerShops": true
    },
    "debug": false
}