CurseForge mod support
This commit is contained in:
parent
d60137a531
commit
0ac146e3ab
|
@ -2,6 +2,7 @@ package io.gitlab.jfronny.inceptum.gson;
|
|||
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import io.gitlab.jfronny.inceptum.model.inceptum.source.CurseforgeModSource;
|
||||
import io.gitlab.jfronny.inceptum.model.inceptum.source.DirectModSource;
|
||||
import io.gitlab.jfronny.inceptum.model.inceptum.source.ModSource;
|
||||
import io.gitlab.jfronny.inceptum.model.inceptum.source.ModrinthModSource;
|
||||
|
@ -29,7 +30,15 @@ public class ModSourceTypeAdapter implements JsonSerializer<ModSource>, JsonDese
|
|||
}
|
||||
}
|
||||
case "curseforge" -> {
|
||||
throw new JsonParseException("Curseforge sources are not yet supported"); //TODO
|
||||
if (!jo.has("projectId"))
|
||||
throw new JsonParseException("Expected a projectId in this curseforge project");
|
||||
if (!jo.has("fileId"))
|
||||
throw new JsonParseException("Expected a fileId in this curseforge project");
|
||||
try {
|
||||
yield new CurseforgeModSource(jo.get("projectId").getAsInt(), jo.get("fileId").getAsInt());
|
||||
} catch (IOException e) {
|
||||
throw new JsonParseException("Could not fetch Curseforge source", e);
|
||||
}
|
||||
}
|
||||
case "direct" -> {
|
||||
if (!jo.has("fileName"))
|
||||
|
@ -58,6 +67,11 @@ public class ModSourceTypeAdapter implements JsonSerializer<ModSource>, JsonDese
|
|||
jo.add("url", new JsonPrimitive(di.url()));
|
||||
jo.add("dependencies", context.serialize(di.dependencies()));
|
||||
}
|
||||
else if (src instanceof CurseforgeModSource cu) {
|
||||
jo.add("type", new JsonPrimitive("curseforge"));
|
||||
jo.add("projectId", new JsonPrimitive(cu.getProjectId()));
|
||||
jo.add("fileId", new JsonPrimitive(cu.getFileId()));
|
||||
}
|
||||
else throw new RuntimeException("ModSources with the type " + src.getClass() + " are not supported");
|
||||
return jo;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
package io.gitlab.jfronny.inceptum.model.curseforge;
|
||||
|
||||
public class CurseforgeDependency {
|
||||
public Integer id;
|
||||
public Integer addonId;
|
||||
public Integer type;
|
||||
public Integer fileId;
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package io.gitlab.jfronny.inceptum.model.curseforge;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class CurseforgeFile {
|
||||
public Integer id;
|
||||
public String displayName;
|
||||
public String fileName;
|
||||
public String fileDate;
|
||||
public Integer fileLength;
|
||||
public Integer releaseType;
|
||||
public Integer fileStatus;
|
||||
public String downloadUrl;
|
||||
public Boolean isAlternate;
|
||||
public Integer alternateFileId;
|
||||
public List<CurseforgeDependency> dependencies;
|
||||
public Boolean isAvailable;
|
||||
public List<CurseforgeModule> modules;
|
||||
public Long packageFingerprint;
|
||||
public List<String> gameVersion;
|
||||
public String gameVersionDateReleased;
|
||||
public Integer serverPackFileId;
|
||||
public Boolean hasInstallScript;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package io.gitlab.jfronny.inceptum.model.curseforge;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class CurseforgeFingerprint {
|
||||
public Boolean isCacheBuilt;
|
||||
public List<Mod> exactMatches;
|
||||
public List<Long> exactFingerprints;
|
||||
|
||||
public static class Mod {
|
||||
public Integer id;
|
||||
public CurseforgeFile file;
|
||||
public List<CurseforgeMod.File> latestFiles;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
package io.gitlab.jfronny.inceptum.model.curseforge;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class CurseforgeMod {
|
||||
public Integer id;
|
||||
public String name;
|
||||
public List<Author> authors;
|
||||
public List<Attachment> attachments;
|
||||
public String issueTrackerUrl;
|
||||
public String wikiUrl;
|
||||
public String sourceUrl;
|
||||
public String websiteUrl;
|
||||
public Integer gameId;
|
||||
public String summary;
|
||||
public Integer downloadFiles;
|
||||
public Integer downloadCount;
|
||||
public List<File> latestFiles;
|
||||
public List<Category> categories;
|
||||
public Integer statue;
|
||||
public Integer primaryCategoryId;
|
||||
public CategorySection categorySection;
|
||||
public String slug;
|
||||
public List<GameVersionLatestFile> gameVersionLatestFiles;
|
||||
public Boolean isFeatured;
|
||||
public Float popularityScore;
|
||||
public Integer gamePopularityRank;
|
||||
public String primaryLanguage;
|
||||
public String gameSlug;
|
||||
public List<String> modLoaders;
|
||||
public String gameName;
|
||||
public String portalName;
|
||||
//public Date dateModified;
|
||||
//public Date dateCreated;
|
||||
//public Date dateReleased;
|
||||
public Boolean isAvailable;
|
||||
public Boolean isExperimental;
|
||||
|
||||
public static class Author {
|
||||
public String name;
|
||||
public String url;
|
||||
public Integer projectId;
|
||||
public Integer id;
|
||||
public Integer userId;
|
||||
public Integer twitchId;
|
||||
}
|
||||
|
||||
public static class Attachment {
|
||||
public Integer id;
|
||||
public Integer projectId;
|
||||
public String description;
|
||||
public Boolean isDefault;
|
||||
public String thumbnailUrl;
|
||||
public String title;
|
||||
public String url;
|
||||
public Integer status;
|
||||
}
|
||||
|
||||
public static class File {
|
||||
public Integer id;
|
||||
public String displayName;
|
||||
public String fileName;
|
||||
//public Date fileDate;
|
||||
public Integer fileLength;
|
||||
public Integer releaseType;
|
||||
public Integer fileStatus;
|
||||
public String downloadUrl;
|
||||
public Boolean isAlternate;
|
||||
public Integer alternatFileId;
|
||||
public List<CurseforgeDependency> dependencies;
|
||||
public Boolean isAvailable;
|
||||
public List<CurseforgeModule> modules;
|
||||
public Long packageFingerprint;
|
||||
public List<String> gameVersion;
|
||||
public List<SortableGameVersion> sortableGameVersion;
|
||||
public String changelog;
|
||||
public Boolean hasInstallScript;
|
||||
public Boolean isCompatibleWithClient;
|
||||
public Integer categorySectionPackageType;
|
||||
public Integer restrictProjectFileAccess;
|
||||
public Integer projectStatus;
|
||||
public Integer renderCacheId;
|
||||
public Integer projectId;
|
||||
public Long packageFingerprintId;
|
||||
//public Date gameVersionDateReleased;
|
||||
public Long gameVersionMappingId;
|
||||
public Integer gameVersionId;
|
||||
public Integer gameId;
|
||||
public Boolean isServerPack;
|
||||
public List<Hash> hashes;
|
||||
|
||||
public static class SortableGameVersion {
|
||||
public String gameVersionPadded;
|
||||
public String gameVersion;
|
||||
//public Date gameVersionReleaseDate;
|
||||
public String gameVersionName;
|
||||
public Integer gameVersionTypeId;
|
||||
}
|
||||
|
||||
public static class Hash {
|
||||
public Integer algorithm;
|
||||
public String value;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Category {
|
||||
public Integer categoryId;
|
||||
public String name;
|
||||
public String url;
|
||||
public String avatarUrl;
|
||||
public Integer parentId;
|
||||
public Integer rootId;
|
||||
public Integer projectId;
|
||||
public Integer avatarId;
|
||||
public Integer gameId;
|
||||
public String slug;
|
||||
//public Date dateModified;
|
||||
}
|
||||
|
||||
public static class CategorySection {
|
||||
public Integer id;
|
||||
public Integer gameId;
|
||||
public String name;
|
||||
public Integer packageType;
|
||||
public String path;
|
||||
public String initialInclusionPattern;
|
||||
public Integer gameCategoryId;
|
||||
}
|
||||
|
||||
public static class GameVersionLatestFile {
|
||||
public String gameVersion;
|
||||
public Integer projectFileId;
|
||||
public String projectFileName;
|
||||
public Integer fileType;
|
||||
public Integer gameVersionTypeId;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package io.gitlab.jfronny.inceptum.model.curseforge;
|
||||
|
||||
public class CurseforgeModule {
|
||||
public String folderName;
|
||||
public Long fingerprint;
|
||||
public Integer type;
|
||||
}
|
|
@ -7,6 +7,7 @@ import java.util.List;
|
|||
public class ModDescription {
|
||||
public List<ModSource> sources;
|
||||
public String sha1;
|
||||
public String murmur2;
|
||||
public List<String> dependents; // by file name
|
||||
public List<String> dependencies; // by file name
|
||||
}
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
package io.gitlab.jfronny.inceptum.model.inceptum.source;
|
||||
|
||||
import io.gitlab.jfronny.inceptum.model.curseforge.CurseforgeDependency;
|
||||
import io.gitlab.jfronny.inceptum.model.curseforge.CurseforgeFile;
|
||||
import io.gitlab.jfronny.inceptum.model.curseforge.CurseforgeMod;
|
||||
import io.gitlab.jfronny.inceptum.util.HashUtils;
|
||||
import io.gitlab.jfronny.inceptum.util.Utils;
|
||||
import io.gitlab.jfronny.inceptum.util.api.CurseforgeApi;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
public final class CurseforgeModSource implements ModSource {
|
||||
private final int projectId;
|
||||
private final int fileId;
|
||||
private final CurseforgeFile current;
|
||||
|
||||
public CurseforgeModSource(int projectId, int fileId) throws IOException {
|
||||
this.projectId = projectId;
|
||||
this.fileId = fileId;
|
||||
current = CurseforgeApi.getFile(projectId, fileId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModDownload download(Path modsDir) throws IOException {
|
||||
//TODO test
|
||||
Path path = modsDir.resolve(current.fileName);
|
||||
Utils.downloadFile(current.downloadUrl.replace("edge.forgecdn.net", "media.forgecdn.net"), path);
|
||||
byte[] data = Files.readAllBytes(path);
|
||||
return new ModDownload(HashUtils.sha1(data), HashUtils.murmur2(data), path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ModSource> getDependencies() throws IOException {
|
||||
Set<ModSource> deps = new HashSet<>();
|
||||
for (CurseforgeDependency dependency : current.dependencies) {
|
||||
if (dependency.type == 3) //TODO support other types (3=required, 2=optional)
|
||||
deps.add(new CurseforgeModSource(dependency.addonId, dependency.fileId));
|
||||
}
|
||||
return deps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ModSource> getUpdate(String gameVersion) throws IOException {
|
||||
//TODO test
|
||||
for (CurseforgeMod.GameVersionLatestFile file : CurseforgeApi.getMod(projectId).gameVersionLatestFiles) {
|
||||
if (file.gameVersion.equals(gameVersion)) {
|
||||
return file.projectFileId == fileId
|
||||
? Optional.empty()
|
||||
: Optional.of(new CurseforgeModSource(projectId, file.projectFileId));
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVersion() {
|
||||
return current.displayName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(ModSource other) {
|
||||
return other instanceof CurseforgeModSource cu && cu.projectId == projectId && cu.fileId == fileId;
|
||||
}
|
||||
|
||||
public int getFileId() {
|
||||
return fileId;
|
||||
}
|
||||
|
||||
public int getProjectId() {
|
||||
return projectId;
|
||||
}
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
package io.gitlab.jfronny.inceptum.model.inceptum.source;
|
||||
|
||||
import io.gitlab.jfronny.inceptum.util.HashUtils;
|
||||
import io.gitlab.jfronny.inceptum.util.Utils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
@ -16,7 +18,8 @@ public record DirectModSource(String fileName, String url, Set<ModSource> depend
|
|||
public ModDownload download(Path modsDir) throws IOException {
|
||||
Path p = modsDir.resolve(fileName);
|
||||
Utils.downloadFile(url, p); //TODO test
|
||||
return new ModDownload(null, p);
|
||||
byte[] data = Files.readAllBytes(p);
|
||||
return new ModDownload(HashUtils.sha1(data), HashUtils.murmur2(data), p);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -33,4 +36,9 @@ public record DirectModSource(String fileName, String url, Set<ModSource> depend
|
|||
public String getVersion() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(ModSource other) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,5 +2,5 @@ package io.gitlab.jfronny.inceptum.model.inceptum.source;
|
|||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public record ModDownload(String sha1, Path file) {
|
||||
public record ModDownload(String sha1, String murmur2, Path file) {
|
||||
}
|
||||
|
|
|
@ -10,4 +10,5 @@ public interface ModSource {
|
|||
Set<ModSource> getDependencies() throws IOException;
|
||||
Optional<ModSource> getUpdate(String gameVersion) throws IOException;
|
||||
String getVersion();
|
||||
boolean equals(ModSource other);
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
package io.gitlab.jfronny.inceptum.model.inceptum.source;
|
||||
|
||||
import io.gitlab.jfronny.inceptum.Inceptum;
|
||||
import io.gitlab.jfronny.inceptum.model.modrinth.ModrinthVersion;
|
||||
import io.gitlab.jfronny.inceptum.util.HashUtils;
|
||||
import io.gitlab.jfronny.inceptum.util.Utils;
|
||||
import io.gitlab.jfronny.inceptum.util.api.ModrinthApi;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
|
@ -27,7 +27,7 @@ public final class ModrinthModSource implements ModSource {
|
|||
ModrinthVersion.File file = current.files.get(0);
|
||||
Path path = modsDir.resolve(file.filename);
|
||||
Utils.downloadFile(file.url, file.hashes.sha1, path);
|
||||
return new ModDownload(file.hashes.sha1, path);
|
||||
return new ModDownload(file.hashes.sha1, HashUtils.murmur2(Files.readAllBytes(path)), path);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -73,6 +73,11 @@ public final class ModrinthModSource implements ModSource {
|
|||
return current.version_number;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(ModSource other) {
|
||||
return other instanceof ModrinthModSource ms && ms.current.mod_id.equals(current.mod_id);
|
||||
}
|
||||
|
||||
public String getVersionId() {
|
||||
return versionId;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
package io.gitlab.jfronny.inceptum.util;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Formatter;
|
||||
|
||||
public class HashUtils {
|
||||
public static String sha1(byte[] data) {
|
||||
Formatter formatter = new Formatter();
|
||||
try {
|
||||
for (byte b : MessageDigest.getInstance("SHA-1").digest(data))
|
||||
formatter.format("%02x", b);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("Could not hash using SHA1", e);
|
||||
}
|
||||
return formatter.toString();
|
||||
}
|
||||
|
||||
public static String murmur2(byte[] data) {
|
||||
final int m = 0x5bd1e995;
|
||||
final int r = 24;
|
||||
long k = 0x0L;
|
||||
int seed = 1;
|
||||
int shift = 0x0;
|
||||
|
||||
long length = 0;
|
||||
char b;
|
||||
// get good bytes from file
|
||||
for (byte datum : data) {
|
||||
b = (char) datum;
|
||||
|
||||
if (b == 0x9 || b == 0xa || b == 0xd || b == 0x20) {
|
||||
continue;
|
||||
}
|
||||
|
||||
length += 1;
|
||||
}
|
||||
long h = (seed ^ length);
|
||||
|
||||
for (byte datum : data) {
|
||||
b = (char) datum;
|
||||
|
||||
if (b == 0x9 || b == 0xa || b == 0xd || b == 0x20) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (b > 255) {
|
||||
while (b > 255) {
|
||||
b -= 255;
|
||||
}
|
||||
}
|
||||
|
||||
k = k | ((long) b << shift);
|
||||
|
||||
shift = shift + 0x8;
|
||||
|
||||
if (shift == 0x20) {
|
||||
h = 0x00000000FFFFFFFFL & h;
|
||||
|
||||
k = k * m;
|
||||
k = 0x00000000FFFFFFFFL & k;
|
||||
|
||||
k = k ^ (k >> r);
|
||||
k = 0x00000000FFFFFFFFL & k;
|
||||
|
||||
k = k * m;
|
||||
k = 0x00000000FFFFFFFFL & k;
|
||||
|
||||
h = h * m;
|
||||
h = 0x00000000FFFFFFFFL & h;
|
||||
|
||||
h = h ^ k;
|
||||
h = 0x00000000FFFFFFFFL & h;
|
||||
|
||||
k = 0x0;
|
||||
shift = 0x0;
|
||||
}
|
||||
}
|
||||
|
||||
if (shift > 0) {
|
||||
h = h ^ k;
|
||||
h = 0x00000000FFFFFFFFL & h;
|
||||
|
||||
h = h * m;
|
||||
h = 0x00000000FFFFFFFFL & h;
|
||||
}
|
||||
|
||||
h = h ^ (h >> 13);
|
||||
h = 0x00000000FFFFFFFFL & h;
|
||||
|
||||
h = h * m;
|
||||
h = 0x00000000FFFFFFFFL & h;
|
||||
|
||||
h = h ^ (h >> 15);
|
||||
h = 0x00000000FFFFFFFFL & h;
|
||||
|
||||
return String.valueOf(h);
|
||||
}
|
||||
}
|
|
@ -13,10 +13,7 @@ import java.nio.charset.StandardCharsets;
|
|||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Formatter;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
|
@ -24,17 +21,6 @@ import java.util.regex.Pattern;
|
|||
public class Utils {
|
||||
public static final Pattern VALID_FILENAME = Pattern.compile("[a-zA-Z0-9_\\-.][a-zA-Z0-9 _\\-.]*[a-zA-Z0-9_\\-.]");
|
||||
|
||||
public static String hash(byte[] data) {
|
||||
Formatter formatter = new Formatter();
|
||||
try {
|
||||
for (byte b : MessageDigest.getInstance("SHA-1").digest(data))
|
||||
formatter.format("%02x", b);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("Could not hash using SHA1", e);
|
||||
}
|
||||
return formatter.toString();
|
||||
}
|
||||
|
||||
public static byte[] downloadData(String url) throws IOException {
|
||||
try (InputStream is = HttpUtils.get(url).sendInputStream()) {
|
||||
return is.readAllBytes();
|
||||
|
@ -44,7 +30,7 @@ public class Utils {
|
|||
public static byte[] downloadData(String url, String sha1) throws IOException {
|
||||
byte[] buf = downloadData(url);
|
||||
if (sha1 == null) return buf;
|
||||
if (!Utils.hash(buf).equals(sha1)) throw new IOException("Invalid hash");
|
||||
if (!HashUtils.sha1(buf).equals(sha1)) throw new IOException("Invalid hash");
|
||||
return buf;
|
||||
}
|
||||
|
||||
|
@ -169,16 +155,6 @@ public class Utils {
|
|||
public static void copyRecursive(Path source, Path destination, CopyOption... copyOptions) throws IOException {
|
||||
if (!Files.exists(destination)) Files.createDirectories(destination);
|
||||
if(Files.isDirectory(source)) {
|
||||
/*Files.walk(source)
|
||||
.forEach(sourcePath -> {
|
||||
try {
|
||||
Path destinationPath = destination.resolve(source.getFileName().toString());
|
||||
Path targetPath = destinationPath.resolve(source.relativize(sourcePath).toString());
|
||||
Files.copy(sourcePath, targetPath, copyOptions);
|
||||
} catch (IOException e) {
|
||||
Inceptum.LOGGER.error("Could not recursively copy", e);
|
||||
}
|
||||
});*/
|
||||
Files.list(source).forEach(sourcePath -> {
|
||||
try {
|
||||
copyRecursive(sourcePath, destination.resolve(sourcePath.getFileName().toString()), copyOptions);
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
package io.gitlab.jfronny.inceptum.util.api;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import io.gitlab.jfronny.inceptum.model.curseforge.CurseforgeFile;
|
||||
import io.gitlab.jfronny.inceptum.model.curseforge.CurseforgeFingerprint;
|
||||
import io.gitlab.jfronny.inceptum.model.curseforge.CurseforgeMod;
|
||||
import io.gitlab.jfronny.inceptum.util.HttpUtils;
|
||||
import io.gitlab.jfronny.inceptum.util.Utils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class CurseforgeApi {
|
||||
private static final String API_URL = "https://addons-ecs.forgesvc.net/api/v2/";
|
||||
private static final Type curseforgeModListType = new TypeToken<List<CurseforgeMod>>() {}.getType();
|
||||
private static final int pageSize = 20;
|
||||
|
||||
//TODO use gameVersion
|
||||
public static List<CurseforgeMod> search(String gameVersion, String query, int page, String sort) throws IOException {
|
||||
return Utils.downloadObject(Utils.buildUrl(API_URL, "addon/search", Map.of(
|
||||
"gameId", "432", // minecraft
|
||||
"modLoaderType", "4", // fabric, forge would be 1
|
||||
"sectionId", "6", // mods
|
||||
"searchFilter", query,
|
||||
"sort", sort,
|
||||
"sortDescending", "true",
|
||||
"pageSize", Integer.toString(pageSize),
|
||||
"index", Integer.toString(page * pageSize)
|
||||
)), curseforgeModListType);
|
||||
}
|
||||
|
||||
public static CurseforgeMod getMod(int id) throws IOException {
|
||||
return Utils.downloadObject(API_URL + "addon/" + id, CurseforgeMod.class);
|
||||
}
|
||||
|
||||
public static CurseforgeFile getFile(int modId, int fileId) throws IOException {
|
||||
return Utils.downloadObject(API_URL + "addon/" + modId + "/file/" + fileId, CurseforgeFile.class);
|
||||
}
|
||||
|
||||
public static CurseforgeFingerprint checkFingerprint(String hash) {
|
||||
return HttpUtils.post(API_URL + "fingerprint").bodyJson("[" + hash + "]").sendJson(CurseforgeFingerprint.class);
|
||||
}
|
||||
}
|
|
@ -16,12 +16,12 @@ public class ModrinthApi {
|
|||
private static final int ITEMS_PER_PAGE = 20;
|
||||
private static final Type modrinthVersionListType = new TypeToken<List<ModrinthVersion>>() {}.getType();
|
||||
|
||||
//TODO search by categories
|
||||
//TODO search by categories: facets:[["versions:$ver","versions:$ver"],["categories:$cat","categories:$cat"]]
|
||||
public static ModrinthSearchResult search(String query, int page, String version) throws IOException {
|
||||
return Utils.downloadObject(Utils.buildUrl(API_HOST, "api/v1/mod", Map.of(
|
||||
"query", query,
|
||||
"filters", "categories=\"fabric\"",
|
||||
//"version", "version=\"" + version + "\"",
|
||||
"facets", "[[\"versions:" + version + "\"]]",
|
||||
"index", "relevance",
|
||||
"offset", Integer.toString(page * ITEMS_PER_PAGE),
|
||||
"limit", Integer.toString(ITEMS_PER_PAGE)
|
||||
|
@ -39,4 +39,8 @@ public class ModrinthApi {
|
|||
public static ModrinthVersion getVersion(String id) throws IOException {
|
||||
return Utils.downloadObject(API_HOST + "api/v1/version/" + id, ModrinthVersion.class);
|
||||
}
|
||||
|
||||
public static ModrinthVersion getVersionByHash(String sha1) throws IOException {
|
||||
return Utils.downloadObject(API_HOST + "api/v1/version_file/" + sha1 + "?algorithm=sha1", ModrinthVersion.class);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,14 +4,18 @@ import imgui.ImGui;
|
|||
import imgui.flag.ImGuiTableFlags;
|
||||
import imgui.type.ImString;
|
||||
import io.gitlab.jfronny.inceptum.Inceptum;
|
||||
import io.gitlab.jfronny.inceptum.model.curseforge.CurseforgeFingerprint;
|
||||
import io.gitlab.jfronny.inceptum.model.curseforge.CurseforgeMod;
|
||||
import io.gitlab.jfronny.inceptum.model.inceptum.InstanceMeta;
|
||||
import io.gitlab.jfronny.inceptum.model.inceptum.ModDescription;
|
||||
import io.gitlab.jfronny.inceptum.model.inceptum.source.CurseforgeModSource;
|
||||
import io.gitlab.jfronny.inceptum.model.inceptum.source.ModDownload;
|
||||
import io.gitlab.jfronny.inceptum.model.inceptum.source.ModSource;
|
||||
import io.gitlab.jfronny.inceptum.model.inceptum.source.ModrinthModSource;
|
||||
import io.gitlab.jfronny.inceptum.model.modrinth.ModrinthSearchResult;
|
||||
import io.gitlab.jfronny.inceptum.model.modrinth.ModrinthVersion;
|
||||
import io.gitlab.jfronny.inceptum.util.Utils;
|
||||
import io.gitlab.jfronny.inceptum.util.api.CurseforgeApi;
|
||||
import io.gitlab.jfronny.inceptum.util.api.ModrinthApi;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -20,23 +24,28 @@ import java.net.URISyntaxException;
|
|||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class AddModWindow extends Window {
|
||||
private final ImString query = new ImString("", 128);
|
||||
private final Path modsDir;
|
||||
private final InstanceMeta instance;
|
||||
private final Map<Path, InstanceEditWindow.IWModDescription> descriptions;
|
||||
private int page = 0;
|
||||
private ModrinthSearchResult mr = null;
|
||||
public AddModWindow(Path modsDir, InstanceMeta instance) {
|
||||
private List<CurseforgeMod> cf = null;
|
||||
public AddModWindow(Path modsDir, InstanceMeta instance, Map<Path, InstanceEditWindow.IWModDescription> descriptions) {
|
||||
super(modsDir.getParent().getFileName().toString() + " - Add Mods");
|
||||
this.modsDir = modsDir;
|
||||
this.instance = instance;
|
||||
this.descriptions = descriptions;
|
||||
}
|
||||
|
||||
private void refreshMR() throws IOException {
|
||||
mr = ModrinthApi.search(query.get(), page, instance.version);
|
||||
mr = ModrinthApi.search(query.get(), page, instance.getMinecraftVersion());
|
||||
cf = CurseforgeApi.search(instance.getMinecraftVersion(), query.get(), page, "Popularity");
|
||||
//TODO move to thread maybe
|
||||
if (mr.hits.isEmpty()) page = 0;
|
||||
//if (mr.hits.isEmpty()) page = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -46,7 +55,7 @@ public class AddModWindow extends Window {
|
|||
refreshMR();
|
||||
}
|
||||
if (mr != null) {
|
||||
boolean hasNext = mr.offset + mr.hits.size() < mr.limit;
|
||||
boolean hasNext = (mr.offset + mr.hits.size() < mr.limit) || cf.size() == 20;
|
||||
if (page > 0 && ImGui.button("Previous Page")) {
|
||||
page--;
|
||||
refreshMR();
|
||||
|
@ -57,50 +66,89 @@ public class AddModWindow extends Window {
|
|||
page++;
|
||||
refreshMR();
|
||||
}
|
||||
if (ImGui.beginTable("AddMod" + modsDir.toString(), 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.Borders)) {
|
||||
for (ModrinthSearchResult.ModResult mod : mr.hits) {
|
||||
String modId = (mod.slug != null ? mod.slug : mod.mod_id);
|
||||
final String idPrefix = "local-";
|
||||
if (mod.mod_id.startsWith(idPrefix))
|
||||
mod.mod_id = mod.mod_id.substring(idPrefix.length());
|
||||
//TODO detail view
|
||||
ImGui.tableNextColumn();
|
||||
ImGui.text(mod.title);
|
||||
ImGui.tableNextColumn();
|
||||
ImGui.text(mod.description);
|
||||
ImGui.tableNextColumn();
|
||||
//TODO check if already added
|
||||
if (ImGui.button("Add##" + mod.mod_id)) {
|
||||
ModrinthVersion stable = null;
|
||||
ModrinthVersion beta = null;
|
||||
ModrinthVersion latest = null;
|
||||
for (ModrinthVersion version : ModrinthApi.getVersions(mod.mod_id)) {
|
||||
//TODO sort versions
|
||||
if (version.game_versions.contains(instance.getMinecraftVersion()) && version.loaders.contains("fabric")) {
|
||||
if (latest == null) latest = version;
|
||||
if (version.version_type == ModrinthVersion.VersionType.beta || version.version_type == ModrinthVersion.VersionType.release) {
|
||||
beta = version;
|
||||
if (ImGui.beginTabBar("ModsSelect")) {
|
||||
if (ImGui.beginTabItem("Modrinth")) {
|
||||
if (ImGui.beginTable("mods" + modsDir, 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.Borders)) {
|
||||
for (ModrinthSearchResult.ModResult mod : mr.hits) {
|
||||
String modId = (mod.slug != null ? mod.slug : mod.mod_id);
|
||||
final String idPrefix = "local-";
|
||||
if (mod.mod_id.startsWith(idPrefix))
|
||||
mod.mod_id = mod.mod_id.substring(idPrefix.length());
|
||||
//TODO detail view
|
||||
ImGui.tableNextColumn();
|
||||
ImGui.text(mod.title);
|
||||
ImGui.tableNextColumn();
|
||||
ImGui.text(mod.description);
|
||||
ImGui.tableNextColumn();
|
||||
//TODO check if already added
|
||||
if (ImGui.button("Add##" + mod.mod_id)) {
|
||||
ModrinthVersion stable = null;
|
||||
ModrinthVersion beta = null;
|
||||
ModrinthVersion latest = null;
|
||||
for (ModrinthVersion version : ModrinthApi.getVersions(mod.mod_id)) {
|
||||
//TODO sort versions
|
||||
if (version.game_versions.contains(instance.getMinecraftVersion()) && version.loaders.contains("fabric")) {
|
||||
if (latest == null) latest = version;
|
||||
if (version.version_type == ModrinthVersion.VersionType.beta || version.version_type == ModrinthVersion.VersionType.release) {
|
||||
beta = version;
|
||||
}
|
||||
if (version.version_type == ModrinthVersion.VersionType.release) {
|
||||
stable = version;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (version.version_type == ModrinthVersion.VersionType.release) {
|
||||
stable = version;
|
||||
if (stable != null) beta = stable;
|
||||
if (beta != null) latest = beta;
|
||||
if (latest == null) {
|
||||
Inceptum.showError("No valid version could be identified for this mod", "No version found");
|
||||
}
|
||||
else {
|
||||
download(new ModrinthModSource(latest.id), modsDir, descriptions).write();
|
||||
}
|
||||
}
|
||||
ImGui.sameLine();
|
||||
if (ImGui.button("Web##" + mod.mod_id)) {
|
||||
Utils.openWebBrowser(new URI("https://modrinth.com/mod/" + modId));
|
||||
}
|
||||
}
|
||||
if (stable != null) beta = stable;
|
||||
if (beta != null) latest = beta;
|
||||
if (latest == null) {
|
||||
Inceptum.showError("No valid version could be identified for this mod", "Nov version found");
|
||||
}
|
||||
else {
|
||||
download(new ModrinthModSource(latest.id), modsDir).write();
|
||||
}
|
||||
}
|
||||
ImGui.sameLine();
|
||||
if (ImGui.button("Web##" + mod.mod_id)) {
|
||||
Utils.openWebBrowser(new URI("https://modrinth.com/mod/" + modId));
|
||||
ImGui.endTable();
|
||||
}
|
||||
ImGui.endTabItem();
|
||||
}
|
||||
ImGui.endTable();
|
||||
if (ImGui.beginTabItem("Curseforge")) {
|
||||
if (ImGui.beginTable("curseforge" + modsDir, 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.Borders)) {
|
||||
for (CurseforgeMod mod : cf) {
|
||||
//TODO detail view
|
||||
ImGui.tableNextColumn();
|
||||
ImGui.text(mod.name);
|
||||
ImGui.tableNextColumn();
|
||||
ImGui.text(mod.summary);
|
||||
ImGui.tableNextColumn();
|
||||
//TODO check if already added
|
||||
if (ImGui.button("Add##" + mod.id)) {
|
||||
CurseforgeMod.GameVersionLatestFile latest = null;
|
||||
for (CurseforgeMod.GameVersionLatestFile file : mod.gameVersionLatestFiles) {
|
||||
if (file.gameVersion.equals(instance.getMinecraftVersion())) {
|
||||
if (latest == null) latest = file;
|
||||
}
|
||||
}
|
||||
if (latest == null) {
|
||||
Inceptum.showError("No valid version could be identified for this mod", "No version found");
|
||||
}
|
||||
else {
|
||||
download(new CurseforgeModSource(mod.id, latest.projectFileId), modsDir, descriptions).write();
|
||||
}
|
||||
}
|
||||
ImGui.sameLine();
|
||||
if (ImGui.button("Web##" + mod.id)) {
|
||||
Utils.openWebBrowser(new URI(mod.websiteUrl));
|
||||
}
|
||||
}
|
||||
ImGui.endTable();
|
||||
}
|
||||
ImGui.endTabItem();
|
||||
}
|
||||
ImGui.endTabBar();
|
||||
}
|
||||
}
|
||||
} catch (IOException | URISyntaxException e) {
|
||||
|
@ -108,16 +156,44 @@ public class AddModWindow extends Window {
|
|||
}
|
||||
}
|
||||
|
||||
public static DownloadMeta download(ModSource ms, Path modsDir) throws IOException {
|
||||
public static DownloadMeta download(ModSource ms, Path modsDir, Map<Path, InstanceEditWindow.IWModDescription> descriptions) throws IOException {
|
||||
for (InstanceEditWindow.IWModDescription value : descriptions.values()) {
|
||||
for (ModSource source : value.mod().sources) {
|
||||
if (ms.equals(source)) {
|
||||
return new DownloadMeta(new ModDownload(value.mod().sha1, value.mod().murmur2, value.path()), value.mod(), source);
|
||||
}
|
||||
}
|
||||
}
|
||||
ModDownload md = ms.download(modsDir);
|
||||
ModDescription manifest = new ModDescription();
|
||||
manifest.sources = List.of(ms); //TODO discover other sources;
|
||||
manifest.sha1 = md.sha1();
|
||||
manifest.murmur2 = md.murmur2();
|
||||
manifest.sources = new ArrayList<>();
|
||||
manifest.sources.add(ms);
|
||||
if (!(ms instanceof ModrinthModSource)) {
|
||||
try {
|
||||
manifest.sources.add(new ModrinthModSource(ModrinthApi.getVersionByHash(manifest.sha1).id));
|
||||
}
|
||||
catch (IOException e) {
|
||||
// not found
|
||||
}
|
||||
}
|
||||
if (!(ms instanceof CurseforgeModSource)) {
|
||||
try {
|
||||
CurseforgeFingerprint cf = CurseforgeApi.checkFingerprint(manifest.murmur2);
|
||||
if (!cf.exactMatches.isEmpty()) {
|
||||
CurseforgeFingerprint.Mod f = cf.exactMatches.get(0);
|
||||
manifest.sources.add(new CurseforgeModSource(f.id, f.file.id));
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
// not found
|
||||
}
|
||||
}
|
||||
manifest.dependents = new ArrayList<>();
|
||||
manifest.dependencies = new ArrayList<>();
|
||||
for (ModSource dependency : ms.getDependencies()) {
|
||||
//TODO check if an dependency for this mod already exists
|
||||
DownloadMeta depMan = download(dependency, modsDir);
|
||||
DownloadMeta depMan = download(dependency, modsDir, descriptions);
|
||||
depMan.description.dependents.add(md.file().getFileName().toString());
|
||||
manifest.dependencies.add(depMan.download.file().getFileName().toString());
|
||||
depMan.write();
|
||||
|
|
|
@ -33,7 +33,7 @@ public class InstanceEditWindow extends Window {
|
|||
private Path selected = null;
|
||||
private boolean reDownload = false;
|
||||
|
||||
private static record IWModDescription(ModDescription mod, FabricModJson fmj, Path path, Path imod, Optional<ModSource> update) {
|
||||
public static record IWModDescription(ModDescription mod, FabricModJson fmj, Path path, Path imod, Optional<ModSource> update) {
|
||||
public String getName() {
|
||||
if (fmj == null) return path.getFileName().toString();
|
||||
String base;
|
||||
|
@ -97,10 +97,11 @@ public class InstanceEditWindow extends Window {
|
|||
ImGui.text("Did you know that every instance in Inceptum is a git repository?");
|
||||
ImGui.endTabItem();
|
||||
}
|
||||
//TODO update all
|
||||
if (instance.isFabric() && Files.exists(path.resolve("mods")) && ImGui.beginTabItem("Mods")) {
|
||||
ImGui.beginChild("mods select", 200, 0);
|
||||
if (ImGui.button("Add")) {
|
||||
InceptumGui.WINDOWS.add(new AddModWindow(path.resolve("mods"), instance));
|
||||
InceptumGui.WINDOWS.add(new AddModWindow(path.resolve("mods"), instance, descriptions));
|
||||
}
|
||||
ImGui.separator();
|
||||
try {
|
||||
|
@ -156,7 +157,7 @@ public class InstanceEditWindow extends Window {
|
|||
}
|
||||
if (md.update.isPresent() && ImGui.button("Update to " + md.update.get().getVersion())) {
|
||||
try {
|
||||
AddModWindow.download(md.update.get(), path.resolve("mods")).write();
|
||||
AddModWindow.download(md.update.get(), path.resolve("mods"), descriptions).write();
|
||||
Files.delete(md.path);
|
||||
if (md.imod != null && Files.exists(md.imod)) Files.delete(md.imod);
|
||||
} catch (IOException e) {
|
||||
|
@ -164,18 +165,22 @@ public class InstanceEditWindow extends Window {
|
|||
}
|
||||
}
|
||||
if (ImGui.button("Delete")) {
|
||||
try {
|
||||
Files.delete(md.path);
|
||||
if (md.imod != null && Files.exists(md.imod)) Files.delete(md.imod);
|
||||
} catch (IOException e) {
|
||||
Inceptum.showError("Couldn't delete the file", e);
|
||||
if (!md.mod.dependents.isEmpty()) {
|
||||
Inceptum.showError("This mod still has the following dependent mods installed: " + String.join(", ", md.mod.dependents), "Dependents present");
|
||||
}
|
||||
else {
|
||||
try {
|
||||
delete(md);
|
||||
} catch (IOException e) {
|
||||
Inceptum.showError("Couldn't delete the file", e);
|
||||
}
|
||||
selected = null;
|
||||
}
|
||||
selected = null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
ImGui.text("This mod was added manually");
|
||||
ImGui.text("Automatic discovery will be added soon. Maybe."); //TODO
|
||||
ImGui.text("Its hash was also not found on modrinth or curseforge.");
|
||||
}
|
||||
}
|
||||
ImGui.endGroup();
|
||||
|
@ -185,6 +190,18 @@ public class InstanceEditWindow extends Window {
|
|||
}
|
||||
}
|
||||
|
||||
private void delete(IWModDescription md) throws IOException {
|
||||
Files.delete(md.path);
|
||||
if (md.imod != null && Files.exists(md.imod)) Files.delete(md.imod);
|
||||
for (String dependency : md.mod.dependencies) {
|
||||
Path dep = path.resolve("mods").resolve(dependency);
|
||||
IWModDescription dmd = descriptions.get(dep);
|
||||
dmd.mod.dependencies.remove(md.path.getFileName().toString());
|
||||
if (dmd.mod.dependencies.isEmpty()) delete(dmd);
|
||||
else Utils.writeObject(dmd.imod, dmd.mod);
|
||||
}
|
||||
}
|
||||
|
||||
private void save() {
|
||||
try {
|
||||
Utils.writeObject(path.resolve("instance.json"), instance);
|
||||
|
|
Loading…
Reference in New Issue