From 531df2978d9ecffb9456d434e4ff64a3bd046c88 Mon Sep 17 00:00:00 2001 From: alexytomi <60690056+alexytomi@users.noreply.github.com> Date: Wed, 17 Dec 2025 19:20:57 +0800 Subject: [PATCH 1/8] Add functionality to import modpacks Lets you import modpacks from mrpacks and zips --- .../net/kdt/pojavlaunch/LauncherActivity.java | 24 +++++ .../contracts/OpenDocumentWithExtension.java | 22 ++++ .../fragments/ModpackCreateFragment.java | 50 +++++++++ .../fragments/ProfileTypeSelectFragment.java | 5 +- .../modloaders/modpacks/api/CommonApi.java | 37 +++++++ .../modpacks/api/CurseforgeApi.java | 8 ++ .../modloaders/modpacks/api/ModpackApi.java | 15 +++ .../modpacks/api/ModpackInstaller.java | 101 +++++++++++++++++- .../modloaders/modpacks/api/ModrinthApi.java | 9 ++ .../fragment_create_modpack_profile.xml | 55 ++++++++++ 10 files changed, 324 insertions(+), 2 deletions(-) create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ModpackCreateFragment.java create mode 100644 app_pojavlauncher/src/main/res/layout/fragment_create_modpack_profile.xml 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..383591b98 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,23 @@ 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 (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/contracts/OpenDocumentWithExtension.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contracts/OpenDocumentWithExtension.java index 8b011a904..d761ebba0 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,23 @@ public class OpenDocumentWithExtension extends ActivityResultContract extensionsMimeType = new ArrayList<>(); + int count = 0; + for (String extension: extensions) { + String extensionMimetype = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + // This somehow works despite finding no code showing where modrinth was defined. + // Guessing that just DocumentsUI supports this, if someone files a bug report saying + // they can't select mrpacks with DocumentsUI, remove this and just don't filter at all. + if (Objects.equals(extension, "mrpack")) extensionMimetype = "application/x-modrinth-modpack+zip"; + 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 +54,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/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/modpacks/api/CommonApi.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/CommonApi.java index cbd43bd30..63747f3d2 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,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; @@ -11,11 +13,16 @@ 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 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 +119,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 +135,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 RuntimeException("Not a modpack 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..57a1289a9 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<>(); 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..168a7ef70 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,11 @@ package net.kdt.pojavlaunch.modloaders.modpacks.api; +import android.app.Activity; +import android.net.Uri; +import android.util.Log; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import com.kdt.mcgui.ProgressLayout; import net.kdt.pojavlaunch.R; @@ -11,10 +17,21 @@ import net.kdt.pojavlaunch.utils.DownloadUtils; import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles; import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile; +import org.apache.commons.codec.binary.Hex; + +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 +86,89 @@ 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(); + String hash = Hex.encodeHexString(digest); + // Parse the JSON to prepare for instance creation + JsonObject packInfoJson = JsonParser.parseString(jsonString.toString()).getAsJsonObject(); + String modpackName = null; + 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 read" + zipEntry.getName() + "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..21f5ba619 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; 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 From cca5cc63de451d5fe34f20beb2c5ee2f26e52b09 Mon Sep 17 00:00:00 2001 From: alexytomi <60690056+alexytomi@users.noreply.github.com> Date: Fri, 26 Dec 2025 21:03:25 +0800 Subject: [PATCH 2/8] fix: Fix mimetype issues with stock android not matching .mrpack We want to be able to grey out files that are not .zip or .mrpack so people can have an easier chance of seeing their modpacks. To do this, we match the `application/x-modrinth-modpack+zip` and `application/zip` mimetypes as prescribed in the Modrinth documentation https://support.modrinth.com/en/articles/8802351-modrinth-modpack-format-mrpack. Stock android does not have `application/x-modrinth-modpack+zip`. OEM vendors seem to include this in their ROMs, possibly from just using https://gitlab.freedesktop.org/xdg/shared-mime-info as part of where they source their additional mimetypes from. There's a tendency for Android 12 and below to not have the `application/x-modrinth-modpack+zip` mimetype, likely due to the addition of it being relatively recent, so we have to account for that by also adding `application/octet-stream`. We cannot only have `application/octet-stream` or else it fails to match .mrpack in systems that have `application/x-modrinth-modpack+zip`. Sadly there seems to be no reliable way to check whether or the the ROM has the `application/x-modrinth-modpack+zip` mimetype so we just match it along with `application/octet-stream` to cover our bases. --- .../contracts/OpenDocumentWithExtension.java | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) 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 d761ebba0..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 @@ -36,10 +36,24 @@ public class OpenDocumentWithExtension extends ActivityResultContract Date: Fri, 26 Dec 2025 21:23:16 +0800 Subject: [PATCH 3/8] fix: Properly show error for incorrect file It was crashing the launcher before, now it shows it in a box --- .../src/main/java/net/kdt/pojavlaunch/LauncherActivity.java | 2 ++ .../kdt/pojavlaunch/modloaders/modpacks/api/CommonApi.java | 6 +++++- .../modloaders/modpacks/api/ModpackInstaller.java | 4 ++-- app_pojavlauncher/src/main/res/values/strings.xml | 1 + 4 files changed, 10 insertions(+), 3 deletions(-) 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 383591b98..16ec0854d 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java @@ -77,6 +77,8 @@ public class LauncherActivity extends BaseActivity { 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); 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 63747f3d2..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 @@ -7,12 +7,16 @@ 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; @@ -157,7 +161,7 @@ public class CommonApi implements ModpackApi { } catch (Exception e) { throw new RuntimeException(e); } - throw new RuntimeException("Not a modpack file!"); + throw new IllegalArgumentException("Zip provided does not contain a manifest file"); } /** Fuse the arrays in a way that's fair for every endpoint */ 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 168a7ef70..c9b83f693 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 @@ -118,7 +118,7 @@ public class ModpackInstaller { String hash = Hex.encodeHexString(digest); // Parse the JSON to prepare for instance creation JsonObject packInfoJson = JsonParser.parseString(jsonString.toString()).getAsJsonObject(); - String modpackName = null; + 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: @@ -165,7 +165,7 @@ public class ModpackInstaller { return modLoaderInfo; } - throw new IOException("Can't read" + zipEntry.getName() + "in modpack provided"); + throw new IOException("Can't find manifest file in modpack provided"); } interface InstallFunction { diff --git a/app_pojavlauncher/src/main/res/values/strings.xml b/app_pojavlauncher/src/main/res/values/strings.xml index 54757db88..aadd05c88 100644 --- a/app_pojavlauncher/src/main/res/values/strings.xml +++ b/app_pojavlauncher/src/main/res/values/strings.xml @@ -498,5 +498,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! From 6f75934b017d55be75865ca0765ff9c9781cb99d Mon Sep 17 00:00:00 2001 From: alexytomi <60690056+alexytomi@users.noreply.github.com> Date: Sat, 27 Dec 2025 03:12:49 +0800 Subject: [PATCH 4/8] fix: workaround android 7 crash with Hex.encodeHexString For some reason it keeps trying to use the system apache commons instead of the one in the app, and because that one is really old, this method is missing so it crashes. Crash report - Time: 26-Dec-2025 6:46:49 PM - Device: lineage_Hol-U19 Hol-U19 - Android version: 7.1.2 - Crash stack trace: - Launcher version: amethyst-legacy-20251226-c798f65-feat/import-modpack java.lang.NoSuchMethodError: No static method encodeHexString([B)Ljava/lang/String; in class Lorg/apache/commons/codec/binary/Hex; or its super classes (declaration of 'org.apache.commons.codec.binary.Hex' appears in /system/framework/org.apache.http.legacy.boot.jar) --- .../modloaders/modpacks/api/ModpackInstaller.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 c9b83f693..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 @@ -2,7 +2,6 @@ package net.kdt.pojavlaunch.modloaders.modpacks.api; import android.app.Activity; import android.net.Uri; -import android.util.Log; import com.google.gson.JsonObject; import com.google.gson.JsonParser; @@ -17,7 +16,6 @@ import net.kdt.pojavlaunch.utils.DownloadUtils; import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles; import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile; -import org.apache.commons.codec.binary.Hex; import java.io.BufferedReader; import java.io.File; @@ -115,7 +113,12 @@ public class ModpackInstaller { while (hashingStream.read(buffer) != -1) {} // just read to update the digest hashingStream.close(); byte[] digest = algorithm.digest(); - String hash = Hex.encodeHexString(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; From d20a20b57f66e7a4ae1b75e38215a794bf17c37a Mon Sep 17 00:00:00 2001 From: alexytomi <60690056+alexytomi@users.noreply.github.com> Date: Sat, 27 Dec 2025 03:13:47 +0800 Subject: [PATCH 5/8] fix: Fix crash report text from Pojav to Amethyst the rebranding should really have been done far earlier. oh well, we're gonna roll with it --- .../src/main/java/net/kdt/pojavlaunch/PojavApplication.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"); From ee1596cbd89a9b3cf7977cb78ab4c8b6bad5435c Mon Sep 17 00:00:00 2001 From: alexytomi <60690056+alexytomi@users.noreply.github.com> Date: Sat, 27 Dec 2025 04:43:20 +0800 Subject: [PATCH 6/8] fix: Neoforge not creating instances on modpack install --- .../modloaders/modpacks/api/CurseforgeApi.java | 3 +++ .../modloaders/modpacks/api/ModLoader.java | 12 ++++++++++++ .../modloaders/modpacks/api/ModrinthApi.java | 3 +++ 3 files changed, 18 insertions(+) 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 57a1289a9..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 @@ -219,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..f6e4c5597 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, minecraftVersion, 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/ModrinthApi.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModrinthApi.java index 21f5ba619..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 @@ -140,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; } From e4ce830f66c87c9eb1d8786b69f4d37a392c320a Mon Sep 17 00:00:00 2001 From: alexytomi <60690056+alexytomi@users.noreply.github.com> Date: Sat, 27 Dec 2025 06:28:35 +0800 Subject: [PATCH 7/8] fix: Neoforge not installing on modpack install Also reworks the neoforge profile install to use neoforge maven https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml This is away from using prism's jsons https://meta.prismlauncher.org/v1/net.neoforged/index.json This is because prism still has 1.20.1 listed, and we can't really run those. We could have ran with prism for the profile select but that would be even more confusing. --- .../fragments/NeoForgeInstallFragment.java | 63 +++++++++++---- .../modloaders/NeoForgeDownloadTask.java | 21 ++--- .../NeoForgeVersionListAdapter.java | 77 +++++++++---------- .../src/main/res/values/strings.xml | 1 + 4 files changed, 100 insertions(+), 62 deletions(-) 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/modloaders/NeoForgeDownloadTask.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/NeoForgeDownloadTask.java index 9cb40e683..05466bccf 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 @@ -4,6 +4,7 @@ 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; @@ -18,10 +19,10 @@ public class NeoForgeDownloadTask implements Runnable, Tools.DownloaderFeedback private String mLoaderVersion; private String mGameVersion; private final ModloaderDownloadListener mListener; - public NeoForgeDownloadTask(ModloaderDownloadListener listener, String forgeVersion) { + public NeoForgeDownloadTask(ModloaderDownloadListener listener, String neoforgeVersion) { 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, neoforgeVersion); + this.mFullVersion = neoforgeVersion; } public NeoForgeDownloadTask(ModloaderDownloadListener listener, String gameVersion, String loaderVersion) { @@ -29,6 +30,9 @@ public class NeoForgeDownloadTask implements Runnable, Tools.DownloaderFeedback 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()) { @@ -73,13 +77,12 @@ 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; + List neoforgeVersions = NeoForgeInstallFragment.downloadNeoForgeVersions(); + if(neoforgeVersions == null) return false; + for(String versionName : neoforgeVersions) { + if(!versionName.startsWith(mLoaderVersion)) continue; mFullVersion = versionName; - mDownloadUrl = ForgeUtils.getInstallerUrl(mFullVersion); + mDownloadUrl = String.format(NEOFORGE_INSTALLER_URL, mLoaderVersion); 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/res/values/strings.xml b/app_pojavlauncher/src/main/res/values/strings.xml index aadd05c88..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 From b134986aaf03c11306f0b0e91d740e6f4f467f31 Mon Sep 17 00:00:00 2001 From: alexytomi <60690056+alexytomi@users.noreply.github.com> Date: Sat, 27 Dec 2025 16:36:50 +0800 Subject: [PATCH 8/8] refactor: NeoForgeDownloadTask to one constructor Simplifies to one constructor. We don't need to copy the approach of forge because neoforge integrates the minecraft version into their version scheme, thus we have no use for the minecraft version provided to us. --- .../modloaders/NeoForgeDownloadTask.java | 33 ++++++++----------- .../modloaders/modpacks/api/ModLoader.java | 2 +- 2 files changed, 14 insertions(+), 21 deletions(-) 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 05466bccf..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,5 +1,7 @@ package net.kdt.pojavlaunch.modloaders; +import androidx.annotation.NonNull; + import com.kdt.mcgui.ProgressLayout; import net.kdt.pojavlaunch.R; @@ -14,21 +16,15 @@ 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 ModloaderDownloadListener mListener; - public NeoForgeDownloadTask(ModloaderDownloadListener listener, String neoforgeVersion) { - this.mListener = listener; - this.mDownloadUrl = String.format(NEOFORGE_INSTALLER_URL, neoforgeVersion); - this.mFullVersion = neoforgeVersion; - } + private final String mDownloadUrl; + private final String mLoaderVersion; - public NeoForgeDownloadTask(ModloaderDownloadListener listener, String gameVersion, String loaderVersion) { + private final ModloaderDownloadListener mListener; + + public NeoForgeDownloadTask(ModloaderDownloadListener listener, @NonNull String loaderVersion) { this.mListener = listener; + this.mDownloadUrl = String.format(NEOFORGE_INSTALLER_URL, loaderVersion); 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"; @@ -36,7 +32,7 @@ public class NeoForgeDownloadTask implements Runnable, Tools.DownloaderFeedback @Override public void run() { if(determineDownloadUrl()) { - downloadForge(); + downloadNeoForge(); } ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK); } @@ -44,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]; @@ -62,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(); @@ -81,8 +76,6 @@ public class NeoForgeDownloadTask implements Runnable, Tools.DownloaderFeedback if(neoforgeVersions == null) return false; for(String versionName : neoforgeVersions) { if(!versionName.startsWith(mLoaderVersion)) continue; - mFullVersion = versionName; - mDownloadUrl = String.format(NEOFORGE_INSTALLER_URL, mLoaderVersion); return true; } return false; 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 f6e4c5597..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 @@ -62,7 +62,7 @@ public class ModLoader { case MOD_LOADER_QUILT: return createFabriclikeTask(listener, FabriclikeUtils.QUILT_UTILS); case MOD_LOADER_NEOFORGE: - return new NeoForgeDownloadTask(listener, minecraftVersion, modLoaderVersion); + return new NeoForgeDownloadTask(listener, modLoaderVersion); default: return null; }