# 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`: ```java 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 ```java 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 } RateLimitConfig { enabled: boolean packetsPerSecond: int burstCapacity: int } // Dynamic configurations modules: Map logLevels: Map modConfig: Map // Storage providers playerStorageProvider: PlayerStorageProvider authCredentialStoreConfig: BsonDocument } ``` ## Plugin Configuration ### Defining Plugin Config Create a configuration class with a Codec: ```java public class MyPluginConfig { // Define the codec for serialization public static final Codec 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 ```java public class MyPlugin extends JavaPlugin { private Config 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: ```json { "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 ```java public class AdvancedConfig { public static final Codec 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 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 ```java public class ListConfig { public static final Codec 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 allowedPlayers; public final List spawnPoints; private ListConfig(List allowedPlayers, List spawnPoints) { this.allowedPlayers = List.copyOf(allowedPlayers); this.spawnPoints = List.copyOf(spawnPoints); } } ``` ### Map Configuration ```java public class MapConfig { public static final Codec 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 playerHomes; public final Map warpPoints; private MapConfig(Map playerHomes, Map warpPoints) { this.playerHomes = Map.copyOf(playerHomes); this.warpPoints = Map.copyOf(warpPoints); } } ``` ## Optional Configuration Values ### Using Optional ```java public class OptionalConfig { public static final Codec 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 optionalValue; private OptionalConfig(String requiredValue, Optional optionalValue) { this.requiredValue = requiredValue; this.optionalValue = optionalValue; } } ``` ### Nullable Values with Defaults ```java .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 ```java public class ValidatedConfig { public static final Codec 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 ```java .with("level", Codec.INTEGER.clamp(1, 100), c -> c.level, 1) ``` ## Enum Configuration ```java public enum Difficulty { EASY, NORMAL, HARD, HARDCORE } public class GameConfig { public static final Codec 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 ```java // 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: ```java // 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 ```java // Server modules can have their own config sections Map 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: ```java public class ShopPluginConfig { public static final Codec 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 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: ```json { "enabled": true, "currency": { "name": "Gold", "symbol": "G", "startingAmount": 100 }, "shops": { "maxItems": 54, "allowPlayerShops": true }, "debug": false } ```