-
Notifications
You must be signed in to change notification settings - Fork 0
Rubric Migration
Every @Config has a version. When a loaded file declares a lower version, Rubric runs registered migration steps in order before decoding. This lets you evolve the schema (rename fields, restructure sections, transform values) without invalidating existing files.
Rubric stores the schema version at the reserved root key rubric_config_version. When reading a file:
-
Migrator.readVersion(tree, assumed = 1)reads the key. If absent, assume version 1. -
Migrator.migrate(tree, fileVersion, schemaVersion, registry)runs registered steps fromfileVersion + 1up toschemaVersion, in order. - After a successful load,
save()callsMigrator.stampVersion(tree, schemaVersion)so the file writes the new version back.
import com.oliveryasuna.mc.rubric.migration.MigrationRegistry;
import com.oliveryasuna.mc.rubric.migration.MigrationStep;
import com.oliveryasuna.mc.rubric.migration.MigrationOps;
final MigrationRegistry migrations = new MigrationRegistry()
.register(MigrationStep.builder(2) // target version
.op(MigrationOps.renameKey("legacyName", "displayName"))
.op(MigrationOps.move("audio.oldVolume", "audio.master"))
.op(MigrationOps.transform("tickInterval", node -> {
// v1 stored ticks; v2 stores milliseconds.
if(node instanceof Scalar s && s.value() instanceof Number ticks) {
return Scalar.of(ticks.intValue() * 50);
}
return node;
}))
.build());
// Hand the registry to the manager at registration time.
Rubric.register(MyModConfig.class, migrations);MigrationOps provides three built-in factories. Every op takes dotted paths ("display.opacity") and is a no-op if the referenced key is absent — so re-running a step on an already-migrated tree is safe.
-
renameKey(oldPath, newPath)— moves the value atoldPathtonewPath. IfnewPathalready has a value, the old one wins. -
move(fromPath, toPath)— same asrenameKeyin practice; the alias exists for readability when you're moving across sections. -
transform(path, Function<ValueNode, ValueNode>)— runs an arbitrary transform on the node atpath. The function may return the same node type, a different node type, ornullto delete.
Custom ops: implement MigrationOp. Each op receives the root Section and mutates it in place. Look at MigrationOps for examples.
Register one step per target version. Rubric picks the shortest path from fileVersion + 1 to schemaVersion — so if a user comes in on version 1 and your schema is version 4, steps for 2, 3, and 4 all run in order.
migrations
.register(MigrationStep.builder(2).op(...).build())
.register(MigrationStep.builder(3).op(...).build())
.register(MigrationStep.builder(4).op(...).build());If the file's version is higher than the schema's — the user launched an older version of your mod after using a newer one — Rubric returns a MigrationReport with downgrade = true and skips the load (the tree is not mutated, defaults are seeded, no file rewrite happens). This is intentional: downgrading almost always destroys data. If your mod knows how to handle a specific downgrade path, register that step and set sync.strict = false — but plan carefully.
LoadResult.migration() returns a MigrationReport:
-
migration.migrated—trueif any step ran. -
migration.applied— the list of target versions the migrator walked through. -
migration.downgrade—trueif the file was newer than the schema. -
migration.fromVersion/migration.toVersion— the endpoints.
Use this to log or surface a message on first launch after a version bump.
Rubric ships headless test doubles (HeadlessPlatform, InMemoryConfigIO) in rubric-core that make unit-testing a migration straightforward. Seed the in-memory IO with the "old" file contents, register the migration, run manager.load(), assert on the resulting POJO.
Rubric
Reference
Runtime
GUI
Help