package dev.isxander.yacl3.gui.image; import com.mojang.blaze3d.systems.RenderSystem; import dev.isxander.yacl3.gui.image.impl.AnimatedDynamicTextureImage; import dev.isxander.yacl3.impl.utils.YACLConstants; import net.minecraft.client.Minecraft; import net.minecraft.resources.ResourceLocation; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.*; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; public class ImageRendererManager { private static final ExecutorService SINGLE_THREAD_EXECUTOR = Executors.newSingleThreadExecutor(task -> new Thread(task, "YACL Image Prep")); private static final Map> IMAGE_CACHE = new ConcurrentHashMap<>(); static final Map PRELOADED_IMAGE_CACHE = new ConcurrentHashMap<>(); static final List PRELOADED_IMAGE_FACTORIES = List.of( new PreloadedImageFactory( location -> location.getPath().endsWith(".webp"), AnimatedDynamicTextureImage::createWEBPFromTexture ), new PreloadedImageFactory( location -> location.getPath().endsWith(".gif"), AnimatedDynamicTextureImage::createGIFFromTexture ) ); public static Optional getImage(ResourceLocation id) { if (PRELOADED_IMAGE_CACHE.containsKey(id)) { return Optional.of((T) PRELOADED_IMAGE_CACHE.get(id)); } if (IMAGE_CACHE.containsKey(id)) { return Optional.ofNullable((T) IMAGE_CACHE.get(id).getNow(null)); } return Optional.empty(); } @SuppressWarnings("unchecked") public static CompletableFuture registerOrGetImage(ResourceLocation id, Supplier factorySupplier) { if (PRELOADED_IMAGE_CACHE.containsKey(id)) { return CompletableFuture.completedFuture((T) PRELOADED_IMAGE_CACHE.get(id)); } if (IMAGE_CACHE.containsKey(id)) { return (CompletableFuture) IMAGE_CACHE.get(id); } var future = new CompletableFuture(); IMAGE_CACHE.put(id, future); ImageRendererFactory factory = factorySupplier.get(); SINGLE_THREAD_EXECUTOR.submit(() -> { Supplier> supplier = factory.requiresOffThreadPreparation() ? new CompletedSupplier<>(safelyPrepareFactory(id, factory)) : () -> safelyPrepareFactory(id, factory); Minecraft.getInstance().execute(() -> completeImageFactory(id, supplier, future)); }); return (CompletableFuture) future; } @Deprecated public static CompletableFuture registerImage(ResourceLocation id, ImageRendererFactory factory) { return registerOrGetImage(id, () -> factory); } private static void completeImageFactory(ResourceLocation id, Supplier> supplier, CompletableFuture future) { RenderSystem.assertOnRenderThread(); ImageRendererFactory.ImageSupplier completableImage = supplier.get().orElse(null); if (completableImage == null) { return; } // sanity check - this should never happen if (future.isDone()) { YACLConstants.LOGGER.error("Image '{}' was already completed", id); return; } ImageRenderer image; try { image = completableImage.completeImage(); } catch (Exception e) { YACLConstants.LOGGER.error("Failed to create image '{}'", id, e); return; } future.complete(image); } public static void closeAll() { SINGLE_THREAD_EXECUTOR.shutdownNow(); IMAGE_CACHE.values().removeIf(future -> { if (future.isDone()) { future.join().close(); } return true; }); } static Optional safelyPrepareFactory(ResourceLocation id, ImageRendererFactory factory) { try { return Optional.of(factory.prepareImage()); } catch (Exception e) { YACLConstants.LOGGER.error("Failed to prepare image '{}'", id, e); IMAGE_CACHE.remove(id); return Optional.empty(); } } public record PreloadedImageFactory(Predicate predicate, Function factory) { } private record CompletedSupplier(T get) implements Supplier { } }