Contenido generado con IA — puede contener errores.

Fundamentos

Configuración

Archivos YAML declarados como records de Java, generados a partir de ellos, y migrados cuando cambian de forma.

El record es la fuente de verdad. El archivo YAML es salida, no entrada: se genera a partir del constructor sin argumentos del record, comentarios incluidos, y se lee de vuelta a la misma forma.

ConfigFile<Settings> settings = Configs.define(this, "config", Settings.class).load();
int pool = settings.get().database().poolSize();

Declarar un archivo

public record Storage(
        @Comment("Conexiones abiertas. Regla rápida: núcleos × 2.")
        @Key("pool-size")
        int poolSize,
 
        @Comment("Dónde viven los datos de jugador.")
        String host) {
 
    /** Los valores con los que se genera el archivo. */
    public Storage() {
        this(10, "localhost");
    }
}
AnotaciónQué hace
@Key("otro-nombre")La clave YAML difiere del nombre del componente.
@Comment("…")Se escribe encima del valor en el archivo. Repetible — varias líneas, en orden.

Los records anidados se convierten en secciones anidadas, así que la estructura del archivo es el árbol de records.

Los comentarios son el manual

Son lo que lee el dueño del servidor en vez de tu documentación. Di qué cambia el valor, en qué unidad y en qué rango — Segundos que un jugador sigue marcado tras recibir un golpe, no duración del tag.

Leer y escribir

LlamadaQué hace
get()La instantánea actual. Un acceso a campo, nunca un reparseo.
reload()Relee el archivo. Devuelve los problemas; lista vacía significa limpio.
onReload(consumer)Se ejecuta tras cada recarga correcta.
save()Escribe la instantánea actual en disco.
update(operador)Cambiar y persistir en un paso.
issues()Problemas de la última carga.
schema()Una descripción de solo lectura del tipo del record.
settings.update(actual -> actual.withHost("10.0.0.5"));

Versiones y migraciones

Un archivo recibe un marcador config-version en cuanto declaras una versión, y las migraciones llevan un archivo antiguo hacia adelante en el primer arranque tras actualizar:

config = Configs.define(this, "config", Settings.class)
        .version(3)
        .migration(1, MIGRATION_FROM_1)
        .migration(2, MIGRATION_FROM_2)
        .load();

Los pasos se ejecutan en orden, por muy atrasado que esté el archivo. Una migración se construye con cuatro operaciones:

OperaciónQué hace
Migration.rename(de, a)Mueve un valor a una ruta nueva. Un origen ausente se deja en paz.
Migration.remove(ruta)Borra una clave.
Migration.transform(ruta, reescritura)Reescribe el valor en el sitio. Se salta si la clave no está.
Migration.all(pasos…)Varias de las anteriores como una sola migración.
public static final Migration MIGRATION_FROM_1 = Migration.all(
        Migration.rename("combat.combat-actionbar.text",
                         "combat.combat-actionbar.action-bar.text"),
        Migration.remove("combat.combat-actionbar.update-interval"));
Un renombrado sin migración borra las ediciones del dueño

Una clave que ningún record declara se elimina al cargar. Eso es lo que mantiene los archivos limpios entre actualizaciones — y es exactamente por lo que reestructurar una sección sin migración tira en silencio lo que el dueño del servidor hubiera escrito ahí. Renombra en el record y migra en el mismo commit.

transform es la herramienta correcta cuando lo que cambió es el significado de un valor y no su sitio — un token renombrado, un número que estaba en ticks y ahora va en segundos. Reescribe lo que hay en vez de reemplazarlo con el valor por defecto, así que las palabras y los colores del dueño sobreviven.

Bloques que nombra el dueño

Un componente declarado Map<String, V> es una sección cuyas claves elige el dueño del servidor — mundos, regiones, materiales — en vez de claves que decidió el código:

public record Limits(
        @Comment("Multiplicador por mundo. Añade los mundos que tenga este servidor.")
        Map<String, Double> worlds,
        Map<String, Item> items) {
 
    public Limits() {
        this(Map.of("world", 7.0), Map.of("ender-pearl", new Item()));
    }
 
    public record Item(double cooldown, int maxUses) {
        public Item() { this(14.0, 1); }
    }
}
worlds:
  arena: 2.5
  survival: 9.0
items:
  ender-pearl:
    cooldown: 14.0
    max-uses: 1

V puede ser una hoja — texto, número, booleano, enum, lista — o un record, que se convierte en un bloque por entrada. La clave tiene que ser String; un Map de Map se rechaza, así que anida un record y el bloque interior tendrá nombre, comentarios y valores por defecto propios.

Nada de dentro se podaLas claves son del dueño. Las claves dentro de un record siguen siendo del código y se podan normal.
Las entradas del constructor son ejemplosSe escriben una vez, al generar el archivo. Una entrada que el dueño borró se queda borrada, y un bloque vaciado se queda vacío.
Una entrada inventada hereda los defaults del recordPara lo que no haya escrito.
Una entrada ilegible cuesta esa entradaSe avisa en su propia ruta; el resto carga.
Se conserva el orden de inserciónEl archivo no se reordena en cada guardado.

Secciones que no hacen nada

Un record que implementa Sparse decide por sí mismo si tiene algo que decir. Mientras isEmpty() responda true, la sección no se escribe en el archivo, leerla de vuelta no dice nada y no vuelve a aparecer en la siguiente carga; un bloque vacío que escribió una versión anterior se elimina en el siguiente guardado.

Eso es lo que evita que un efecto con una sola boss bar escriba además un título, una action bar, un sonido, una partícula y un firework vacíos, cada uno con un comentario por clave. Solo vale para secciones cuyos valores por defecto están vacíos por naturaleza — una con valores reales tiene que quedarse en el archivo, o nadie se entera de que existe.

Claves desconocidas

Cualquier clave que ningún record declare se elimina al cargar y se reporta una vez como UNKNOWN_KEY. Es una operación de un solo sentido, así que una sección que se está retirando a propósito merece una nota en el código diciéndolo.

Recargar todo lo de un plugin

List<ConfigIssue> issues = Configs.reloadAll(this);

Cada ConfigIssue dice qué archivo, qué ruta y qué estaba mal. Un valor malo cuesta ese valor, no el archivo: el resto carga y el valor por defecto rellena el hueco.

¿Falta algo en esta página? Dínoslo en Discord