CurseForge mod support

This commit is contained in:
JFronny 2021-11-02 19:52:35 +01:00
parent d60137a531
commit 0ac146e3ab
No known key found for this signature in database
GPG Key ID: BEC5ACBBD4EE17E5
18 changed files with 603 additions and 89 deletions

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,7 @@
package io.gitlab.jfronny.inceptum.model.curseforge;
public class CurseforgeModule {
public String folderName;
public Long fingerprint;
public Integer type;
}

View File

@ -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
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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) {
}

View 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);
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();

View File

@ -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);