458 lines
13 KiB
Markdown
458 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
|
|
}
|
|
```
|