Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 43 additions & 26 deletions src/main/java/alexiil/mods/load/MinecraftDisplayer.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import org.lwjgl.opengl.SharedDrawable;

import alexiil.mods.load.ProgressDisplayer.IDisplayer;
import alexiil.mods.load.imgbb.ImgbbCacheManager;
import alexiil.mods.load.imgur.ImgurCacheManager;
import alexiil.mods.load.json.Area;
import alexiil.mods.load.json.EPosition;
Expand Down Expand Up @@ -143,6 +144,7 @@ public class MinecraftDisplayer implements IDisplayer {
private float[] lbRGB = new float[] { 1, 1, 0 };
private float loadingBarsAlpha = 0.5F;
private boolean useImgur = false;
private boolean useImgbb = false;

private boolean saltBGhasBeenRendered = false;

Expand All @@ -157,7 +159,7 @@ public class MinecraftDisplayer implements IDisplayer {
private static String newBlendImage = "none";
private static int nonStaticElementsToGo;

private ImgurCacheManager imgurCacheManager = null;
private RemoteCacheManager<?> remoteCacheManager = null;

private ScheduledExecutorService backgroundExec = null;
private boolean scheduledTipExecSet = false;
Expand Down Expand Up @@ -652,6 +654,10 @@ public void open(Configuration cfg) {
String comment30 = "Set to true if you want to load images from an imgur gallery and use them as backgrounds.";
useImgur = cfg.getBoolean("useImgur", "imgur", useImgur, comment30);

// imgbb
String commentImgbb = "Set to true if you want to load images from an imgbb album and use them as backgrounds.";
useImgbb = cfg.getBoolean("useImgbb", "imgbb", useImgbb, commentImgbb);

// tips
String comment32 = "Set to true if you want to display random tips. Tips are stored in a separate file";
tipsEnabled = cfg.getBoolean("tipsEnabled", "tips", tipsEnabled, comment32);
Expand Down Expand Up @@ -735,20 +741,14 @@ public void run() {
}
}, changeFrequency, changeFrequency, TimeUnit.SECONDS);

if (useImgur) {
imgurCacheManager = new ImgurCacheManager();
imgurCacheManager.loadConfig(cfg);

List<String> imgurBackgrounds = new ArrayList<>();
imgurCacheManager.setupImgurGallery(res -> {
// Override the default background with the first image we get, otherwise the image will only
// be visible after the first blend occurs
if (imgurBackgrounds.isEmpty()) background = res.toString();

// Progressively add each image to the list of random backgrounds
imgurBackgrounds.add(res.toString());
randomBackgroundArray = imgurBackgrounds.toArray(new String[0]);
});
if (useImgur && useImgbb) {
BetterLoadingScreen.log.warn(
"Both useImgur and useImgbb are enabled. Only one remote image provider can be active at a time. Using imgbb.");
setupRemoteCacheManager(new ImgbbCacheManager(), cfg);
} else if (useImgbb) {
setupRemoteCacheManager(new ImgbbCacheManager(), cfg);
} else if (useImgur) {
setupRemoteCacheManager(new ImgurCacheManager(), cfg);
}
}
}
Expand Down Expand Up @@ -1412,13 +1412,15 @@ private void bindTexture(String resourceLocation) {
ResourceLocation res = new ResourceLocation(resourceLocation);

// We cannot go through the default texture loader, because it can't load from the file system
AbstractTexture texture = imgurCacheManager != null ? imgurCacheManager.getCachedTexture(res) : null;
if (texture != null) {
// Add the texture to TextureManager's cache to disable the loading logic in bindTexture
try {
textureManager.loadTexture(res, texture);
} catch (Exception e) {
BetterLoadingScreen.log.error("Failed to load imgur texture: " + res.getResourcePath(), e);
if (remoteCacheManager != null) {
AbstractTexture texture = remoteCacheManager.getCachedTexture(res);
if (texture != null) {
// Add the texture to TextureManager's cache to disable the loading logic in bindTexture
try {
textureManager.loadTexture(res, texture);
} catch (Exception e) {
BetterLoadingScreen.log.error("Failed to load cached texture: " + res.getResourcePath(), e);
}
}
}

Expand Down Expand Up @@ -1508,7 +1510,22 @@ public ImageRender[] getImageData() {
return images;
}

@Override
private void setupRemoteCacheManager(RemoteCacheManager<?> manager, Configuration cfg) {
manager.loadConfig(cfg);

List<String> remoteBackgrounds = new ArrayList<>();
manager.setup(res -> {
// Override the default background with the first image we get, otherwise the image will only
// be visible after the first blend occurs
if (remoteBackgrounds.isEmpty()) background = res.toString();

// Progressively add each image to the list of random backgrounds
remoteBackgrounds.add(res.toString());
randomBackgroundArray = remoteBackgrounds.toArray(new String[0]);
});
remoteCacheManager = manager;
}

public void close() {
if (splashRenderThread != null && splashRenderThread.isAlive()) {
BetterLoadingScreen.log.info("BLS Splash loading thread closing");
Expand All @@ -1531,9 +1548,9 @@ public void close() {
}
getOnlyList().remove(myPack);

if (imgurCacheManager != null) {
imgurCacheManager.cleanUp();
imgurCacheManager = null;
if (remoteCacheManager != null) {
remoteCacheManager.cleanUp();
remoteCacheManager = null;
}
}
}
204 changes: 204 additions & 0 deletions src/main/java/alexiil/mods/load/RemoteCacheManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package alexiil.mods.load;

import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.imageio.ImageIO;

import net.minecraft.client.renderer.texture.AbstractTexture;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.common.config.Configuration;

import alexiil.mods.load.imgur.LateInitDynamicTexture;

public abstract class RemoteCacheManager<C extends AutoCloseable> {

private static final boolean OFFLINE_MODE = Boolean.getBoolean("bls.offlineMode");

private final String cacheDir;
private final String providerName;
private final Map<String, AbstractTexture> textureCache = new ConcurrentHashMap<>();
private volatile boolean cancelSetup;

protected RemoteCacheManager(String cacheDir, String providerName) {
this.cacheDir = cacheDir;
this.providerName = providerName;
}

public abstract void loadConfig(Configuration config);

protected abstract C createClient() throws Exception;

protected abstract List<ImageEntry> fetchRemoteImages(C client) throws Exception;

protected abstract byte[] downloadImage(C client, ImageEntry entry) throws Exception;

public AbstractTexture getCachedTexture(ResourceLocation location) {
if (!location.getResourceDomain().equals(cacheDir)) return null;

return textureCache.get(location.getResourcePath());
}

public void cleanUp() {
textureCache.values().forEach(AbstractTexture::deleteGlTexture);
textureCache.clear();

cancelSetup = true;
}

public void setup(Consumer<ResourceLocation> textureLocationConsumer) {
Path cacheFolder = Paths.get(cacheDir);
if (Files.notExists(cacheFolder)) {
try {
Files.createDirectory(cacheFolder);
} catch (IOException e) {
BetterLoadingScreen.log.error("Error while creating " + providerName + " cache directory", e);
return;
}
}

List<String> cachedImageIDs = getCachedImageIDs();
if (cachedImageIDs == null) return;

// Load any image that is already cached to get something rendering quickly
loadAnyImageFromDisk(cachedImageIDs, textureLocationConsumer);

CompletableFuture.runAsync(() -> {
try (C client = OFFLINE_MODE ? null : createClient()) {
Consumer<ImageEntry> imageHandler = entry -> {
String imageID = entry.cacheId;

synchronized (cachedImageIDs) {
cachedImageIDs.remove(imageID);
}

if (cancelSetup) return;

if (textureCache.containsKey(imageID)) return;

Path imageFile = getCachedImagePath(imageID);

try {
if (Files.exists(imageFile)) {
readAndCacheImageFromStream(
imageID,
new BufferedInputStream(Files.newInputStream(imageFile), 1024 * 1024),
false);
} else {
if (OFFLINE_MODE) return;

readAndCacheImageFromStream(
imageID,
new ByteArrayInputStream(downloadImage(client, entry)),
true);
}
} catch (Exception e) {
BetterLoadingScreen.log.error("Error while loading " + providerName + " image", e);
return;
}

synchronized (textureLocationConsumer) {
textureLocationConsumer.accept(new ResourceLocation(cacheDir, imageID));
}
};

if (OFFLINE_MODE) {
cachedImageIDs.stream().parallel().map(id -> new ImageEntry(id, id)).forEach(imageHandler);
} else {
fetchRemoteImages(client).stream().parallel().forEach(imageHandler);
}
} catch (Exception e) {
BetterLoadingScreen.log.error("Error while fetching " + providerName + " images", e);
}
}).thenRunAsync(() -> {
if (OFFLINE_MODE) return;

// Delete cached images that are no longer in the album
try {
for (String id : cachedImageIDs) {
Files.deleteIfExists(getCachedImagePath(id));
}
} catch (IOException e) {
BetterLoadingScreen.log.error("Error while deleting unused cached " + providerName + " images", e);
}
});
}

private void loadAnyImageFromDisk(List<String> cachedImageIDs, Consumer<ResourceLocation> textureLocationConsumer) {
if (cachedImageIDs.isEmpty()) return;

String imageID = cachedImageIDs.get(ThreadLocalRandom.current().nextInt(cachedImageIDs.size()));
try {
readAndCacheImageFromDisk(imageID);
} catch (IOException e) {
BetterLoadingScreen.log.error("Error while loading first cached " + providerName + " image", e);
return;
}

synchronized (textureLocationConsumer) {
textureLocationConsumer.accept(new ResourceLocation(cacheDir, imageID));
}
}

private void readAndCacheImageFromStream(String imageID, InputStream imageStream, boolean saveToDisk)
throws IOException {
BufferedImage image = ImageIO.read(imageStream);
textureCache.put(imageID, new LateInitDynamicTexture(image, image.getWidth(), image.getHeight()));

if (saveToDisk && Files.notExists(getCachedImagePath(imageID))) writeImageToCache(imageID, image);
}

private void readAndCacheImageFromDisk(String imageID) throws IOException {
readAndCacheImageFromStream(
imageID,
new BufferedInputStream(Files.newInputStream(getCachedImagePath(imageID)), 1024 * 1024),
false);
}

private void writeImageToCache(String imageID, BufferedImage image) throws IOException {
ImageIO.write(
image,
"png",
new BufferedOutputStream(Files.newOutputStream(getCachedImagePath(imageID)), 1024 * 1024));
}

private Path getCachedImagePath(String imageID) {
return Paths.get(cacheDir).resolve(imageID + ".png");
}

private List<String> getCachedImageIDs() {
try (Stream<Path> cacheFolderStream = Files.list(Paths.get(cacheDir))) {
return cacheFolderStream.map(path -> path.getFileName().toString().replace(".png", ""))
.collect(Collectors.toList());
} catch (IOException e) {
BetterLoadingScreen.log.error("Error while iterating " + providerName + " cache folder", e);
return null;
}
}

public static class ImageEntry {

public final String cacheId;
public final String downloadRef;

public ImageEntry(String cacheId, String downloadRef) {
this.cacheId = cacheId;
this.downloadRef = downloadRef;
}
}
}
61 changes: 61 additions & 0 deletions src/main/java/alexiil/mods/load/imgbb/ImgbbCacheManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package alexiil.mods.load.imgbb;

import java.util.List;
import java.util.stream.Collectors;

import net.minecraftforge.common.config.Configuration;

import alexiil.mods.load.RemoteCacheManager;

public class ImgbbCacheManager extends RemoteCacheManager<ImgbbClient> {

private String albumId;
private int requestTimeout;

public ImgbbCacheManager() {
super("bls-imgbb-cache", "imgbb");
}

@Override
public void loadConfig(Configuration config) {
albumId = config.getString("imgbbAlbumId", "imgbb", "", "ID of the imgbb album. For example: X7wpP4");
requestTimeout = config.getInt(
"imgbbRequestTimeout",
"imgbb",
5000,
100,
Integer.MAX_VALUE,
"Request timeout (ms) for imgbb requests");
}

@Override
protected ImgbbClient createClient() {
return new ImgbbClient(requestTimeout);
}

@Override
protected List<ImageEntry> fetchRemoteImages(ImgbbClient client) throws Exception {
return client.fetchAlbumImageURLs(albumId).stream().map(url -> new ImageEntry(urlToImageID(url), url))
.collect(Collectors.toList());
}

@Override
protected byte[] downloadImage(ImgbbClient client, ImageEntry entry) {
return client.fetchImage(entry.downloadRef);
}

/**
* Extracts a stable image ID from an imgbb direct URL. Uses the URL path code as the ID. For example,
* {@code https://i.ibb.co/j9rBbcHy/02.png} becomes {@code j9rBbcHy_02}.
*/
static String urlToImageID(String url) {
// URL format: https://i.ibb.co/{code}/{filename}.{ext}
String path = url.replace("https://i.ibb.co/", "");
// path is now "{code}/{filename}.{ext}"
int lastDot = path.lastIndexOf('.');
if (lastDot != -1) {
path = path.substring(0, lastDot);
}
return path.replace('/', '_');
}
}
Loading