Compare commits

...

15 Commits

Author SHA1 Message Date
Johannes Frohnmeyer 981d10d426
chore: update to 1.20.5
ci/woodpecker/push/jfmod Pipeline was successful Details
ci/woodpecker/tag/jfmod Pipeline was successful Details
2024-04-25 20:44:31 +02:00
Johannes Frohnmeyer 6df388aad7
feat: rewrite config/network IO for 1.20.5 2024-04-25 20:42:31 +02:00
Johannes Frohnmeyer 88f071d919
feat: add option to disable additional processors
ci/woodpecker/push/jfmod Pipeline was successful Details
ci/woodpecker/tag/jfmod Pipeline was successful Details
2024-04-01 15:12:02 +02:00
Johannes Frohnmeyer e8d0b38121
fix: add translation for new options
ci/woodpecker/push/jfmod Pipeline was successful Details
2024-03-31 13:09:14 +02:00
Johannes Frohnmeyer 12dd3f3301
chore: add some more optional logging
ci/woodpecker/push/jfmod Pipeline was successful Details
2024-03-31 12:59:22 +02:00
Johannes Frohnmeyer 6495212095
chore: update to 1.20.4
ci/woodpecker/push/jfmod Pipeline was successful Details
2023-12-07 20:01:56 +01:00
Johannes Frohnmeyer c4d424b792
chore: update to 1.20.2
ci/woodpecker/push/jfmod Pipeline was successful Details
2023-09-22 20:34:51 +02:00
Johannes Frohnmeyer d828de9464
Bump to 1.20
ci/woodpecker/push/jfmod Pipeline was successful Details
ci/woodpecker/tag/jfmod Pipeline was successful Details
2023-06-09 17:02:42 +02:00
Johannes Frohnmeyer 4f6e31f584
Configure CF publish
ci/woodpecker/push/jfmod Pipeline was successful Details
2023-03-18 11:36:10 +01:00
Johannes Frohnmeyer e9dd5644e8
Port to 1.19.4 and libjf-config. Also remove the unused API as mods can still use mixin
ci/woodpecker/push/jfmod Pipeline is pending Details
ci/woodpecker/tag/jfmod Pipeline was successful Details
2023-03-14 20:58:21 +01:00
Johannes Frohnmeyer 79ff7b6279
Optimize imports
ci/woodpecker/push/jfmod Pipeline was successful Details
2023-02-26 14:00:38 +01:00
Johannes Frohnmeyer 28957f946e
1.19.4-pre1 port + modrinth fetcher 2023-02-26 14:00:07 +01:00
Johannes Frohnmeyer 618c4c5f42
Add devutil
ci/woodpecker/push/jfmod Pipeline was successful Details
2022-12-29 14:34:16 +01:00
Johannes Frohnmeyer a8fa45837b
Update to 1.19.3
ci/woodpecker/push/jfmod Pipeline was successful Details
ci/woodpecker/tag/jfmod Pipeline was successful Details
2022-12-07 22:59:57 +01:00
Johannes Frohnmeyer 01221ca98a
Update to new infrastructure
ci/woodpecker/manual/jfmod Pipeline was successful Details
2022-12-02 17:53:44 +01:00
40 changed files with 669 additions and 399 deletions

View File

@ -1,2 +0,0 @@
include:
- remote: 'https://jfmods.gitlab.io/scripts/jfmod.yml'

1
.woodpecker.yml Normal file
View File

@ -0,0 +1 @@
#link https://pages.frohnmeyer-wds.de/scripts/jfmod.yml

100
README.md
View File

@ -3,43 +3,65 @@ Packs are updated on startup whenever possible, meaning that very large packs wi
Example config:
```
[
{
"fetcher": "file",
"source": "https://github.com/FaithfulTeam/Faithful/raw/releases/1.16.zip",
"name": "Faithful"
},
{
"fetcher": "github",
"source": "seasnail8169/SnailPack",
"name": "SnailPack"
},
{
"fetcher": "github",
"source": "spiralhalo/LumiLights/release",
"name": "LumiLights",
"forceDownload": true
},
{
"fetcher": "github",
"source": "spiralhalo/LumiPBRExt/tag/v0.7",
"name": "LumiPBRExt"
},
{
"fetcher": "github",
"source": "FaithfulTeam/Faithful/branch/1.16",
"name": "Faithful"
},
{
"fetcher": "file",
"source": "https://media.forgecdn.net/files/3031/178/BattyCoordinates_005.zip",
"name": "Battys Coordinates"
},
{
"fetcher": "curseforge",
"source": "325017",
"name": "Vanilla Additions",
"forceEnable": true
}
]
{
// The packs to be loaded by resclone
"packs": [
{
"fetcher": "file",
"source": "https://github.com/FaithfulTeam/Faithful/raw/releases/1.16.zip",
"name": "Faithful",
"forceDownload": false,
"forceEnable": false
},
{
"fetcher": "github",
"source": "seasnail8169/SnailPack",
"name": "SnailPack",
"forceDownload": false,
"forceEnable": false
},
{
"fetcher": "github",
"source": "spiralhalo/LumiLights/release",
"name": "LumiLights",
"forceDownload": true,
"forceEnable": false
},
{
"fetcher": "github",
"source": "spiralhalo/LumiPBRExt/tag/v0.7",
"name": "LumiPBRExt",
"forceDownload": false,
"forceEnable": false
},
{
"fetcher": "github",
"source": "FaithfulTeam/Faithful/branch/1.16",
"name": "Faithful",
"forceDownload": false,
"forceEnable": false
},
{
"fetcher": "file",
"source": "https://media.forgecdn.net/files/3031/178/BattyCoordinates_005.zip",
"name": "Battys Coordinates",
"forceDownload": false,
"forceEnable": false
},
{
"fetcher": "curseforge",
"source": "325017",
"name": "Vanilla Additions",
"forceDownload": false,
"forceEnable": true
},
{
"fetcher": "modrinth",
"source": "new-in-town",
"name": "New in Town"
}
],
// Whether to prune unused packs from the cache
"pruneUnused": true
}
```

View File

@ -1,9 +0,0 @@
apply from: "https://jfmods.gitlab.io/scripts/gradle/v2.gradle"
dependencies {
include modImplementation("io.gitlab.jfronny.libjf:libjf-base:${project.libjf_version}") // for JfCommons
include modImplementation(fabricApi.module("fabric-resource-loader-v0", "$project.fabric_version"))
// Dev env
modLocalRuntime("com.terraformersmc:modmenu:4.0.6")
}

47
build.gradle.kts Normal file
View File

@ -0,0 +1,47 @@
plugins {
id("jfmod") version "1.6-SNAPSHOT"
}
loom {
accessWidenerPath.set(file("src/main/resources/resclone.accesswidener"))
}
allprojects { group = "io.gitlab.jfronny" }
base.archivesName = "resclone"
val modmenuVersion = "10.0.0-beta.1"
val commonsVersion = "1.7-SNAPSHOT"
jfMod {
minecraftVersion = "1.20.5"
yarn("build.1")
loaderVersion = "0.15.10"
libJfVersion = "3.15.4"
fabricApiVersion = "0.97.6+1.20.5"
modrinth {
projectId = "resclone"
optionalDependencies.add("fabric-api")
}
curseforge {
projectId = "839008"
optionalDependencies.add("fabric-api")
}
}
dependencies {
include(modImplementation("io.gitlab.jfronny.libjf:libjf-base")!!) // for JfCommons
include(modImplementation("io.gitlab.jfronny.libjf:libjf-config-core-v2")!!) // for JfCommons
include(modImplementation("net.fabricmc.fabric-api:fabric-resource-loader-v0")!!)
compileOnly("io.gitlab.jfronny:commons-serialize-generator-annotations:$commonsVersion")
annotationProcessor("io.gitlab.jfronny:commons-serialize-generator:$commonsVersion")
// Dev env
modLocalRuntime("io.gitlab.jfronny.libjf:libjf-config-ui-tiny")
modLocalRuntime("io.gitlab.jfronny.libjf:libjf-devutil")
modLocalRuntime("com.terraformersmc:modmenu:$modmenuVersion")
// for modmenu
modLocalRuntime("net.fabricmc.fabric-api:fabric-resource-loader-v0")
modLocalRuntime("net.fabricmc.fabric-api:fabric-screen-api-v1")
modLocalRuntime("net.fabricmc.fabric-api:fabric-key-binding-api-v1")
}

View File

@ -1,11 +0,0 @@
# https://fabricmc.net/develop
minecraft_version=1.19.2
yarn_mappings=build.8
loader_version=0.14.9
maven_group=io.gitlab.jfronny
archives_base_name=resclone
fabric_version=0.60.0+1.19.2
libjf_version=3.0.3
modrinth_id=resclone

9
settings.gradle.kts Normal file
View File

@ -0,0 +1,9 @@
pluginManagement {
repositories {
maven("https://maven.fabricmc.net/") // FabricMC
maven("https://maven.frohnmeyer-wds.de/artifacts") // scripts
gradlePluginPortal()
}
}
rootProject.name = "resclone"

View File

@ -1,128 +1,121 @@
package io.gitlab.jfronny.resclone;
import com.google.gson.*;
import io.gitlab.jfronny.commons.log.*;
import io.gitlab.jfronny.commons.serialize.gson.api.*;
import io.gitlab.jfronny.resclone.api.*;
import io.gitlab.jfronny.resclone.data.*;
import io.gitlab.jfronny.commons.logger.SystemLoggerPlus;
import io.gitlab.jfronny.resclone.data.PackMetaLoaded;
import io.gitlab.jfronny.resclone.data.PackMetaUnloaded;
import io.gitlab.jfronny.resclone.fetchers.*;
import io.gitlab.jfronny.resclone.processors.*;
import io.gitlab.jfronny.resclone.util.*;
import net.fabricmc.api.*;
import net.fabricmc.loader.api.*;
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.*;
import java.net.*;
import java.nio.file.FileSystem;
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 class Resclone implements ModInitializer {
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 SystemLoggerPlus LOGGER = SystemLoggerPlus.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.");
GsonHolder.register();
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 RootPathProcessor()); //This should be run before any other processor to make sure the root is correct
addFetcher(new BasicFileFetcher());
addFetcher(new GitHubFetcher());
addFetcher(new CurseforgeFetcher());
addFetcher(new ModrinthFetcher());
if (RescloneConfig.filterPacks) {
if (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT) addProcessor(new PruneVanillaProcessor());
addProcessor(new RemoveEmptyProcessor());
}
addProcessor(new RemoveEmptyProcessor());
reload();
LOGGER.info("Installed {} resource pack{}.", packCount, packCount == 1 ? "" : "s");
LOGGER.info("Installed {0} resource pack{1}.", 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));
RescloneConfig.packs.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) {
if (RescloneConfig.packs.isEmpty()) {
LOGGER.info("No resclone pack was specified, add one");
}
else {
try (ExecutorService pool = Executors.newFixedThreadPool(RescloneConfig.packs.size())) {
for (PackMetaUnloaded s : RescloneConfig.packs) {
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);
}
} catch (InterruptedException e) {
LOGGER.error("Could not execute pack download task", e);
}
urlCache.save();
DOWNLOADED_PACKS.clear();
DOWNLOADED_PACKS.addAll(metas);
if (pruneUnused) pruneCache();
if (RescloneConfig.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);
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);
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);
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()) {
@ -142,7 +135,7 @@ public class Resclone implements ModInitializer, RescloneApi {
throw new Exception("Failed to download pack", e);
}
} catch (Throwable e) {
LOGGER.error("Encountered issue while preparing " + meta.name, e);
LOGGER.error("Encountered issue while preparing " + meta.name(), e);
}
};
}
@ -169,9 +162,8 @@ public class Resclone implements ModInitializer, RescloneApi {
}
}
@Override
public Path getConfigPath() {
Path configPath = FabricLoader.getInstance().getConfigDir().resolve("resclone");
public static Path getConfigPath() {
Path configPath = FabricLoader.getInstance().getConfigDir().resolve(MOD_ID);
if (!Files.isDirectory(configPath.resolve("cache"))) {
try {
Files.createDirectories(configPath.resolve("cache"));
@ -181,9 +173,4 @@ public class Resclone implements ModInitializer, RescloneApi {
}
return configPath;
}
@Override
public void setPruneUnused(boolean pruneUnused) {
this.pruneUnused = pruneUnused;
}
}

View File

@ -0,0 +1,156 @@
package io.gitlab.jfronny.resclone;
import com.google.gson.reflect.TypeToken;
import io.gitlab.jfronny.commons.serialize.MalformedDataException;
import io.gitlab.jfronny.commons.serialize.Token;
import io.gitlab.jfronny.commons.serialize.json.JsonReader;
import io.gitlab.jfronny.commons.serialize.json.JsonWriter;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.libjf.config.api.v2.JfCustomConfig;
import io.gitlab.jfronny.libjf.config.api.v2.dsl.DSL;
import io.gitlab.jfronny.resclone.data.GC_PackMetaUnloaded;
import io.gitlab.jfronny.resclone.data.PackMetaUnloaded;
import io.gitlab.jfronny.resclone.util.ListAdaptation;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Set;
public class RescloneConfig implements JfCustomConfig {
public static Set<PackMetaUnloaded> packs;
public static boolean pruneUnused;
public static boolean filterPacks;
public static boolean logProcessing;
private static final String ERR_DUPLICATE = "Unexpected duplicate \"%s\" in Resclone config";
private static final String PACKS = "packs";
private static final String PRUNE_UNUSED = "pruneUnused";
private static final String FILTER_PACKS = "filterPacks";
private static final String LOG_PROCESSING = "logProcessing";
private static void load(Path path) throws IOException {
if (!Files.exists(path)) {
packs = new HashSet<>();
pruneUnused = true;
filterPacks = true;
logProcessing = false;
write(path);
return;
}
boolean updateRequired = false;
try (BufferedReader br = Files.newBufferedReader(path);
JsonReader reader = LibJf.LENIENT_TRANSPORT.createReader(br)) {
if (reader.peek() == Token.BEGIN_ARRAY) {
// Legacy format compatibility
packs = ListAdaptation.deserializeSet(reader, GC_PackMetaUnloaded::deserialize);
updateRequired = true;
} else if (reader.peek() == Token.BEGIN_OBJECT) {
// New format
reader.beginObject();
Set<PackMetaUnloaded> packs = null;
Boolean pruneUnused = null;
Boolean filterPacks = null;
Boolean logProcessing = null;
while (reader.peek() != Token.END_OBJECT) {
final String name = reader.nextName();
switch (name) {
case PACKS -> {
if (packs != null) throw new MalformedDataException(ERR_DUPLICATE.formatted(PACKS));
if (reader.peek() == Token.BEGIN_ARRAY) {
packs = ListAdaptation.deserializeSet(reader, GC_PackMetaUnloaded::deserialize);
} else {
packs = Set.of(GC_PackMetaUnloaded.deserialize(reader));
}
}
case PRUNE_UNUSED -> {
if (pruneUnused != null) throw new MalformedDataException(ERR_DUPLICATE.formatted(PRUNE_UNUSED));
pruneUnused = reader.nextBoolean();
}
case FILTER_PACKS -> {
if (filterPacks != null) throw new MalformedDataException(ERR_DUPLICATE.formatted(FILTER_PACKS));
filterPacks = reader.nextBoolean();
}
case LOG_PROCESSING -> {
if (logProcessing != null) throw new MalformedDataException(ERR_DUPLICATE.formatted(LOG_PROCESSING));
logProcessing = reader.nextBoolean();
}
default -> throw new MalformedDataException("Unexpected element: \"" + name + "\" in Resclone config");
}
}
reader.endObject();
if (packs == null) throw new MalformedDataException("Expected Resclone config object to contain packs");
if (pruneUnused == null) {
pruneUnused = true;
updateRequired = true;
}
if (filterPacks == null) {
filterPacks = true;
updateRequired = true;
}
if (logProcessing == null) {
logProcessing = false;
updateRequired = true;
}
RescloneConfig.packs = packs;
RescloneConfig.pruneUnused = pruneUnused;
RescloneConfig.filterPacks = filterPacks;
RescloneConfig.logProcessing = logProcessing;
} else throw new MalformedDataException("Expected Resclone config to be an object or array");
}
if (updateRequired) write(path);
}
private static void write(Path path) throws IOException {
try (BufferedWriter bw = Files.newBufferedWriter(path);
JsonWriter writer = LibJf.LENIENT_TRANSPORT.createWriter(bw)) {
writer.beginObject()
.comment("The packs to be loaded by resclone")
.name(PACKS)
.beginArray();
for (PackMetaUnloaded pack : packs) {
GC_PackMetaUnloaded.serialize(pack, writer);
}
writer.endArray()
.comment("Automatically remove all downloaded packs that are not in the config to free up unneeded space")
.name(PRUNE_UNUSED)
.value(pruneUnused)
.comment("Whether to filter packs to remove files unchanged from vanilla and empty directories")
.name(FILTER_PACKS)
.value(filterPacks)
.comment("Log automatic processing steps applied to downloaded packs")
.name(LOG_PROCESSING)
.value(logProcessing)
.endObject();
}
}
static {
Path path = Resclone.getConfigPath().resolve("config.json");
DSL.create(Resclone.MOD_ID).register(builder ->
builder.setLoadMethod(configInstance -> {
try {
load(path);
} catch (IOException e) {
Resclone.LOGGER.error("Could not load config", e);
}
}).setWriteMethod(configInstance -> {
try {
write(path);
} catch (IOException e) {
Resclone.LOGGER.error("Could not write config", e);
}
}).setPath(path)
.<Set<PackMetaUnloaded>>value(PACKS, new HashSet<>(), Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, io.gitlab.jfronny.libjf.config.api.v2.type.Type.ofClass(new TypeToken<Set<PackMetaUnloaded>>(){}.getType()), 100, () -> packs, p -> packs = p)
.value(PRUNE_UNUSED, pruneUnused, () -> pruneUnused, p -> pruneUnused = p)
.value(FILTER_PACKS, filterPacks, () -> filterPacks, p -> filterPacks = p)
.value(LOG_PROCESSING, logProcessing, () -> logProcessing, p -> logProcessing = p)
).load();
}
@Override
public void register(DSL.Defaulted dsl) {
}
}

View File

@ -1,25 +0,0 @@
package io.gitlab.jfronny.resclone;
import io.gitlab.jfronny.commons.serialize.gson.api.GsonHolder;
import io.gitlab.jfronny.resclone.api.RescloneApi;
import io.gitlab.jfronny.resclone.api.RescloneEntry;
import io.gitlab.jfronny.resclone.util.config.*;
import io.gitlab.jfronny.resclone.fetchers.BasicFileFetcher;
import io.gitlab.jfronny.resclone.fetchers.CurseforgeFetcher;
import io.gitlab.jfronny.resclone.fetchers.GitHubFetcher;
import io.gitlab.jfronny.resclone.processors.PruneVanillaProcessor;
import net.fabricmc.api.EnvType;
import net.fabricmc.loader.api.FabricLoader;
public class RescloneEntryDefault implements RescloneEntry {
@Override
public void init(RescloneApi api) {
GsonHolder.registerTypeAdapter(RescloneConfig.class, new RescloneConfigTypeAdapter());
api.addFetcher(new BasicFileFetcher());
api.addFetcher(new GitHubFetcher());
api.addFetcher(new CurseforgeFetcher());
if (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT)
api.addProcessor(new PruneVanillaProcessor());
ConfigLoader.load(api);
}
}

View File

@ -3,26 +3,57 @@ package io.gitlab.jfronny.resclone;
import net.fabricmc.fabric.api.resource.ModResourcePack;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.metadata.ModMetadata;
import net.minecraft.resource.ZipResourcePack;
import net.minecraft.resource.*;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class RescloneResourcePack extends ZipResourcePack implements ModResourcePack {
private static final ModMetadata METADATA = FabricLoader.getInstance().getModContainer(Resclone.MOD_ID).orElseThrow().getMetadata();
private final String name;
private final ResourcePackInfo info;
private final ZipFileWrapper file;
public RescloneResourcePack(File file, String name) {
super(file);
this.name = name;
}
@Override
public String getName() {
return name;
RescloneResourcePack(ZipFileWrapper file, ResourcePackInfo info, String overlay) {
super(info, file, overlay);
this.info = info;
this.file = file;
}
@Override
public ModMetadata getFabricModMetadata() {
return METADATA;
}
@Override
public ModResourcePack createOverlay(String overlay) {
return new RescloneResourcePack(this.file, this.info, overlay);
}
public static class Factory implements ResourcePackProfile.PackFactory {
private final File file;
private final ResourcePackInfo info;
public Factory(File file, ResourcePackInfo info) {
this.file = file;
this.info = info;
}
@Override
public ResourcePack open(ResourcePackInfo info) {
ZipFileWrapper zipFileWrapper = new ZipFileWrapper(this.file);
return new RescloneResourcePack(zipFileWrapper, this.info, "");
}
@Override
public ResourcePack openWithOverlays(ResourcePackInfo info, ResourcePackProfile.Metadata metadata) {
ZipFileWrapper zipFileWrapper = new ZipFileWrapper(this.file);
ZipResourcePack resourcePack = new RescloneResourcePack(zipFileWrapper, this.info, "");
List<String> overlays = metadata.overlays();
if (overlays.isEmpty()) return resourcePack;
List<ResourcePack> overlayPacks = new ArrayList<>(overlays.size());
for (String string : overlays) overlayPacks.add(new RescloneResourcePack(zipFileWrapper, this.info, string));
return new OverlayResourcePack(resourcePack, overlayPacks);
}
}
}

View File

@ -1,21 +0,0 @@
package io.gitlab.jfronny.resclone.api;
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 addPack(String fetcher, String pack, String name, boolean forceRedownload, boolean forceEnable);
void reload();
Path getConfigPath();
void setPruneUnused(boolean pruneUnused);
}

View File

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

View File

@ -1,20 +1,7 @@
package io.gitlab.jfronny.resclone.data;
// Represents a pack as present in the config
// Can't be a record since it'll need to be parsed by Gson
@SuppressWarnings("ClassCanBeRecord")
public class PackMetaUnloaded {
public final String fetcher;
public final String source;
public final String name;
public final boolean forceDownload;
public final boolean forceEnable;
import io.gitlab.jfronny.commons.serialize.generator.annotations.GSerializable;
public PackMetaUnloaded(String fetcher, String source, String name, boolean forceDownload, boolean forceEnable) {
this.fetcher = fetcher;
this.source = source;
this.name = name;
this.forceDownload = forceDownload;
this.forceEnable = forceEnable;
}
@GSerializable
public record PackMetaUnloaded(String fetcher, String source, String name, boolean forceDownload, boolean forceEnable) {
}

View File

@ -1,10 +1,15 @@
package io.gitlab.jfronny.resclone.data.curseforge;
import java.util.*;
import io.gitlab.jfronny.commons.serialize.generator.annotations.GSerializable;
import java.util.Date;
import java.util.List;
@GSerializable
public class GetModFilesResponse {
public List<Data> data;
@GSerializable
public static class Data {
public String downloadUrl;
public Date fileDate;

View File

@ -1,8 +1,12 @@
package io.gitlab.jfronny.resclone.data.curseforge;
import io.gitlab.jfronny.commons.serialize.generator.annotations.GSerializable;
@GSerializable
public class GetModResponse {
public Data data;
@GSerializable
public static class Data {
public Boolean allowModDistribution;
}

View File

@ -1,11 +1,15 @@
package io.gitlab.jfronny.resclone.data.github;
import java.util.*;
import io.gitlab.jfronny.commons.serialize.generator.annotations.GSerializable;
import java.util.List;
@GSerializable
public class Release {
public List<Asset> assets;
public String zipball_url;
@GSerializable
public static class Asset {
public String name;
public String content_type;

View File

@ -1,5 +1,8 @@
package io.gitlab.jfronny.resclone.data.github;
import io.gitlab.jfronny.commons.serialize.generator.annotations.GSerializable;
@GSerializable
public class Repository {
public String default_branch;
}

View File

@ -0,0 +1,69 @@
package io.gitlab.jfronny.resclone.data.modrinth;
import io.gitlab.jfronny.commons.serialize.annotations.SerializedName;
import io.gitlab.jfronny.commons.serialize.generator.annotations.GSerializable;
import org.jetbrains.annotations.Nullable;
import java.util.Date;
import java.util.List;
@GSerializable
public class Version {
public String name;
public String version_number;
@Nullable public String changelog;
public List<Dependency> dependencies;
public List<String> game_versions;
public VersionType version_type;
public List<String> loaders;
public Boolean featured;
public Status status;
@Nullable public Status requested_status;
public String id;
public String project_id;
public String author_id;
public Date date_published;
public Integer downloads;
public List<File> files;
@GSerializable
public static class Dependency {
@Nullable public String version_id;
@Nullable public String project_id;
@Nullable public String file_name;
public Type dependency_type;
public enum Type {
required, optional, incompatible, embedded
}
}
public enum VersionType {
release, beta, alpha
}
public enum Status {
listed, archived, draft, unlisted, scheduled, unknown
}
@GSerializable
public static class File {
public Hashes hashes;
public String url;
public String filename;
public Boolean primary;
public Integer size;
@Nullable public Type file_type;
@GSerializable
public static class Hashes {
public String sha512;
public String sha1;
}
public enum Type {
@SerializedName("required-resource-pack") REQUIRED_RESOURCE_PACK,
@SerializedName("optional-resource-pack") OPTIONAL_RESOURCE_PACK
}
}
}

View File

@ -1,11 +1,9 @@
package io.gitlab.jfronny.resclone.fetchers;
import io.gitlab.jfronny.commons.http.client.HttpClient;
import io.gitlab.jfronny.resclone.Resclone;
import io.gitlab.jfronny.resclone.api.PackFetcher;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
@ -34,8 +32,8 @@ public abstract class BasePackFetcher implements PackFetcher {
Resclone.LOGGER.info("Downloading pack: " + url);
try (InputStream is = new URL(url).openStream()) {
FileOutputStream os = new FileOutputStream(p.toFile());
try (InputStream is = HttpClient.get(url).userAgent(Resclone.USER_AGENT).sendInputStream();
OutputStream os = Files.newOutputStream(p)) {
byte[] dataBuffer = new byte[1024];
int bytesRead;
while ((bytesRead = is.read(dataBuffer, 0, 1024)) != -1) {
@ -43,7 +41,7 @@ public abstract class BasePackFetcher implements PackFetcher {
}
Resclone.LOGGER.info("Finished downloading.");
} catch (Throwable e) {
throw new Exception("Could not download pack", e);
throw new IOException("Could not download pack", e);
}
Resclone.packCount++;
return new Result(p, true);

View File

@ -1,13 +1,22 @@
package io.gitlab.jfronny.resclone.fetchers;
import io.gitlab.jfronny.commons.*;
import io.gitlab.jfronny.resclone.*;
import io.gitlab.jfronny.resclone.data.curseforge.*;
import net.minecraft.*;
import io.gitlab.jfronny.commons.http.client.HttpClient;
import io.gitlab.jfronny.commons.serialize.json.JsonReader;
import io.gitlab.jfronny.commons.throwable.ThrowingFunction;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.resclone.Resclone;
import io.gitlab.jfronny.resclone.data.curseforge.GC_GetModFilesResponse;
import io.gitlab.jfronny.resclone.data.curseforge.GC_GetModResponse;
import io.gitlab.jfronny.resclone.data.curseforge.GetModFilesResponse;
import io.gitlab.jfronny.resclone.data.curseforge.GetModResponse;
import net.minecraft.MinecraftVersion;
import java.io.*;
import java.net.*;
import java.util.*;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Reader;
import java.net.URISyntaxException;
import java.util.Date;
import java.util.Random;
public class CurseforgeFetcher extends BasePackFetcher {
// So you found the API key.
@ -30,7 +39,7 @@ public class CurseforgeFetcher extends BasePackFetcher {
@Override
public String getDownloadUrl(String baseUrl) throws Exception {
try {
GetModResponse response = GET(baseUrl, GetModResponse.class);
GetModResponse response = GET(baseUrl, GC_GetModResponse::deserialize);
if (!response.data.allowModDistribution)
throw new Exception("The author of " + baseUrl + " disabled access to this pack outside of the curseforge launcher");
@ -40,9 +49,8 @@ public class CurseforgeFetcher extends BasePackFetcher {
Date latestDate = null;
boolean foundMatchingVersion = false;
for (GetModFilesResponse.Data addon : GET(baseUrl + "/files", GetModFilesResponse.class).data) {
if (foundMatchingVersion && !addon.gameVersions.contains(version))
continue;
for (GetModFilesResponse.Data addon : GET(baseUrl + "/files", GC_GetModFilesResponse::deserialize).data) {
if (foundMatchingVersion && !addon.gameVersions.contains(version)) continue;
if (!foundMatchingVersion && addon.gameVersions.contains(version)) {
foundMatchingVersion = true;
latest = null;
@ -53,16 +61,19 @@ public class CurseforgeFetcher extends BasePackFetcher {
}
}
if (!foundMatchingVersion)
Resclone.LOGGER.error("Could not find pack for matching version, using latest");
if (latest == null) throw new FileNotFoundException("Could not identify valid version");
if (!foundMatchingVersion) Resclone.LOGGER.error("Could not find matching version of " + baseUrl + ", using latest");
return latest.downloadUrl;
} catch (Throwable e) {
throw new Exception("Could not get CF download for " + baseUrl, e);
throw new IOException("Could not get CurseForge download for " + baseUrl, e);
}
}
private static <T> T GET(String suffix, Class<T> klazz) throws URISyntaxException, IOException {
return HttpUtils.get("https://api.curseforge.com/v1/mods/" + suffix).header("x-api-key", API_KEY).sendSerialized(klazz);
private static <T> T GET(String suffix, ThrowingFunction<JsonReader, T, IOException> klazz) throws URISyntaxException, IOException {
try (Reader r = HttpClient.get("https://api.curseforge.com/v1/mods/" + suffix).header("x-api-key", API_KEY).sendReader();
JsonReader jr = LibJf.LENIENT_TRANSPORT.createReader(r)) {
return klazz.apply(jr);
}
}
private static byte[] unsalt(byte[] data, int salt) {

View File

@ -1,12 +1,17 @@
package io.gitlab.jfronny.resclone.fetchers;
import io.gitlab.jfronny.commons.*;
import io.gitlab.jfronny.commons.http.client.HttpClient;
import io.gitlab.jfronny.commons.serialize.json.JsonReader;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.resclone.Resclone;
import io.gitlab.jfronny.resclone.data.github.*;
import io.gitlab.jfronny.resclone.data.github.GC_Release;
import io.gitlab.jfronny.resclone.data.github.GC_Repository;
import io.gitlab.jfronny.resclone.data.github.Release;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.net.*;
import java.io.Reader;
import java.net.URISyntaxException;
public class GitHubFetcher extends BasePackFetcher {
@Override
@ -38,8 +43,9 @@ public class GitHubFetcher extends BasePackFetcher {
//"user/repo/release" - Gets from the latest release.
else if (parts[2].equalsIgnoreCase("release")) {
try {
Release latestRelease = HttpUtils.get("https://api.github.com/repos/" + parts[0] + "/" + parts[1] + "/releases/latest").sendSerialized(Release.class);
try (Reader r = HttpClient.get("https://api.github.com/repos/" + parts[0] + "/" + parts[1] + "/releases/latest").sendReader();
JsonReader jr = LibJf.LENIENT_TRANSPORT.createReader(r)) {
Release latestRelease = GC_Release.deserialize(jr);
String res = null;
for (Release.Asset asset : latestRelease.assets) {
@ -72,8 +78,9 @@ public class GitHubFetcher extends BasePackFetcher {
private String getFromBranch(String repo, @Nullable String branch) {
if (branch == null) {
try {
branch = HttpUtils.get("https://api.github.com/repos/" + repo).<Repository>sendSerialized(Repository.class).default_branch;
try (Reader r = HttpClient.get("https://api.github.com/repos/" + repo).sendReader();
JsonReader jr = LibJf.LENIENT_TRANSPORT.createReader(r)) {
branch = GC_Repository.deserialize(jr).default_branch;
} catch (IOException | URISyntaxException e) {
Resclone.LOGGER.error("Failed to fetch branch for " + repo + ". Choosing \"main\"", e);
branch = "main";

View File

@ -0,0 +1,69 @@
package io.gitlab.jfronny.resclone.fetchers;
import io.gitlab.jfronny.commons.http.client.HttpClient;
import io.gitlab.jfronny.commons.serialize.json.JsonReader;
import io.gitlab.jfronny.libjf.LibJf;
import io.gitlab.jfronny.resclone.Resclone;
import io.gitlab.jfronny.resclone.data.modrinth.GC_Version;
import io.gitlab.jfronny.resclone.data.modrinth.Version;
import io.gitlab.jfronny.resclone.util.ListAdaptation;
import net.minecraft.MinecraftVersion;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Reader;
import java.util.Date;
import java.util.List;
public class ModrinthFetcher extends BasePackFetcher {
@Override
public String getSourceTypeName() {
return "modrinth";
}
@Override
public String getDownloadUrl(String baseUrl) throws Exception {
try {
String version = MinecraftVersion.CURRENT.getName();
Version latest = null;
Date latestDate = null;
boolean foundMatchingVersion = false;
List<Version> versions;
try (Reader r = HttpClient.get("https://api.modrinth.com/v2/project/" + baseUrl + "/version")
.userAgent(Resclone.USER_AGENT)
.sendReader();
JsonReader jr = LibJf.LENIENT_TRANSPORT.createReader(r)) {
versions = ListAdaptation.deserializeList(jr, GC_Version::deserialize);
}
for (Version ver : versions) {
if (foundMatchingVersion && !ver.game_versions.contains(version)) continue;
if (ver.files.isEmpty()) continue;
if (!foundMatchingVersion && ver.game_versions.contains(version)) {
foundMatchingVersion = true;
latest = null;
}
if (latest == null || ver.date_published.after(latestDate)) {
latest = ver;
latestDate = ver.date_published;
}
}
if (latest == null) throw new FileNotFoundException("Could not identify valid version");
if (!foundMatchingVersion) Resclone.LOGGER.error("Could not find matching version of " + baseUrl + ", using latest");
for (Version.File file : latest.files) {
if (file.primary) return file.url;
}
Resclone.LOGGER.error("Could not identify primary file of " + baseUrl + ", attempting identification by file_type");
for (Version.File file : latest.files) {
if (file.file_type == Version.File.Type.REQUIRED_RESOURCE_PACK) return file.url;
}
Resclone.LOGGER.error("Identification failed, using first file of " + baseUrl);
return latest.files.getFirst().url;
} catch (Throwable e) {
throw new IOException("Could not get Modrinth download for " + baseUrl, e);
}
}
}

View File

@ -1,4 +1,4 @@
package io.gitlab.jfronny.resclone.api;
package io.gitlab.jfronny.resclone.fetchers;
import java.nio.file.Path;

View File

@ -3,32 +3,39 @@ 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.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;
import net.minecraft.resource.*;
import net.minecraft.text.Text;
import org.spongepowered.asm.mixin.*;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.Optional;
import java.util.function.Consumer;
@Mixin(FileResourcePackProvider.class)
public class FileResourcePackProviderMixin {
@Shadow @Final private ResourcePackSource source;
@Shadow @Final private ResourceType type;
@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) {
@Inject(at = @At("TAIL"), method = "register(Ljava/util/function/Consumer;)V")
public void registerExtra(Consumer<ResourcePackProfile> consumer, CallbackInfo info) {
for (PackMetaLoaded meta : Resclone.DOWNLOADED_PACKS) {
ResourcePackProfile resourcePackProfile = ResourcePackProfile.of(
"resclone/" + meta.name(),
meta.forceEnable(),
() -> new RescloneResourcePack(meta.zipPath().toFile(), meta.name()),
factory,
ResourcePackProfile.InsertionPosition.TOP,
this.source
ResourcePackInfo ifo = new ResourcePackInfo(
"resclone/" + meta.name(),
Text.literal(meta.name()),
source,
Optional.empty()
);
ResourcePackProfile resourcePackProfile = ResourcePackProfile.create(
ifo,
new RescloneResourcePack.Factory(meta.zipPath().toFile(), ifo),
type,
new ResourcePackPosition(
meta.forceEnable(),
ResourcePackProfile.InsertionPosition.TOP,
false
)
);
if (resourcePackProfile != null) {
consumer.accept(resourcePackProfile);

View File

@ -1,4 +1,4 @@
package io.gitlab.jfronny.resclone.api;
package io.gitlab.jfronny.resclone.processors;
import java.nio.file.FileSystem;

View File

@ -1,16 +1,14 @@
package io.gitlab.jfronny.resclone.processors;
import io.gitlab.jfronny.resclone.*;
import io.gitlab.jfronny.resclone.api.PackProcessor;
import io.gitlab.jfronny.resclone.Resclone;
import io.gitlab.jfronny.resclone.RescloneConfig;
import io.gitlab.jfronny.resclone.util.io.PathPruneVisitor;
import net.minecraft.server.MinecraftServer;
import org.apache.commons.io.IOUtils;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.nio.file.*;
public class PruneVanillaProcessor implements PackProcessor {
@Override
@ -25,7 +23,10 @@ public class PruneVanillaProcessor implements PackProcessor {
InputStream vn = cl.getResourceAsStream(p.getPath("/").relativize(s).toString());
if (vn != null) {
try (InputStream pk = Files.newInputStream(s, StandardOpenOption.READ)) {
return IOUtils.contentEquals(vn, pk);
if (IOUtils.contentEquals(vn, pk)) {
if (RescloneConfig.logProcessing) Resclone.LOGGER.info("Pruning file unchanged from vanilla: {}", s);
return true;
}
}
}
} catch (Throwable e) {

View File

@ -1,14 +1,11 @@
package io.gitlab.jfronny.resclone.processors;
import io.gitlab.jfronny.resclone.*;
import io.gitlab.jfronny.resclone.api.PackProcessor;
import io.gitlab.jfronny.resclone.Resclone;
import io.gitlab.jfronny.resclone.RescloneConfig;
import io.gitlab.jfronny.resclone.util.io.PathPruneVisitor;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.*;
public class RemoveEmptyProcessor implements PackProcessor {
@Override
@ -18,7 +15,10 @@ public class RemoveEmptyProcessor implements PackProcessor {
Files.walkFileTree(p.getPath("/assets"), new PathPruneVisitor(s -> {
if (Files.isDirectory(s)) {
try (DirectoryStream<Path> paths = Files.newDirectoryStream(s)) {
return !paths.iterator().hasNext();
if (!paths.iterator().hasNext()) {
if (RescloneConfig.logProcessing) Resclone.LOGGER.info("Pruning empty directory: {}", s);
return true;
}
} catch (IOException e) {
Resclone.LOGGER.error("Could not check whether directory has entries", e);
}

View File

@ -1,6 +1,7 @@
package io.gitlab.jfronny.resclone.processors;
import io.gitlab.jfronny.resclone.api.PackProcessor;
import io.gitlab.jfronny.resclone.Resclone;
import io.gitlab.jfronny.resclone.RescloneConfig;
import io.gitlab.jfronny.resclone.util.io.MoveDirVisitor;
import java.io.IOException;
@ -15,6 +16,7 @@ public class RootPathProcessor implements PackProcessor {
try (DirectoryStream<Path> paths = Files.newDirectoryStream(root)) {
for (Path path : paths) {
if (Files.isDirectory(path) && Files.exists(path.resolve("pack.mcmeta"))) {
if (RescloneConfig.logProcessing) Resclone.LOGGER.info("Moving discovered root out of: {}", path);
Files.walkFileTree(path, new MoveDirVisitor(path, root, StandardCopyOption.REPLACE_EXISTING));
}
}

View File

@ -0,0 +1,31 @@
package io.gitlab.jfronny.resclone.util;
import io.gitlab.jfronny.commons.serialize.SerializeReader;
import io.gitlab.jfronny.commons.throwable.ThrowingFunction;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
public class ListAdaptation {
public static <T, TEx extends Exception, Reader extends SerializeReader<TEx, Reader>> List<T> deserializeList(Reader reader, ThrowingFunction<Reader, T, TEx> deserializeOne) throws TEx {
List<T> result = new ArrayList<>();
reader.beginArray();
while (reader.hasNext()) {
result.add(deserializeOne.apply(reader));
}
reader.endArray();
return result;
}
public static <T, TEx extends Exception, Reader extends SerializeReader<TEx, Reader>> Set<T> deserializeSet(Reader reader, ThrowingFunction<Reader, T, TEx> serializeOne) throws TEx {
Set<T> result = new LinkedHashSet<>();
reader.beginArray();
while (reader.hasNext()) {
result.add(serializeOne.apply(reader));
}
reader.endArray();
return result;
}
}

View File

@ -1,10 +1,8 @@
package io.gitlab.jfronny.resclone.util;
import io.gitlab.jfronny.resclone.*;
import io.gitlab.jfronny.resclone.Resclone;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;

View File

@ -1,42 +0,0 @@
package io.gitlab.jfronny.resclone.util.config;
import io.gitlab.jfronny.commons.serialize.gson.api.GsonHolder;
import io.gitlab.jfronny.resclone.Resclone;
import io.gitlab.jfronny.resclone.api.RescloneApi;
import io.gitlab.jfronny.resclone.data.PackMetaUnloaded;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
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)) {
save(api, new HashSet<>(), true);
}
try (BufferedReader reader = Files.newBufferedReader(configPath)) {
RescloneConfig data = GsonHolder.getGson().fromJson(reader, RescloneConfig.class);
api.setPruneUnused(data.pruneUnused());
for (PackMetaUnloaded meta : data.packs()) {
api.addPack(meta.fetcher, meta.source, meta.name, meta.forceDownload, meta.forceEnable);
}
if (data.updateRequired()) {
save(api, data.packs(), data.pruneUnused());
}
} catch (IOException e) {
Resclone.LOGGER.error("Could not load config", e);
}
}
public static void save(RescloneApi api, Set<PackMetaUnloaded> packs, boolean pruneUnused) {
try (var writer = Files.newBufferedWriter(api.getConfigPath().resolve("config.json"))) {
GsonHolder.getGson().toJson(new RescloneConfig(packs, pruneUnused), writer);
} catch (IOException e) {
Resclone.LOGGER.error("Could not write config", e);
}
}
}

View File

@ -1,11 +0,0 @@
package io.gitlab.jfronny.resclone.util.config;
import io.gitlab.jfronny.resclone.data.PackMetaUnloaded;
import java.util.Set;
public record RescloneConfig(Set<PackMetaUnloaded> packs, boolean pruneUnused, boolean updateRequired) {
public RescloneConfig(Set<PackMetaUnloaded> packs, boolean pruneUnused) {
this(packs, pruneUnused, false);
}
}

View File

@ -1,64 +0,0 @@
package io.gitlab.jfronny.resclone.util.config;
import com.google.gson.reflect.TypeToken;
import io.gitlab.jfronny.commons.serialize.gson.api.GsonHolder;
import io.gitlab.jfronny.gson.*;
import io.gitlab.jfronny.gson.stream.*;
import io.gitlab.jfronny.resclone.data.PackMetaUnloaded;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.Set;
public class RescloneConfigTypeAdapter extends TypeAdapter<RescloneConfig> {
private static final String PACKS = "packs";
private static final String PRUNE_UNUSED = "pruneUnused";
private static final Type META_SET = new TypeToken<Set<PackMetaUnloaded>>(){}.getType();
@Override
public RescloneConfig read(JsonReader jsonReader) throws IOException {
if (jsonReader.peek() == JsonToken.BEGIN_ARRAY) {
// Legacy format compatibility
return new RescloneConfig(GsonHolder.getGson().fromJson(jsonReader, META_SET), false, true);
} else if (jsonReader.peek() == JsonToken.BEGIN_OBJECT) {
// New format
jsonReader.beginObject();
Set<PackMetaUnloaded> packs = null;
Boolean pruneUnused = null;
boolean updateRequired = false;
while (jsonReader.peek() != JsonToken.END_OBJECT) {
final String name = jsonReader.nextName();
switch (name) {
case PACKS -> {
if (packs != null) throw new JsonParseException("Unexpected duplicate \"" + PACKS + "\" in Resclone config");
packs = GsonHolder.getGson().fromJson(jsonReader, META_SET);
}
case PRUNE_UNUSED -> {
if (pruneUnused != null) throw new JsonParseException("Unexpected duplicate \"" + PRUNE_UNUSED + "\" in Resclone config");
pruneUnused = jsonReader.nextBoolean();
}
default -> throw new JsonParseException("Unexpected element: \"" + name + "\" in Resclone config");
}
}
jsonReader.endObject();
if (packs == null) throw new JsonParseException("Expected Resclone config object to contain packs");
if (pruneUnused == null) {
pruneUnused = true;
updateRequired = true;
}
return new RescloneConfig(packs, pruneUnused, updateRequired);
} else throw new JsonParseException("Expected Resclone config to be an object or array");
}
@Override
public void write(JsonWriter jsonWriter, RescloneConfig rescloneConfig) throws IOException {
jsonWriter.beginObject()
.comment("The packs to be loaded by resclone")
.name(PACKS);
GsonHolder.getGson().toJson(rescloneConfig.packs(), META_SET, jsonWriter);
jsonWriter.comment("Whether to prune unused packs from the cache")
.name(PRUNE_UNUSED)
.value(rescloneConfig.pruneUnused())
.endObject();
}
}

View File

@ -1,10 +1,7 @@
package io.gitlab.jfronny.resclone.util.io;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.function.Predicate;

View File

@ -1,10 +1,7 @@
package io.gitlab.jfronny.resclone.util.io;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
public class RemoveDirVisitor extends SimpleFileVisitor<Path> {

View File

@ -0,0 +1,11 @@
{
"resclone.jfconfig.title": "Resclone",
"resclone.jfconfig.packs": "Packs",
"resclone.jfconfig.packs.tooltip": "The packs to download and add",
"resclone.jfconfig.pruneUnused": "Prune Unused",
"resclone.jfconfig.pruneUnused.tooltip": "Automatically remove all downloaded packs that are not in the config to free up unneeded space",
"resclone.jfconfig.filterPacks": "Filter Packs",
"resclone.jfconfig.filterPacks.tooltip": "Whether to filter packs to remove files unchanged from vanilla and empty directories",
"resclone.jfconfig.logProcessing": "Log Processing",
"resclone.jfconfig.logProcessing.tooltip": "Log automatic processing steps applied to downloaded packs"
}

View File

@ -1,22 +1,23 @@
{
"schemaVersion": 1,
"id": "resclone",
"version": "${version}",
"name": "Resclone",
"description": "Downloads and updates resourcepacks.",
"authors": [
"JFronny"
],
"contact": {},
"version": "${version}",
"description": "Downloads and updates resource packs",
"authors": ["JFronny"],
"contact": {
"email": "projects.contact@frohnmeyer-wds.de",
"homepage": "https://jfronny.gitlab.io",
"issues": "https://git.frohnmeyer-wds.de/JfMods/Resclone/issues",
"sources": "https://git.frohnmeyer-wds.de/JfMods/Resclone"
},
"license": "MIT",
"icon": "assets/resclone/icon.png",
"environment": "*",
"entrypoints": {
"main": [
"io.gitlab.jfronny.resclone.Resclone"
],
"resclone": [
"io.gitlab.jfronny.resclone.RescloneEntryDefault"
"main": ["io.gitlab.jfronny.resclone.Resclone"],
"libjf:config": [
"io.gitlab.jfronny.resclone.RescloneConfig"
]
},
"mixins": [
@ -26,6 +27,7 @@
"environment": "client"
}
],
"accessWidener": "resclone.accesswidener",
"depends": {
"fabricloader": ">=0.12.0",
"minecraft": "*",

View File

@ -0,0 +1,5 @@
accessWidener v2 named
accessible class net/minecraft/resource/ZipResourcePack$ZipFileWrapper
accessible method net/minecraft/resource/ZipResourcePack <init> (Lnet/minecraft/resource/ResourcePackInfo;Lnet/minecraft/resource/ZipResourcePack$ZipFileWrapper;Ljava/lang/String;)V
accessible method net/minecraft/resource/ZipResourcePack$ZipFileWrapper <init> (Ljava/io/File;)V

View File

@ -2,7 +2,6 @@
"required": true,
"minVersion": "0.8",
"package": "io.gitlab.jfronny.resclone.mixin",
"compatibilityLevel": "JAVA_8",
"mixins": [
"FileResourcePackProviderMixin"
],