13 KiB
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
- Use immutable configs - Make config fields
finaland useList.copyOf(),Map.copyOf() - Provide sensible defaults - Every config field should have a reasonable default
- Validate early - Use codec validation to catch invalid values at load time
- Document config options - Create documentation for all config options
- Use nested configs - Organize related settings into nested config classes
- Keep configs simple - Don't over-complicate with too many options
- Type safety - Use enums for fixed choices, proper types for values
- Test default configs - Ensure your plugin works with all-default configuration
- Handle missing configs - Gracefully handle when config file doesn't exist
- 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
}