Merge pull request #155 from AngelAuraMC/feat/import-modpack

Add functionality to import modpacks
This commit is contained in:
alexytomi
2025-12-30 17:23:03 +08:00
committed by GitHub
16 changed files with 476 additions and 81 deletions

View File

@@ -9,6 +9,7 @@ import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ImageButton;
import android.widget.Toast;
@@ -36,6 +37,10 @@ import net.kdt.pojavlaunch.fragments.SelectAuthFragment;
import net.kdt.pojavlaunch.lifecycle.ContextAwareDoneListener;
import net.kdt.pojavlaunch.lifecycle.ContextExecutor;
import net.kdt.pojavlaunch.modloaders.modpacks.ModloaderInstallTracker;
import net.kdt.pojavlaunch.modloaders.modpacks.api.CommonApi;
import net.kdt.pojavlaunch.modloaders.modpacks.api.ModLoader;
import net.kdt.pojavlaunch.modloaders.modpacks.api.ModpackInstaller;
import net.kdt.pojavlaunch.modloaders.modpacks.api.NotificationDownloadListener;
import net.kdt.pojavlaunch.modloaders.modpacks.imagecache.IconCacheJanitor;
import net.kdt.pojavlaunch.prefs.LauncherPreferences;
import net.kdt.pojavlaunch.prefs.screens.LauncherPreferenceFragment;
@@ -50,7 +55,9 @@ import net.kdt.pojavlaunch.utils.NotificationUtils;
import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles;
import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
public class LauncherActivity extends BaseActivity {
@@ -60,6 +67,25 @@ public class LauncherActivity extends BaseActivity {
registerForActivityResult(new OpenDocumentWithExtension("jar"), (data)->{
if(data != null) Tools.launchModInstaller(this, data);
});
public final ActivityResultLauncher<Object> modpackImportLauncher =
registerForActivityResult(new OpenDocumentWithExtension(new String[]{"zip", "mrpack"}), (data)->{
if(data != null) {
PojavApplication.sExecutorService.execute(() -> {
try {
ModLoader loaderInfo = new CommonApi(getString(R.string.curseforge_api_key)).importModpack(this, data);
if (loaderInfo == null) return;
loaderInfo.getDownloadTask(new NotificationDownloadListener(this, loaderInfo)).run();
} catch (IOException e) {
Tools.showErrorRemote(this, R.string.modpack_install_download_failed, e);
} catch (IllegalArgumentException e) {
Tools.showError(this, R.string.not_modpack_file, e);
} catch (NoSuchAlgorithmException e) {
// Should literally never happen because SHA-1 is required Java spec
throw new RuntimeException(e);
}
});
}
});
private mcAccountSpinner mAccountSpinner;
private FragmentContainerView mFragmentView;

View File

@@ -39,7 +39,7 @@ public class PojavApplication extends Application {
// Write to file, since some devices may not able to show error
FileUtils.ensureParentDirectory(crashFile);
PrintStream crashStream = new PrintStream(crashFile);
crashStream.append("PojavLauncher crash report\n");
crashStream.append("Amethyst crash report\n");
crashStream.append(" - Time: ").append(DateFormat.getDateTimeInstance().format(new Date())).append("\n");
crashStream.append(" - Device: ").append(Build.PRODUCT).append(" ").append(Build.MODEL).append("\n");
crashStream.append(" - Android version: ").append(Build.VERSION.RELEASE).append("\n");

View File

@@ -10,10 +10,14 @@ import androidx.activity.result.contract.ActivityResultContract;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Objects;
// Android's OpenDocument contract is the basicmost crap that doesn't allow
// you to specify practically anything. So i made this instead.
public class OpenDocumentWithExtension extends ActivityResultContract<Object, Uri> {
private final String mimeType;
private final String[] mimeTypes;
/**
* Create a new OpenDocumentWithExtension contract.
@@ -25,6 +29,37 @@ public class OpenDocumentWithExtension extends ActivityResultContract<Object, Ur
String extensionMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if(extensionMimeType == null) extensionMimeType = "*/*";
mimeType = extensionMimeType;
mimeTypes = null;
}
public OpenDocumentWithExtension(String[] extensions) {
ArrayList<String> extensionsMimeType = new ArrayList<>();
int count = 0;
for (String extension: extensions) {
String extensionMimetype = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (Objects.equals(extension, "mrpack")) {
// Special handling here because depending on whether the ROM has
// `x-modrinth-modpack+zip` in their mimetypes or not because if it does,
// `octet-stream` will no longer match mrpack files.
// Checking this with MimeTypeMap.hasExtension() and .hasMimeType() always returns
// false so we do both instead.
// `octet-stream` highlights a lot of unrelated files but it's the best
// we can do. Mimetypes are built into the ROM after all.
// See https://android.googlesource.com/platform/external/mime-support/+/refs/heads/main
// or https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/mime/java-res/android.mime.types
extensionsMimeType.add("application/octet-stream");
extensionsMimeType.add("application/x-modrinth-modpack+zip");
count++;
continue;
}
if(extensionMimetype == null) continue; // If null is passed, it matches all files
extensionsMimeType.add(extensionMimetype);
count++;
}
mimeType = "*/*";
mimeTypes = extensionsMimeType.toArray(new String[0]);
}
@NonNull
@@ -33,6 +68,7 @@ public class OpenDocumentWithExtension extends ActivityResultContract<Object, Ur
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType(mimeType);
if(mimeTypes != null) intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
return intent;
}

View File

@@ -0,0 +1,50 @@
package net.kdt.pojavlaunch.fragments;
import static net.kdt.pojavlaunch.Tools.hasNoOnlineProfileDialog;
import static net.kdt.pojavlaunch.Tools.hasOnlineProfile;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.kdt.mcgui.MineButton;
import net.kdt.pojavlaunch.BaseActivity;
import net.kdt.pojavlaunch.LauncherActivity;
import net.kdt.pojavlaunch.R;
import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.contracts.OpenDocumentWithExtension;
import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchFilters;
public class ModpackCreateFragment extends Fragment {
public static final String TAG = "ModpackCreateFragment";
public ModpackCreateFragment() {
super(R.layout.fragment_create_modpack_profile);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
view.findViewById(R.id.button_browse_modpacks).setOnClickListener(v -> {
tryInstall(SearchModFragment.class, SearchModFragment.TAG);
});
view.findViewById(R.id.button_import_modpack).setOnClickListener(v -> {
Activity launcheractivity = requireActivity();
if (!(launcheractivity instanceof LauncherActivity))
throw new IllegalStateException("Cannot import modpack without LauncherActivity");
((LauncherActivity) launcheractivity).modpackImportLauncher.launch(null);
});;
}
private void tryInstall(Class<? extends Fragment> fragmentClass, String tag){
if(!hasOnlineProfile()){
hasNoOnlineProfileDialog(requireActivity());
} else {
Tools.swapFragment(requireActivity(), fragmentClass, tag, null);
}
}
}

View File

@@ -7,28 +7,41 @@ import android.widget.ExpandableListAdapter;
import androidx.annotation.NonNull;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import net.kdt.pojavlaunch.JavaGUILauncherActivity;
import net.kdt.pojavlaunch.R;
import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.modloaders.ForgeDownloadTask;
import net.kdt.pojavlaunch.modloaders.ForgeUtils;
import net.kdt.pojavlaunch.modloaders.ForgeVersionListHandler;
import net.kdt.pojavlaunch.modloaders.ModloaderListenerProxy;
import net.kdt.pojavlaunch.modloaders.NeoForgeDownloadTask;
import net.kdt.pojavlaunch.modloaders.NeoForgeVersionListAdapter;
import net.kdt.pojavlaunch.utils.DownloadUtils;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.io.StringReader;
import java.util.List;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
public class NeoForgeInstallFragment extends ModVersionListFragment<List<String>> {
public static final String TAG = "NeoForgeInstallFragment";
public NeoForgeInstallFragment() {
super(TAG);
}
private static final String NEOFORGE_METADATA_URL = "https://meta.prismlauncher.org/v1/net.neoforged/index.json";
private static final String NEOFORGE_METADATA_URL = "https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml";
@Override
@@ -48,19 +61,39 @@ public class NeoForgeInstallFragment extends ModVersionListFragment<List<String>
@Override
public List<String> loadVersionList() {
String test = null;
try {
test = DownloadUtils.downloadStringCached(NEOFORGE_METADATA_URL, "neoforge_versions", input -> input);
} catch (Exception e) {
Tools.showErrorRemote(e);
}
return Collections.singletonList(test);
// Moved the parsing logic to the adapter because there is no way to get this info easily, we use prism's index
// since neoforge doesn't actually give this information easily anywhere.
// To clarify, neoforge does not provide maven APIs to get supported Minecraft versions for each loader version
return downloadNeoForgeVersions();
}
public static List<String> downloadNeoForgeVersions() {
SAXParser saxParser;
try {
SAXParserFactory parserFactory = SAXParserFactory.newInstance();
saxParser = parserFactory.newSAXParser();
}catch (SAXException | ParserConfigurationException e) {
e.printStackTrace();
// if we cant make a parser we might as well not even try to parse anything
return null;
}
try {
//of_test();
return DownloadUtils.downloadStringCached(NEOFORGE_METADATA_URL, "neoforge_versions", input -> {
try {
ForgeVersionListHandler handler = new ForgeVersionListHandler();
saxParser.parse(new InputSource(new StringReader(input)), handler);
return handler.getVersions();
// IOException is present here StringReader throws it only if the parser called close()
// sooner than needed, which is a parser issue and not an I/O one
}catch (SAXException | IOException e) {
throw new DownloadUtils.ParseException(e);
}
});
}catch (DownloadUtils.ParseException | IOException e) {
e.printStackTrace();
return null;
}
}
@Override
public ExpandableListAdapter createAdapter(List<String> versionList, LayoutInflater layoutInflater) {
return new NeoForgeVersionListAdapter(versionList, layoutInflater);
@@ -74,7 +107,9 @@ public class NeoForgeInstallFragment extends ModVersionListFragment<List<String>
@Override
public void onDownloadFinished(Context context, File downloadedFile) {
Intent modInstallerStartIntent = new Intent(context, JavaGUILauncherActivity.class);
modInstallerStartIntent.putExtra("javaArgs", "-jar "+downloadedFile.getAbsolutePath()+" --install-client");
modInstallerStartIntent
.putExtra("javaArgs", "-jar "+downloadedFile.getAbsolutePath()+" --install-client")
.putExtra("openLogOutput", true);
context.startActivity(modInstallerStartIntent);
}
}

View File

@@ -20,6 +20,9 @@ public class ProfileTypeSelectFragment extends Fragment {
public ProfileTypeSelectFragment() {
super(R.layout.fragment_profile_type);
}
public ProfileTypeSelectFragment(int layout) {
super(layout);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
@@ -39,7 +42,7 @@ public class ProfileTypeSelectFragment extends Fragment {
view.findViewById(R.id.modded_profile_neoforge).setOnClickListener((v)->
tryInstall(NeoForgeInstallFragment.class, NeoForgeInstallFragment.TAG));
view.findViewById(R.id.modded_profile_modpack).setOnClickListener((v)->
tryInstall(SearchModFragment.class, SearchModFragment.TAG));
tryInstall(ModpackCreateFragment.class, ModpackCreateFragment.TAG));
view.findViewById(R.id.modded_profile_quilt).setOnClickListener((v)->
tryInstall(QuiltInstallFragment.class, QuiltInstallFragment.TAG));
view.findViewById(R.id.modded_profile_bta).setOnClickListener((v)->

View File

@@ -1,9 +1,12 @@
package net.kdt.pojavlaunch.modloaders;
import androidx.annotation.NonNull;
import com.kdt.mcgui.ProgressLayout;
import net.kdt.pojavlaunch.R;
import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.fragments.NeoForgeInstallFragment;
import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper;
import net.kdt.pojavlaunch.utils.DownloadUtils;
@@ -13,26 +16,23 @@ import java.io.IOException;
import java.util.List;
public class NeoForgeDownloadTask implements Runnable, Tools.DownloaderFeedback {
private String mDownloadUrl;
private String mFullVersion;
private String mLoaderVersion;
private String mGameVersion;
private final String mDownloadUrl;
private final String mLoaderVersion;
private final ModloaderDownloadListener mListener;
public NeoForgeDownloadTask(ModloaderDownloadListener listener, String forgeVersion) {
public NeoForgeDownloadTask(ModloaderDownloadListener listener, @NonNull String loaderVersion) {
this.mListener = listener;
this.mDownloadUrl = "https://maven.neoforged.net/releases/net/neoforged/neoforge/"+ forgeVersion +"/neoforge-"+forgeVersion+"-installer.jar";
this.mFullVersion = forgeVersion;
this.mDownloadUrl = String.format(NEOFORGE_INSTALLER_URL, loaderVersion);
this.mLoaderVersion = loaderVersion;
}
public NeoForgeDownloadTask(ModloaderDownloadListener listener, String gameVersion, String loaderVersion) {
this.mListener = listener;
this.mLoaderVersion = loaderVersion;
this.mGameVersion = gameVersion;
}
private static final String NEOFORGE_INSTALLER_URL = "https://maven.neoforged.net/releases/net/neoforged/neoforge/%1$s/neoforge-%1$s-installer.jar";
@Override
public void run() {
if(determineDownloadUrl()) {
downloadForge();
downloadNeoForge();
}
ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK);
}
@@ -40,11 +40,11 @@ public class NeoForgeDownloadTask implements Runnable, Tools.DownloaderFeedback
@Override
public void updateProgress(int curr, int max) {
int progress100 = (int)(((float)curr / (float)max)*100f);
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, progress100, R.string.forge_dl_progress, mFullVersion);
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, progress100, R.string.forge_dl_progress, mLoaderVersion);
}
private void downloadForge() {
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.forge_dl_progress, mFullVersion);
private void downloadNeoForge() {
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.forge_dl_progress, mLoaderVersion);
try {
File destinationFile = new File(Tools.DIR_CACHE, "neoforge-installer.jar");
byte[] buffer = new byte[8192];
@@ -58,8 +58,7 @@ public class NeoForgeDownloadTask implements Runnable, Tools.DownloaderFeedback
}
public boolean determineDownloadUrl() {
if(mDownloadUrl != null && mFullVersion != null) return true;
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.forge_dl_searching);
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.neoforge_dl_searching);
try {
if(!findVersion()) {
mListener.onDataNotAvailable();
@@ -73,13 +72,10 @@ public class NeoForgeDownloadTask implements Runnable, Tools.DownloaderFeedback
}
public boolean findVersion() throws IOException {
List<String> forgeVersions = ForgeUtils.downloadForgeVersions();
if(forgeVersions == null) return false;
String versionStart = mGameVersion+"-"+mLoaderVersion;
for(String versionName : forgeVersions) {
if(!versionName.startsWith(versionStart)) continue;
mFullVersion = versionName;
mDownloadUrl = ForgeUtils.getInstallerUrl(mFullVersion);
List<String> neoforgeVersions = NeoForgeInstallFragment.downloadNeoForgeVersions();
if(neoforgeVersions == null) return false;
for(String versionName : neoforgeVersions) {
if(!versionName.startsWith(mLoaderVersion)) continue;
return true;
}
return false;

View File

@@ -7,58 +7,57 @@ import android.widget.BaseExpandableListAdapter;
import android.widget.ExpandableListAdapter;
import android.widget.TextView;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
public class NeoForgeVersionListAdapter extends BaseExpandableListAdapter implements ExpandableListAdapter {
private final List<String> mGameVersions;
private final List<List<String>> mNeoForgeVersions;
private final LayoutInflater mLayoutInflater;
private final LinkedHashMap<String, LinkedHashSet<String>> minecraftToLoaderVersionsHashmap;
private LinkedHashSet<String> generatedHashSet = null;
public NeoForgeVersionListAdapter(List<String> forgeVersions, LayoutInflater layoutInflater) {
public NeoForgeVersionListAdapter(List<String> neoforgeVersions, LayoutInflater layoutInflater) {
this.mLayoutInflater = layoutInflater;
minecraftToLoaderVersionsHashmap = new LinkedHashMap<>();
JsonArray versionsJsonArray = JsonParser.parseString(forgeVersions.get(0)).getAsJsonObject().getAsJsonArray("versions");
ArrayList<JsonElement> sortedVersionsList = new ArrayList<>();
for (JsonElement elem : versionsJsonArray) {
sortedVersionsList.add(elem);
}
Collections.sort(sortedVersionsList, (o1, o2) -> {
String versionString1 = ((JsonObject) o1).get("requires").getAsJsonArray().get(0).getAsJsonObject().get("equals").getAsString();
String versionString2 = ((JsonObject) o2).get("requires").getAsJsonArray().get(0).getAsJsonObject().get("equals").getAsString();
return versionString2.compareTo(versionString1); // Sorts by Minecraft version
});
for (JsonElement sortedVersionPick : sortedVersionsList) {
String loaderVersion = ((JsonObject) sortedVersionPick).get("version").getAsString();
String minecraftVersion = ((JsonObject) sortedVersionPick).get("requires").getAsJsonArray().get(0).getAsJsonObject().get("equals").getAsString();
if (minecraftToLoaderVersionsHashmap.containsKey(minecraftVersion)) {
minecraftToLoaderVersionsHashmap.get(minecraftVersion).add(loaderVersion);
} else {
generatedHashSet = new LinkedHashSet<>();
generatedHashSet.add(loaderVersion);
minecraftToLoaderVersionsHashmap.put(minecraftVersion, generatedHashSet);
mGameVersions = new ArrayList<>();
mNeoForgeVersions = new ArrayList<>();
for(String version : neoforgeVersions) {
String[] parts = version.split("\\.");
String gameVersion;
try {
if (Integer.parseInt(parts[1]) < 25) { // Actual logic for normal mcvers
gameVersion = "1." + parts[0] + "." + parts[1];
} else gameVersion = parts[0] + "." + parts[1];
} catch (NumberFormatException ignored) {
// Handling for april fools version
gameVersion = parts[0] + "." + parts[1];
}
List<String> versionList;
int gameVersionIndex = mGameVersions.indexOf(gameVersion);
if(gameVersionIndex != -1) versionList = mNeoForgeVersions.get(gameVersionIndex);
else {
versionList = new ArrayList<>();
mGameVersions.add(gameVersion);
mNeoForgeVersions.add(versionList);
}
versionList.add(version);
}
// Make it latest to oldest, top to down.
Collections.reverse(mGameVersions);
Collections.reverse(mNeoForgeVersions);
for (List<String> mNeoForgeVersion : mNeoForgeVersions){
Collections.reverse(mNeoForgeVersion);
}
}
@Override
public int getGroupCount() {
return minecraftToLoaderVersionsHashmap.size();
return mGameVersions.size();
}
@Override
public int getChildrenCount(int i) {
return new ArrayList<>(minecraftToLoaderVersionsHashmap.values()).get(i).size();
return mNeoForgeVersions.get(i).size();
}
@Override
@@ -68,7 +67,7 @@ public class NeoForgeVersionListAdapter extends BaseExpandableListAdapter implem
@Override
public Object getChild(int i, int i1) {
return getForgeVersion(i, i1);
return getNeoForgeVersion(i, i1);
}
@Override
@@ -100,16 +99,16 @@ public class NeoForgeVersionListAdapter extends BaseExpandableListAdapter implem
public View getChildView(int i, int i1, boolean b, View convertView, ViewGroup viewGroup) {
if(convertView == null)
convertView = mLayoutInflater.inflate(android.R.layout.simple_expandable_list_item_1, viewGroup, false);
((TextView) convertView).setText(getForgeVersion(i, i1));
((TextView) convertView).setText(getNeoForgeVersion(i, i1));
return convertView;
}
private String getGameVersion(int i) {
return minecraftToLoaderVersionsHashmap.keySet().toArray()[i].toString();
return mGameVersions.get(i);
}
private String getForgeVersion(int i, int i1){
return new ArrayList<>(minecraftToLoaderVersionsHashmap.values()).get(i).toArray()[i1].toString();
private String getNeoForgeVersion(int i, int i1){
return mNeoForgeVersions.get(i).get(i1);
}
@Override

View File

@@ -1,21 +1,32 @@
package net.kdt.pojavlaunch.modloaders.modpacks.api;
import android.app.Activity;
import android.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull;
import net.kdt.pojavlaunch.PojavApplication;
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.ModDetail;
import net.kdt.pojavlaunch.modloaders.modpacks.models.ModItem;
import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchFilters;
import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchResult;
import org.jdom2.IllegalDataException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* Group all apis under the same umbrella, as another layer of abstraction
@@ -112,6 +123,11 @@ public class CommonApi implements ModpackApi {
return getModpackApi(modDetail.apiSource).installMod(modDetail, selectedVersion);
}
@Override
public ModLoader importModpack(Activity activity, Uri zipUri) throws IOException, NoSuchAlgorithmException {
return getModpackApi(activity, zipUri).importModpack(activity, zipUri);
}
private @NonNull ModpackApi getModpackApi(int apiSource) {
switch (apiSource) {
case Constants.SOURCE_MODRINTH:
@@ -123,6 +139,31 @@ public class CommonApi implements ModpackApi {
}
}
private @NonNull ModpackApi getModpackApi(Activity activity, Uri zipUri){
String modrinthPackInfoFileName = "modrinth.index.json";
String curseforgePackInfoFileName = "manifest.json";
InputStream inputStream = null;
try {
inputStream = activity.getContentResolver().openInputStream(zipUri);
ZipInputStream zipInputStream = new ZipInputStream(inputStream);
ZipEntry zipEntry;
boolean isModrinth;
boolean isCurseforge;
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
isModrinth = zipEntry.getName().equals(modrinthPackInfoFileName);
isCurseforge = zipEntry.getName().equals(curseforgePackInfoFileName);
if(isModrinth) {
return mModrinthApi;
} else if (isCurseforge) {
return mCurseforgeApi;
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
throw new IllegalArgumentException("Zip provided does not contain a manifest file");
}
/** Fuse the arrays in a way that's fair for every endpoint */
private ModItem[] buildFusedResponse(List<ModItem[]> modMatrix){
int totalSize = 0;

View File

@@ -1,5 +1,7 @@
package net.kdt.pojavlaunch.modloaders.modpacks.api;
import android.app.Activity;
import android.net.Uri;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -25,6 +27,7 @@ import net.kdt.pojavlaunch.utils.ZipUtils;
import java.io.File;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.regex.Pattern;
@@ -137,6 +140,11 @@ public class CurseforgeApi implements ModpackApi{
return ModpackInstaller.installModpack(modDetail, selectedVersion, this::installCurseforgeZip);
}
@Override
public ModLoader importModpack(Activity activity, Uri zipUri) throws IOException, NoSuchAlgorithmException {
return ModpackInstaller.importModpack(activity, zipUri, this::installCurseforgeZip);
}
private int getPaginatedDetails(ArrayList<JsonObject> objectList, int index, String modId) {
HashMap<String, Object> params = new HashMap<>();
@@ -211,6 +219,9 @@ public class CurseforgeApi implements ModpackApi{
case "fabric":
modLoaderTypeInt = ModLoader.MOD_LOADER_FABRIC;
break;
case "neoforge":
modLoaderTypeInt = ModLoader.MOD_LOADER_NEOFORGE;
break;
default:
return null;
//TODO: Quilt is also Forge? How does that work?

View File

@@ -9,6 +9,7 @@ import net.kdt.pojavlaunch.modloaders.FabriclikeUtils;
import net.kdt.pojavlaunch.modloaders.ForgeDownloadTask;
import net.kdt.pojavlaunch.modloaders.ForgeUtils;
import net.kdt.pojavlaunch.modloaders.ModloaderDownloadListener;
import net.kdt.pojavlaunch.modloaders.NeoForgeDownloadTask;
import java.io.File;
@@ -16,6 +17,7 @@ public class ModLoader {
public static final int MOD_LOADER_FORGE = 0;
public static final int MOD_LOADER_FABRIC = 1;
public static final int MOD_LOADER_QUILT = 2;
public static final int MOD_LOADER_NEOFORGE = 3;
public final int modLoaderType;
public final String modLoaderVersion;
public final String minecraftVersion;
@@ -38,6 +40,8 @@ public class ModLoader {
return "fabric-loader-"+modLoaderVersion+"-"+minecraftVersion;
case MOD_LOADER_QUILT:
return "quilt-loader-"+modLoaderVersion+"-"+minecraftVersion;
case MOD_LOADER_NEOFORGE:
return "neoforge-"+modLoaderVersion;
default:
return null;
}
@@ -57,6 +61,8 @@ public class ModLoader {
return createFabriclikeTask(listener, FabriclikeUtils.FABRIC_UTILS);
case MOD_LOADER_QUILT:
return createFabriclikeTask(listener, FabriclikeUtils.QUILT_UTILS);
case MOD_LOADER_NEOFORGE:
return new NeoForgeDownloadTask(listener, modLoaderVersion);
default:
return null;
}
@@ -77,6 +83,11 @@ public class ModLoader {
case MOD_LOADER_FORGE:
ForgeUtils.addAutoInstallArgs(baseIntent, modInstallerJar, getVersionId());
return baseIntent;
case MOD_LOADER_NEOFORGE:
return baseIntent
.putExtra("javaArgs", "-jar "+modInstallerJar.getAbsolutePath()+" --install-client")
.putExtra("openLogOutput", true)
;
case MOD_LOADER_QUILT:
case MOD_LOADER_FABRIC:
default:
@@ -91,6 +102,7 @@ public class ModLoader {
public boolean requiresGuiInstallation() {
switch (modLoaderType) {
case MOD_LOADER_FORGE:
case MOD_LOADER_NEOFORGE:
return true;
case MOD_LOADER_FABRIC:
case MOD_LOADER_QUILT:

View File

@@ -1,7 +1,9 @@
package net.kdt.pojavlaunch.modloaders.modpacks.api;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import com.kdt.mcgui.ProgressLayout;
@@ -12,8 +14,12 @@ import net.kdt.pojavlaunch.modloaders.modpacks.models.ModDetail;
import net.kdt.pojavlaunch.modloaders.modpacks.models.ModItem;
import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchFilters;
import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchResult;
import net.kdt.pojavlaunch.modloaders.modpacks.models.Constants;
import java.io.IOException;
import java.io.File;
import java.security.NoSuchAlgorithmException;
/**
*
@@ -70,4 +76,13 @@ public interface ModpackApi {
* @param selectedVersion The selected version
*/
ModLoader installMod(ModDetail modDetail, int selectedVersion) throws IOException;
/**
* Imports the mod(pack) from a file.
* May require the download of additional files.
* May requires launching the installation of a modloader
* @param activity any activity
* @param zipUri URI to DocumentsUI selected zip file
*/
ModLoader importModpack(Activity activity, Uri zipUri) throws IOException, NoSuchAlgorithmException;
}

View File

@@ -1,5 +1,10 @@
package net.kdt.pojavlaunch.modloaders.modpacks.api;
import android.app.Activity;
import android.net.Uri;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.kdt.mcgui.ProgressLayout;
import net.kdt.pojavlaunch.R;
@@ -11,10 +16,20 @@ import net.kdt.pojavlaunch.utils.DownloadUtils;
import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles;
import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Locale;
import java.util.concurrent.Callable;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
public class ModpackInstaller {
@@ -69,7 +84,94 @@ public class ModpackInstaller {
return modLoaderInfo;
}
interface InstallFunction {
public static ModLoader importModpack(Activity activity, Uri zipUri, InstallFunction installFunction) throws IOException, NoSuchAlgorithmException {
String modrinthPackInfoFileName = "modrinth.index.json";
String curseforgePackInfoFileName = "manifest.json";
InputStream inputStream = null;
inputStream = activity.getContentResolver().openInputStream(zipUri);
ZipInputStream zipInputStream = new ZipInputStream(inputStream);
ZipEntry zipEntry;
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
boolean isModrinth = zipEntry.getName().equals(modrinthPackInfoFileName);
boolean isCurseforge = zipEntry.getName().equals(curseforgePackInfoFileName);
if (!(isModrinth || isCurseforge)) continue;
// Read Manifest JSON
BufferedReader reader = new BufferedReader(new InputStreamReader(zipInputStream));
String str;
StringBuilder jsonString = new StringBuilder();
while ((str = reader.readLine()) != null) {
jsonString.append(str).append("\n");
}
zipInputStream.close();
// Hash the ZIP File
inputStream = activity.getContentResolver().openInputStream(zipUri);
MessageDigest algorithm = MessageDigest.getInstance("SHA-1");
DigestInputStream hashingStream = new DigestInputStream(inputStream, algorithm);
byte[] buffer = new byte[8192];
while (hashingStream.read(buffer) != -1) {} // just read to update the digest
hashingStream.close();
byte[] digest = algorithm.digest();
StringBuilder sb = new StringBuilder(digest.length * 2);
for (byte b : digest) {
sb.append(String.format("%02x", b));
}
String hash = sb.toString();
// Parse the JSON to prepare for instance creation
JsonObject packInfoJson = JsonParser.parseString(jsonString.toString()).getAsJsonObject();
String modpackName;
if(isModrinth){
// Added a for because there is an awkward __ that I can't be bothered to fix
// FO only deduplication be like:
modpackName = (packInfoJson.get("name").getAsString().toLowerCase(Locale.ROOT) +
packInfoJson.get("versionId") + "for" +
packInfoJson.get("dependencies").getAsJsonObject().get("minecraft"));
} else {
modpackName = (packInfoJson.get("name").getAsString().toLowerCase(Locale.ROOT) +
packInfoJson.get("version") + "for" +
packInfoJson.get("minecraft").getAsJsonObject().get("version"));
}
modpackName = modpackName.trim().replaceAll("[\\\\/:*?\"<>| \\t\\n]", "_");
modpackName = modpackName + hash;
// Copy ZIP file to cache
if(modpackName == null) throw new IOException("Corrupt Modpack manifest file.");
File modpackFile = null;
modpackFile = new File(Tools.DIR_CACHE, modpackName + ".cf");
inputStream = activity.getContentResolver().openInputStream(zipUri);
FileOutputStream output = new FileOutputStream(modpackFile);
byte[] b = new byte[4 * 1024];
int read;
while ((read = inputStream.read(b)) != -1) {
output.write(b, 0, read);
}
output.flush();
// Install the actual pack into custom_instances
ModLoader modLoaderInfo = installFunction.installModpack(modpackFile, new File(Tools.DIR_GAME_HOME, "custom_instances/"+modpackName));
// We have to do this because installModpack doesn't clean up after itself
ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK);
modpackFile.delete();
if(modLoaderInfo == null) {
return null;
}
// Create the instance (We don't have a picture guys)
MinecraftProfile profile = new MinecraftProfile();
profile.gameDir = "./custom_instances/" + modpackName;
profile.name = packInfoJson.get("name").getAsString();
profile.lastVersionId = modLoaderInfo.getVersionId();
LauncherProfiles.mainProfileJson.profiles.put(modpackName, profile);
LauncherProfiles.write();
return modLoaderInfo;
}
throw new IOException("Can't find manifest file in modpack provided");
}
interface InstallFunction {
ModLoader installModpack(File modpackFile, File instanceDestination) throws IOException;
}
}

View File

@@ -1,5 +1,8 @@
package net.kdt.pojavlaunch.modloaders.modpacks.api;
import android.app.Activity;
import android.net.Uri;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.kdt.mcgui.ProgressLayout;
@@ -17,6 +20,7 @@ import net.kdt.pojavlaunch.utils.ZipUtils;
import java.io.File;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.ZipFile;
@@ -116,6 +120,11 @@ public class ModrinthApi implements ModpackApi{
return ModpackInstaller.installModpack(modDetail, selectedVersion, this::installMrpack);
}
@Override
public ModLoader importModpack(Activity activity, Uri zipUri) throws IOException, NoSuchAlgorithmException {
return ModpackInstaller.importModpack(activity, zipUri, this::installMrpack);
}
private static ModLoader createInfo(ModrinthIndex modrinthIndex) {
if(modrinthIndex == null) return null;
Map<String, String> dependencies = modrinthIndex.dependencies;
@@ -131,6 +140,9 @@ public class ModrinthApi implements ModpackApi{
if((modLoaderVersion = dependencies.get("quilt-loader")) != null) {
return new ModLoader(ModLoader.MOD_LOADER_QUILT, modLoaderVersion, mcVersion);
}
if((modLoaderVersion = dependencies.get("neoforge")) != null) {
return new ModLoader(ModLoader.MOD_LOADER_NEOFORGE, modLoaderVersion, mcVersion);
}
return null;
}

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:background="@color/background_app"
android:gravity="center"
android:orientation="vertical"
android:paddingHorizontal="@dimen/fragment_padding_medium">
<View
android:id="@+id/create_modpack_profile_menu"
android:layout_width="match_parent"
android:layout_height="@dimen/_200sdp"
android:background="@drawable/background_card"
android:translationZ="-1dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.kdt.mcgui.MineButton
android:id="@+id/button_browse_modpacks"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Browse Modpacks"
android:textSize="@dimen/_12ssp"
android:layout_marginHorizontal="@dimen/_25sdp"
android:layout_marginBottom="@dimen/_20sdp"
app:layout_constraintBottom_toTopOf="@+id/button_import_modpack"
app:layout_constraintEnd_toEndOf="@+id/create_modpack_profile_menu"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="@+id/create_modpack_profile_menu"
app:layout_constraintTop_toTopOf="@id/create_modpack_profile_menu"
app:layout_constraintVertical_chainStyle="packed"
/>
<com.kdt.mcgui.MineButton
android:id="@+id/button_import_modpack"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Import Zip or mrpack"
android:textSize="@dimen/_12ssp"
android:layout_marginHorizontal="@dimen/_25sdp"
app:layout_constraintBottom_toBottomOf="@id/create_modpack_profile_menu"
app:layout_constraintEnd_toEndOf="@+id/create_modpack_profile_menu"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="@+id/create_modpack_profile_menu"
app:layout_constraintTop_toBottomOf="@+id/button_browse_modpacks" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -301,6 +301,7 @@
<string name="version_select_hint">Select a version</string>
<string name="forge_dl_progress">Downloading installer for %s</string>
<string name="forge_dl_searching">Searching for Forge version number</string>
<string name="neoforge_dl_searching">Searching for NeoForge version number</string>
<string name="modloader_dl_failed_to_load_list">Failed to load the version list!</string>
<string name="forge_dl_no_installer">Sorry, but this version of Forge does not have an installer, which is not yet supported.</string>
<string name="forge_dl_select_version">Select Forge version</string>
@@ -498,5 +499,6 @@
<string name="sodium_math_warning_message" translatable="false">
Sodium is unsupported, you are on your own. No support will be given in the discord server.\nUsing sodium may result in bugs, glitches, and crashes. No help will be given even if you lose any of your worlds or saves.\n\nNow what\'s %1$s multiplied by %2$s plus %3$s minus %4$s?
</string>
<string name="not_modpack_file">Not a modpack file!</string>
</resources>