Resclone/src/main/java/io/gitlab/jfronny/resclone/Resclone.java

196 lines
7.7 KiB
Java

package io.gitlab.jfronny.resclone;
import io.gitlab.jfronny.commons.log.Logger;
import io.gitlab.jfronny.commons.serialize.gson.api.v1.GsonHolders;
import io.gitlab.jfronny.resclone.api.*;
import io.gitlab.jfronny.resclone.data.PackMetaLoaded;
import io.gitlab.jfronny.resclone.data.PackMetaUnloaded;
import io.gitlab.jfronny.resclone.processors.RemoveEmptyProcessor;
import io.gitlab.jfronny.resclone.processors.RootPathProcessor;
import io.gitlab.jfronny.resclone.util.PackUrlCache;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.loader.api.FabricLoader;
import java.io.IOException;
import java.net.URI;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Resclone implements ModInitializer, RescloneApi {
public static final Set<PackMetaUnloaded> CONF = new LinkedHashSet<>();
public static final Map<String, PackFetcher> FETCHER_INSTANCES = new LinkedHashMap<>();
public static final Set<PackProcessor> PROCESSORS = new LinkedHashSet<>();
public static final Set<PackMetaLoaded> DOWNLOADED_PACKS = new LinkedHashSet<>();
public static final Set<PackMetaLoaded> NEW_PACKS = new LinkedHashSet<>(); // Client-only!
public static final String MOD_ID = "resclone";
public static final Logger LOGGER = Logger.forName(MOD_ID);
public static final String USER_AGENT = "jfmods/" + MOD_ID + "/" + FabricLoader.getInstance()
.getModContainer(MOD_ID).orElseThrow()
.getMetadata()
.getVersion()
.getFriendlyString();
public static PackUrlCache urlCache;
public static int packCount = 0;
private boolean pruneUnused = false;
@Override
public void onInitialize() {
LOGGER.info("Initialising Resclone.");
GsonHolders.registerSerializer();
urlCache = new PackUrlCache(getConfigPath().resolve("urlCache.properties"));
CONF.clear();
FETCHER_INSTANCES.clear();
PROCESSORS.clear();
DOWNLOADED_PACKS.clear();
addProcessor(new RootPathProcessor()); //This should be run before any other processor to make sure the path is valid
for (RescloneEntry entry : FabricLoader.getInstance().getEntrypoints(MOD_ID, RescloneEntry.class)) {
try {
entry.init(this);
} catch (Exception e) {
Resclone.LOGGER.error("Could not initialize resclone pack supplier", e);
}
}
addProcessor(new RemoveEmptyProcessor());
reload();
LOGGER.info("Installed {} resource pack{}.", packCount, packCount == 1 ? "" : "s");
}
@Override
public void addFetcher(PackFetcher fetcher) {
FETCHER_INSTANCES.put(fetcher.getSourceTypeName(), fetcher);
}
@Override
public void addProcessor(PackProcessor processor) {
PROCESSORS.add(processor);
}
@Override
public void addPack(String fetcher, String pack, String name) {
addPack(fetcher, pack, name, false);
}
@Override
public void addPack(String fetcher, String pack, String name, boolean forceRedownload) {
addPack(fetcher, pack, name, forceRedownload, false);
}
@Override
public void addPack(String fetcher, String pack, String name, boolean forceRedownload, boolean forceEnable) {
CONF.add(new PackMetaUnloaded(fetcher, pack, name, forceRedownload, forceEnable));
}
@Override
public void reload() {
Set<PackMetaLoaded> metas = new LinkedHashSet<>();
try {
if (CONF.isEmpty()) {
LOGGER.info("No resclone pack was specified, add one");
}
else {
ExecutorService pool = Executors.newFixedThreadPool(CONF.size());
for (PackMetaUnloaded s : CONF) {
pool.submit(generateTask(s, metas));
}
pool.shutdown();
if (!pool.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS)) {
LOGGER.error("Download timed out. This shouldn't be possible");
}
}
} catch (InterruptedException e) {
LOGGER.error("Could not execute pack download task", e);
}
urlCache.save();
DOWNLOADED_PACKS.clear();
DOWNLOADED_PACKS.addAll(metas);
if (pruneUnused) pruneCache();
}
private Runnable generateTask(PackMetaUnloaded meta, Set<PackMetaLoaded> metas) {
return () -> {
try {
if (!FETCHER_INSTANCES.containsKey(meta.fetcher))
throw new Exception("Invalid fetcher: " + meta.fetcher);
Path cacheDir = getConfigPath().resolve("cache");
PackMetaLoaded p;
try {
boolean isNew = !urlCache.containsKey(meta.source);
//Download
PackFetcher.Result fr = FETCHER_INSTANCES.get(meta.fetcher).get(meta.source, cacheDir, meta.forceDownload);
p = new PackMetaLoaded(fr.downloadPath(), meta.name, meta.forceEnable);
metas.add(p);
if (isNew && FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT) NEW_PACKS.add(p);
if (fr.freshDownload()) {
//Process
Map<String, String> props = new HashMap<>();
props.put("create", "false");
URI zipfile = URI.create("jar:" + p.zipPath().toUri());
try (FileSystem zipfs = FileSystems.newFileSystem(zipfile, props)) {
for (PackProcessor processor : PROCESSORS) {
processor.process(zipfs);
}
} catch (Throwable e) {
LOGGER.error("Could not run pack processors on " + p.zipPath(), e);
}
}
} catch (Throwable e) {
throw new Exception("Failed to download pack", e);
}
} catch (Throwable e) {
LOGGER.error("Encountered issue while preparing " + meta.name, e);
}
};
}
private void pruneCache() {
Set<Path> loadedPacks = DOWNLOADED_PACKS.stream().map(PackMetaLoaded::zipPath).collect(Collectors.toUnmodifiableSet());
Set<Path> toDelete = new HashSet<>();
try (Stream<Path> cacheEntries = Files.list(getConfigPath().resolve("cache"))) {
cacheEntries
.filter(s -> !Files.isRegularFile(s) || !loadedPacks.contains(s))
.forEach(toDelete::add);
} catch (IOException e) {
LOGGER.error("Could find cache entries to prune", e);
}
if (!toDelete.isEmpty()) {
LOGGER.info("Pruning " + toDelete.size() + " unused cache entries");
for (Path path : toDelete) {
try {
Files.delete(path);
} catch (IOException e) {
LOGGER.error("Could not delete unused cache entry: " + path, e);
}
}
}
}
@Override
public Path getConfigPath() {
Path configPath = FabricLoader.getInstance().getConfigDir().resolve("resclone");
if (!Files.isDirectory(configPath.resolve("cache"))) {
try {
Files.createDirectories(configPath.resolve("cache"));
} catch (IOException e) {
LOGGER.error("Could not create cache directory", e);
}
}
return configPath;
}
@Override
public void setPruneUnused(boolean pruneUnused) {
this.pruneUnused = pruneUnused;
}
}