CurseForge modpack download (WIP) + more generalization

This commit is contained in:
BuildTools 2023-08-12 23:11:51 +03:00 committed by ArtDev
parent b430edbc0b
commit 19a781ad18
8 changed files with 179 additions and 57 deletions

View File

@ -987,6 +987,12 @@ public final class Tools {
return prefixedName.substring(Tools.LAUNCHERPROFILES_RTPREFIX.length()); 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) { public static String getSelectedRuntime(MinecraftProfile minecraftProfile) {
String runtime = LauncherPreferences.PREF_DEFAULT_RUNTIME; String runtime = LauncherPreferences.PREF_DEFAULT_RUNTIME;
String profileRuntime = getRuntimeName(minecraftProfile.javaDir); String profileRuntime = getRuntimeName(minecraftProfile.javaDir);

View File

@ -5,14 +5,16 @@ import android.util.Log;
import com.google.gson.JsonArray; import com.google.gson.JsonArray;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.kdt.mcgui.ProgressLayout;
import net.kdt.pojavlaunch.R;
import net.kdt.pojavlaunch.Tools; import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.modloaders.modpacks.models.Constants; import net.kdt.pojavlaunch.modloaders.modpacks.models.Constants;
import net.kdt.pojavlaunch.modloaders.modpacks.models.CurseManifest; import net.kdt.pojavlaunch.modloaders.modpacks.models.CurseManifest;
import net.kdt.pojavlaunch.modloaders.modpacks.models.ModDetail; import net.kdt.pojavlaunch.modloaders.modpacks.models.ModDetail;
import net.kdt.pojavlaunch.modloaders.modpacks.models.ModItem; 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.modloaders.modpacks.models.SearchFilters;
import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper;
import net.kdt.pojavlaunch.utils.ZipUtils; import net.kdt.pojavlaunch.utils.ZipUtils;
import java.io.File; 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) // 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_MOD_CLASS_ID = 6;
private static final int CURSEFORGE_PAGINATION_SIZE = 50; 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"); 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) { public ModDetail getModDetails(ModItem item) {
ArrayList<JsonObject> allModDetails = new ArrayList<>(); ArrayList<JsonObject> allModDetails = new ArrayList<>();
int index = 0; int index = 0;
while(index != -1 && index != -2) { while(index != CURSEFORGE_PAGINATION_END_REACHED &&
index != CURSEFORGE_PAGINATION_ERROR) {
index = getPaginatedDetails(allModDetails, index, item.id); index = getPaginatedDetails(allModDetails, index, item.id);
} }
if(index == -2) return null; if(index == CURSEFORGE_PAGINATION_ERROR) return null;
int length = allModDetails.size(); int length = allModDetails.size();
String[] versionNames = new String[length]; String[] versionNames = new String[length];
String[] mcVersionNames = new String[length]; String[] mcVersionNames = new String[length];
@ -99,8 +104,8 @@ public class CurseforgeApi implements ModpackApi{
@Override @Override
public void installMod(ModDetail modDetail, int selectedVersion) { public void installMod(ModDetail modDetail, int selectedVersion) {
String versionUrl = modDetail.versionUrls[selectedVersion]; //TODO considering only modpacks for now
File modpackFile = new File(Tools.DIR_CACHE, modDetail.id+".zip"); ModpackInstaller.installModpack(modDetail, selectedVersion, this::installCurseforgeZip);
} }
@ -109,16 +114,16 @@ public class CurseforgeApi implements ModpackApi{
params.put("index", index); params.put("index", index);
params.put("pageSize", CURSEFORGE_PAGINATION_SIZE); params.put("pageSize", CURSEFORGE_PAGINATION_SIZE);
JsonObject response = mApiHandler.get("mods/"+modId+"/files", params, JsonObject.class); 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"); 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++) { for(int i = 0; i < data.size(); i++) {
JsonObject fileInfo = data.get(i).getAsJsonObject(); JsonObject fileInfo = data.get(i).getAsJsonObject();
if(fileInfo.get("isServerPack").getAsBoolean()) continue; if(fileInfo.get("isServerPack").getAsBoolean()) continue;
objectList.add(fileInfo); objectList.add(fileInfo);
} }
if(data.size() < CURSEFORGE_PAGINATION_SIZE) { 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(); return index + data.size();
} }
@ -128,7 +133,72 @@ public class CurseforgeApi implements ModpackApi{
CurseManifest curseManifest = Tools.GLOBAL_GSON.fromJson( CurseManifest curseManifest = Tools.GLOBAL_GSON.fromJson(
Tools.read(ZipUtils.getEntryStream(modpackZipFile, "manifest.json")), Tools.read(ZipUtils.getEntryStream(modpackZipFile, "manifest.json")),
CurseManifest.class); 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;
} }
} }

View File

@ -20,15 +20,28 @@ public class ModDownloader {
private final AtomicLong mDownloadSize = new AtomicLong(0); private final AtomicLong mDownloadSize = new AtomicLong(0);
private final Object mExceptionSyncPoint = new Object(); private final Object mExceptionSyncPoint = new Object();
private final File mDestinationDirectory; private final File mDestinationDirectory;
private final boolean mUseFileCount;
private IOException mFirstIOException; private IOException mFirstIOException;
private long mTotalSize; private long mTotalSize;
public ModDownloader(File destinationDirectory) { public ModDownloader(File destinationDirectory) {
this(destinationDirectory, false);
}
public ModDownloader(File destinationDirectory, boolean useFileCount) {
this.mDestinationDirectory = destinationDirectory; this.mDestinationDirectory = destinationDirectory;
this.mUseFileCount = useFileCount;
} }
public void submitDownload(int fileSize, String relativePath, String... url) { 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))); mDownloadPool.execute(new DownloadTask(url, new File(mDestinationDirectory, relativePath)));
} }
@ -95,6 +108,7 @@ public class ModDownloader {
for (int i = 0; i < 5; i++) { for (int i = 0; i < 5; i++) {
try { try {
DownloadUtils.downloadFileMonitored(sourceUrl, mDestination, getThreadLocalBuffer(), this); DownloadUtils.downloadFileMonitored(sourceUrl, mDestination, getThreadLocalBuffer(), this);
if(mUseFileCount) mDownloadSize.addAndGet(1);
return null; return null;
} catch (InterruptedIOException e) { } catch (InterruptedIOException e) {
throw new InterruptedException(); throw new InterruptedException();
@ -102,14 +116,17 @@ public class ModDownloader {
e.printStackTrace(); e.printStackTrace();
exception = e; exception = e;
} }
mDownloadSize.addAndGet(-last); if(!mUseFileCount) {
last = 0; mDownloadSize.addAndGet(-last);
last = 0;
}
} }
return exception; return exception;
} }
@Override @Override
public void updateProgress(int curr, int max) { public void updateProgress(int curr, int max) {
if(mUseFileCount) return;
mDownloadSize.addAndGet(curr - last); mDownloadSize.addAndGet(curr - last);
last = curr; last = curr;
} }

View File

@ -1,5 +1,10 @@
package net.kdt.pojavlaunch.modloaders.modpacks.api; package net.kdt.pojavlaunch.modloaders.modpacks.api;
import android.content.Context;
import android.content.Intent;
import net.kdt.pojavlaunch.JavaGUILauncherActivity;
public class ModLoaderInfo { public class ModLoaderInfo {
public static final int MOD_LOADER_FORGE = 0; public static final int MOD_LOADER_FORGE = 0;
public static final int MOD_LOADER_FABRIC = 1; public static final int MOD_LOADER_FABRIC = 1;
@ -21,7 +26,7 @@ public class ModLoaderInfo {
case MOD_LOADER_FABRIC: case MOD_LOADER_FABRIC:
return "fabric-loader-"+modLoaderVersion+"-"+minecraftVersion; return "fabric-loader-"+modLoaderVersion+"-"+minecraftVersion;
case MOD_LOADER_QUILT: case MOD_LOADER_QUILT:
// TODO throw new RuntimeException("Quilt is gay af");
default: default:
return null; return null;
} }

View File

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

View File

@ -6,22 +6,17 @@ import com.kdt.mcgui.ProgressLayout;
import net.kdt.pojavlaunch.R; import net.kdt.pojavlaunch.R;
import net.kdt.pojavlaunch.Tools; 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.Constants;
import net.kdt.pojavlaunch.modloaders.modpacks.models.ModDetail; import net.kdt.pojavlaunch.modloaders.modpacks.models.ModDetail;
import net.kdt.pojavlaunch.modloaders.modpacks.models.ModItem; import net.kdt.pojavlaunch.modloaders.modpacks.models.ModItem;
import net.kdt.pojavlaunch.modloaders.modpacks.models.ModrinthIndex; import net.kdt.pojavlaunch.modloaders.modpacks.models.ModrinthIndex;
import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchFilters; import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchFilters;
import net.kdt.pojavlaunch.progresskeeper.DownloaderProgressWrapper; import net.kdt.pojavlaunch.progresskeeper.DownloaderProgressWrapper;
import net.kdt.pojavlaunch.utils.DownloadUtils;
import net.kdt.pojavlaunch.utils.ZipUtils; 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.File;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.zip.ZipFile; import java.util.zip.ZipFile;
@ -93,41 +88,7 @@ public class ModrinthApi implements ModpackApi{
@Override @Override
public void installMod(ModDetail modDetail, int selectedVersion) { public void installMod(ModDetail modDetail, int selectedVersion) {
//TODO considering only modpacks for now //TODO considering only modpacks for now
String versionUrl = modDetail.versionUrls[selectedVersion]; ModpackInstaller.installModpack(modDetail, selectedVersion, this::installMrpack);
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();
} }
private static ModLoaderInfo createInfo(ModrinthIndex modrinthIndex) { private static ModLoaderInfo createInfo(ModrinthIndex modrinthIndex) {
@ -143,13 +104,12 @@ public class ModrinthApi implements ModpackApi{
return new ModLoaderInfo(ModLoaderInfo.MOD_LOADER_FABRIC, modLoaderVersion, mcVersion); return new ModLoaderInfo(ModLoaderInfo.MOD_LOADER_FABRIC, modLoaderVersion, mcVersion);
} }
if((modLoaderVersion = dependencies.get("quilt-loader")) != null) { 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; 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)){ try (ZipFile modpackZipFile = new ZipFile(mrpackFile)){
ModrinthIndex modrinthIndex = Tools.GLOBAL_GSON.fromJson( ModrinthIndex modrinthIndex = Tools.GLOBAL_GSON.fromJson(
Tools.read(ZipUtils.getEntryStream(modpackZipFile, "modrinth.index.json")), Tools.read(ZipUtils.getEntryStream(modpackZipFile, "modrinth.index.json")),
@ -164,7 +124,7 @@ public class ModrinthApi implements ModpackApi{
ZipUtils.zipExtract(modpackZipFile, "overrides/", instanceDestination); ZipUtils.zipExtract(modpackZipFile, "overrides/", instanceDestination);
ProgressLayout.setProgress(ProgressLayout.INSTALL_MODPACK, 50, R.string.modpack_download_applying_overrides, 2, 2); ProgressLayout.setProgress(ProgressLayout.INSTALL_MODPACK, 50, R.string.modpack_download_applying_overrides, 2, 2);
ZipUtils.zipExtract(modpackZipFile, "client-overrides/", instanceDestination); ZipUtils.zipExtract(modpackZipFile, "client-overrides/", instanceDestination);
return modrinthIndex; return createInfo(modrinthIndex);
} }
} }
} }

View File

@ -12,6 +12,7 @@ public class CurseManifest {
public static class CurseFile { public static class CurseFile {
public long projectID; public long projectID;
public long fileID; public long fileID;
public boolean required;
} }
public static class CurseMinecraft { public static class CurseMinecraft {
public String version; public String version;

View File

@ -416,5 +416,7 @@
<string name="modpack_download_downloading_metadata">Downloading modpack metadata (%.2f MB / %.2f MB)</string> <string name="modpack_download_downloading_metadata">Downloading modpack metadata (%.2f MB / %.2f MB)</string>
<string name="modpack_download_downloading_mods">Downloading mods (%.2f MB / %.2f MB)</string> <string name="modpack_download_downloading_mods">Downloading mods (%.2f MB / %.2f MB)</string>
<string name="modpack_download_getting_links">Downloading mod links (%d/%d)</string>
<string name="modpack_download_downloading_mods_fc">Downloading mods (File %d out of %d)</string>
<string name="modpack_download_applying_overrides">Applying overrides (%d/%d)</string> <string name="modpack_download_applying_overrides">Applying overrides (%d/%d)</string>
</resources> </resources>