Skip to content
ProstStuff edited this page Jul 10, 2026 · 6 revisions

Warning

Some part is still work in progress and only provide a brief and short explanation.

Note

Reflecting Utilitary 1.0.0 and Minecraft 26.1 release for the documentation

Utilitary config system utilizes Minecraft codec to serialize and deserialize values. In addition, the config has their own unique structure of separation.

{
	// The metadata of the config
	"config": {},
	// The main content of the config
	"data": {},
}

Setting Up Config

Make sure Utilitary is ready to use, if not you can go back to Installation and come back here

Preparing A Config Class

The class of the config can be either record or regular class depending on preferences

public record Config() {}
public class Config {
	public Config() {}
}

And that's pretty much it. No need for an interface to implements.

Creating Codec

Use RecordCodecBuilder.create() to create a config codec

Since the config class above is empty and can't serve as a good example, we're going to use Skinmatics 2.0.0 config as an example.

public record SkinmaticsConfig(
	String profile,
	int updateRange,
	boolean forceStrongerEmissiveGlow,
	GUI gui,
	PaperDoll paperDoll,
	LinkedHashMap<UUID, String> autoAssigns
) {
}
public static final Codec<SkinmaticsConfig> CODEC = RecordCodecBuilder.create(  
        inst -> inst.group(    
                Codec.STRING.fieldOf("profile").forGetter(SkinmaticsConfig::profile),  
                Codec.INT.fieldOf("updateRange").forGetter(SkinmaticsConfig::updateRange),  
                Codec.BOOL.fieldOf("forceStrongerEmissiveGlow").forGetter(SkinmaticsConfig::forceStrongerEmissiveGlow),  
                GUI.CODEC.fieldOf("gui").forGetter(SkinmaticsConfig::gui),  
                PaperDoll.CODEC.fieldOf("paperDoll").forGetter(SkinmaticsConfig::paperDoll),  
                Codec.unboundedMap(UUIDUtil.CODEC, Codec.STRING).xmap(LinkedHashMap::new, (map) -> map)  
                        .fieldOf("autoAssigns").forGetter(SkinmaticsConfig::autoAssigns)  
        ).apply(inst, SkinmaticsConfig::new)  
);

Tip

You can directly store it in ConfigCodec using ConfigCodec.of(<Codec>).

Creating Config Type

The last step is creating a config type.

public static final ConfigType<SkinmaticsConfig, SimpleMetadataType.Metadata, SimpleMetadataType> TYPE =  
        new ConfigType<>(  
                Identifier.fromNamespaceAndPath("skinmatics", "config"),  // Config type id
                1,  // config version
                ConfigCodec.of(CODEC),  
                (_) -> new SkinmaticsConfig(...),  
                null,
                SimpleMetadataType.INSTANCE,
                JsonConfigFormat.INSTANCE
        );

And all set! Now you only have to load and save the config.


Loading, Saving, and Deleting Config

Note

The file name of the config always use SimpleIdentifier.

Loading Config

ConfigManager.Result<Config, ConfigMetadata> result = ConfigManager.load(<file_name>, <config_type>);

Config config = result.config();
ConfigMetadata metadata = result.metadata();
ProfileManager.Status status = result.status();

Saving Config

ProfileManager.Status status = ConfigManager.save(<file_name>, <config_yype>, <config>)

Deleting Config

boolean deleted = ConfigManager.delete(<file_name>, <config_type>)

Adding Comments To Config

config/utilitary/utilitary.json5

{  
  // Config version  
  config: 2,  
  // Utilitary Configuration  
  data: {  
    // Disable saving and loading config file. Loading config file always returns default  
    safeMode: false,  
    // Prints additional information to the console  
    debugPrinting: false,  
  },  
}

Before you start adding comments, make sure to change your ConfigType's ConfigFormat to Json5ConfigFormat

new ConfigType<>(  
        ...,
        Json5ConfigFormat.INSTANCE
);

Preparing Comments

Use Comment.of() to start making comments.

Note

This comment only apply to data field. config (metadata) field has their own Comment provided by the metadata type.

Comment comment = Comment.of("Utilitary Configuration");  
comment.addComment("safeMode", "Disable saving and loading config file. Loading config file always returns default");  
comment.addComment("debugPrinting", "Prints additional information to the console");

Tip

You can use Component for comments. However, the language file might not be loaded when you load it. Use Component.translatableWithFallback() if your config load before the language load.

Creating FormatSettings

The final step is to create a FormatSettings

// Returns a FormatSettings with both metadata comments and the supplied comments
Json5ConfigFormat.withComment(<config_type>, <comments>)

Then change the argument of how you load and save from

ConfigManager.load(<file_name>, <config_type>);
ConfigManager.save(<file_name>, <config_type>, <config>);

to

ConfigManager.load(<file_name>, <config_type>, <format_settings>);
ConfigManager.save(<file_name>, <config_type>, <format_settings>, <config>);

Config Metadata

Config metadata serves as a way to store constant values or be a clear separation between data and metadata values

SimpleMetadataType

This is the only metadata type Utilitary provided by default. Only serializes the mandatory version number

public class SimpleMetadataType implements ConfigMetadataType<SimpleMetadataType.Metadata> {  
    public static final Codec<Metadata> CODEC = Codec.INT.comapFlatMap(SimpleMetadataType::read, Metadata::version);  
    public static final SimpleMetadataType INSTANCE = new SimpleMetadataType();  
  
    private SimpleMetadataType() {}  
  
    @Override  
    public @NonNull Codec<Metadata> codec() {  
        return CODEC;  
    }  
  
    @Override  
    public <T> SimpleMetadataType.@NonNull Metadata create(Context<T> context) {  
        return new Metadata(context.version());  
    }  
  
    @Override  
    public JsonElement migrate(JsonElement original) {  
        if (original.isJsonObject()) {  
            return original.getAsJsonObject().get("version");  
        }  
  
        return original;  
    }  
  
    @Override  
    public Comment comment() {  
        return Comment.of("Config version");  
    }  
  
    public static DataResult<Metadata> read(Integer version) {  
        return DataResult.success(new Metadata(version));  
    }  
  
    public record Metadata(int version) implements ConfigMetadata {}  
}
{
	"config": 1
}

Custom Config Metadata

Important

It's recommended to use "version" as the config version, and always convert a number only config field as the config version.

We'll be using Skinmatics ProfileMetadataType as the example of this part

Record Metadata

Prepare a metadata record class by implementing ConfigMetadata

public record Metadata(
	int version,
	String skinmatics,
	String utilitary
) implements ConfigMetadata {}

Preparing Codec

public static final Codec<Metadata> CODEC = RecordCodecBuilder.create(  
        inst -> inst.group(  
                Codec.INT.fieldOf("version").forGetter(Metadata::version),  
                Codec.STRING.fieldOf("skinmatics").forGetter(Metadata::skinmatics),  
                Codec.STRING.fieldOf("utilitary").forGetter(Metadata::utilitary)  
            ).apply(inst, Metadata::new)
);

The Main Class

public class ProfileMetadataType implements ConfigMetadataType<ProfileMetadataType.Metadata> {
    @Override  
    public @NonNull Codec<Metadata> codec() {  
        return CODEC;  
    }  
  
    @Override  
    public @NonNull <T> Metadata create(Context<T> context) {
        return new Metadata(context.version(), SkinmaticsMod.getVersion(), SkinmaticsMod.getVersion(Utilitary.ID));  
    }  
}

Using It

Just plug it in the last second argument of new ConfigType<>() and test it with ConfigManager.save()

new ConfigType<>(  
    <id>,  
    <version>,  
    <codec>,  
    <defaults>,  
    <migrations>,  
    new ProfileMetadataType(),
    <config_format>
);
{
	"config": {
		"version": 1,
		"skinmatics": "2.0.0",
		"utilitary": "1.0.0"  
	}
}

Migrating Config

Be sure to change the version value of your config and add migrations. For each version or change you made, add one migration to your config.

public static final ConfigType<UtilitaryConfig, SimpleMetadataType.Metadata, SimpleMetadataType> TYPE;  
  
static {  
    NavigableMap<Integer, ConfigType.Migration> migrations = new TreeMap<>();  
  
    migrations.put(1, (context) -> {
        JsonObject object = context.data().getAsJsonObject();  
        object.add("debugPrinting", object.remove("debug"));  
        return object;  
    });  
  
    TYPE = new ConfigType<>(  
            Identifier.fromNamespaceAndPath(Utilitary.ID, "config_type"),  
            2,  
            CODEC,  
            (_) -> new UtilitaryConfig(false, false),  
            migrations,  
            new SimpleMetadataType()
    );
}

Warning

Missing migration version will cause an error

Migrating Metadata

You only need to override .migrate() method of ConfigMetadataType

SimpleMetadataType

@Override  
public JsonElement migrate(JsonElement original) {  
    if (original.isJsonObject()) {
        return original.getAsJsonObject().get("version");  
    }

    return original;  
}  

Syncing Config

Current Utilitary version does not have their own config sync system. You have to create your own network payload and stream codec for this.


Config Format

Config format (ConfigFormat) handles the writing and reading of your config file

public interface ConfigFormat<S> {  
    @NonNull JsonElement read(Path path, S formatSettings) throws IOException;  
    void write(Path path, JsonElement element, S formatSettings) throws IOException;
  
    <C, M extends ConfigMetadata> @NonNull S create(SimpleIdentifier fileName, ConfigType<C, M, S> type);
    
    @NonNull String suffix();
}

Utilitary only provides two ConfigFormat, JsonConfigFormat and Json5ConfigFormat

Custom Config Format

You can add your own custom config format for other file type by implementing ConfigFormat. As long as your implementation is able to handle JsonElement classes then you're good to go.

public class CustomConfigFormat implements ConfigFormat<CustomConfigFormat.FormatSettings> {  
    public static final CustomConfigFormat INSTANCE = new CustomConfigFormat();  
      
    private CustomConfigFormat() {}  
      
    @Override  
    public @NonNull JsonElement read(Path path, FormatSettings formatSettings) throws IOException {  
        // Read from file  
    }  
  
    @Override  
    public void write(Path path, JsonElement element, FormatSettings formatSettings) throws IOException {  
        // Write to file  
    }  
  
    @Override  
    public @NonNull <C, M extends ConfigMetadata> FormatSettings create(SimpleIdentifier fileName, ConfigType<C, M, FormatSettings> type) {  
        // Default FormatSettings
        return new FormatSettings();
    }  
  
    @Override  
    public @NonNull String suffix() {  
        // File format (<file_name>.<suffix()>)  
	    return "";
    }  
  
    public record FormatSettings() {}  
}

Important

ConfigFormat.save() always get the complete JsonElement (data and config already stored)

Important

Be sure to return JsonElement as JsonObject that has data and config

Clone this wiki locally