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

459 lines
13 KiB
Markdown

# 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<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:
```java
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
```java
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:
```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<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
```java
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
```java
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
```java
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
```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<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
```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<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
```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<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:
```java
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:
```json
{
"enabled": true,
"currency": {
"name": "Gold",
"symbol": "G",
"startingAmount": 100
},
"shops": {
"maxItems": 54,
"allowPlayerShops": true
},
"debug": false
}
```