Skip to content

Configuration

Petrus Pradella edited this page Jul 23, 2026 · 3 revisions

Configuration

EverNifeCore reads and writes YAML configuration through the EveryConfig library (br.com.finalcraft.everyconfig.*). A config file is a Config handle: you open it, seed defaults with inline comments, read and write typed values by path, and save. The framework wraps that library with ConfigFactory, which bakes in a shared, type-aware codec so that framework types (ItemStack, Location, FancyText, cooldowns, the vector families, ...) cross to and from config for free. This page covers the framework-side surface; for the library itself (binding model, comment engine, async back-store), the EveryConfig wiki is the recommended deep dive.

Migration note. Older EverNifeCore (2.x) exposed a Simple-YAML Config wrapper and a @Loadable / Salvable annotation contract for custom serialization. On 3.0.1 that wrapper has moved into the external EveryConfig library, and the annotation contract is gone. What replaces it is described in Custom types below - either a one-line ConfigFactory type registration, or a plain POJO that implements ConfigLifecycle.

Opening a config

ConfigManager holds the two core files (config.yml and Cooldowns.yml). Your own plugin opens its files through ConfigFactory. The plugin-scoped overload resolves the file under your plugin's data folder and stamps the standard FinalCraft header:

import br.com.finalcraft.evernifecore.config.ConfigFactory;
import br.com.finalcraft.everyconfig.config.Config;

// Resolved under this plugin's data folder, header seeded automatically.
Config config = ConfigFactory.open(ecPluginData, "config.yml");

There are also plain overloads that take a File, Path or file-name String (no data-folder relocation, no header), plus overloads that accept an explicit Codec or a BackStore.Durability. The codec is inferred from the file extension - .yml, .toml, .json and .jsonc are all supported by the shared type-aware registry.

Reading and writing values

Values are addressed by a dotted path. The most useful method is getOrSetValueIfAbsent: it returns the stored value, or writes your default (with optional comment lines) the first time the key is missing. This is how every core setting is declared - see ECSettings for real examples:

String zoneId = config.getOrSetValueIfAbsent(
        "Settings.Time.ZONE_ID_OF_DAY_OF_TODAY",
        ZoneId.systemDefault().getId(),
        "The timezone used for EverNifeCore and its sub-plugins!",
        "You can use GMT zones, for example 'GMT-3' or 'GMT+8'."
);

int refresh = config.getOrSetValueIfAbsent("Settings.PageViewers.REFRESH_TIME", 5);
boolean warn = config.getOrSetValueIfAbsent("Settings.Warn.enabled", true);

Other common accessors mirror the usual config surface:

Call Purpose
getValue(path, Type.class) Read a typed value (any registered type).
setValue(path, value) Write a value; null removes the key.
getInt / getBoolean / getString(path[, default]) Primitive/string reads with optional default.
getKeys(path) The child keys under a section (for iterating a map-shaped node).
getFile() The backing File, or null for an in-memory config.

Because the codec is type-aware, a registered type works everywhere the same way - as a solo value, a field of another POJO, a map value, or a list element:

config.setValue("spawn.icon", myItemStack);            // ItemStack -> config
ItemStack icon = config.getValue("spawn.icon", ItemStack.class);

Comments and the header

Comments come from two places. Inline comments are the trailing varargs on getOrSetValueIfAbsent (shown above). The banner at the top of the file is the header; the plugin-scoped open overload seeds the standard FinalCraft banner, and you can override it with config.setHeader(String[] lines).

Saving

Saving is explicit. There is no periodic auto-save thread on this branch - a config is written when you ask it to be:

  • config.save() - save synchronously.
  • config.saveAsync() - hand the write to EveryConfig's async back-store and return immediately. This is what the framework uses for hot paths (for example the cooldown file writes with saveAsync()).

A common seed-then-save pattern, taken from ECSettings.initialize(), only writes when defaults were actually added:

// ... declare every setting with getOrSetValueIfAbsent ...
if (config.hasNewSeededDefaults()) {
    config.save();
    config.clearNewSeededDefaults();
}

hasNewSeededDefaults() reports whether any getOrSetValueIfAbsent call inserted a missing default during this pass, so a first boot (or a version that adds new keys) writes the file exactly once and an unchanged config is never rewritten.

Custom types

EveryConfig is Jackson-first, so "teaching" the framework a new type is one serializer/deserializer pair registered on ConfigFactory. Register it once during your bootstrap and the whole config surface composes for free. There are three registration styles plus a list-element add-on:

import br.com.finalcraft.evernifecore.config.ConfigFactory;

// Object-shaped: the value becomes a nested map (nested registered types recurse automatically).
ConfigFactory.register(MyData.class).asMap(MyData::toMap, MyData::fromMap);

// Scalar-shaped: the value becomes a single string, and is usable as a map key.
ConfigFactory.register(MyId.class).asString(MyId::toString, MyId::parse);

// Full control: hand Jackson the serializer/deserializer directly.
ConfigFactory.register(MyThing.class).jackson(mySerializer, myDeserializer);

Registration is allowed at any time. A late registration invalidates the shared codec, which is rebuilt lazily on the next open (copy-on-write): configs opened afterwards see the new type; a config already open keeps the codec it captured. The framework registers its built-ins this way - the position/vector family, Cooldown/GenericCooldown and FancyText in ECBuiltinTypes, and the Bukkit types (ItemStack, Location, GUI LayoutIcon) during the platform's registerConfigTypes() hook.

POJOs with lifecycle hooks

For a value you own end to end, the simplest path is a plain Jackson bean (no-arg constructor, accessors) that optionally implements ConfigLifecycle (br.com.finalcraft.everyconfig.binding.ConfigLifecycle) to run postLoad / postSave logic. This is the pattern that replaced the old @Loadable / Salvable contract; FCPlayerInventory is a live example. Combined with @JsonAnyGetter, nested bound entities compose without any central registration.

The same type authority also crosses into the EveryDatabase storage layer, so a type that serializes to a config file also serializes into a player-data section unchanged - see PlayerData and PDSections and Storage Backends.

See also

Clone this wiki locally