Save and load settings

Keep settings in your own class, load them at startup, save them from Done off the main thread, and offer Cancel with a draft copy.

Last updated

Steps

The library stores nothing: widgets read your values and write changes back to your code. This page shows where to keep the values, when to write them to disk, and how to offer a Cancel button.

Keep the values in one class#

MyConfig.java
package com.example.mymod;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;

public final class MyConfig {
    public boolean sounds = true;
    public int volume = 80;
    public String greeting = "Hello";

    public static MyConfig load(Path file) {
        MyConfig c = new MyConfig();
        if (!Files.exists(file)) {
            return c;
        }
        Properties p = new Properties();
        try (InputStream in = Files.newInputStream(file)) {
            p.load(in);
        } catch (IOException e) {
            return c;   // keep the defaults
        }
        c.sounds = Boolean.parseBoolean(p.getProperty("sounds", "true"));
        c.volume = Integer.parseInt(p.getProperty("volume", "80"));
        c.greeting = p.getProperty("greeting", "Hello");
        return c;
    }

    public void save(Path file) throws IOException {
        Properties p = new Properties();
        p.setProperty("sounds", String.valueOf(sounds));
        p.setProperty("volume", String.valueOf(volume));
        p.setProperty("greeting", greeting);
        Files.createDirectories(file.getParent());
        try (OutputStream out = Files.newOutputStream(file)) {
            p.store(out, "My Mod settings");
        }
    }
}

Load it in your mod's init, before register. A relative path such as Paths.get("config", "mymod.properties") is in the game directory on every loader.

Save from Done, not from onChange#

onChange runs often: a slider calls it for every step while the player drags. Write to your fields there, and write the file once, when the player leaves the screen. Do the file IO with runAsync so the game doesn't stall:

.footer(Button.builder("done")
        .label(UiTexts.DONE)
        .onPress(() -> host.runAsync(() -> {
            config.save(file);
            return null;
        }, (ignored, error) -> {
            if (error != null) {
                status.show(StatusLine.Kind.ERROR, Text.translatable("mymod.save.failed", "Couldn't save the settings"));
            } else {
                host.back();
            }
        }))
        .build())

On failure the screen stays open and says why; on success it closes.

Offer Cancel with a draft#

Widgets change your object directly. To let the player throw edits away, point the screen at a copy and copy it back on Done:

@Override
public Screen create(UiHost host) {
    MyConfig draft = config.copy();     // a field-by-field copy you write
    return MenuScreen.builder("main")
            .title(Text.translatable("mymod.title", "My Mod"))
            .add(Toggle.builder("sounds")
                    .label(Text.translatable("mymod.sounds", "Sounds"))
                    .value(() -> draft.sounds)
                    .onChange(v -> draft.sounds = v)
                    .build())
            .footer(Button.builder("done").label(UiTexts.DONE).onPress(() -> {
                config.copyFrom(draft);   // copyFrom: also yours
                host.runAsync(() -> {
                    config.save(file);     // a Callable may throw IOException
                    return null;
                }, (ignored, error) -> host.back());
            }).build())
            .footer(Button.builder("cancel").label(UiTexts.CANCEL).onPress(host::back).build())
            .build();
}

Because create runs on every opening, each opening starts a fresh draft.

Values that need converting#

Value Store it as Read it back
a Key key.name(), e.g. key.keyboard.r Key.fromName(text); null if the text is no key name
a color (int) hex text ColorPicker.parseHex(text, alpha); null if it isn't a color
an enum value.name() Enum.valueOf, with a fallback for unknown text
a list of strings one line or one property per item your own split

Remember layout state#

A collapsible Section starts collapsed. To reopen it as the player left it, store the state in your config:

Section.builder("advanced")
        .title(Text.translatable("mymod.advanced", "Advanced"))
        .expanded(config.advancedOpen)
        .onToggle(open -> config.advancedOpen = open)

Next steps#