diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java index 792c3deb7..b729b040f 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java @@ -987,6 +987,12 @@ public final class Tools { return prefixedName.substring(Tools.LAUNCHERPROFILES_RTPREFIX.length()); } + public static String getFileName(String pathOrUrl) { + int lastSlashIndex = pathOrUrl.lastIndexOf('/'); + if(lastSlashIndex == -1) return null; + return pathOrUrl.substring(lastSlashIndex); + } + public static String getSelectedRuntime(MinecraftProfile minecraftProfile) { String runtime = LauncherPreferences.PREF_DEFAULT_RUNTIME; String profileRuntime = getRuntimeName(minecraftProfile.javaDir); diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/CurseforgeApi.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/CurseforgeApi.java index 8773c81ba..29e825666 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/CurseforgeApi.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/CurseforgeApi.java @@ -5,14 +5,16 @@ import android.util.Log; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.kdt.mcgui.ProgressLayout; +import net.kdt.pojavlaunch.R; import net.kdt.pojavlaunch.Tools; import net.kdt.pojavlaunch.modloaders.modpacks.models.Constants; import net.kdt.pojavlaunch.modloaders.modpacks.models.CurseManifest; import net.kdt.pojavlaunch.modloaders.modpacks.models.ModDetail; import net.kdt.pojavlaunch.modloaders.modpacks.models.ModItem; -import net.kdt.pojavlaunch.modloaders.modpacks.models.ModrinthIndex; import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchFilters; +import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper; import net.kdt.pojavlaunch.utils.ZipUtils; import java.io.File; @@ -31,6 +33,8 @@ public class CurseforgeApi implements ModpackApi{ // https://api.curseforge.com/v1/categories?gameId=432 and search for "Mods" (case-sensitive) private static final int CURSEFORGE_MOD_CLASS_ID = 6; private static final int CURSEFORGE_PAGINATION_SIZE = 50; + private static final int CURSEFORGE_PAGINATION_END_REACHED = -1; + private static final int CURSEFORGE_PAGINATION_ERROR = -2; private final ApiHandler mApiHandler = new ApiHandler("https://api.curseforge.com/v1", "$2a$10$Vxkj4kH1Ekf8EsS4Mx8b2eVTHsht107Lk2erVEUtnbqvojsLy.jYq"); @@ -71,10 +75,11 @@ public class CurseforgeApi implements ModpackApi{ public ModDetail getModDetails(ModItem item) { ArrayList allModDetails = new ArrayList<>(); int index = 0; - while(index != -1 && index != -2) { + while(index != CURSEFORGE_PAGINATION_END_REACHED && + index != CURSEFORGE_PAGINATION_ERROR) { index = getPaginatedDetails(allModDetails, index, item.id); } - if(index == -2) return null; + if(index == CURSEFORGE_PAGINATION_ERROR) return null; int length = allModDetails.size(); String[] versionNames = new String[length]; String[] mcVersionNames = new String[length]; @@ -99,8 +104,8 @@ public class CurseforgeApi implements ModpackApi{ @Override public void installMod(ModDetail modDetail, int selectedVersion) { - String versionUrl = modDetail.versionUrls[selectedVersion]; - File modpackFile = new File(Tools.DIR_CACHE, modDetail.id+".zip"); + //TODO considering only modpacks for now + ModpackInstaller.installModpack(modDetail, selectedVersion, this::installCurseforgeZip); } @@ -109,16 +114,16 @@ public class CurseforgeApi implements ModpackApi{ params.put("index", index); params.put("pageSize", CURSEFORGE_PAGINATION_SIZE); JsonObject response = mApiHandler.get("mods/"+modId+"/files", params, JsonObject.class); - if(response == null) return -2; + if(response == null) return CURSEFORGE_PAGINATION_ERROR; JsonArray data = response.getAsJsonArray("data"); - if(data == null) return -2; + if(data == null) return CURSEFORGE_PAGINATION_ERROR; for(int i = 0; i < data.size(); i++) { JsonObject fileInfo = data.get(i).getAsJsonObject(); if(fileInfo.get("isServerPack").getAsBoolean()) continue; objectList.add(fileInfo); } if(data.size() < CURSEFORGE_PAGINATION_SIZE) { - return -1; // we read the remainder! yay! + return CURSEFORGE_PAGINATION_END_REACHED; // we read the remainder! yay! } return index + data.size(); } @@ -128,7 +133,72 @@ public class CurseforgeApi implements ModpackApi{ CurseManifest curseManifest = Tools.GLOBAL_GSON.fromJson( Tools.read(ZipUtils.getEntryStream(modpackZipFile, "manifest.json")), CurseManifest.class); + if(!verifyManifest(curseManifest)) { + Log.i("CurseforgeApi","manifest verification failed"); + return null; + } + ModDownloader modDownloader = new ModDownloader(new File(instanceDestination,"mods"), true); + int fileCount = curseManifest.files.length; + for(int i = 0; i < fileCount; i++) { + CurseManifest.CurseFile curseFile = curseManifest.files[i]; + String downloadUrl = getDownloadUrl(curseFile.projectID, curseFile.fileID); + if(downloadUrl == null && curseFile.required) throw new IOException("Failed to obtain download URL for "+curseFile.projectID+" "+curseFile.fileID); + else if(downloadUrl == null) continue; + modDownloader.submitDownload(Tools.getFileName(downloadUrl), downloadUrl); + ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, (int) Math.max((float)i/fileCount*100,0), R.string.modpack_download_getting_links, i, fileCount); + } + modDownloader.awaitFinish((c,m)->{ // insert joke about semen + ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, (int) Math.max((float)c/m*100,0), R.string.modpack_download_downloading_mods_fc, c, m); + }); + String overridesDir = "overrides"; + if(curseManifest.overrides != null) overridesDir = curseManifest.overrides; + ZipUtils.zipExtract(modpackZipFile, overridesDir, instanceDestination); + return createInfo(curseManifest.minecraft); } - return null; + } + + private ModLoaderInfo createInfo(CurseManifest.CurseMinecraft minecraft) { + CurseManifest.CurseModLoader primaryModLoader = null; + for(CurseManifest.CurseModLoader modLoader : minecraft.modLoaders) { + if(modLoader.primary) { + primaryModLoader = modLoader; + break; + } + } + if(primaryModLoader == null) primaryModLoader = minecraft.modLoaders[0]; + String modLoaderId = primaryModLoader.id; + int dashIndex = modLoaderId.indexOf('-'); + String modLoaderName = modLoaderId.substring(0, dashIndex); + String modLoaderVersion = modLoaderId.substring(dashIndex+1); + int modLoaderTypeInt = -1; + switch (modLoaderName) { + case "forge": + modLoaderTypeInt = ModLoaderInfo.MOD_LOADER_FORGE; + break; + case "fabric": + modLoaderTypeInt = ModLoaderInfo.MOD_LOADER_FABRIC; + break; + //TODO: Quilt is also Forge? How does that work? + } + if(modLoaderTypeInt == -1) return null; + return new ModLoaderInfo(modLoaderTypeInt, modLoaderVersion, minecraft.version); + } + + private String getDownloadUrl(long projectID, long fileID) { + JsonObject response = mApiHandler.get("mods/"+projectID+"/files/"+fileID+"/download-url", JsonObject.class); + if(response == null) return null; + JsonElement data = response.get("data"); + if(data == null || data.isJsonNull()) return null; + return data.getAsString(); + } + + private boolean verifyManifest(CurseManifest manifest) { + if(!"minecraftModpack".equals(manifest.manifestType)) return false; + if(manifest.manifestVersion != 1) return false; + if(manifest.minecraft == null) return false; + if(manifest.minecraft.version == null) return false; + if(manifest.minecraft.modLoaders == null) return false; + if(manifest.minecraft.modLoaders.length < 1) return false; + return true; } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModDownloader.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModDownloader.java index b9e790294..505df2944 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModDownloader.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModDownloader.java @@ -20,15 +20,28 @@ public class ModDownloader { private final AtomicLong mDownloadSize = new AtomicLong(0); private final Object mExceptionSyncPoint = new Object(); private final File mDestinationDirectory; + private final boolean mUseFileCount; private IOException mFirstIOException; private long mTotalSize; public ModDownloader(File destinationDirectory) { + this(destinationDirectory, false); + } + + public ModDownloader(File destinationDirectory, boolean useFileCount) { this.mDestinationDirectory = destinationDirectory; + this.mUseFileCount = useFileCount; } public void submitDownload(int fileSize, String relativePath, String... url) { - mTotalSize += fileSize; + if(mUseFileCount) mTotalSize += 1; + else mTotalSize += fileSize; + mDownloadPool.execute(new DownloadTask(url, new File(mDestinationDirectory, relativePath))); + } + + public void submitDownload(String relativePath, String... url) { + if(!mUseFileCount) throw new RuntimeException("This method can only be used in a file-counting ModDownloader"); + mTotalSize += 1; mDownloadPool.execute(new DownloadTask(url, new File(mDestinationDirectory, relativePath))); } @@ -95,6 +108,7 @@ public class ModDownloader { for (int i = 0; i < 5; i++) { try { DownloadUtils.downloadFileMonitored(sourceUrl, mDestination, getThreadLocalBuffer(), this); + if(mUseFileCount) mDownloadSize.addAndGet(1); return null; } catch (InterruptedIOException e) { throw new InterruptedException(); @@ -102,14 +116,17 @@ public class ModDownloader { e.printStackTrace(); exception = e; } - mDownloadSize.addAndGet(-last); - last = 0; + if(!mUseFileCount) { + mDownloadSize.addAndGet(-last); + last = 0; + } } return exception; } @Override public void updateProgress(int curr, int max) { + if(mUseFileCount) return; mDownloadSize.addAndGet(curr - last); last = curr; } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModLoaderInfo.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModLoaderInfo.java index 562f6bc34..9464573b6 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModLoaderInfo.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModLoaderInfo.java @@ -1,5 +1,10 @@ package net.kdt.pojavlaunch.modloaders.modpacks.api; +import android.content.Context; +import android.content.Intent; + +import net.kdt.pojavlaunch.JavaGUILauncherActivity; + public class ModLoaderInfo { public static final int MOD_LOADER_FORGE = 0; public static final int MOD_LOADER_FABRIC = 1; @@ -21,7 +26,7 @@ public class ModLoaderInfo { case MOD_LOADER_FABRIC: return "fabric-loader-"+modLoaderVersion+"-"+minecraftVersion; case MOD_LOADER_QUILT: - // TODO + throw new RuntimeException("Quilt is gay af"); default: return null; } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModpackInstaller.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModpackInstaller.java new file mode 100644 index 000000000..3deaa1ec4 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModpackInstaller.java @@ -0,0 +1,61 @@ +package net.kdt.pojavlaunch.modloaders.modpacks.api; + +import com.kdt.mcgui.ProgressLayout; + +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.Tools; +import net.kdt.pojavlaunch.modloaders.modpacks.imagecache.ModIconCache; +import net.kdt.pojavlaunch.modloaders.modpacks.models.ModDetail; +import net.kdt.pojavlaunch.progresskeeper.DownloaderProgressWrapper; +import net.kdt.pojavlaunch.utils.DownloadUtils; +import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles; +import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile; + +import java.io.File; +import java.io.IOException; +import java.util.Locale; + +public class ModpackInstaller { + + public static void installModpack(ModDetail modDetail, int selectedVersion, InstallFunction installFunction) { + String versionUrl = modDetail.versionUrls[selectedVersion]; + String modpackName = modDetail.title.toLowerCase(Locale.ROOT).trim().replace(" ", "_" ); + + // Build a new minecraft instance, folder first + + // Get the modpack file + File modpackFile = new File(Tools.DIR_CACHE, modpackName + ".cf"); // Cache File + ModLoaderInfo modLoaderInfo; + try { + byte[] downloadBuffer = new byte[8192]; + DownloadUtils.downloadFileMonitored(versionUrl, modpackFile, downloadBuffer, + new DownloaderProgressWrapper(R.string.modpack_download_downloading_metadata, + ProgressLayout.INSTALL_MODPACK)); + // Install the modpack + modLoaderInfo = installFunction.installModpack(modpackFile, new File(Tools.DIR_GAME_HOME, "custom_instances/"+modpackName)); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + modpackFile.delete(); + ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK); + } + if(modLoaderInfo == null) { + return; + } + + // Create the instance + MinecraftProfile profile = new MinecraftProfile(); + profile.gameDir = "./custom_instances/" + modpackName; + profile.name = modDetail.title; + profile.lastVersionId = modLoaderInfo.getVersionId(); + profile.icon = ModIconCache.getBase64Image(modDetail.getIconCacheTag()); + + + LauncherProfiles.mainProfileJson.profiles.put(modpackName, profile); + LauncherProfiles.update(); + } + + interface InstallFunction { + ModLoaderInfo installModpack(File modpackFile, File instanceDestination) throws IOException; + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModrinthApi.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModrinthApi.java index 04951f51a..580e44f2c 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModrinthApi.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModrinthApi.java @@ -6,22 +6,17 @@ import com.kdt.mcgui.ProgressLayout; import net.kdt.pojavlaunch.R; import net.kdt.pojavlaunch.Tools; -import net.kdt.pojavlaunch.modloaders.modpacks.imagecache.ModIconCache; import net.kdt.pojavlaunch.modloaders.modpacks.models.Constants; import net.kdt.pojavlaunch.modloaders.modpacks.models.ModDetail; import net.kdt.pojavlaunch.modloaders.modpacks.models.ModItem; import net.kdt.pojavlaunch.modloaders.modpacks.models.ModrinthIndex; import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchFilters; import net.kdt.pojavlaunch.progresskeeper.DownloaderProgressWrapper; -import net.kdt.pojavlaunch.utils.DownloadUtils; import net.kdt.pojavlaunch.utils.ZipUtils; -import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles; -import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile; import java.io.File; import java.io.IOException; import java.util.HashMap; -import java.util.Locale; import java.util.Map; import java.util.zip.ZipFile; @@ -93,41 +88,7 @@ public class ModrinthApi implements ModpackApi{ @Override public void installMod(ModDetail modDetail, int selectedVersion) { //TODO considering only modpacks for now - String versionUrl = modDetail.versionUrls[selectedVersion]; - String modpackName = modDetail.title.toLowerCase(Locale.ROOT).trim().replace(" ", "_" ); - - // Build a new minecraft instance, folder first - - // Get the mrpack - File modpackFile = new File(Tools.DIR_CACHE, modpackName + ".mrpack"); - ModLoaderInfo modLoaderInfo; - try { - byte[] downloadBuffer = new byte[8192]; - DownloadUtils.downloadFileMonitored(versionUrl, modpackFile, downloadBuffer, - new DownloaderProgressWrapper(R.string.modpack_download_downloading_metadata, - ProgressLayout.INSTALL_MODPACK)); - ModrinthIndex modrinthIndex = installMrpack(modpackFile, new File(Tools.DIR_GAME_HOME, "custom_instances/"+modpackName)); - modLoaderInfo = createInfo(modrinthIndex); - } catch (IOException e) { - throw new RuntimeException(e); - } finally { - modpackFile.delete(); - ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK); - } - if(modLoaderInfo == null) { - return; - } - - // Create the instance - MinecraftProfile profile = new MinecraftProfile(); - profile.gameDir = "./custom_instances/" + modpackName; - profile.name = modDetail.title; - profile.lastVersionId = modLoaderInfo.getVersionId(); - profile.icon = ModIconCache.getBase64Image(modDetail.getIconCacheTag()); - - - LauncherProfiles.mainProfileJson.profiles.put(modpackName, profile); - LauncherProfiles.update(); + ModpackInstaller.installModpack(modDetail, selectedVersion, this::installMrpack); } private static ModLoaderInfo createInfo(ModrinthIndex modrinthIndex) { @@ -143,13 +104,12 @@ public class ModrinthApi implements ModpackApi{ return new ModLoaderInfo(ModLoaderInfo.MOD_LOADER_FABRIC, modLoaderVersion, mcVersion); } if((modLoaderVersion = dependencies.get("quilt-loader")) != null) { - throw new RuntimeException("Quilt is gay af"); - //return new ModLoaderInfo(ModLoaderInfo.MOD_LOADER_QUILT, modLoaderVersion, mcVersion); + return new ModLoaderInfo(ModLoaderInfo.MOD_LOADER_QUILT, modLoaderVersion, mcVersion); } return null; } - private ModrinthIndex installMrpack(File mrpackFile, File instanceDestination) throws IOException { + private ModLoaderInfo installMrpack(File mrpackFile, File instanceDestination) throws IOException { try (ZipFile modpackZipFile = new ZipFile(mrpackFile)){ ModrinthIndex modrinthIndex = Tools.GLOBAL_GSON.fromJson( Tools.read(ZipUtils.getEntryStream(modpackZipFile, "modrinth.index.json")), @@ -164,7 +124,7 @@ public class ModrinthApi implements ModpackApi{ ZipUtils.zipExtract(modpackZipFile, "overrides/", instanceDestination); ProgressLayout.setProgress(ProgressLayout.INSTALL_MODPACK, 50, R.string.modpack_download_applying_overrides, 2, 2); ZipUtils.zipExtract(modpackZipFile, "client-overrides/", instanceDestination); - return modrinthIndex; + return createInfo(modrinthIndex); } } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/CurseManifest.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/CurseManifest.java index 2079cc81b..f7b82a4ca 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/CurseManifest.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/models/CurseManifest.java @@ -12,6 +12,7 @@ public class CurseManifest { public static class CurseFile { public long projectID; public long fileID; + public boolean required; } public static class CurseMinecraft { public String version; diff --git a/app_pojavlauncher/src/main/res/values/strings.xml b/app_pojavlauncher/src/main/res/values/strings.xml index 3d3526213..03188d94f 100644 --- a/app_pojavlauncher/src/main/res/values/strings.xml +++ b/app_pojavlauncher/src/main/res/values/strings.xml @@ -416,5 +416,7 @@ Downloading modpack metadata (%.2f MB / %.2f MB) Downloading mods (%.2f MB / %.2f MB) + Downloading mod links (%d/%d) + Downloading mods (File %d out of %d) Applying overrides (%d/%d)