diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java index 26d957a77..ca42dc069 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/TexturesLoader.java @@ -35,6 +35,7 @@ import java.util.EnumMap; import java.util.Map; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.logging.Level; @@ -163,20 +164,23 @@ public final class TexturesLoader { return Optional.empty(); } }) - .flatMap(it -> Optional.ofNullable(it.get(TextureType.SKIN)))) + .flatMap(it -> Optional.ofNullable(it.get(TextureType.SKIN))) + .filter(it -> StringUtils.isNotBlank(it.getUrl()))) .asyncMap(it -> { if (it.isPresent()) { Texture texture = it.get(); - try { - return loadTexture(texture); - } catch (IOException e) { - LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e); - return getDefaultSkin(TextureModel.detectModelName(texture.getMetadata())); - } + return CompletableFuture.supplyAsync(() -> { + try { + return loadTexture(texture); + } catch (IOException e) { + LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e); + return uuidFallback; + } + }, POOL); } else { - return uuidFallback; + return CompletableFuture.completedFuture(uuidFallback); } - }, uuidFallback, POOL); + }, uuidFallback); } // ==== diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java index 195d7cf18..01e55f007 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java @@ -220,4 +220,8 @@ public final class Lang { consumer.accept(t); return t; } + + public static void handleUncaughtException(Throwable e) { + Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e); + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/MultiStepBinding.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/MultiStepBinding.java index 7f6ce0cb0..56cc3f6c2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/MultiStepBinding.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/MultiStepBinding.java @@ -18,13 +18,13 @@ package org.jackhuang.hmcl.util.javafx; import static java.util.Objects.requireNonNull; +import static org.jackhuang.hmcl.util.Lang.handleUncaughtException; import java.util.Objects; -import java.util.concurrent.Executor; +import java.util.concurrent.CompletableFuture; import java.util.function.Function; import java.util.function.Supplier; -import org.jackhuang.hmcl.util.InvocationDispatcher; import javafx.application.Platform; import javafx.beans.binding.ObjectBinding; @@ -58,8 +58,8 @@ public abstract class MultiStepBinding extends ObjectBinding { return new FlatMappedBinding<>(map(mapper), nullAlternative); } - public MultiStepBinding asyncMap(Function mapper, V initial, Executor executor) { - return new AsyncMappedBinding<>(this, mapper, executor, initial); + public MultiStepBinding asyncMap(Function> mapper, V initial) { + return new AsyncMappedBinding<>(this, mapper, initial); } private static class SimpleBinding extends MultiStepBinding { @@ -79,8 +79,8 @@ public abstract class MultiStepBinding extends ObjectBinding { } @Override - public MultiStepBinding asyncMap(Function mapper, V initial, Executor executor) { - return new AsyncMappedBinding<>(predecessor, mapper, executor, initial); + public MultiStepBinding asyncMap(Function> mapper, V initial) { + return new AsyncMappedBinding<>(predecessor, mapper, initial); } } @@ -136,49 +136,79 @@ public abstract class MultiStepBinding extends ObjectBinding { private static class AsyncMappedBinding extends MultiStepBinding { - private final InvocationDispatcher dispatcher; - private boolean initialized = false; private T prev; private U value; - public AsyncMappedBinding(ObservableValue predecessor, Function mapper, Executor executor, U initial) { + private final Function> mapper; + private T computingPrev; + private boolean computing = false; + + public AsyncMappedBinding(ObservableValue predecessor, Function> mapper, U initial) { super(predecessor); this.value = initial; + this.mapper = mapper; + } - dispatcher = InvocationDispatcher.runOn(executor, arg -> { - synchronized (this) { - if (initialized && Objects.equals(arg, prev)) { - return; - } + private void tryUpdateValue(T currentPrev) { + synchronized (this) { + if ((initialized && Objects.equals(prev, currentPrev)) + || isComputing(currentPrev)) { + return; } - U newValue = mapper.apply(arg); - synchronized (this) { - prev = arg; - value = newValue; - initialized = true; + computing = true; + computingPrev = currentPrev; + } + + CompletableFuture task; + try { + task = requireNonNull(mapper.apply(currentPrev)); + } catch (Throwable e) { + valueUpdateFailed(currentPrev); + throw e; + } + + task.handle((result, e) -> { + if (e == null) { + valueUpdate(currentPrev, result); + Platform.runLater(this::invalidate); + } else { + handleUncaughtException(e); + valueUpdateFailed(currentPrev); } - Platform.runLater(this::invalidate); + return null; }); } - // called on FX thread, this method is serial - @Override - protected U computeValue() { - T currentPrev = predecessor.getValue(); - U value; - boolean updateNeeded = false; + private void valueUpdate(T currentPrev, U computed) { synchronized (this) { - value = this.value; - if (!initialized || !Objects.equals(currentPrev, prev)) { - updateNeeded = true; + if (isComputing(currentPrev)) { + computing = false; + computingPrev = null; + prev = currentPrev; + value = computed; + initialized = true; } } - if (updateNeeded) { - dispatcher.accept(currentPrev); - } - return value; } + private void valueUpdateFailed(T currentPrev) { + synchronized (this) { + if (isComputing(currentPrev)) { + computing = false; + computingPrev = null; + } + } + } + + private boolean isComputing(T prev) { + return computing && Objects.equals(prev, computingPrev); + } + + @Override + protected U computeValue() { + tryUpdateValue(predecessor.getValue()); + return value; + } } }