diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 9b16852673..f63bb28e7b 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -106,6 +106,7 @@ jobs: rm -rf app_pojavlauncher/src/main/assets/components/jre rm -rf app_pojavlauncher/src/main/assets/components/jre-new rm -rf app_pojavlauncher/src/main/assets/components/jre-21 + rm -rf app_pojavlauncher/src/main/assets/components/jre-25 ./gradlew assembleDebug mv app_pojavlauncher/build/outputs/apk/debug/app_pojavlauncher-debug.apk out/app-debug-noruntime.apk diff --git a/app_pojavlauncher/build.gradle b/app_pojavlauncher/build.gradle index a9aa6c8203..ae3491794f 100644 --- a/app_pojavlauncher/build.gradle +++ b/app_pojavlauncher/build.gradle @@ -7,7 +7,7 @@ static def getDate() { return new Date().format('yyyyMMdd') } def getVersionName = { // Get the last version tag, as well as the short head of the last commit ByteArrayOutputStream TAG = new ByteArrayOutputStream() - ByteArrayOutputStream BRANCH = new ByteArrayOutputStream() + String semVer = "1.0.0" // Used by the fallback for github actions ByteArrayOutputStream TAG_PART_COMMIT = new ByteArrayOutputStream() String TAG_STRING @@ -48,17 +48,7 @@ def getVersionName = { } - exec { - try { - commandLine 'git', 'branch', '--show-current' - ignoreExitValue true - standardOutput = BRANCH - } catch (Exception e) { - e.printStackTrace(); - } - } - - return TAG_STRING.trim().replace("-g", "-") + "-" + BRANCH.toString().trim() + return TAG_STRING.trim().replace("-g", "-") + "-" + semVer } def getCFApiKey = { diff --git a/app_pojavlauncher/libs/MobileGlues-release.aar b/app_pojavlauncher/libs/MobileGlues-release.aar index f2f7b1945f..070161c9c7 100644 Binary files a/app_pojavlauncher/libs/MobileGlues-release.aar and b/app_pojavlauncher/libs/MobileGlues-release.aar differ diff --git a/app_pojavlauncher/src/main/assets/components/lwjgl3/lwjgl-glfw-classes.jar b/app_pojavlauncher/src/main/assets/components/lwjgl3/lwjgl-glfw-classes.jar deleted file mode 100644 index 19277017b4..0000000000 Binary files a/app_pojavlauncher/src/main/assets/components/lwjgl3/lwjgl-glfw-classes.jar and /dev/null differ diff --git a/app_pojavlauncher/src/main/assets/components/lwjgl3/version b/app_pojavlauncher/src/main/assets/components/lwjgl3/version deleted file mode 100644 index f8349b9a4a..0000000000 --- a/app_pojavlauncher/src/main/assets/components/lwjgl3/version +++ /dev/null @@ -1 +0,0 @@ -5aeae791eead9c6c47670c0886d77b1b185947fd \ No newline at end of file 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 dc4e524e1f..083066e141 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java @@ -268,6 +268,7 @@ private static File getGameDir() { /** * Searches for mod in mods directory of current selected profile + * Not case-sensitive * @param filenames Filename(s) of the .jar mod(s) * @return Whether or not the .jar is found */ @@ -278,7 +279,7 @@ public static boolean hasMods(String... filenames) { if (modFiles == null) return false; for (File file : modFiles) { for (String filename : filenames) - if (file.getName().contains(filename)) return true; + if (file.getName().toLowerCase().contains(filename.toLowerCase())) return true; } return false; } @@ -320,10 +321,12 @@ private static boolean affectedByRenderDistanceIssue() { return info.isAdreno() && info.glesMajorVersion >= 3; } + private static String[] sodiumMods = {"sodium", "embeddium", "rubidium", "xenon"}; + private static boolean affectedByLTWRenderDistanceIssue() { if(!"opengles3_ltw".equals(Tools.LOCAL_RENDERER)) return false; if(!affectedByRenderDistanceIssue()) return false; - if(hasMods("sodium", "embeddium", "rubidium")) return false; + if(hasMods(sodiumMods)) return false; int renderDistance; try { @@ -432,7 +435,37 @@ public static void launchMinecraft(final AppCompatActivity activity, MinecraftAc javaArgList.add("-Dimgui.library.name=imgui-java"); // We use an abomination to support all DH versions with a single library. javaArgList.add("-DZstdNativePath="+Tools.NATIVE_LIB_DIR+"/libzstd-jni-1.5.7-6-dhcompat.so"); + // We only ever reach this point when user has already used the force run switch + boolean hasSodiumMod = false; + for (String modName : sodiumMods) { + if (hasMods(sodiumMods)) { + hasSodiumMod = true; + File mixinPropertiesConfigFile = new File(getGameDir(), "config/" + modName + "-mixins.properties"); + // Write mixin configs to somewhat help stability. We don't want more people complaining. + String[] propertiesToAdd = { + "mixin.features.buffer_builder.intrinsics=false", + "mixin.features.chunk_rendering=false" + }; + List mixinPropertiesConfigStrings = null; + try { + mixinPropertiesConfigStrings = org.apache.commons.io.FileUtils.readLines(mixinPropertiesConfigFile, "UTF-8"); + } catch (IOException ignored) {} + if (mixinPropertiesConfigStrings == null) { + mixinPropertiesConfigStrings = new ArrayList<>(); + } + for (String newLine : propertiesToAdd) { + if (!mixinPropertiesConfigStrings.contains(newLine)) { + mixinPropertiesConfigStrings.add(newLine); + } + } + try { + org.apache.commons.io.FileUtils.writeLines(mixinPropertiesConfigFile, mixinPropertiesConfigStrings); + } catch (IOException ignored) {} // If we can't write it, we tried our best. + } + } + // We use a janky lwjgl setup. We don't want more people complaining it crashes. + if (hasSodiumMod) javaArgList.add("-Dsodium.checks.issue2561=false"); javaArgList.add(versionInfo.mainClass); javaArgList.addAll(Arrays.asList(launchArgs)); // ctx.appendlnToLog("full args: "+javaArgList.toString()); 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 137320896d..4bb93ac42e 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,13 +1,10 @@ package net.kdt.pojavlaunch.fragments; -import static net.kdt.pojavlaunch.Tools.dialogOnUiThread; import static net.kdt.pojavlaunch.Tools.hasNoOnlineProfileDialog; import static net.kdt.pojavlaunch.Tools.hasOnlineProfile; import static net.kdt.pojavlaunch.Tools.openPath; -import static net.kdt.pojavlaunch.Tools.runOnUiThread; import static net.kdt.pojavlaunch.Tools.shareLog; -import android.app.Dialog; import android.content.Intent; import android.os.Bundle; import android.view.View; @@ -17,7 +14,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import com.kdt.mcgui.mcVersionSpinner; @@ -39,63 +35,93 @@ public class MainMenuFragment extends Fragment { private mcVersionSpinner mVersionSpinner; - public MainMenuFragment(){ + public MainMenuFragment() { super(R.layout.fragment_launcher); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - Button mNewsButton = view.findViewById(R.id.news_button); - Button mDiscordButton = view.findViewById(R.id.discord_button); - Button mCustomControlButton = view.findViewById(R.id.custom_control_button); - Button mInstallJarButton = view.findViewById(R.id.install_jar_button); - Button mShareLogsButton = view.findViewById(R.id.share_logs_button); - Button mOpenDirectoryButton = view.findViewById(R.id.open_files_button); + Button mNewsButton = view.findViewById(R.id.news_button); + Button mDiscordButton = view.findViewById(R.id.discord_button); + Button mCustomControlButton = view.findViewById(R.id.custom_control_button); + Button mInstallJarButton = view.findViewById(R.id.install_jar_button); + Button mShareLogsButton = view.findViewById(R.id.share_logs_button); + Button mManageModsButton = view.findViewById(R.id.open_files_button); + Button mOpenDirectoryButton = view.findViewById(R.id.open_directory_button); + Button mModStoreButton = view.findViewById(R.id.mod_store_button); ImageButton mEditProfileButton = view.findViewById(R.id.edit_profile_button); Button mPlayButton = view.findViewById(R.id.play_button); mVersionSpinner = view.findViewById(R.id.mc_version_spinner); + // Wiki mNewsButton.setOnClickListener(v -> Tools.openURL(requireActivity(), Tools.URL_HOME)); + + // Discord mDiscordButton.setOnClickListener(v -> Tools.openURL(requireActivity(), getString(R.string.discord_invite))); - mCustomControlButton.setOnClickListener(v -> startActivity(new Intent(requireContext(), CustomControlsActivity.class))); + + // Custom controls + mCustomControlButton.setOnClickListener(v -> + startActivity(new Intent(requireContext(), CustomControlsActivity.class))); + + // Mod Store → individual mod search + if (mModStoreButton != null) + mModStoreButton.setOnClickListener(v -> + Tools.swapFragment(requireActivity(), ModsSearchFragment.class, ModsSearchFragment.TAG, null)); + + // Execute .jar if (hasOnlineProfile()) { mInstallJarButton.setOnClickListener(v -> runInstallerWithConfirmation(false)); mInstallJarButton.setOnLongClickListener(v -> { runInstallerWithConfirmation(true); return true; }); - } else mInstallJarButton.setOnClickListener(v -> hasNoOnlineProfileDialog(requireActivity())); - mEditProfileButton.setOnClickListener(v -> mVersionSpinner.openProfileEditor(requireActivity())); - - mPlayButton.setOnClickListener(v -> { - ExtraCore.setValue(ExtraConstants.LAUNCH_GAME, true); - }); - - mShareLogsButton.setOnClickListener((v) -> shareLog(requireContext())); - - mOpenDirectoryButton.setOnClickListener((v)-> { - if (Tools.isDemoProfile(v.getContext())){ // Say a different message when on demo profile since they might see the hidden demo folder - hasNoOnlineProfileDialog(getActivity(), getString(R.string.demo_unsupported), getString(R.string.change_account)); - } else if (!hasOnlineProfile()) { // Otherwise display the generic pop-up to log in - hasNoOnlineProfileDialog(requireActivity()); - } else openPath(v.getContext(), getCurrentProfileDirectory(), false); + } else { + mInstallJarButton.setOnClickListener(v -> hasNoOnlineProfileDialog(requireActivity())); + } + + // Share logs + if (mShareLogsButton != null) + mShareLogsButton.setOnClickListener(v -> shareLog(requireContext())); + + // Manage Mods & Tools → new UI + mManageModsButton.setOnClickListener(v -> + Tools.swapFragment(requireActivity(), ManageModsFragment.class, ManageModsFragment.TAG, null)); + + // Open game directory (original behaviour) + if (mOpenDirectoryButton != null) { + mOpenDirectoryButton.setOnClickListener(v -> { + if (Tools.isDemoProfile(v.getContext())) { + hasNoOnlineProfileDialog(getActivity(), + getString(R.string.demo_unsupported), getString(R.string.change_account)); + } else if (!hasOnlineProfile()) { + hasNoOnlineProfileDialog(requireActivity()); + } else { + openPath(v.getContext(), getCurrentProfileDirectory(), false); + } + }); + } - }); + // Edit profile + mEditProfileButton.setOnClickListener(v -> mVersionSpinner.openProfileEditor(requireActivity())); + // Play + mPlayButton.setOnClickListener(v -> ExtraCore.setValue(ExtraConstants.LAUNCH_GAME, true)); - mNewsButton.setOnLongClickListener((v)->{ + // Long-press wiki → gamepad mapper (hidden) + mNewsButton.setOnLongClickListener(v -> { Tools.swapFragment(requireActivity(), GamepadMapperFragment.class, GamepadMapperFragment.TAG, null); return true; }); } private File getCurrentProfileDirectory() { - String currentProfile = LauncherPreferences.DEFAULT_PREF.getString(LauncherPreferences.PREF_KEY_CURRENT_PROFILE, null); - if(!Tools.isValidString(currentProfile)) return new File(Tools.DIR_GAME_NEW); + String currentProfile = LauncherPreferences.DEFAULT_PREF + .getString(LauncherPreferences.PREF_KEY_CURRENT_PROFILE, null); + if (!Tools.isValidString(currentProfile)) return new File(Tools.DIR_GAME_NEW); LauncherProfiles.load(); MinecraftProfile profileObject = LauncherProfiles.mainProfileJson.profiles.get(currentProfile); - if(profileObject == null) return new File(Tools.DIR_GAME_NEW); + if (profileObject == null) return new File(Tools.DIR_GAME_NEW); return Tools.getGameDirPath(profileObject); } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ManageModsFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ManageModsFragment.java new file mode 100644 index 0000000000..57b620947f --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ManageModsFragment.java @@ -0,0 +1,95 @@ +package net.kdt.pojavlaunch.fragments; + +import android.os.Bundle; +import android.view.View; +import android.widget.ImageButton; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.fragments.ModsSearchFragment; +import net.kdt.pojavlaunch.Tools; +import net.kdt.pojavlaunch.modloaders.InstalledModAdapter; +import net.kdt.pojavlaunch.prefs.LauncherPreferences; +import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles; +import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile; + +import java.io.File; + +public class ManageModsFragment extends Fragment { + + public static final String TAG = "ManageModsFragment"; + + public ManageModsFragment() { + super(R.layout.fragment_manage_mods); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + ImageButton backButton = view.findViewById(R.id.manage_mods_back); + ImageButton addButton = view.findViewById(R.id.manage_mods_add); + TextView title = view.findViewById(R.id.manage_mods_title); + RecyclerView recycler = view.findViewById(R.id.manage_mods_recycler); + View emptyState = view.findViewById(R.id.manage_mods_empty); + + // Back + backButton.setOnClickListener(v -> Tools.removeCurrentFragment(requireActivity())); + + // Add → open mod store + addButton.setOnClickListener(v -> + Tools.swapFragment(requireActivity(), ModsSearchFragment.class, ModsSearchFragment.TAG, null)); + + // Title: "ProfileName - Mods" + String profileName = getCurrentProfileName(); + title.setText(profileName.isEmpty() + ? getString(R.string.mcl_button_manage_mods) + : profileName + " - Mods"); + + // Build mod list + File modsDir = getModsDir(); + InstalledModAdapter adapter = new InstalledModAdapter(modsDir, isEmpty -> { + recycler.setVisibility(isEmpty ? View.GONE : View.VISIBLE); + emptyState.setVisibility(isEmpty ? View.VISIBLE : View.GONE); + }); + + recycler.setLayoutManager(new LinearLayoutManager(requireContext())); + recycler.setAdapter(adapter); + } + + // ── Helpers ───────────────────────────────────────────────────────────── + + private String getCurrentProfileName() { + try { + String key = LauncherPreferences.DEFAULT_PREF + .getString(LauncherPreferences.PREF_KEY_CURRENT_PROFILE, null); + if (key == null || key.isEmpty()) return ""; + LauncherProfiles.load(); + MinecraftProfile profile = LauncherProfiles.mainProfileJson.profiles.get(key); + if (profile == null) return ""; + return profile.name != null ? profile.name : key; + } catch (Exception e) { + return ""; + } + } + + private File getModsDir() { + try { + String key = LauncherPreferences.DEFAULT_PREF + .getString(LauncherPreferences.PREF_KEY_CURRENT_PROFILE, null); + if (key != null && !key.isEmpty()) { + LauncherProfiles.load(); + MinecraftProfile profile = LauncherProfiles.mainProfileJson.profiles.get(key); + if (profile != null) { + File gameDir = Tools.getGameDirPath(profile); + return new File(gameDir, "mods"); + } + } + } catch (Exception ignored) {} + return new File(Tools.DIR_GAME_NEW, "mods"); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ModsSearchFragment.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ModsSearchFragment.java new file mode 100644 index 0000000000..701bc13199 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/fragments/ModsSearchFragment.java @@ -0,0 +1,298 @@ +package net.kdt.pojavlaunch.fragments; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.os.Bundle; +import android.util.Log; +import android.widget.Toast; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.core.math.MathUtils; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.kdt.mcgui.ProgressLayout; + +import net.kdt.pojavlaunch.PojavApplication; +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.Tools; +import net.kdt.pojavlaunch.modloaders.modpacks.ModItemAdapter; +import net.kdt.pojavlaunch.modloaders.modpacks.api.CommonApi; +import net.kdt.pojavlaunch.modloaders.modpacks.api.ModpackApi; +import net.kdt.pojavlaunch.modloaders.modpacks.models.ModDetail; +import net.kdt.pojavlaunch.modloaders.modpacks.models.SearchFilters; +import net.kdt.pojavlaunch.modloaders.modpacks.models.Constants; +import net.kdt.pojavlaunch.prefs.LauncherPreferences; +import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper; +import net.kdt.pojavlaunch.profiles.VersionSelectorDialog; +import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles; +import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile; +import net.kdt.pojavlaunch.utils.DownloadUtils; + +import java.io.File; +import java.io.IOException; + +/** + * Searches and installs individual mods (not modpacks) into the current instance's mods folder. + * Auto-detects MC version from the selected profile's lastVersionId. + */ +public class ModsSearchFragment extends Fragment implements ModItemAdapter.SearchResultCallback { + + public static final String TAG = "ModsSearchFragment"; + + private View mOverlay; + private float mOverlayTopCache; + + private final RecyclerView.OnScrollListener mOverlayPositionListener = new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + mOverlay.setY(MathUtils.clamp(mOverlay.getY() - dy, -mOverlay.getHeight(), mOverlayTopCache)); + } + }; + + private EditText mSearchEditText; + private ImageButton mFilterButton; + private RecyclerView mRecyclerview; + private ModItemAdapter mModItemAdapter; + private ProgressBar mSearchProgressBar; + private TextView mStatusTextView; + private ColorStateList mDefaultTextColor; + + private ModpackApi mModpackApi; + private final SearchFilters mSearchFilters; + + public ModsSearchFragment() { + super(R.layout.fragment_mod_search); + mSearchFilters = new SearchFilters(); + mSearchFilters.isModpack = false; // individual mods, not modpacks + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + // Wrap CommonApi so installation goes to mods folder instead of creating a new instance + mModpackApi = new ModsInstallApi(context.getString(R.string.curseforge_api_key)); + // Auto-detect MC version from current profile + mSearchFilters.mcVersion = detectMcVersion(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + mModItemAdapter = new ModItemAdapter(getResources(), mModpackApi, this); + ProgressKeeper.addTaskCountListener(mModItemAdapter); + mOverlayTopCache = getResources().getDimension(R.dimen.fragment_padding_medium); + + mOverlay = view.findViewById(R.id.search_mod_overlay); + mSearchEditText = view.findViewById(R.id.search_mod_edittext); + mSearchProgressBar = view.findViewById(R.id.search_mod_progressbar); + mRecyclerview = view.findViewById(R.id.search_mod_list); + mStatusTextView = view.findViewById(R.id.search_mod_status_text); + mFilterButton = view.findViewById(R.id.search_mod_filter); + + mDefaultTextColor = mStatusTextView.getTextColors(); + + mRecyclerview.setLayoutManager(new LinearLayoutManager(getContext())); + mRecyclerview.setAdapter(mModItemAdapter); + mRecyclerview.addOnScrollListener(mOverlayPositionListener); + + mSearchEditText.setOnEditorActionListener((v, actionId, event) -> { + searchMods(mSearchEditText.getText().toString()); + mSearchEditText.clearFocus(); + return false; + }); + + mOverlay.post(() -> { + int overlayHeight = mOverlay.getHeight(); + mRecyclerview.setPadding( + mRecyclerview.getPaddingLeft(), + mRecyclerview.getPaddingTop() + overlayHeight, + mRecyclerview.getPaddingRight(), + mRecyclerview.getPaddingBottom()); + }); + + mFilterButton.setOnClickListener(v -> displayFilterDialog()); + + // Override hint — the layout uses fragment_mod_search which says "Search for modpacks" + mSearchEditText.setHint(R.string.hint_search_mod); + + searchMods(null); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + ProgressKeeper.removeTaskCountListener(mModItemAdapter); + mRecyclerview.removeOnScrollListener(mOverlayPositionListener); + } + + @Override + public void onSearchFinished() { + mSearchProgressBar.setVisibility(View.GONE); + mStatusTextView.setVisibility(View.GONE); + } + + @Override + public void onSearchError(int error) { + mSearchProgressBar.setVisibility(View.GONE); + mStatusTextView.setVisibility(View.VISIBLE); + switch (error) { + case ERROR_INTERNAL: + mStatusTextView.setTextColor(Color.RED); + mStatusTextView.setText(R.string.search_modpack_error); + break; + case ERROR_NO_RESULTS: + mStatusTextView.setTextColor(mDefaultTextColor); + mStatusTextView.setText(R.string.search_modpack_no_result); + break; + } + } + + private void searchMods(String name) { + mSearchProgressBar.setVisibility(View.VISIBLE); + mSearchFilters.name = name == null ? "" : name; + mModItemAdapter.performSearchQuery(mSearchFilters); + } + + private void displayFilterDialog() { + AlertDialog dialog = new AlertDialog.Builder(requireContext()) + .setView(R.layout.dialog_mod_filters) + .create(); + + dialog.setOnShowListener(dialogInterface -> { + TextView mSelectedVersion = dialog.findViewById(R.id.search_mod_selected_mc_version_textview); + Button mSelectVersionButton = dialog.findViewById(R.id.search_mod_mc_version_button); + Button mApplyButton = dialog.findViewById(R.id.search_mod_apply_filters); + + assert mSelectedVersion != null; + assert mSelectVersionButton != null; + assert mApplyButton != null; + + mSelectVersionButton.setOnClickListener(v -> + VersionSelectorDialog.open(v.getContext(), true, + (id, snapshot) -> mSelectedVersion.setText(id))); + + mSelectedVersion.setText(mSearchFilters.mcVersion); + + mApplyButton.setOnClickListener(v -> { + mSearchFilters.mcVersion = mSelectedVersion.getText().toString(); + searchMods(mSearchEditText.getText().toString()); + dialogInterface.dismiss(); + }); + }); + + dialog.show(); + } + + // ── Helpers ────────────────────────────────────────────────────────────── + + /** Parse MC version from lastVersionId like "fabric-loader-0.16.14-1.21.4" → "1.21.4" or "1.21.4" → "1.21.4" */ + private String detectMcVersion() { + try { + String key = LauncherPreferences.DEFAULT_PREF + .getString(LauncherPreferences.PREF_KEY_CURRENT_PROFILE, null); + if (key == null || key.isEmpty()) return null; + LauncherProfiles.load(); + MinecraftProfile profile = LauncherProfiles.mainProfileJson.profiles.get(key); + if (profile == null || profile.lastVersionId == null) return null; + + String vid = profile.lastVersionId; + // fabric-loader-X.Y.Z-MC → last segment after last "-MC" block + // forge-MC-loaderVer → second segment + // 1.21.4 → as-is + if (vid.startsWith("fabric-loader-") || vid.startsWith("quilt-loader-")) { + // fabric-loader-0.16.14-1.21.4 — MC is everything after the loader version + String[] parts = vid.split("-"); + if (parts.length >= 4) return parts[parts.length - 1]; + } + if (vid.contains("-forge-") || vid.contains("-neoforge-")) { + // 1.21.4-forge-xxx — MC is first part + return vid.split("-")[0]; + } + // Plain version id + return vid; + } catch (Exception e) { + return null; + } + } + + // ── ModsInstallApi ─────────────────────────────────────────────────────── + + /** + * Wraps CommonApi and overrides handleInstallation to download the mod JAR + * directly into the current instance's mods/ folder instead of installing a modpack. + */ + private static class ModsInstallApi extends CommonApi { + + ModsInstallApi(String curseforgeApiKey) { + super(curseforgeApiKey); + } + + @Override + public void handleInstallation(Context context, ModDetail modDetail, int selectedVersion) { + if (modDetail.isModpack) { + // Fall back to normal modpack install + super.handleInstallation(context, modDetail, selectedVersion); + return; + } + + String url = modDetail.versionUrls[selectedVersion]; + if (url == null || url.isEmpty()) { + Tools.showErrorRemote(context, R.string.modpack_install_download_failed, + new IOException("No download URL available for this mod")); + return; + } + + // Determine file name from URL + String fileName = url.substring(url.lastIndexOf('/') + 1); + if (!fileName.endsWith(".jar")) fileName += ".jar"; + // Strip query params if present + if (fileName.contains("?")) fileName = fileName.substring(0, fileName.indexOf('?')); + + File modsDir = getModsDir(); + if (!modsDir.exists()) modsDir.mkdirs(); + File destFile = new File(modsDir, fileName); + + String finalFileName = fileName; + ProgressLayout.setProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.global_waiting); + PojavApplication.sExecutorService.execute(() -> { + try { + DownloadUtils.downloadFile(url, destFile); + ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK); + Tools.runOnUiThread(() -> + Toast.makeText(context, + context.getString(R.string.mod_install_success, finalFileName), + Toast.LENGTH_LONG).show() + ); + } catch (Exception e) { + ProgressLayout.clearProgress(ProgressLayout.INSTALL_MODPACK); + Tools.showErrorRemote(context, R.string.modpack_install_download_failed, e); + } + }); + } + + private static File getModsDir() { + try { + String key = LauncherPreferences.DEFAULT_PREF + .getString(LauncherPreferences.PREF_KEY_CURRENT_PROFILE, null); + if (key != null && !key.isEmpty()) { + LauncherProfiles.load(); + MinecraftProfile profile = LauncherProfiles.mainProfileJson.profiles.get(key); + if (profile != null) { + return new File(Tools.getGameDirPath(profile), "mods"); + } + } + } catch (Exception ignored) {} + return new File(Tools.DIR_GAME_NEW, "mods"); + } + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/InstalledModAdapter.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/InstalledModAdapter.java new file mode 100644 index 0000000000..68a81977e7 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/InstalledModAdapter.java @@ -0,0 +1,367 @@ +package net.kdt.pojavlaunch.modloaders; + +import android.app.AlertDialog; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.SwitchCompat; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import net.kdt.pojavlaunch.PojavApplication; +import net.kdt.pojavlaunch.R; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +public class InstalledModAdapter extends RecyclerView.Adapter { + + private static final String TAG = "ModAdapter"; + + public interface EmptyStateListener { + void onEmptyStateChanged(boolean isEmpty); + } + + private final List mMods = new ArrayList<>(); + private final EmptyStateListener mEmptyListener; + private final Handler mMainHandler = new Handler(Looper.getMainLooper()); + + public InstalledModAdapter(File modsDir, EmptyStateListener listener) { + mEmptyListener = listener; + if (modsDir != null && modsDir.isDirectory()) { + File[] files = modsDir.listFiles(f -> f.isFile() && + (f.getName().endsWith(".jar") || f.getName().endsWith(".jar.disabled"))); + if (files != null) { + Arrays.sort(files, (a, b) -> a.getName().compareToIgnoreCase(b.getName())); + for (File f : files) mMods.add(new ModEntry(f)); + } + } + notifyEmptyState(); + } + + @NonNull + @Override + public ModViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_installed_mod, parent, false); + return new ModViewHolder(v); + } + + @Override + public void onBindViewHolder(@NonNull ModViewHolder holder, int position) { + holder.bind(mMods.get(position)); + } + + @Override + public void onViewRecycled(@NonNull ModViewHolder holder) { + // Clear the tag so any in-flight load doesn't update this recycled view + holder.icon.setTag(null); + holder.icon.setImageResource(R.drawable.ic_add_modded); + super.onViewRecycled(holder); + } + + @Override + public int getItemCount() { return mMods.size(); } + + private void notifyEmptyState() { + if (mEmptyListener != null) mEmptyListener.onEmptyStateChanged(mMods.isEmpty()); + } + + // ── ViewHolder ──────────────────────────────────────────────────────── + + class ModViewHolder extends RecyclerView.ViewHolder { + final ImageView icon; + final TextView name, version; + final SwitchCompat toggle; + final ImageButton delete; + + ModViewHolder(@NonNull View itemView) { + super(itemView); + icon = itemView.findViewById(R.id.installed_mod_icon); + name = itemView.findViewById(R.id.installed_mod_name); + version = itemView.findViewById(R.id.installed_mod_version); + toggle = itemView.findViewById(R.id.installed_mod_toggle); + delete = itemView.findViewById(R.id.installed_mod_delete); + } + + void bind(ModEntry entry) { + name.setText(entry.displayName()); + version.setText(entry.file.getName()); + + // Tag the ImageView with the file path so we can verify it hasn't been recycled + icon.setTag(entry.file.getAbsolutePath()); + icon.setImageResource(R.drawable.ic_add_modded); + + final String expectedTag = entry.file.getAbsolutePath(); + final WeakReference iconRef = new WeakReference<>(icon); + final File jarFile = entry.file; + + PojavApplication.sExecutorService.execute(() -> { + Bitmap bmp = extractModIcon(jarFile); + if (bmp == null) return; + mMainHandler.post(() -> { + ImageView iv = iconRef.get(); + // Only update if the view still belongs to the same mod + if (iv != null && expectedTag.equals(iv.getTag())) { + iv.setImageBitmap(bmp); + } + }); + }); + + toggle.setOnCheckedChangeListener(null); + toggle.setChecked(entry.enabled); + toggle.setOnCheckedChangeListener((btn, checked) -> entry.setEnabled(checked)); + + delete.setOnClickListener(v -> { + Context ctx = v.getContext(); + new AlertDialog.Builder(ctx) + .setTitle(ctx.getString(R.string.manage_mods_delete_confirm, entry.displayName())) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok, (d, i) -> { + entry.file.delete(); + int p = getBindingAdapterPosition(); + if (p != RecyclerView.NO_POSITION) { + mMods.remove(p); + notifyItemRemoved(p); + notifyEmptyState(); + } + }) + .show(); + }); + } + } + + // ── ModEntry ────────────────────────────────────────────────────────── + + static class ModEntry { + File file; + boolean enabled; + + ModEntry(File f) { + this.file = f; + this.enabled = !f.getName().endsWith(".disabled"); + } + + String displayName() { + String n = file.getName(); + if (n.endsWith(".jar.disabled")) n = n.substring(0, n.length() - 13); + else if (n.endsWith(".jar")) n = n.substring(0, n.length() - 4); + return n; + } + + void setEnabled(boolean enable) { + if (enable == this.enabled) return; + File target = enable + ? new File(file.getParent(), file.getName().replace(".jar.disabled", ".jar")) + : new File(file.getParent(), file.getName() + ".disabled"); + if (file.renameTo(target)) { + file = target; + this.enabled = enable; + } + } + } + + // ── Icon extraction ─────────────────────────────────────────────────── + + @Nullable + private static Bitmap extractModIcon(File jarFile) { + try (ZipFile zip = new ZipFile(jarFile)) { + String iconPath = resolveIconPath(zip); + Log.d(TAG, jarFile.getName() + " → icon path: " + iconPath); + + if (iconPath != null) { + Bitmap bmp = loadEntryAsBitmap(zip, iconPath); + if (bmp != null) return bmp; + Log.w(TAG, "Icon path resolved but bitmap failed: " + iconPath); + } + + // Fallback scan — some old mods don't declare icon in metadata + for (String fallback : new String[]{"pack.png", "icon.png", "logo.png"}) { + Bitmap bmp = loadEntryAsBitmap(zip, fallback); + if (bmp != null) return bmp; + } + } catch (Exception e) { + Log.w(TAG, "Failed to open JAR: " + jarFile.getName() + " — " + e.getMessage()); + } + return null; + } + + @Nullable + private static String resolveIconPath(ZipFile zip) { + // 1. Fabric — fabric.mod.json → "icon" (string OR {"64":"path"} object) + String content = readEntry(zip, "fabric.mod.json"); + if (content != null) { + try { + JsonObject obj = JsonParser.parseString(content).getAsJsonObject(); + if (obj.has("icon")) { + JsonElement iconEl = obj.get("icon"); + if (iconEl.isJsonPrimitive()) { + return iconEl.getAsString(); + } else if (iconEl.isJsonObject()) { + // Size map e.g. {"64": "path64.png", "128": "path128.png"} + // Pick the largest available + JsonObject sizeMap = iconEl.getAsJsonObject(); + String best = null; + int bestSize = 0; + for (String key : sizeMap.keySet()) { + try { + int sz = Integer.parseInt(key); + if (sz > bestSize) { + bestSize = sz; + best = sizeMap.get(key).getAsString(); + } + } catch (NumberFormatException ignored) { + best = sizeMap.get(key).getAsString(); + } + } + if (best != null) return best; + } + } + } catch (Exception e) { + Log.w(TAG, "fabric.mod.json parse error: " + e.getMessage()); + } + } + + // 2. Quilt — quilt.mod.json → quilt_loader.metadata.icon + content = readEntry(zip, "quilt.mod.json"); + if (content != null) { + try { + JsonObject root = JsonParser.parseString(content).getAsJsonObject(); + JsonObject ql = root.has("quilt_loader") ? + root.getAsJsonObject("quilt_loader") : null; + if (ql != null && ql.has("metadata")) { + JsonObject meta = ql.getAsJsonObject("metadata"); + if (meta.has("icon") && meta.get("icon").isJsonPrimitive()) + return meta.get("icon").getAsString(); + } + } catch (Exception e) { + Log.w(TAG, "quilt.mod.json parse error: " + e.getMessage()); + } + } + + // 3. Forge legacy — mcmod.info → logoFile + content = readEntry(zip, "mcmod.info"); + if (content != null) { + try { + // mcmod.info is a JSON array + JsonArray arr = JsonParser.parseString(content).getAsJsonArray(); + if (arr.size() > 0 && arr.get(0).isJsonObject()) { + JsonObject mod = arr.get(0).getAsJsonObject(); + if (mod.has("logoFile")) { + String logo = mod.get("logoFile").getAsString(); + if (!logo.isEmpty()) return logo; + } + } + } catch (Exception e) { + Log.w(TAG, "mcmod.info parse error: " + e.getMessage()); + } + } + + // 4. Forge/NeoForge — TOML — logoFile = "path" + for (String toml : new String[]{"META-INF/neoforge.mods.toml", "META-INF/mods.toml"}) { + content = readEntry(zip, toml); + if (content != null) { + String logo = tomlStringField(content, "logoFile"); + if (logo != null && !logo.isEmpty()) return logo; + } + } + + return null; + } + + // ── Low-level helpers ───────────────────────────────────────────────── + + /** + * Load a ZipEntry as a Bitmap. + * IMPORTANT: BitmapFactory.decodeStream needs mark/reset support. + * ZipInputStream doesn't support it, so we buffer all bytes first. + */ + @Nullable + private static Bitmap loadEntryAsBitmap(ZipFile zip, String entryPath) { + // ZipFile.getEntry is case-sensitive — try exact then case-insensitive scan + ZipEntry entry = zip.getEntry(entryPath); + if (entry == null) { + // Case-insensitive fallback + String lower = entryPath.toLowerCase(); + Enumeration entries = zip.entries(); + while (entries.hasMoreElements()) { + ZipEntry e = entries.nextElement(); + if (e.getName().toLowerCase().equals(lower)) { + entry = e; + break; + } + } + } + if (entry == null) return null; + + try (InputStream is = zip.getInputStream(entry)) { + // Buffer into byte array — BitmapFactory needs mark/reset + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buf = new byte[8192]; + int read; + while ((read = is.read(buf)) != -1) baos.write(buf, 0, read); + byte[] bytes = baos.toByteArray(); + return BitmapFactory.decodeByteArray(bytes, 0, bytes.length); + } catch (Exception e) { + Log.w(TAG, "loadEntryAsBitmap failed for " + entryPath + ": " + e.getMessage()); + return null; + } + } + + @Nullable + private static String readEntry(ZipFile zip, String entryPath) { + ZipEntry entry = zip.getEntry(entryPath); + if (entry == null) return null; + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(zip.getInputStream(entry), "UTF-8"))) { + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) sb.append(line).append('\n'); + return sb.toString(); + } catch (Exception e) { + return null; + } + } + + @Nullable + private static String tomlStringField(String toml, String field) { + for (String line : toml.split("\n")) { + line = line.trim(); + if (line.startsWith(field + " ") || line.startsWith(field + "=")) { + int eq = line.indexOf('='); + if (eq < 0) continue; + String val = line.substring(eq + 1).trim(); + if (val.startsWith("\"") && val.endsWith("\"")) + val = val.substring(1, val.length() - 1); + if (!val.isEmpty()) return val; + } + } + return null; + } +} 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 cb4c17ab18..f263763974 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 @@ -117,7 +117,15 @@ public ModDetail getModDetails(ModItem item) { versionNames[i] = modDetail.get("displayName").getAsString(); JsonElement downloadUrl = modDetail.get("downloadUrl"); - versionUrls[i] = downloadUrl.getAsString(); + if (downloadUrl == null || downloadUrl.isJsonNull()) { + // CF restricts direct download for some mods — build edge CDN URL instead + int fileId = modDetail.get("id").getAsInt(); + String fileName = modDetail.get("fileName").getAsString(); + versionUrls[i] = String.format("https://edge.forgecdn.net/files/%s/%s/%s", + fileId / 1000, fileId % 1000, fileName); + } else { + versionUrls[i] = downloadUrl.getAsString(); + } JsonArray gameVersions = modDetail.getAsJsonArray("gameVersions"); for(JsonElement jsonElement : gameVersions) { diff --git a/app_pojavlauncher/src/main/res/drawable/ic_folder_managed.xml b/app_pojavlauncher/src/main/res/drawable/ic_folder_managed.xml new file mode 100644 index 0000000000..fe658c7b08 --- /dev/null +++ b/app_pojavlauncher/src/main/res/drawable/ic_folder_managed.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/app_pojavlauncher/src/main/res/layout-land/fragment_launcher.xml b/app_pojavlauncher/src/main/res/layout-land/fragment_launcher.xml index 3cf2f0b6a3..ded29b0e21 100644 --- a/app_pojavlauncher/src/main/res/layout-land/fragment_launcher.xml +++ b/app_pojavlauncher/src/main/res/layout-land/fragment_launcher.xml @@ -2,12 +2,9 @@ + + + + + + + app:layout_constraintTop_toBottomOf="@id/mod_store_button"/> + + + + + app:layout_constraintTop_toBottomOf="@id/open_directory_button"/> + app:layout_constraintTop_toBottomOf="@id/install_jar_button"/> - app:layout_constraintTop_toBottomOf="@id/share_logs_button" /> - - - - - @@ -140,8 +156,6 @@ android:id="@+id/play_button" android:layout_width="0dp" android:layout_height="wrap_content" - - android:layout_marginEnd="@dimen/_8sdp" android:text="@string/main_play" android:textAllCaps="true" @@ -150,4 +164,3 @@ app:layout_constraintTop_toTopOf="@+id/mc_version_spinner" /> - diff --git a/app_pojavlauncher/src/main/res/layout/activity_pojav_launcher.xml b/app_pojavlauncher/src/main/res/layout/activity_pojav_launcher.xml index b4d20e0378..cd011b9934 100644 --- a/app_pojavlauncher/src/main/res/layout/activity_pojav_launcher.xml +++ b/app_pojavlauncher/src/main/res/layout/activity_pojav_launcher.xml @@ -10,10 +10,8 @@ android:id="@+id/account_spinner" android:layout_width="match_parent" android:layout_height="@dimen/_52sdp" - android:dropDownWidth="@dimen/_250sdp" android:dropDownVerticalOffset="@dimen/_52sdp" - app:layout_constraintTop_toTopOf="parent" /> - - - - - \ No newline at end of file + diff --git a/app_pojavlauncher/src/main/res/layout/fragment_launcher.xml b/app_pojavlauncher/src/main/res/layout/fragment_launcher.xml index ad6f7229d1..3cf9d3a9e7 100644 --- a/app_pojavlauncher/src/main/res/layout/fragment_launcher.xml +++ b/app_pojavlauncher/src/main/res/layout/fragment_launcher.xml @@ -1,142 +1,200 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/fragment_menu_main" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/background_app"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - diff --git a/app_pojavlauncher/src/main/res/layout/fragment_manage_mods.xml b/app_pojavlauncher/src/main/res/layout/fragment_manage_mods.xml new file mode 100644 index 0000000000..0b3ded9e97 --- /dev/null +++ b/app_pojavlauncher/src/main/res/layout/fragment_manage_mods.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app_pojavlauncher/src/main/res/layout/item_installed_mod.xml b/app_pojavlauncher/src/main/res/layout/item_installed_mod.xml new file mode 100644 index 0000000000..85ec97f470 --- /dev/null +++ b/app_pojavlauncher/src/main/res/layout/item_installed_mod.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app_pojavlauncher/src/main/res/layout/view_mod.xml b/app_pojavlauncher/src/main/res/layout/view_mod.xml index cfca0a84a2..fb01be89a6 100644 --- a/app_pojavlauncher/src/main/res/layout/view_mod.xml +++ b/app_pojavlauncher/src/main/res/layout/view_mod.xml @@ -5,35 +5,34 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@drawable/background_line" - android:paddingHorizontal="@dimen/_2sdp" - android:paddingVertical="@dimen/_2sdp" + android:background="@drawable/background_card" android:layout_marginVertical="@dimen/padding_medium" - > + android:foreground="?attr/selectableItemBackground" + android:clickable="true" + android:focusable="true"> + - tools:src="@mipmap/ic_launcher_foreground" - /> - + + tools:src="@drawable/ic_modrinth" /> + + tools:text="Sodium" /> + + tools:text="The fastest and most compatible rendering optimization mod for Minecraft." /> - - - - + app:layout_constraintTop_toBottomOf="@id/mod_thumbnail_imageview" /> - \ 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 29abdf299d..e224268e21 100644 --- a/app_pojavlauncher/src/main/res/values/strings.xml +++ b/app_pojavlauncher/src/main/res/values/strings.xml @@ -92,6 +92,12 @@ Wait Select Retry + Back + Add + No mods installed for this profile. + Delete %s? + %s installed to mods folder. + Search for mods Default Off @@ -426,6 +432,13 @@ Change controller key bindings Allows you to modify the keyboard keys bound to each controller button Discord + Quick Actions + Instance Tools + Mod Store + Settings + Manage Mods & Tools + Install Modpack (.mrpack/zip) + Delete Your GPU is not capable of rendering above 7 render distance without Sodium or other similar mods. The render distance will be automatically reduced when you click "OK". Open game directory https://discord.gg/8WzF2RVAq4 diff --git a/jre_lwjgl3glfw/src/main/java/org/lwjgl/glfw/GLFW.java b/jre_lwjgl3glfw/src/main/java/org/lwjgl/glfw/GLFW.java index 1f04ed0bf4..84135b450e 100644 --- a/jre_lwjgl3glfw/src/main/java/org/lwjgl/glfw/GLFW.java +++ b/jre_lwjgl3glfw/src/main/java/org/lwjgl/glfw/GLFW.java @@ -1047,19 +1047,27 @@ public static long glfwCreateWindow(int width, int height, CharSequence title, l boolean turnipLoad = System.getenv("POJAV_LOAD_TURNIP") != null && System.getenv("POJAV_LOAD_TURNIP").equals("1"); // These values can be found at headings_array.xml - if (turnipLoad && System.getenv("AMETHYST_RENDERER").equals("vulkan_zink")) { + String glDriver = System.getenv("AMETHYST_RENDERER"); + if (turnipLoad && glDriver.equals("vulkan_zink")) { glMajor = 4; glMinor = 6; - } else if (System.getenv("AMETHYST_RENDERER").equals("opengles3_virgl")) { + } else if (glDriver.equals("opengles3_virgl")) { glMajor = 4; glMinor = 3; - } else if (System.getenv("AMETHYST_RENDERER").equals("opengles_mobileglues")) { + } else if (glDriver.equals("opengles_mobileglues")) { glMajor = 4; glMinor = 0; } - // Get the real values properly + // Get the real values properly, but only if they're higher FunctionProvider functionProvider = org.lwjgl.opengl.GL.getFunctionProvider(); if (functionProvider != null) { + // Save the old context so we can swap back to it later after getting driver info + // This is because sometimes there are early loading windows like forge that get context + // and not returning it causes some obvious issues + long oldPtr = glfwGetCurrentContext(); + // Need to swap context to us so glFuncs work + glfwMakeContextCurrent(ptr); + // We don't assume createCapabilities has been called nor do we call it // This was based from LWJGL GL.createCapabilities() long GetError = functionProvider.getFunctionAddress("glGetError"); @@ -1073,19 +1081,24 @@ public static long glfwCreateWindow(int width, int height, CharSequence title, l APIVersion apiVersion = apiParseVersion(versionString); if (3 <= apiVersion.major && apiVersion.major <= 4) glMajor = apiVersion.major; if (3 <= apiVersion.minor && apiVersion.minor <= 6) glMinor = apiVersion.minor; + System.out.println("Driver "+glDriver+" GL string returned "+apiVersion.major+apiVersion.minor); } catch (Throwable ignored){} // In case the string is invalid/garbage } - // Try to get values from GL30+ driver directly, only use if higher ver try (MemoryStack stack = stackPush()) { - IntBuffer version = stack.ints(0); - callPV(GL_MAJOR_VERSION, memAddress(version), GetIntegerv); + IntBuffer version = stack.ints(0, 0); + callPV(GL_MAJOR_VERSION, memAddress(version, 0), GetIntegerv); if (callI(GetError) == GL_NO_ERROR && 3 <= version.get(0) && version.get(0) <= 4) glMajor = version.get(0); - callPV(GL_MINOR_VERSION, memAddress(version), GetIntegerv); + callPV(GL_MINOR_VERSION, memAddress(version, 1), GetIntegerv); if (callI(GetError) == GL_NO_ERROR && - 3 <= version.get(0) && version.get(0) <= 4) glMinor = version.get(0); + 3 <= version.get(0) && version.get(1) <= 4) glMinor = version.get(1); + System.out.println("Driver "+glDriver+" GL version returned "+version.get(0)+version.get(1)); } + System.out.println("Using GL version "+glMajor+glMinor+" for GLFW window context!"); + + // We finished getting the driver info, we can return it back to its original state now + glfwMakeContextCurrent(oldPtr); } win.windowAttribs.put(GLFW_CONTEXT_VERSION_MAJOR, glMajor); win.windowAttribs.put(GLFW_CONTEXT_VERSION_MINOR, glMinor);