diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java index 8498e93fb..16ec0854d 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java @@ -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 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; diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavApplication.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavApplication.java index c65aa58ad..6a4972758 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavApplication.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavApplication.java @@ -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"); diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contracts/OpenDocumentWithExtension.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contracts/OpenDocumentWithExtension.java index 8b011a904..8bd5e4547 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contracts/OpenDocumentWithExtension.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contracts/OpenDocumentWithExtension.java @@ -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 { private final String mimeType; + private final String[] mimeTypes; /** * Create a new OpenDocumentWithExtension contract. @@ -25,6 +29,37 @@ public class OpenDocumentWithExtension extends ActivityResultContract 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 { + 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 fragmentClass, String tag){ + if(!hasOnlineProfile()){ + hasNoOnlineProfileDialog(requireActivity()); + } else { + Tools.swapFragment(requireActivity(), fragmentClass, tag, null); + } + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/NeoForgeInstallFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/NeoForgeInstallFragment.java index b901da6e8..51fb7bd7d 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/NeoForgeInstallFragment.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/NeoForgeInstallFragment.java @@ -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> { 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 @Override public List 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 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 versionList, LayoutInflater layoutInflater) { return new NeoForgeVersionListAdapter(versionList, layoutInflater); @@ -74,7 +107,9 @@ public class NeoForgeInstallFragment extends ModVersionListFragment @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); } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ProfileTypeSelectFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ProfileTypeSelectFragment.java index 9acb9d15a..c8870e55d 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ProfileTypeSelectFragment.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ProfileTypeSelectFragment.java @@ -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)-> diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/NeoForgeDownloadTask.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/NeoForgeDownloadTask.java index 9cb40e683..86021ce92 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/NeoForgeDownloadTask.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/NeoForgeDownloadTask.java @@ -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 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 neoforgeVersions = NeoForgeInstallFragment.downloadNeoForgeVersions(); + if(neoforgeVersions == null) return false; + for(String versionName : neoforgeVersions) { + if(!versionName.startsWith(mLoaderVersion)) continue; return true; } return false; diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/NeoForgeVersionListAdapter.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/NeoForgeVersionListAdapter.java index e02670329..df7db75b1 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/NeoForgeVersionListAdapter.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/NeoForgeVersionListAdapter.java @@ -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 mGameVersions; + private final List> mNeoForgeVersions; private final LayoutInflater mLayoutInflater; - private final LinkedHashMap> minecraftToLoaderVersionsHashmap; - private LinkedHashSet generatedHashSet = null; - public NeoForgeVersionListAdapter(List forgeVersions, LayoutInflater layoutInflater) { + public NeoForgeVersionListAdapter(List neoforgeVersions, LayoutInflater layoutInflater) { this.mLayoutInflater = layoutInflater; - minecraftToLoaderVersionsHashmap = new LinkedHashMap<>(); - JsonArray versionsJsonArray = JsonParser.parseString(forgeVersions.get(0)).getAsJsonObject().getAsJsonArray("versions"); - - ArrayList 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 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 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 diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/CommonApi.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/CommonApi.java index cbd43bd30..7383e2c2b 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/CommonApi.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/CommonApi.java @@ -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 modMatrix){ int totalSize = 0; 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 940e4452a..cb4c17ab1 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 @@ -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 objectList, int index, String modId) { HashMap 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? diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModLoader.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModLoader.java index 1eef3567b..59f1dd28a 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModLoader.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModLoader.java @@ -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: diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModpackApi.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModpackApi.java index 141468af8..a6ea9c8dc 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModpackApi.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModpackApi.java @@ -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; } 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 index 048458d30..0c614b8ca 100644 --- 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 @@ -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; } } 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 73a5863f6..325336624 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 @@ -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 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; } diff --git a/app_pojavlauncher/src/main/res/layout/fragment_create_modpack_profile.xml b/app_pojavlauncher/src/main/res/layout/fragment_create_modpack_profile.xml new file mode 100644 index 000000000..bcaf6386e --- /dev/null +++ b/app_pojavlauncher/src/main/res/layout/fragment_create_modpack_profile.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app_pojavlauncher/src/main/res/values/strings.xml b/app_pojavlauncher/src/main/res/values/strings.xml index 54757db88..7f77aca54 100644 --- a/app_pojavlauncher/src/main/res/values/strings.xml +++ b/app_pojavlauncher/src/main/res/values/strings.xml @@ -301,6 +301,7 @@ Select a version Downloading installer for %s Searching for Forge version number + Searching for NeoForge version number Failed to load the version list! Sorry, but this version of Forge does not have an installer, which is not yet supported. Select Forge version @@ -498,5 +499,6 @@ 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? + Not a modpack file!