From 97a62cf8d1dd3820cdffb1ac1c507c06644240ff Mon Sep 17 00:00:00 2001 From: tomikun <60690056+alexytomi@users.noreply.github.com> Date: Thu, 14 May 2026 00:02:03 +0800 Subject: [PATCH] feat: LWJGL3ify support Now should run lwjgl3ify ootb (assuming non-debug builds, the forgePatches url is broken there, not our bug) --- .../net/kdt/pojavlaunch/LauncherActivity.java | 46 +++ .../main/java/net/kdt/pojavlaunch/Tools.java | 23 +- .../fragments/LWJGL3ifyInstallFragment.java | 86 ++++++ .../fragments/MainMenuFragment.java | 5 +- .../fragments/ProfileTypeSelectFragment.java | 2 + .../modloaders/LWJGL3ifyDownloadTask.java | 139 +++++++++ .../modloaders/LWJGL3ifyUtils.java | 274 ++++++++++++++++++ .../LWJGL3ifyVersionListAdapter.java | 95 ++++++ .../net/kdt/pojavlaunch/utils/JREUtils.java | 3 +- .../main/res/layout/fragment_profile_type.xml | 8 + .../src/main/res/values/strings.xml | 4 + 11 files changed, 678 insertions(+), 7 deletions(-) create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/LWJGL3ifyInstallFragment.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/LWJGL3ifyDownloadTask.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/LWJGL3ifyUtils.java create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/LWJGL3ifyVersionListAdapter.java 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 16ec0854d..6bed305c6 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java @@ -1,7 +1,10 @@ package net.kdt.pojavlaunch; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static net.kdt.pojavlaunch.Tools.getMods; +import static net.kdt.pojavlaunch.Tools.hasMods; import static net.kdt.pojavlaunch.Tools.hasNoOnlineProfileDialog; +import static net.kdt.pojavlaunch.Tools.isOnline; import android.Manifest; import android.app.NotificationManager; @@ -36,6 +39,7 @@ import net.kdt.pojavlaunch.fragments.MicrosoftLoginFragment; import net.kdt.pojavlaunch.fragments.SelectAuthFragment; import net.kdt.pojavlaunch.lifecycle.ContextAwareDoneListener; import net.kdt.pojavlaunch.lifecycle.ContextExecutor; +import net.kdt.pojavlaunch.modloaders.LWJGL3ifyUtils; import net.kdt.pojavlaunch.modloaders.modpacks.ModloaderInstallTracker; import net.kdt.pojavlaunch.modloaders.modpacks.api.CommonApi; import net.kdt.pojavlaunch.modloaders.modpacks.api.ModLoader; @@ -55,10 +59,12 @@ import net.kdt.pojavlaunch.utils.NotificationUtils; import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles; import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile; +import java.io.File; import java.io.IOException; import java.lang.ref.WeakReference; import java.security.NoSuchAlgorithmException; import java.text.ParseException; +import java.util.List; public class LauncherActivity extends BaseActivity { public static final String SETTING_FRAGMENT_TAG = "SETTINGS_FRAGMENT"; @@ -172,6 +178,46 @@ public class LauncherActivity extends BaseActivity { } } + // Override whatever version is in use and replace it with lwjgl3ify if needed + List lwjgl3ifyJars = getMods("lwjgl3ify-3"); + if (!lwjgl3ifyJars.isEmpty()) { + if (lwjgl3ifyJars.size() > 1) { + // "Duplicate LWJGL3ify jars found, cannot launch." + Tools.dialogOnUiThread(this, R.string.global_error, R.string.mc_download_failed); + return false; + } + + File lwjgl3ifyJar = lwjgl3ifyJars.get(0); + + // If the version contains lwjgl3ify, its probably someone who knows what they're doing + // so lets leave that alone + if (!prof.lastVersionId.toLowerCase().contains("lwjgl3ify")) { + try { + prof.lastVersionId = LWJGL3ifyUtils.installJson(lwjgl3ifyJar).id; + } catch (IOException e) { + throw new RuntimeException(e); + } + LauncherProfiles.mainProfileJson.profiles.put(selectedProfile, prof); + LauncherProfiles.write(); + + + } + // We just installed a json, we need internet + online acc to download so we add super + // basic detection whether lwjgl3ify assets were downloaded + try { + String jsonPath = LWJGL3ifyUtils.getJsonPath(LWJGL3ifyUtils.getProfileID(lwjgl3ifyJar)); + File lwjgl3ifyClientJar = new File(jsonPath.replace(".json", ".jar")); + if (!lwjgl3ifyClientJar.exists()){ + if (mAccountSpinner.getSelectedAccount().isLocal() || !isOnline(this)){ + Tools.dialogOnUiThread(this, R.string.global_error, R.string.mc_download_failed); + return false; + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + new MinecraftDownloader().start( this, mcVersion, diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java index 68aef5bdb..ea63479eb 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java @@ -280,15 +280,30 @@ public final class Tools { * @return Whether or not the .jar is found */ public static boolean hasMods(String... filenames) { + return !getMods(filenames).isEmpty(); + } + + /** + * Searches for mod in mods directory of current selected profile + * Not case-sensitive + * @param filenames Filename(s) of the .jar mod(s) + * @return The found mods + */ + public static List getMods(String... filenames) { File gameDir = getGameDir(); File modsDir = new File(gameDir, "mods"); - File[] modFiles = modsDir.listFiles(file -> file.isFile() && file.getName().endsWith(".jar")); - if (modFiles == null) return false; + File[] modFiles = modsDir.listFiles(file -> file.isFile() && file.getName().toLowerCase().endsWith(".jar")); + if (modFiles == null) return new ArrayList<>(); + List foundModFiles = new ArrayList<>(); for (File file : modFiles) { for (String filename : filenames) - if (file.getName().toLowerCase().contains(filename.toLowerCase())) return true; + if (file.getName().toLowerCase().contains(filename.toLowerCase()) && + file.getName().toLowerCase().endsWith(".jar")) { + foundModFiles.add(file); + break; + } } - return false; + return foundModFiles; } /** diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/LWJGL3ifyInstallFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/LWJGL3ifyInstallFragment.java new file mode 100644 index 000000000..951bf9500 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/LWJGL3ifyInstallFragment.java @@ -0,0 +1,86 @@ +package net.kdt.pojavlaunch.fragments; + +import android.content.Context; +import android.view.LayoutInflater; +import android.widget.ExpandableListAdapter; + +import androidx.annotation.NonNull; + +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.modloaders.LWJGL3ifyDownloadTask; +import net.kdt.pojavlaunch.modloaders.LWJGL3ifyUtils; +import net.kdt.pojavlaunch.modloaders.LWJGL3ifyVersionListAdapter; +import net.kdt.pojavlaunch.modloaders.ModloaderListenerProxy; +import net.kdt.pojavlaunch.modloaders.modpacks.api.CommonApi; +import net.kdt.pojavlaunch.modloaders.modpacks.api.ModpackApi; + +import java.io.File; +import java.io.IOException; + +public class LWJGL3ifyInstallFragment extends ModVersionListFragment{ + public static final String TAG = "LWJGL3ifyInstallFragment"; + private ModpackApi modpackApi; + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + modpackApi = new CommonApi(context.getString(R.string.curseforge_api_key)); + } + public LWJGL3ifyInstallFragment() { + super(TAG); + } + + /** + * @return + */ + @Override + public int getTitleText() { + return R.string.select_lwjgl3ify_version; + } + + /** + * @return + */ + @Override + public int getNoDataMsg() { + return R.string.modloader_dl_failed_to_load_list; + } + + /** + * @return + * @throws IOException + */ + @Override + public LWJGL3ifyUtils.LWJGL3ifyVersionList loadVersionList() throws IOException { + return LWJGL3ifyUtils.getLWJGL3ifyVersionList(modpackApi); + } + + /** + * @param versionList + * @param layoutInflater + * @return + */ + @Override + public ExpandableListAdapter createAdapter(LWJGL3ifyUtils.LWJGL3ifyVersionList versionList, LayoutInflater layoutInflater) { + return new LWJGL3ifyVersionListAdapter(versionList, layoutInflater); + } + + /** + * @param selectedVersion + * @param listenerProxy + * @return + */ + @Override + public Runnable createDownloadTask(Object selectedVersion, ModloaderListenerProxy listenerProxy) { + return new LWJGL3ifyDownloadTask(listenerProxy, (LWJGL3ifyUtils.LWJGL3ifyMod) selectedVersion, requireActivity()); + } + + /** + * @param context + * @param downloadedFile + */ + @Override + public void onDownloadFinished(Context context, File downloadedFile) { + // Nothing to do. + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/MainMenuFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/MainMenuFragment.java index 3c55940a4..19bb36b23 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/MainMenuFragment.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/MainMenuFragment.java @@ -1,6 +1,7 @@ package net.kdt.pojavlaunch.fragments; import static net.kdt.pojavlaunch.Tools.dialogOnUiThread; +import static net.kdt.pojavlaunch.Tools.hasMods; import static net.kdt.pojavlaunch.Tools.hasNoOnlineProfileDialog; import static net.kdt.pojavlaunch.Tools.hasOnlineProfile; import static net.kdt.pojavlaunch.Tools.openPath; @@ -27,12 +28,14 @@ import net.kdt.pojavlaunch.R; import net.kdt.pojavlaunch.Tools; import net.kdt.pojavlaunch.extra.ExtraConstants; import net.kdt.pojavlaunch.extra.ExtraCore; +import net.kdt.pojavlaunch.modloaders.LWJGL3ifyUtils; import net.kdt.pojavlaunch.prefs.LauncherPreferences; import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper; import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles; import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile; import java.io.File; +import java.util.List; public class MainMenuFragment extends Fragment { public static final String TAG = "MainMenuFragment"; @@ -80,8 +83,6 @@ public class MainMenuFragment extends Fragment { .create(); sodiumWarningDialog.show(); } else ExtraCore.setValue(ExtraConstants.LAUNCH_GAME, true); - - }); mShareLogsButton.setOnClickListener((v) -> shareLog(requireContext())); 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 c8870e55d..9f923e2e8 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 @@ -43,6 +43,8 @@ public class ProfileTypeSelectFragment extends Fragment { tryInstall(NeoForgeInstallFragment.class, NeoForgeInstallFragment.TAG)); view.findViewById(R.id.modded_profile_modpack).setOnClickListener((v)-> tryInstall(ModpackCreateFragment.class, ModpackCreateFragment.TAG)); + view.findViewById(R.id.modded_profile_lwjgl3ify).setOnClickListener((v)-> + tryInstall(LWJGL3ifyInstallFragment.class, LWJGL3ifyInstallFragment.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/LWJGL3ifyDownloadTask.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/LWJGL3ifyDownloadTask.java new file mode 100644 index 000000000..51148420b --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/LWJGL3ifyDownloadTask.java @@ -0,0 +1,139 @@ +package net.kdt.pojavlaunch.modloaders; + +import android.app.Activity; + +import com.kdt.mcgui.ProgressLayout; + +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.Tools; +import net.kdt.pojavlaunch.modloaders.modpacks.api.CommonApi; +import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper; +import net.kdt.pojavlaunch.tasks.AsyncMinecraftDownloader; +import net.kdt.pojavlaunch.tasks.MinecraftDownloader; +import net.kdt.pojavlaunch.utils.DownloadUtils; +import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles; +import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.Objects; + +public class LWJGL3ifyDownloadTask implements Runnable, Tools.DownloaderFeedback, AsyncMinecraftDownloader.DoneListener { + protected static final String TAG = "LWJGL3ifyDownloadTask"; + private final ModloaderDownloadListener mListener; + private final LWJGL3ifyUtils.LWJGL3ifyMod mLWJGL3ifyMod; + private final Activity mActivity; + + public LWJGL3ifyDownloadTask(ModloaderDownloadListener mListener, LWJGL3ifyUtils.LWJGL3ifyMod mLWJGL3ifyMod, Activity activity) { + this.mListener = mListener; + this.mLWJGL3ifyMod = mLWJGL3ifyMod; + this.mActivity = activity; + } + + @Override + public void run() { + ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.fabric_dl_progress, "BTA"); + try { + runCatching(); + mListener.onDownloadFinished(null); + }catch (Exception e) { + mListener.onDownloadError(e); + }finally { + ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK); + } + + } + + private File tryDownloadModJar() throws IOException { + try { + File jarFile = new File(Tools.DIR_CACHE, "lwjgl3ify-jars/lwjgl3ify-"+ mLWJGL3ifyMod.versionName+".jar"); + if (!(jarFile.exists() && Objects.equals(getSha1(jarFile), mLWJGL3ifyMod.hash))) + DownloadUtils.downloadFileMonitored( + mLWJGL3ifyMod.downloadUrl, + jarFile, + new byte[8192], + this + ); + return jarFile; + } catch (IOException | NoSuchAlgorithmException e) { + throw new IOException("Unable to download LWJGL3ify from " + mLWJGL3ifyMod.downloadUrl, e); + } + } + + private void tryDownloadDeps(File modsDir) throws IOException { + List deps = LWJGL3ifyUtils.collectDependencies(mLWJGL3ifyMod, new CommonApi(mActivity.getString(R.string.curseforge_api_key))); + for (int i=0; i < deps.size(); ++i) { + URI uri = null; + try { + uri = new URI(deps.get(i).downloadUrl); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + String path = uri.getPath(); + String fileName = new File(path).getName(); + try { + DownloadUtils.downloadFileMonitored( + deps.get(i).downloadUrl, + new File(modsDir, fileName), + new byte[8192], + this + ); + } catch (IOException e) { + throw new IOException("Unable to download"+deps.get(i).versionName+" from " + deps.get(i).downloadUrl, e); + } + } + } + + public String getSha1(File file) throws IOException, NoSuchAlgorithmException { + MessageDigest algorithm = MessageDigest.getInstance("SHA-1"); + //noinspection IOStreamConstructor It will reccomend you use an API26 function like a dumb + DigestInputStream hashingStream = new DigestInputStream(new FileInputStream(file), 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)); + } + return sb.toString(); + } + + public void runCatching() throws IOException { + File modJar = tryDownloadModJar(); + // This cannot be allowed to match the mod.jar ID otherwise conflicts occur and GLFW input breaks + String LWJGL3ifyProfileID = LWJGL3ifyUtils.getProfileID(modJar); + if (!modJar.exists()) throw new IOException("Failed to download LWJGL3ify "+ mLWJGL3ifyMod.versionName); + MinecraftProfile profile = LWJGL3ifyUtils.createProfile(LWJGL3ifyProfileID, mLWJGL3ifyMod.versionName, mLWJGL3ifyMod.iconUrl); + new MinecraftDownloader().start(mActivity, LWJGL3ifyUtils.installJson(modJar), LWJGL3ifyProfileID, this); + LWJGL3ifyUtils.createInstance(profile, modJar, mLWJGL3ifyMod.versionName); + tryDownloadDeps(new File(Tools.DIR_GAME_HOME, profile.gameDir+"/mods")); + + LauncherProfiles.load(); + LauncherProfiles.insertMinecraftProfile(profile); + LauncherProfiles.write(); + } + + @Override + public void updateProgress(int curr, int max) { + int progress100 = (int)(((float)curr / (float)max)*100f); + ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, progress100, R.string.of_dl_progress, mLWJGL3ifyMod.versionName); + } + + @Override + public void onDownloadDone() { + + } + + @Override + public void onDownloadFailed(Throwable throwable) { + + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/LWJGL3ifyUtils.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/LWJGL3ifyUtils.java new file mode 100644 index 000000000..cce3c2ffd --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/LWJGL3ifyUtils.java @@ -0,0 +1,274 @@ +package net.kdt.pojavlaunch.modloaders; + +import android.util.Base64; +import android.util.Base64OutputStream; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.gson.JsonObject; + +import net.kdt.pojavlaunch.JMinecraftVersionList; +import net.kdt.pojavlaunch.Tools; +import net.kdt.pojavlaunch.modloaders.modpacks.api.ModpackApi; +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.utils.DownloadUtils; +import net.kdt.pojavlaunch.utils.FileUtils; +import net.kdt.pojavlaunch.utils.ZipUtils; +import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.ZipFile; + +public class LWJGL3ifyUtils { + public static LWJGL3ifyVersionList getLWJGL3ifyVersionList(ModpackApi modpackApi) throws IOException { + ModDetail lwjgl3ifyModDetail = getLWJGL3ifyModDetail(modpackApi); + List supportedVersions = new ArrayList<>(), brokenVersions = new ArrayList<>(); + for (int i = 0; i < lwjgl3ifyModDetail.versionNames.length; i++) { + String normalizedVersion = normalizeVersionName(lwjgl3ifyModDetail.versionNames[i], lwjgl3ifyModDetail.apiSource); +// boolean isSupportedVersion = Integer.parseInt(normalizedVersion.split("\\.")[0]) < 3; + + LWJGL3ifyMod version = new LWJGL3ifyMod( + normalizedVersion, + lwjgl3ifyModDetail.versionUrls[i], + lwjgl3ifyModDetail.imageUrl, + lwjgl3ifyModDetail.id, + lwjgl3ifyModDetail.versionHashes[i], + lwjgl3ifyModDetail.dependencies[i] + ); + // It's fixed, probably. +// // LWJGL3ify 3.x uses SDL which needs to be fixed first +// (isSupportedVersion ? supportedVersions : brokenVersions).add(version); + supportedVersions.add(version); + + } + return new LWJGL3ifyVersionList(supportedVersions, brokenVersions); + } + + /** + * @param jarName LWJGL3ify jar name, the same as Curseforge {@code versionNames} (ex. {@code 3.0.16}, {@code 2.1.18}) + * @param source Either {@link Constants#SOURCE_MODRINTH} or {@link Constants#SOURCE_CURSEFORGE} + * @return Filled out {@link LWJGL3ifyMod} corresponding to version provided + * @throws IllegalArgumentException If LWJGL3ify version was not found in the source provided + * @throws IOException If provided source is not what was expected + */ + public static LWJGL3ifyMod getLWJGL3ifyVersion(String jarName, ModpackApi source) throws IOException { + // This is a hack but it should work + String providedNormalizedVersion = normalizeVersionName(jarName, Constants.SOURCE_CURSEFORGE); + ModDetail lwjgl3ifyModDetail = getLWJGL3ifyModDetail(source); + for (int i = 0; i < lwjgl3ifyModDetail.versionNames.length; i++) { + String normalizedVersion = normalizeVersionName(lwjgl3ifyModDetail.versionNames[i], lwjgl3ifyModDetail.apiSource); + if (providedNormalizedVersion.equals(normalizedVersion)) + return new LWJGL3ifyMod( + providedNormalizedVersion, + lwjgl3ifyModDetail.versionUrls[i], + lwjgl3ifyModDetail.imageUrl, + lwjgl3ifyModDetail.id, + lwjgl3ifyModDetail.versionHashes[i], + lwjgl3ifyModDetail.dependencies[i] + ); + } + String sourceName = (lwjgl3ifyModDetail.apiSource == Constants.SOURCE_MODRINTH) ? "Modrinth" : "Curseforge"; + throw new IllegalArgumentException("Cannot find LWJGL3ify version "+providedNormalizedVersion+" from "+sourceName); + } + + /** + * @return Flat list of all dependencies needed by {@code lwjgl3ifyMod}. + */ + public static List collectDependencies(LWJGL3ifyMod lwjgl3ifyMod, ModpackApi modpackApi) { + List allDeps = new ArrayList<>(); + for (ModDetail.Dependencies dep : lwjgl3ifyMod.dependencies) { + ModDetail detail = getModDetail(modpackApi, dep.project_id); + if (detail != null) { + LWJGL3ifyMod newMod = new LWJGL3ifyMod( + detail.versionNames[0], + detail.versionUrls[0], + detail.imageUrl, + detail.id, + detail.versionHashes[0], + detail.dependencies[0] + ); + + allDeps.add(newMod); + // omg recursion!?!?! + allDeps.addAll(collectDependencies(newMod, modpackApi)); + } + } + return allDeps; + } + + @NonNull + private static String normalizeVersionName(String versionName, int apiSource) { + if (apiSource == Constants.SOURCE_MODRINTH) { // Ex. 3.0.16 - 1.7.10 + versionName = versionName.replaceAll(" - .*", ""); + }else if (apiSource == Constants.SOURCE_CURSEFORGE) { // Ex. lwjgl3ify-3.0.16.jar - 1.7.10 + versionName = versionName.split("-")[1].replace(".jar", ""); + }else throw new IllegalArgumentException("LWJGL3ify is only available on Modrinth or Curseforge!"); + return versionName; + } + + @Nullable + private static ModDetail getModDetail(ModpackApi modpackApi, String id){ + ModDetail modDetail = null; + try { + // Modrinth is more complete in this context. Curseforge is missing some releases. + if (id != null && !id.isEmpty()) { + modDetail = fetch(modpackApi, Constants.SOURCE_MODRINTH, id); + } + if (modDetail == null && id != null && !id.isEmpty()) { + Integer.parseInt(id); // Triggers exception, skipping call to CF if provided id isn't int + modDetail = fetch(modpackApi, Constants.SOURCE_CURSEFORGE, id); + } + } catch (NumberFormatException ignored) {} + return modDetail; + } + + private static ModDetail fetch(ModpackApi modpackApi, int source, String id) { + ModItem item = new ModItem(source, false, id, null, null, null); + return modpackApi.getModDetails(item); + } + + @NonNull + private static ModDetail getLWJGL3ifyModDetail(ModpackApi modpackApi) throws IOException { + ModDetail lwjgl3ifyModDetail = getModDetail(modpackApi, "lwjgl3ify"); + if (lwjgl3ifyModDetail == null) // Hardcoded ID is a bad idea, but it'll work well enough + lwjgl3ifyModDetail = getModDetail(modpackApi, "998880"); + if (lwjgl3ifyModDetail == null) throw new IOException("Unable to fetch LWJGL3ify version list from Curseforge and Modrinth. " + + "Please check your internet connection and whether Modrinth and Curseforge are accessible."); + return lwjgl3ifyModDetail; + } + + public static JMinecraftVersionList.Version installJson(File modJar) throws IOException { + String profileID = getProfileID(modJar); + String jsonPath = getJsonPath(profileID); + try { + JMinecraftVersionList.Version version = Tools.GLOBAL_GSON.fromJson( + Tools.read( + ZipUtils.getEntryStream( + new ZipFile(modJar), + "me/eigenraven/lwjgl3ify/relauncher/version.json" + ) + ), + JMinecraftVersionList.Version.class); + version.id = profileID; + if (!org.apache.commons.io.FileUtils.getFile(jsonPath).exists()) + Tools.write(jsonPath, Tools.GLOBAL_GSON.toJson(version)); + return version; + } catch (IOException e) { + throw new IOException("Failed to install "+profileID+" json file."); + } + } + + @NonNull + public static String getJsonPath(String profileID) { + return Tools.DIR_HOME_VERSION + "/" + profileID + "/" + profileID + ".json"; + } + + @NonNull + public static String getProfileID(File modJar) throws IOException { + return "1.7.10-LWJGL3ify-"+getVersionFromJar(modJar); + } + + public static String getVersionFromJar(File modJar) throws IOException { + try (ZipFile zipFile = new ZipFile(modJar)) { + JsonObject root = Tools.GLOBAL_GSON.fromJson( + Tools.read(ZipUtils.getEntryStream(zipFile, "mcmod.info")), + JsonObject.class); + return root.getAsJsonArray("modList") + .get(0).getAsJsonObject() + .get("version").getAsString(); + } + } + + public static void createInstance(MinecraftProfile profile, File modJar, String versionName) throws IOException { + File modsDir = new File(Tools.DIR_GAME_HOME, profile.gameDir+"/mods"); + if (modsDir.isFile()) { + if (!modsDir.delete()) { + throw new IOException("Failed to delete file where directory should be: " + modsDir.getAbsolutePath()); + } + } + try { + FileUtils.ensureDirectory(modsDir); + } catch (IOException e) { + throw new IOException("Failed to create folder " + modsDir.getAbsolutePath()); + } + // Copy downloaded cached mod jar + try (FileInputStream fis = new FileInputStream(modJar); + FileOutputStream fos = new FileOutputStream(new File(modsDir, "lwjgl3ify-"+ versionName+".jar"))) { + byte[] buffer = new byte[8192]; + int length; + while ((length = fis.read(buffer)) > 0) { + fos.write(buffer, 0, length); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static MinecraftProfile createProfile(String profileID, String versionName, String iconUrl) { + MinecraftProfile LWJGL3ifyProfile = new MinecraftProfile(); + LWJGL3ifyProfile.lastVersionId = profileID; + LWJGL3ifyProfile.name = "LWJGL3ify - "+ versionName; + LWJGL3ifyProfile.gameDir = String.format("./custom_instances/LWJGL3ify_%s", versionName); + LWJGL3ifyProfile.icon = tryDownloadIcon(iconUrl); + return LWJGL3ifyProfile; + } + + public static String tryDownloadIcon(String iconUrl) { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + try (Base64OutputStream base64OutputStream = new Base64OutputStream(byteArrayOutputStream, Base64.DEFAULT)){ + // Instead of appending and wasting memory with a StringBuilder, just write the prefix + // to the stream before the base64 icon data. + byteArrayOutputStream.write("data:image/png;base64,".getBytes(StandardCharsets.US_ASCII)); + DownloadUtils.download(iconUrl, base64OutputStream); + return new String(byteArrayOutputStream.toByteArray(), StandardCharsets.US_ASCII); + }catch (IOException e) { + Log.w(LWJGL3ifyDownloadTask.TAG, "Failed to download base64 icon", e); + }finally { + try { + byteArrayOutputStream.close(); + } catch (IOException e) { + Log.wtf(LWJGL3ifyDownloadTask.TAG, "Failed to close a byte array stream??", e); + } + } + return null; + } + + // TODO: Turn this into a generic ModItem class for general mods and refactor that crusty modpack naming scheme + public static class LWJGL3ifyMod { + public final String versionName; + public final String downloadUrl; + public final String iconUrl; + public final String id; + public final String hash; + public final ModDetail.Dependencies[] dependencies; + + public LWJGL3ifyMod(String versionName, String downloadUrl, String iconUrl, String id, String hash, ModDetail.Dependencies[] dependencies) { + this.versionName = versionName; + this.downloadUrl = downloadUrl; + this.iconUrl = iconUrl; + this.id = id; + this.hash = hash; + this.dependencies = dependencies; + } + } + public static class LWJGL3ifyVersionList { + public final List supportedVersions; + public final List brokenVersions; // SDL versions for now + + public LWJGL3ifyVersionList(List mSupportedVersions, List mBrokenVersions) { + this.supportedVersions = mSupportedVersions; + this.brokenVersions = mBrokenVersions; + } + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/LWJGL3ifyVersionListAdapter.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/LWJGL3ifyVersionListAdapter.java new file mode 100644 index 000000000..70ab6a078 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/LWJGL3ifyVersionListAdapter.java @@ -0,0 +1,95 @@ +package net.kdt.pojavlaunch.modloaders; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseExpandableListAdapter; +import android.widget.ExpandableListAdapter; +import android.widget.TextView; + +import net.kdt.pojavlaunch.R; + +import java.util.ArrayList; +import java.util.List; + +public class LWJGL3ifyVersionListAdapter extends BaseExpandableListAdapter implements ExpandableListAdapter { + private final LayoutInflater mLayoutInflater; + private final ArrayList mGroupNames; + private final ArrayList> mGroups; + + public LWJGL3ifyVersionListAdapter(LWJGL3ifyUtils.LWJGL3ifyVersionList versionList, LayoutInflater mLayoutInflater) { + this.mLayoutInflater = mLayoutInflater; + Context context = mLayoutInflater.getContext(); + mGroupNames = new ArrayList<>(2); + mGroups = new ArrayList<>(2); + if(!versionList.supportedVersions.isEmpty()) { + mGroupNames.add(context.getString(R.string.lwjgl3ify_installer_available_versions)); + mGroups.add(versionList.supportedVersions); + } + if(!versionList.brokenVersions.isEmpty()) { + mGroupNames.add(context.getString(R.string.lwjgl3ify_installer_broken_versions)); + mGroups.add(versionList.brokenVersions); + } + mGroupNames.trimToSize(); + mGroups.trimToSize(); + } + + @Override + public int getGroupCount() { + return mGroups.size(); + } + + @Override + public int getChildrenCount(int i) { + return mGroups.get(i).size(); + } + + @Override + public Object getGroup(int i) { + return mGroupNames.get(i); + } + + @Override + public LWJGL3ifyUtils.LWJGL3ifyMod getChild(int i, int i1) { + return mGroups.get(i).get(i1); + } + + @Override + public long getGroupId(int i) { + return i; + } + + @Override + public long getChildId(int i, int i1) { + return i1; + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public View getGroupView(int i, 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((String)getGroup(i)); + + return convertView; + } + + @Override + 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(getChild(i,i1).versionName); + return convertView; + } + + @Override + public boolean isChildSelectable(int i, int i1) { + return true; + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/JREUtils.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/JREUtils.java index b1f4fe58e..b3e832f89 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/JREUtils.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/JREUtils.java @@ -311,7 +311,8 @@ public class JREUtils { // Has to run after SDL env vars are set try { Os.setenv("SDL_OPENGL_LIBRARY", graphicsLib, true); - Os.setenv("SDL_EGL_LIBRARY", NATIVE_LIB_DIR+"/"+Os.getenv("POJAVEXEC_EGL"), true); + if (Os.getenv("POJAVEXEC_EGL") != null) + Os.setenv("SDL_EGL_LIBRARY", NATIVE_LIB_DIR+"/"+Os.getenv("POJAVEXEC_EGL"), true); } catch (ErrnoException e) { Log.wtf("RENDER_LIBRARY", "Failed to load set SDL env vars"); } diff --git a/app_pojavlauncher/src/main/res/layout/fragment_profile_type.xml b/app_pojavlauncher/src/main/res/layout/fragment_profile_type.xml index 5ce18cd93..59d9d771e 100644 --- a/app_pojavlauncher/src/main/res/layout/fragment_profile_type.xml +++ b/app_pojavlauncher/src/main/res/layout/fragment_profile_type.xml @@ -119,6 +119,14 @@ android:layout_marginTop="@dimen/padding_large" android:text="@string/modpack_install_button" /> + + Error! The key assigned to this button is invalid. Did you try updating? Error! Invalid value! Your Minecraft username is not set! You must set it in the Minecraft website before being able to log in! + Create LWJGL3ify profile + Select LWJGL3ify version + Supported LWJGL3IFY versions + Broken LWJGL3ify versions (SDL)