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.
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):
- Starts
taskoff the main thread. Meanwhile the current screen shows Working…, its widgets are inactive and it ignores input, andhost.isBusy()istrue. - When
taskreturns, runsonDone(result, null)on the main thread. When it throws, runsonDone(null, exception). - Refreshes the screen, so the status line and every supplier show the new state.
Rules:
taskmust not touch the game or the screen. Read what it needs into local variables before callingrunAsync, and apply results inonDone.onDonemay do anything an action may: change state, write to aStatusLine, open a screen.- For no result, make the task a
Callable<Void>andreturn null;. - A registered menu's tasks run on a thread of their own, one at a time, in the order they were started. A task that hangs holds up only your menu, never another mod's.
- If
onDoneitself throws, the error is logged with your mod id and the menu stays open.
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#
- Threads: the full threading rules.
- Save and load settings: what to save and when.