Formatting cleanup + Logger

This commit is contained in:
seasnail8169 2021-04-03 01:40:38 +01:00
parent 5af47bf460
commit 2123042c9a
26 changed files with 188 additions and 145 deletions

View File

@ -8,8 +8,9 @@ import java.nio.file.Path;
import java.util.Properties;
public class PackUrlCache {
private final Path file;
Properties properties = new Properties();
private Path file;
public PackUrlCache(Path file) {
this.file = file;
@ -41,4 +42,5 @@ public class PackUrlCache {
public void set(String key, String value) {
properties.setProperty(key, value);
}
}
}

View File

@ -1,17 +1,19 @@
package io.gitlab.jfronny.resclone;
import com.google.gson.Gson;
import io.gitlab.jfronny.resclone.data.PackMetaLoaded;
import io.gitlab.jfronny.resclone.api.RescloneApi;
import io.gitlab.jfronny.resclone.api.RescloneEntry;
import io.gitlab.jfronny.resclone.data.PackMetaLoaded;
import io.gitlab.jfronny.resclone.data.PackMetaUnloaded;
import io.gitlab.jfronny.resclone.data.RescloneException;
import io.gitlab.jfronny.resclone.fetchers.PackFetcher;
import io.gitlab.jfronny.resclone.processors.PackProcessor;
import io.gitlab.jfronny.resclone.processors.RemoveEmptyProcessor;
import io.gitlab.jfronny.resclone.processors.RootPathProcessor;
import io.gitlab.jfronny.resclone.util.Result;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.loader.api.FabricLoader;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.net.URI;
@ -22,33 +24,40 @@ import java.nio.file.Path;
import java.util.*;
public class Resclone implements ModInitializer, RescloneApi {
public static final Set<PackMetaUnloaded> conf = new LinkedHashSet<>();
public static final Map<String, PackFetcher> fetcherInstances = new LinkedHashMap<>();
public static final Set<PackProcessor> processors = new LinkedHashSet<>();
public static final Set<PackMetaLoaded> downloadedPacks = new LinkedHashSet<>();
public static final Gson gson = new Gson();
public static final String MOD_ID = "resclone";
public static final Logger LOGGER = LogManager.getLogger(MOD_ID);
public static PackUrlCache urlCache;
@Override
public void onInitialize() {
System.out.println("[resclone] Beginning init, will download packs");
LOGGER.info("Initialising Resclone.");
urlCache = new PackUrlCache(getConfigPath().resolve("urlCache.properties"));
conf.clear();
fetcherInstances.clear();
processors.clear();
downloadedPacks.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 (RescloneException e) {
} catch (Exception e) {
e.printStackTrace();
}
}
addProcessor(new RemoveEmptyProcessor());
reload();
System.out.println("[resclone] Completed");
LOGGER.info("Completed");
}
@Override
@ -79,19 +88,19 @@ public class Resclone implements ModInitializer, RescloneApi {
for (PackMetaUnloaded s : conf) {
try {
if (!fetcherInstances.containsKey(s.fetcher))
throw new RescloneException("Invalid fetcher: " + s.fetcher);
throw new Exception("Invalid fetcher: " + s.fetcher);
Path cacheDir = getConfigPath().resolve("cache");
PackMetaLoaded p;
try {
//Download
PackFetcher.Result fr = fetcherInstances.get(s.fetcher).get(s.source, cacheDir, s.forceDownload);
Result fr = fetcherInstances.get(s.fetcher).get(s.source, cacheDir, s.forceDownload);
p = new PackMetaLoaded(fr.downloadPath, s.name);
metas.add(p);
if (fr.freshDownload) {
//Process
Map<String, String> props = new HashMap<>();
props.put("create", "false");
URI zipfile = URI.create("jar:" + p.zipPath.toUri().toString());
URI zipfile = URI.create("jar:" + p.zipPath.toUri());
try (FileSystem zipfs = FileSystems.newFileSystem(zipfile, props)) {
for (PackProcessor processor : processors) {
processor.process(zipfs);
@ -101,9 +110,8 @@ public class Resclone implements ModInitializer, RescloneApi {
e.printStackTrace();
}
}
}
catch (Throwable e) {
throw new RescloneException("Failed to download pack", e);
} catch (Throwable e) {
throw new Exception("Failed to download pack", e);
}
} catch (Throwable e) {
System.err.println("Encountered issue while preparing " + s.name);
@ -128,4 +136,5 @@ public class Resclone implements ModInitializer, RescloneApi {
}
return configPath;
}
}
}

View File

@ -12,6 +12,7 @@ import net.fabricmc.api.EnvType;
import net.fabricmc.loader.api.FabricLoader;
public class RescloneEntryDefault implements RescloneEntry {
@Override
public void init(RescloneApi api) {
api.addFetcher(new BasicFileFetcher());
@ -22,4 +23,5 @@ public class RescloneEntryDefault implements RescloneEntry {
api.addProcessor(new PruneVanillaProcessor());
ConfigLoader.load(api);
}
}
}

View File

@ -8,7 +8,9 @@ import net.minecraft.resource.ZipResourcePack;
import java.io.File;
public class RescloneResourcePack extends ZipResourcePack implements ModResourcePack {
private final String name;
public RescloneResourcePack(File file, String name) {
super(file);
this.name = name;
@ -23,4 +25,5 @@ public class RescloneResourcePack extends ZipResourcePack implements ModResource
public ModMetadata getFabricModMetadata() {
return FabricLoader.getInstance().getModContainer(Resclone.MOD_ID).get().getMetadata();
}
}
}

View File

@ -6,10 +6,17 @@ import io.gitlab.jfronny.resclone.processors.PackProcessor;
import java.nio.file.Path;
public interface RescloneApi {
void addFetcher(PackFetcher fetcher);
void addProcessor(PackProcessor processor);
void addPack(String fetcher, String pack, String name);
void addPack(String fetcher, String pack, String name, boolean forceRedownload);
void reload();
Path getConfigPath();
}
}

View File

@ -1,7 +1,5 @@
package io.gitlab.jfronny.resclone.api;
import io.gitlab.jfronny.resclone.data.RescloneException;
public interface RescloneEntry {
void init(RescloneApi api) throws RescloneException;
}
void init(RescloneApi api) throws Exception;
}

View File

@ -12,6 +12,7 @@ import java.util.HashSet;
import java.util.Set;
public class ConfigLoader {
public static void load(RescloneApi api) {
Path configPath = api.getConfigPath().resolve("config.json");
if (!Files.exists(configPath)) {
@ -42,4 +43,5 @@ public class ConfigLoader {
e.printStackTrace();
}
}
}
}

View File

@ -3,7 +3,9 @@ package io.gitlab.jfronny.resclone.data;
import java.util.Set;
public class CfAddon {
public String downloadUrl;
public String fileDate;
public Set<String> gameVersion;
}
}

View File

@ -3,6 +3,7 @@ package io.gitlab.jfronny.resclone.data;
import java.nio.file.Path;
public class PackMetaLoaded {
public Path zipPath;
public String name;
@ -10,4 +11,5 @@ public class PackMetaLoaded {
this.zipPath = zipPath;
this.name = name;
}
}
}

View File

@ -1,6 +1,7 @@
package io.gitlab.jfronny.resclone.data;
public class PackMetaUnloaded {
public final String fetcher;
public final String source;
public final String name;
@ -12,4 +13,5 @@ public class PackMetaUnloaded {
this.name = name;
this.forceDownload = forceDownload;
}
}
}

View File

@ -1,11 +0,0 @@
package io.gitlab.jfronny.resclone.data;
public class RescloneException extends Exception {
public RescloneException(String message) {
super(message);
}
public RescloneException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -1,6 +1,7 @@
package io.gitlab.jfronny.resclone.fetchers;
public class BasicFileFetcher extends PackFetcher {
@Override
public String getSourceTypeName() {
return "file";
@ -10,4 +11,5 @@ public class BasicFileFetcher extends PackFetcher {
public String getDownloadUrl(String baseUrl) {
return baseUrl;
}
}
}

View File

@ -1,20 +1,21 @@
package io.gitlab.jfronny.resclone.fetchers;
import io.gitlab.jfronny.resclone.data.CfAddon;
import io.gitlab.jfronny.resclone.data.RescloneException;
import io.gitlab.jfronny.resclone.util.UrlUtils;
import net.minecraft.MinecraftVersion;
import java.text.SimpleDateFormat;
import java.util.Date;
public class CurseforgeFetcher extends PackFetcher {
@Override
public String getSourceTypeName() {
return "curseforge";
}
@Override
public String getDownloadUrl(String baseUrl) throws RescloneException {
public String getDownloadUrl(String baseUrl) throws Exception {
try {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss.SSS'Z'");
@ -24,7 +25,7 @@ public class CurseforgeFetcher extends PackFetcher {
Date latestDate = null;
boolean foundMatchingVersion = false;
for (CfAddon addon : readJsonFromURLSet("https://addons-ecs.forgesvc.net/api/v2/addon/" + baseUrl + "/files", CfAddon.class)) {
for (CfAddon addon : UrlUtils.readJsonFromURLSet("https://addons-ecs.forgesvc.net/api/v2/addon/" + baseUrl + "/files", CfAddon.class)) {
Date d = df.parse(addon.fileDate);
if (foundMatchingVersion && !addon.gameVersion.contains(version))
continue;
@ -42,7 +43,8 @@ public class CurseforgeFetcher extends PackFetcher {
System.err.println("Could not find pack for matching version, using latest");
return latest.downloadUrl;
} catch (Throwable e) {
throw new RescloneException("Could not get CF download for " + baseUrl, e);
throw new Exception("Could not get CF download for " + baseUrl, e);
}
}
}
}

View File

@ -2,20 +2,22 @@ package io.gitlab.jfronny.resclone.fetchers;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import io.gitlab.jfronny.resclone.data.RescloneException;
import io.gitlab.jfronny.resclone.Resclone;
import io.gitlab.jfronny.resclone.util.UrlUtils;
public class GithubReleaseFetcher extends PackFetcher {
@Override
public String getSourceTypeName() {
return "github-release";
}
@Override
String getDownloadUrl(String baseUrl) throws RescloneException {
String getDownloadUrl(String baseUrl) throws Exception {
String[] parts = baseUrl.split("/");
if (parts.length == 2) {
try {
JsonObject latestRelease = readJsonFromURL("https://api.github.com/repos/" + parts[0] + "/" + parts[1] + "/releases/latest", JsonObject.class);
JsonObject latestRelease = UrlUtils.readJsonFromURL("https://api.github.com/repos/" + parts[0] + "/" + parts[1] + "/releases/latest", JsonObject.class);
String res = null;
for (JsonElement element : latestRelease.get("assets").getAsJsonArray()) {
JsonObject o = element.getAsJsonObject();
@ -26,16 +28,16 @@ public class GithubReleaseFetcher extends PackFetcher {
}
}
if (res == null) {
System.out.println("Could not find release asset for " + baseUrl + ", using zipball");
Resclone.LOGGER.info("Could not find release asset for " + baseUrl + ", using zipball");
return latestRelease.get("zipball_url").getAsString();
}
return res;
} catch (Throwable e) {
throw new RescloneException("Failed to get github release asset", e);
throw new Exception("Failed to get github release asset", e);
}
}
else {
throw new RescloneException("Format for github-release is USER/REPO");
} else {
throw new Exception("Format for github-release is USER/REPO");
}
}
}
}

View File

@ -1,72 +1,37 @@
package io.gitlab.jfronny.resclone.fetchers;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import io.gitlab.jfronny.resclone.Resclone;
import io.gitlab.jfronny.resclone.data.RescloneException;
import io.gitlab.jfronny.resclone.util.Result;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Scanner;
import java.util.Set;
public abstract class PackFetcher {
abstract public String getSourceTypeName(); // The name for users to specify in the config
abstract String getDownloadUrl(String baseUrl) throws RescloneException; // Return the actual download URL for the file based on the provided string
protected final Gson gson = Resclone.gson;
protected boolean urlValid(String url) {
try {
HttpURLConnection connection = (HttpURLConnection)new URL(url).toURI().toURL().openConnection();
connection.setRequestMethod("GET");
connection.connect();
return connection.getResponseCode() == 200;
}
catch (Throwable e) {
return false;
}
}
protected String readStringFromURL(String requestURL) throws IOException {
try (Scanner scanner = new Scanner(new URL(requestURL).openStream(), StandardCharsets.UTF_8.toString()))
{
scanner.useDelimiter("\\A");
return scanner.hasNext() ? scanner.next() : "";
}
}
abstract String getDownloadUrl(String baseUrl) throws Exception; // Return the actual download URL for the file based on the provided string
public <T> T readJsonFromURL(String requestUrl, Class<T> classOfT) throws IOException {
return gson.fromJson(readStringFromURL(requestUrl), classOfT);
}
public <T> Set<T> readJsonFromURLSet(String requestUrl, Class<T> classOfT) throws IOException {
return gson.fromJson(readStringFromURL(requestUrl), TypeToken.getParameterized(Set.class, classOfT).getType());
}
public Result get(String baseUrl, Path targetDir, boolean forceDownload) throws RescloneException {
public Result get(String baseUrl, Path targetDir, boolean forceDownload) throws Exception {
String url;
try {
url = getDownloadUrl(baseUrl);
Resclone.urlCache.set(baseUrl, url);
}
catch (RescloneException e) {
} catch (Exception e) {
if (Resclone.urlCache.containsKey(baseUrl)) {
e.printStackTrace();
url = Resclone.urlCache.get(baseUrl);
}
else {
} else {
throw e;
}
}
Path p = targetDir.resolve(Integer.toHexString(url.hashCode()));
if (!forceDownload && Files.exists(p))
return new Result(p, false);
System.out.println("Downloading pack " + url);
Resclone.LOGGER.info("Downloading pack " + url);
try (InputStream is = new URL(url).openStream()) {
FileOutputStream os = new FileOutputStream(p.toFile());
byte[] dataBuffer = new byte[1024];
@ -74,21 +39,11 @@ public abstract class PackFetcher {
while ((bytesRead = is.read(dataBuffer, 0, 1024)) != -1) {
os.write(dataBuffer, 0, bytesRead);
}
System.out.println("Completed download");
}
catch (Throwable e) {
throw new RescloneException("Could not download pack", e);
Resclone.LOGGER.info("Completed download");
} catch (Throwable e) {
throw new Exception("Could not download pack", e);
}
return new Result(p, true);
}
public class Result {
public final Path downloadPath;
public final boolean freshDownload;
public Result(Path downloadPath, boolean freshDownload) {
this.downloadPath = downloadPath;
this.freshDownload = freshDownload;
}
}
}
}

View File

@ -5,29 +5,27 @@ import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
public class MoveDirVisitor extends SimpleFileVisitor<Path> {
private final Path fromPath;
private final Path toPath;
private final CopyOption copyOption;
public MoveDirVisitor(Path fromPath, Path toPath, CopyOption copyOption)
{
public MoveDirVisitor(Path fromPath, Path toPath, CopyOption copyOption) {
this.fromPath = fromPath;
this.toPath = toPath;
this.copyOption = copyOption;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException
{
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
Path targetPath = toPath.resolve(fromPath.relativize(dir));
if(!Files.exists(targetPath))
if (!Files.exists(targetPath))
Files.createDirectory(targetPath);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException
{
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.copy(file, toPath.resolve(fromPath.relativize(file)), copyOption);
Files.delete(file);
return FileVisitResult.CONTINUE;
@ -38,4 +36,5 @@ public class MoveDirVisitor extends SimpleFileVisitor<Path> {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
}
}

View File

@ -9,7 +9,9 @@ import java.nio.file.attribute.BasicFileAttributes;
import java.util.function.Predicate;
public class PathPruneVisitor extends SimpleFileVisitor<Path> {
Predicate<Path> removalSelector;
public PathPruneVisitor(Predicate<Path> removalSelector) {
this.removalSelector = removalSelector;
}
@ -25,4 +27,5 @@ public class PathPruneVisitor extends SimpleFileVisitor<Path> {
if (removalSelector.test(dir)) Files.walkFileTree(dir, new RemoveDirVisitor());
return super.postVisitDirectory(dir, exc);
}
}
}

View File

@ -1,10 +1,14 @@
package io.gitlab.jfronny.resclone.io;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
public class RemoveDirVisitor extends SimpleFileVisitor<Path> {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
@ -16,4 +20,5 @@ public class RemoveDirVisitor extends SimpleFileVisitor<Path> {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
}
}

View File

@ -3,7 +3,9 @@ package io.gitlab.jfronny.resclone.mixin;
import io.gitlab.jfronny.resclone.Resclone;
import io.gitlab.jfronny.resclone.RescloneResourcePack;
import io.gitlab.jfronny.resclone.data.PackMetaLoaded;
import net.minecraft.resource.*;
import net.minecraft.resource.FileResourcePackProvider;
import net.minecraft.resource.ResourcePackProfile;
import net.minecraft.resource.ResourcePackSource;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
@ -15,7 +17,10 @@ import java.util.function.Consumer;
@Mixin(FileResourcePackProvider.class)
public class FileResourcePackProviderMixin {
@Shadow @Final private ResourcePackSource field_25345;
@Shadow
@Final
private ResourcePackSource field_25345;
@Inject(at = @At("TAIL"), method = "register(Ljava/util/function/Consumer;Lnet/minecraft/resource/ResourcePackProfile$Factory;)V")
public void registerExtra(Consumer<ResourcePackProfile> consumer, ResourcePackProfile.Factory factory, CallbackInfo info) {
@ -33,4 +38,5 @@ public class FileResourcePackProviderMixin {
}
}
}
}
}

View File

@ -1,9 +1,7 @@
package io.gitlab.jfronny.resclone.processors;
import io.gitlab.jfronny.resclone.data.RescloneException;
import java.nio.file.FileSystem;
public abstract class PackProcessor {
public abstract void process(FileSystem p) throws RescloneException;
}
public abstract void process(FileSystem p) throws Exception;
}

View File

@ -1,13 +1,7 @@
package io.gitlab.jfronny.resclone.processors;
import io.gitlab.jfronny.resclone.data.RescloneException;
import io.gitlab.jfronny.resclone.io.PathPruneVisitor;
import net.minecraft.client.MinecraftClient;
import net.minecraft.resource.ResourcePack;
import net.minecraft.resource.ResourcePackManager;
import net.minecraft.resource.ResourcePackProfile;
import net.minecraft.resource.ResourceType;
import net.minecraft.util.Identifier;
import org.apache.commons.io.IOUtils;
import java.io.IOException;
@ -17,8 +11,9 @@ import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
public class PruneVanillaProcessor extends PackProcessor {
@Override
public void process(FileSystem p) throws RescloneException {
public void process(FileSystem p) throws Exception {
ClassLoader cl = MinecraftClient.class.getClassLoader();
try {
if (Files.isDirectory(p.getPath("/assets/minecraft"))) {
@ -32,15 +27,15 @@ public class PruneVanillaProcessor extends PackProcessor {
return IOUtils.contentEquals(vn, pk);
}
}
}
catch (Throwable e) {
} catch (Throwable e) {
e.printStackTrace();
}
return false;
}));
}
} catch (IOException e) {
throw new RescloneException("Could not prune vanilla files", e);
throw new Exception("Could not prune vanilla files", e);
}
}
}
}

View File

@ -1,6 +1,5 @@
package io.gitlab.jfronny.resclone.processors;
import io.gitlab.jfronny.resclone.data.RescloneException;
import io.gitlab.jfronny.resclone.io.PathPruneVisitor;
import java.io.IOException;
@ -10,8 +9,9 @@ import java.nio.file.Files;
import java.nio.file.Path;
public class RemoveEmptyProcessor extends PackProcessor {
@Override
public void process(FileSystem p) throws RescloneException {
public void process(FileSystem p) throws Exception {
if (Files.exists(p.getPath("/assets"))) {
try {
Files.walkFileTree(p.getPath("/assets"), new PathPruneVisitor(s -> {
@ -25,8 +25,9 @@ public class RemoveEmptyProcessor extends PackProcessor {
return false;
}));
} catch (Throwable e) {
throw new RescloneException("Failed to prune empty directories", e);
throw new Exception("Failed to prune empty directories", e);
}
}
}
}
}

View File

@ -1,14 +1,14 @@
package io.gitlab.jfronny.resclone.processors;
import io.gitlab.jfronny.resclone.io.MoveDirVisitor;
import io.gitlab.jfronny.resclone.data.RescloneException;
import java.io.IOException;
import java.nio.file.*;
public class RootPathProcessor extends PackProcessor {
@Override
public void process(FileSystem p) throws RescloneException {
public void process(FileSystem p) throws Exception {
if (!Files.exists(p.getPath("/pack.mcmeta"))) {
try {
Path root = p.getPath("/");
@ -20,8 +20,9 @@ public class RootPathProcessor extends PackProcessor {
}
}
} catch (IOException e) {
throw new RescloneException("Could not fix root path", e);
throw new Exception("Could not fix root path", e);
}
}
}
}
}

View File

@ -0,0 +1,15 @@
package io.gitlab.jfronny.resclone.util;
import java.nio.file.Path;
public class Result {
public final Path downloadPath;
public final boolean freshDownload;
public Result(Path downloadPath, boolean freshDownload) {
this.downloadPath = downloadPath;
this.freshDownload = freshDownload;
}
}

View File

@ -0,0 +1,41 @@
package io.gitlab.jfronny.resclone.util;
import com.google.gson.reflect.TypeToken;
import io.gitlab.jfronny.resclone.Resclone;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
import java.util.Set;
public class UrlUtils {
public static boolean urlValid(String url) {
try {
HttpURLConnection connection = (HttpURLConnection) new URL(url).toURI().toURL().openConnection();
connection.setRequestMethod("GET");
connection.connect();
return connection.getResponseCode() == 200;
} catch (Throwable e) {
return false;
}
}
public static String readStringFromURL(String requestURL) throws IOException {
try (Scanner scanner = new Scanner(new URL(requestURL).openStream(), StandardCharsets.UTF_8.toString())) {
scanner.useDelimiter("\\A");
return scanner.hasNext() ? scanner.next() : "";
}
}
public static <T> T readJsonFromURL(String requestUrl, Class<T> classOfT) throws IOException {
return Resclone.gson.fromJson(readStringFromURL(requestUrl), classOfT);
}
public static <T> Set<T> readJsonFromURLSet(String requestUrl, Class<T> classOfT) throws IOException {
return Resclone.gson.fromJson(readStringFromURL(requestUrl), TypeToken.getParameterized(Set.class, classOfT).getType());
}
}

View File

@ -3,7 +3,7 @@
"id": "resclone",
"version": "${version}",
"name": "Resclone",
"description": "Downloads and updates resourcepacks",
"description": "Downloads and updates resourcepacks.",
"authors": [
"JFronny"
],