Settings¶
How UniversalAdmin's typed configuration system works, and how a module (or, later, an extension) registers its own settings. For the end-user-facing reference (every current default, valid range, description), see docs/user/configuration.md.
Why not config.getString(...)¶
Scattered FileConfiguration.getString("foo.bar") calls have no single
place to see what keys exist, no shared validation, and no consistent
fallback behavior when a user typos a value. UniversalAdmin has exactly one
path from config.yml to application code: a registered
SettingDefinition<T>, resolved through SettingsService. Nothing else
reads config.yml directly.
The pieces¶
| Type | Package | Role |
|---|---|---|
SettingKey<T> |
settings |
Namespaced identifier + the literal dotted path in config.yml |
SettingType<T> |
settings |
Parses a raw YAML value into T (see SettingTypes for the built-ins) |
SettingValidator<T> |
settings |
Extra constraints beyond parsing (min/max, regex, ...) - see SettingValidators |
SettingDefinition<T> |
settings |
Key + type + default + description + requiresRestart + validator |
SettingRegistry |
settings |
Every registered SettingDefinition, keyed by config path |
SettingValue<T> |
settings |
A definition paired with its current resolved value |
SettingsService |
settings |
Runtime access: get(key), getValue(key), reload() |
YamlSettingsService |
settings |
The (only) implementation, backed by a Bukkit FileConfiguration |
SettingKey namespacing vs. the YAML path¶
SettingKey<Integer> GUI_PAGE_SIZE = SettingKey.of("core", "gui.page-size");
Two different strings, two different jobs:
configPath("gui.page-size") is the literal dotted path intoconfig.yml. It must be globally unique -SettingRegistry.registerthrows if two definitions claim the same path, regardless of namespace, because they'd be fighting over the same line in the same file.namespace("core") is who owns the setting for registry bookkeeping - it is not part of the YAML path. Core platform settings use"core"; a built-in module uses its ownModuleDescriptor.settingsNamespace(); a future extension would use its extension id.
Registering a setting¶
registry.register(SettingDefinition.builder(GUI_PAGE_SIZE, SettingTypes.INTEGER, 45)
.description("Inventory GUI page size; must be a multiple of 9 between 9 and 54.")
.validator(SettingValidators.multipleOf(9).and(SettingValidators.intRange(9, 54)))
.build());
- Default is mandatory and is itself validated at registration time -
SettingDefinition's constructor throwsIllegalArgumentExceptionif the default fails its own validator. A setting can never be defined in a state where its own default is invalid. requiresRestart(true)for anything that can't safely change while the plugin is running (database connection parameters, which built-in modules are registered at all). See Reload below.- Built-in
SettingTypes:STRING,BOOLEAN,INTEGER,LONG,DOUBLE,DURATION(accepts30s,5m,1h,2d, or a bare number of seconds),STRING_LIST, andSettingTypes.enumOf(MyEnum.class). - Built-in
SettingValidators:intRange/longRange/doubleRange/durationRange,multipleOf,matches(regex, description),notBlank, and.and(...)to combine two.
A module registering its own settings¶
Core settings live in CoreSettings (see below); a module registers its
own the same way, in onEnable, under its own namespace:
@Override
public void onEnable(ModuleContext context) {
SettingKey<Duration> cooldown = SettingKey.of(descriptor().settingsNamespace(), "kick-cooldown");
context.platform().settingRegistry().register(SettingDefinition.builder(cooldown, SettingTypes.DURATION, Duration.ofSeconds(30))
.description("Minimum time between two kicks of the same player.")
.build());
}
(SettingRegistry is reachable the same way every other shared registry is
- through UniversalAdmin, handed to the module via ModuleContext.) No
built-in module does this yet - only CoreSettings (the platform-level
general/database/gui/audit/modules/performance/maintenance/web
sections) is registered today. This is the pattern for when one needs to.
Reading a setting¶
int pageSize = context.platform().settings().get(CoreSettings.GUI_PAGE_SIZE);
Never fails: an absent key returns the default; a value that fails to
parse or validate is logged as a warning and also falls back to the
default. This is deliberate - see docs/architecture/overview.md - a typo in
config.yml should degrade gracefully, not take the server down.
Reload¶
SettingsService.reload() re-reads config.yml, re-runs
ConfigMigrationRunner if needed, and re-resolves every registered
setting:
- A changed value for a setting without
requiresRestartis applied immediately. - A changed value for a setting with
requiresRestartis reported inConfigReloadResult.pendingRestart()and not applied - the old value stays live until the next actual restart.
/admin reload (see ReloadConfigAction
and UniversalAdminCommand)
is the only way to trigger this - never Bukkit's global /reload, which
would bypass every plugin's lifecycle on the server, not just
UniversalAdmin's. The action itself runs off the main thread (see
docs/architecture/threading.md) since it's file IO, even though it isn't a
startup-blocking migration.
Config file versioning¶
config-version at the top of config.yml is managed by
dev.universaladmin.config.ConfigMigrationRunner, not by hand. A future
schema change to config.yml (renaming a key, restructuring a section)
gets a new ConfigMigration (mirrors storage.Migration for the
database) instead of assuming every user's file already matches the new
shape - see ConfigMigration.
Version 1 is the baseline (the runner stamps it into a file that
predates this system entirely). Existing migrations:
ModuleSettingsLayoutMigration (2 - module settings moved under their
module, commands section) and NetworkSettingsMigration (3 - network
section, staff tool range 40 -> 128 only if it still was the old
default). A migration only adds or moves keys; a value the server owner
set is never overwritten.
Localization¶
MessageService/MessageKey are unrelated to SettingsService in code,
but general.language (a CoreSettings entry) controls which locale
YamlLocaleMessageService reads from. See
configuration.md for the
fallback chain and how to add a new locale.
Changing settings at runtime¶
SettingsService#set(path, text) / #reset(path) parse the text with
SettingType#parseText (lists are comma-separated), validate it, write
config.yml through JavaPlugin#saveConfig (comments are preserved) and
apply the value live - unless requiresRestart is set, then it is saved but
reported as pending. The in-game editor and /admin settings call these
only through the audited core:settings.set action (SetSettingAction).
SettingType#format/#toYaml/#suggestionsdefine how a value is shown, stored and offered for completion; the built-in types implement them (durations are stored as30s/5m/2h, enums in lower case).SettingDefinition.Builder#sensitive(true)marks a secret: masked in the GUI and commands, refused bySetSettingAction.- A setting registered after startup (every module and extension setting)
is resolved from the file on its first read, so its configured value
applies immediately - no
/admin reloadneeded.