Slow work: files and network

Run file IO and network requests off the main thread with runAsync, show progress and report the result in a StatusLine.

Last updated

Steps

Every action runs on the game's main thread, so a slow action freezes the game. Move slow work into host.runAsync and report the result with a StatusLine.

The pattern#

StatusLine status = StatusLine.builder("status").build();

Button save = Button.builder("save")
        .label(Text.translatable("mymod.save", "Save"))
        .onPress(() -> host.runAsync(() -> {
            store.save(config);                // background thread: no game, no screen
            return null;
        }, (ignored, error) -> {                // main thread again
            if (error != null) {
                status.show(StatusLine.Kind.ERROR, Text.translatable("mymod.save.failed", "Couldn't save: %s",
                        Text.literal(String.valueOf(error.getMessage()))));
            } else {
                status.show(StatusLine.Kind.SUCCESS, Text.translatable("mymod.saved", "Saved"));
            }
        }))
        .build();

MenuScreen screen = MenuScreen.builder("main")
        .title(Text.translatable("mymod.title", "My Mod"))
        .add(status)
        .add(save)
        .build();

What runAsync does#

host.runAsync(task, onDone):

  1. Starts task off the main thread. Meanwhile the current screen shows Working…, its widgets are inactive and it ignores input, and host.isBusy() is true.
  2. When task returns, runs onDone(result, null) on the main thread. When it throws, runs onDone(null, exception).
  3. Refreshes the screen, so the status line and every supplier show the new state.

Rules:

A task with a result#

host.runAsync(() -> store.load(name), (profile, error) -> {
    if (error != null) {
        status.show(StatusLine.Kind.ERROR, Text.translatable("mymod.load.failed", "Couldn't load %s",
                Text.literal(name)));
    } else {
        config.apply(profile);
        status.show(StatusLine.Kind.SUCCESS, Text.translatable("mymod.loaded", "Loaded %s", Text.literal(name)));
    }
});

The result type follows from the task: here store.load returns the type that profile gets.

Work that starts elsewhere#

If your own thread or a callback from another library changes what the menu shows, get back to the main thread with runOnMainThread and refresh yourself. It is the one UiHost method that is safe to call from any thread:

downloader.onFinished(file -> host.runOnMainThread(() -> {
    status.show(StatusLine.Kind.SUCCESS, Text.translatable("mymod.downloaded", "Downloaded"));
    host.refresh();     // runOnMainThread doesn't refresh by itself
}));

StatusLine.show and clear are safe from any thread too; the message appears on the next refresh.

Status line kinds#

Kind Use it for
INFO neutral information
SUCCESS the action worked
WARNING the player should notice something, or the action partly failed
ERROR the action failed

Backends color the text by kind. A status line is empty until the first show, and clear() empties it.

Test it#

FakeHost runs async tasks synchronously, so the result is there right after the click:

host.click("save");
assertTrue(host.render().contains("[status status] SUCCESS Saved"));

Next steps#