diff --git a/app_pojavlauncher/src/main/assets/arc_dns_injector.jar b/app_pojavlauncher/src/main/assets/arc_dns_injector.jar deleted file mode 100644 index fe94aba81..000000000 Binary files a/app_pojavlauncher/src/main/assets/arc_dns_injector.jar and /dev/null differ diff --git a/app_pojavlauncher/src/main/assets/components/arc_dns_injector/arc_dns_injector.jar b/app_pojavlauncher/src/main/assets/components/arc_dns_injector/arc_dns_injector.jar new file mode 100644 index 000000000..447ec038b Binary files /dev/null and b/app_pojavlauncher/src/main/assets/components/arc_dns_injector/arc_dns_injector.jar differ diff --git a/app_pojavlauncher/src/main/assets/components/forge_installer/forge_installer.jar b/app_pojavlauncher/src/main/assets/components/forge_installer/forge_installer.jar new file mode 100644 index 000000000..b1cf6365f Binary files /dev/null and b/app_pojavlauncher/src/main/assets/components/forge_installer/forge_installer.jar differ diff --git a/app_pojavlauncher/src/main/assets/components/lwjgl3/version b/app_pojavlauncher/src/main/assets/components/lwjgl3/version index 92042ee01..750d4da31 100644 --- a/app_pojavlauncher/src/main/assets/components/lwjgl3/version +++ b/app_pojavlauncher/src/main/assets/components/lwjgl3/version @@ -1 +1 @@ -1678189863424 \ No newline at end of file +1687691695259 \ 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 306eb7cc0..115486537 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java @@ -590,11 +590,15 @@ public final class Tools { } public static void dialogOnUiThread(final Activity activity, final CharSequence title, final CharSequence message) { - activity.runOnUiThread(() -> new AlertDialog.Builder(activity) + activity.runOnUiThread(()->dialog(activity, title, message)); + } + + public static void dialog(final Context context, final CharSequence title, final CharSequence message) { + new AlertDialog.Builder(context) .setTitle(title) .setMessage(message) .setPositiveButton(android.R.string.ok, null) - .show()); + .show(); } public static void openURL(Activity act, String url) { 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 52dbeba07..edc715fa9 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 @@ -3,10 +3,9 @@ package net.kdt.pojavlaunch.fragments; import static net.kdt.pojavlaunch.Tools.shareLog; import android.content.Intent; -import android.net.Uri; import android.os.Bundle; -import android.provider.DocumentsContract; import android.view.View; +import android.view.ViewGroup; import android.widget.Button; import android.widget.ImageButton; import android.widget.Toast; @@ -15,12 +14,12 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; - import net.kdt.pojavlaunch.CustomControlsActivity; 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.ForgeDownloaderDialog; import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper; public class MainMenuFragment extends Fragment { @@ -52,6 +51,11 @@ public class MainMenuFragment extends Fragment { mPlayButton.setOnClickListener(v -> ExtraCore.setValue(ExtraConstants.LAUNCH_GAME, true)); mShareLogsButton.setOnClickListener((v) -> shareLog(requireContext())); + + mNewsButton.setOnLongClickListener((v)->{ + new ForgeDownloaderDialog().show(view.getContext(), (ViewGroup) view); + return true; + }); } private void runInstallerWithConfirmation(boolean isCustomArgs) { if (ProgressKeeper.getTaskCount() == 0) diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeDownloadListener.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeDownloadListener.java new file mode 100644 index 000000000..085e227e8 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeDownloadListener.java @@ -0,0 +1,7 @@ +package net.kdt.pojavlaunch.modloaders; + +public interface ForgeDownloadListener { + void onDownloadFinished(); + void onInstallerNotAvailable(); + void onDownloadError(Exception e); +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeDownloadTask.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeDownloadTask.java new file mode 100644 index 000000000..52c9f539a --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeDownloadTask.java @@ -0,0 +1,48 @@ +package net.kdt.pojavlaunch.modloaders; + +import com.kdt.mcgui.ProgressLayout; + +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.Tools; +import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper; +import net.kdt.pojavlaunch.utils.DownloadUtils; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +public class ForgeDownloadTask implements Runnable, Tools.DownloaderFeedback { + private final String mForgeUrl; + private final String mForgeVersion; + private final File mDestinationFile; + private final ForgeDownloadListener mListener; + public ForgeDownloadTask(ForgeDownloadListener listener, String forgeVersion, File destinationFile) { + this.mListener = listener; + this.mForgeUrl = ForgeUtils.getInstallerUrl(forgeVersion); + this.mForgeVersion = forgeVersion; + this.mDestinationFile = destinationFile; + } + @Override + public void run() { + ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.forge_dl_progress, mForgeVersion); + try { + byte[] buffer = new byte[8192]; + DownloadUtils.downloadFileMonitored(mForgeUrl, mDestinationFile, buffer, this); + mListener.onDownloadFinished(); + }catch (IOException e) { + if(e instanceof FileNotFoundException) { + mListener.onInstallerNotAvailable(); + }else{ + mListener.onDownloadError(e); + } + } + ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, -1, -1); + } + + @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, mForgeVersion); + } + +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeDownloaderDialog.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeDownloaderDialog.java new file mode 100644 index 000000000..26a96bec6 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeDownloaderDialog.java @@ -0,0 +1,94 @@ +package net.kdt.pojavlaunch.modloaders; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.Looper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ExpandableListView; +import android.widget.ProgressBar; + +import net.kdt.pojavlaunch.JavaGUILauncherActivity; +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.Tools; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +public class ForgeDownloaderDialog implements Runnable, ExpandableListView.OnChildClickListener, ForgeDownloadListener { + private final Handler mHandler = new Handler(Looper.getMainLooper()); + private ExpandableListView mExpandableListView; + private ProgressBar mProgressBar; + private AlertDialog mAlertDialog; + private File mDestinationFile; + private Context mContext; + public void show(Context context, ViewGroup root) { + this.mContext = context; + this.mDestinationFile = new File(context.getCacheDir(), "forge-installer.jar"); + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context); + View dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_expandable_forge_list, root, false); + mProgressBar = dialogView.findViewById(R.id.forge_list_progress_bar); + mExpandableListView = dialogView.findViewById(R.id.forge_expandable_version_list); + mExpandableListView.setOnChildClickListener(this); + dialogBuilder.setView(dialogView); + mAlertDialog = dialogBuilder.show(); + new Thread(this).start(); + } + + @Override + public void run() { + try { + List forgeVersions = ForgeUtils.downloadForgeVersions(); + mHandler.post(()->{ + if(forgeVersions != null) { + mProgressBar.setVisibility(View.GONE); + mExpandableListView.setAdapter(new ForgeVersionListAdapter(forgeVersions, LayoutInflater.from(mContext))); + }else{ + mAlertDialog.dismiss(); + } + }); + }catch (IOException e) { + mHandler.post(()->{ + mAlertDialog.dismiss(); + Tools.showError(mContext, e); + }); + } + } + + + + @Override + public boolean onChildClick(ExpandableListView expandableListView, View view, int i, int i1, long l) { + String forgeVersion = (String)expandableListView.getExpandableListAdapter().getChild(i, i1); + new Thread(new ForgeDownloadTask(this, forgeVersion, mDestinationFile)).start(); + mAlertDialog.dismiss(); + return true; + } + + @Override + public void onDownloadFinished() { + Intent intent = new Intent(mContext, JavaGUILauncherActivity.class); + ForgeUtils.addAutoInstallArgs(intent, mDestinationFile, true); // since it's a user-invoked install, we want to create a new profile + mContext.startActivity(intent); + } + + @Override + public void onInstallerNotAvailable() { + mHandler.post(()-> { + mAlertDialog.dismiss(); + Tools.dialog(mContext, + mContext.getString(R.string.global_error), + mContext.getString(R.string.forge_dl_no_installer)); + }); + } + + @Override + public void onDownloadError(Exception e) { + mHandler.post(mAlertDialog::dismiss); + Tools.showError(mContext, e); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeUtils.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeUtils.java new file mode 100644 index 000000000..588b90430 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeUtils.java @@ -0,0 +1,46 @@ +package net.kdt.pojavlaunch.modloaders; + +import android.content.Intent; + +import net.kdt.pojavlaunch.Tools; +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.io.StringReader; +import java.util.List; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +public class ForgeUtils { + private static final String FORGE_METADATA_URL = "https://maven.minecraftforge.net/net/minecraftforge/forge/maven-metadata.xml"; + private static final String FORGE_INSTALLER_URL = "https://maven.minecraftforge.net/net/minecraftforge/forge/%1$s/forge-%1$s-installer.jar"; + public static List downloadForgeVersions() throws IOException { + String forgeMetadata = DownloadUtils.downloadString(FORGE_METADATA_URL); + try { + SAXParserFactory parserFactory = SAXParserFactory.newInstance(); + SAXParser parser = parserFactory.newSAXParser(); + ForgeVersionListHandler handler = new ForgeVersionListHandler(); + parser.parse(new InputSource(new StringReader(forgeMetadata)), handler); + return handler.getVersions(); + }catch (SAXException | ParserConfigurationException e) { + e.printStackTrace(); + return null; + } + } + public static String getInstallerUrl(String version) { + return String.format(FORGE_INSTALLER_URL, version); + } + + public static void addAutoInstallArgs(Intent intent, File modInstallerJar, boolean createProfile) { + intent.putExtra("javaArgs", "-javaagent:"+ Tools.DIR_DATA+"/forge_installer/forge_installer.jar" + + (createProfile ? "=NPS" : "") + // No Profile Suppression + " -jar "+modInstallerJar.getAbsolutePath()); + intent.putExtra("skipDetectMod", true); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeVersionListAdapter.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeVersionListAdapter.java new file mode 100644 index 000000000..4b96cd03e --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeVersionListAdapter.java @@ -0,0 +1,102 @@ +package net.kdt.pojavlaunch.modloaders; + +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 java.util.ArrayList; +import java.util.List; + +public class ForgeVersionListAdapter extends BaseExpandableListAdapter implements ExpandableListAdapter { + private final List mGameVersions; + private final List> mForgeVersions; + private final LayoutInflater mLayoutInflater; + + public ForgeVersionListAdapter(List forgeVersions, LayoutInflater layoutInflater) { + this.mLayoutInflater = layoutInflater; + mGameVersions = new ArrayList<>(); + mForgeVersions = new ArrayList<>(); + for(String version : forgeVersions) { + int dashIndex = version.indexOf("-"); + String gameVersion = version.substring(0, dashIndex); + List versionList; + int gameVersionIndex = mGameVersions.indexOf(gameVersion); + if(gameVersionIndex != -1) versionList = mForgeVersions.get(gameVersionIndex); + else { + versionList = new ArrayList<>(); + mGameVersions.add(gameVersion); + mForgeVersions.add(versionList); + } + versionList.add(version); + } + } + + @Override + public int getGroupCount() { + return mGameVersions.size(); + } + + @Override + public int getChildrenCount(int i) { + return mForgeVersions.get(i).size(); + } + + @Override + public Object getGroup(int i) { + return getGameVersion(i); + } + + @Override + public Object getChild(int i, int i1) { + return getForgeVersion(i, 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(getGameVersion(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(getForgeVersion(i, i1)); + return convertView; + } + + private String getGameVersion(int i) { + return mGameVersions.get(i); + } + + private String getForgeVersion(int i, int i1){ + return mForgeVersions.get(i).get(i1); + } + + @Override + public boolean isChildSelectable(int i, int i1) { + return true; + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeVersionListHandler.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeVersionListHandler.java new file mode 100644 index 000000000..8de024ccd --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/ForgeVersionListHandler.java @@ -0,0 +1,39 @@ +package net.kdt.pojavlaunch.modloaders; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import java.util.ArrayList; +import java.util.List; + +public class ForgeVersionListHandler extends DefaultHandler { + private List mForgeVersions; + private StringBuilder mCurrentVersion = null; + @Override + public void startDocument() throws SAXException { + mForgeVersions = new ArrayList<>(); + } + + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + if(mCurrentVersion != null) mCurrentVersion.append(ch, start, length); + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { + if(qName.equals("version")) mCurrentVersion = new StringBuilder(); + } + + @Override + public void endElement(String uri, String localName, String qName) throws SAXException { + if (qName.equals("version")) { + String version = mCurrentVersion.toString(); + mForgeVersions.add(version); + mCurrentVersion = null; + } + } + public List getVersions() { + return mForgeVersions; + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/AsyncAssetManager.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/AsyncAssetManager.java index dffd6a899..0b13f357d 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/AsyncAssetManager.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/AsyncAssetManager.java @@ -68,7 +68,6 @@ public class AsyncAssetManager { Tools.copyAssetFile(ctx, "launcher_profiles.json", Tools.DIR_GAME_NEW, false); Tools.copyAssetFile(ctx,"resolv.conf",Tools.DIR_DATA, false); - Tools.copyAssetFile(ctx,"arc_dns_injector.jar",Tools.DIR_DATA, false); } catch (IOException e) { Log.e("AsyncAssetManager", "Failed to unpack critical components !"); } @@ -86,6 +85,8 @@ public class AsyncAssetManager { // we repack them to a single file here unpackComponent(ctx, "lwjgl3", false); unpackComponent(ctx, "security", true); + unpackComponent(ctx, "arc_dns_injector", true); + unpackComponent(ctx, "forge_installer", true); } catch (IOException e) { Log.e("AsyncAssetManager", "Failed o unpack components !",e ); } 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 6b537a6be..542d724ac 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 @@ -352,7 +352,7 @@ public class JREUtils { "-Dfml.earlyprogresswindow=false" //Forge 1.14+ workaround )); if(LauncherPreferences.PREF_ARC_CAPES) { - overridableArguments.add("-javaagent:"+new File(Tools.DIR_DATA,"arc_dns_injector.jar").getAbsolutePath()+"=23.95.137.176"); + overridableArguments.add("-javaagent:"+new File(Tools.DIR_DATA,"arc_dns_injector/arc_dns_injector.jar").getAbsolutePath()+"=23.95.137.176"); } List additionalArguments = new ArrayList<>(); for(String arg : overridableArguments) { diff --git a/app_pojavlauncher/src/main/res/layout/dialog_expandable_forge_list.xml b/app_pojavlauncher/src/main/res/layout/dialog_expandable_forge_list.xml new file mode 100644 index 000000000..39f67a91e --- /dev/null +++ b/app_pojavlauncher/src/main/res/layout/dialog_expandable_forge_list.xml @@ -0,0 +1,19 @@ + + + + + + + \ 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 e8059262e..07d94bccf 100644 --- a/app_pojavlauncher/src/main/res/values/strings.xml +++ b/app_pojavlauncher/src/main/res/values/strings.xml @@ -372,4 +372,7 @@ Increase it if the joystick drifts Force renderer to run on the big core Forces the Minecraft render thread to run on the core with the highest max frequency + Downloading installer for %s + Failed to load the version list + Sorry, but this version of Forge does not have an installer, which is not yet supported. diff --git a/arc_dns_injector/build.gradle b/arc_dns_injector/build.gradle index 01240331b..efb772202 100644 --- a/arc_dns_injector/build.gradle +++ b/arc_dns_injector/build.gradle @@ -11,5 +11,7 @@ jar { attributes("Manifest-Version": "1.0", "PreMain-Class": "git.artdeell.arcdns.ArcDNSInjectorAgent") } - destinationDirectory.set(file("../app_pojavlauncher/src/main/assets/")) + File versionFile = file("../app_pojavlauncher/src/main/assets/components/arc_dns_injector/version") + versionFile.write(String.valueOf(new Date().getTime())) + destinationDirectory.set(file("../app_pojavlauncher/src/main/assets/components/arc_dns_injector/")) } diff --git a/forge_installer/.gitignore b/forge_installer/.gitignore new file mode 100644 index 000000000..b63da4551 --- /dev/null +++ b/forge_installer/.gitignore @@ -0,0 +1,42 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/forge_installer/build.gradle b/forge_installer/build.gradle new file mode 100644 index 000000000..d836fede3 --- /dev/null +++ b/forge_installer/build.gradle @@ -0,0 +1,25 @@ +plugins { + id 'java-library' +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { + implementation 'org.json:json:20230618' +} + +jar { + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } + File versionFile = file("../app_pojavlauncher/src/main/assets/components/forge_installer/version") + versionFile.write(String.valueOf(new Date().getTime())) + manifest { + attributes("Manifest-Version": "1.0", + "PreMain-Class": "git.artdeell.forgeinstaller.Agent") + } + destinationDirectory.set(file("../app_pojavlauncher/src/main/assets/components/forge_installer/")) +} \ No newline at end of file diff --git a/forge_installer/src/main/java/git/artdeell/forgeinstaller/Agent.java b/forge_installer/src/main/java/git/artdeell/forgeinstaller/Agent.java new file mode 100644 index 000000000..a36d9a2f7 --- /dev/null +++ b/forge_installer/src/main/java/git/artdeell/forgeinstaller/Agent.java @@ -0,0 +1,98 @@ +package git.artdeell.forgeinstaller; + +import java.awt.AWTEvent; +import java.awt.Component; +import java.awt.Container; +import java.awt.EventQueue; +import java.awt.Toolkit; +import java.awt.Window; +import java.awt.event.AWTEventListener; +import java.awt.event.WindowEvent; +import java.lang.instrument.Instrumentation; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.AbstractButton; +import javax.swing.JDialog; +import javax.swing.JOptionPane; + +public class Agent implements AWTEventListener { + private boolean forgeWindowHandled = false; + private final boolean suppressProfileCreation; + + public Agent(boolean ps) { + this.suppressProfileCreation = ps; + } + + @Override + public void eventDispatched(AWTEvent event) { + WindowEvent windowEvent = (WindowEvent) event; + Window window = windowEvent.getWindow(); + if(windowEvent.getID() == WindowEvent.WINDOW_OPENED) { + if(!forgeWindowHandled) { // false at startup, so we will handle the first window as the Forge one + handleForgeWindow(window); + forgeWindowHandled = true; + }else if(window instanceof JDialog) { // expecting a new dialog + handleDialog(window); + } + } + } + + public void handleForgeWindow(Window window) { + List components = new ArrayList<>(); + insertAllComponents(components, window, new MainWindowFilter()); + AbstractButton okButton = null; + for(Component component : components) { + if(component instanceof AbstractButton) { + AbstractButton abstractButton = (AbstractButton) component; + switch(abstractButton.getText()) { + case "OK": + okButton = abstractButton; // store the button, so we can press it after processing other stuff + break; + case "Install client": + abstractButton.doClick(); // It should be the default, but let's make sure + } + + } + } + if(okButton == null) { + System.out.println("Failed to set all the UI components."); + System.exit(17); + }else{ + ProfileFixer.storeProfile(); + EventQueue.invokeLater(okButton::doClick); // do that after forge actually builds its window, otherwise we set the path too fast + } + } + + public void handleDialog(Window window) { + List components = new ArrayList<>(); + insertAllComponents(components, window, new DialogFilter()); // ensure that it's a JOptionPane dialog + if(components.size() == 1) { + // another common trait of them - they only have one option pane in them, + // so we can discard the rest of the dialog structure + JOptionPane optionPane = (JOptionPane) components.get(0); + if(optionPane.getMessageType() == JOptionPane.INFORMATION_MESSAGE) { // forge doesn't emit information messages for other reasons yet + System.out.println("The install was successful!"); + ProfileFixer.reinsertProfile(suppressProfileCreation); + System.exit(0); // again, forge doesn't call exit for some reason, so we do that ourselves here + } + } + } + + public void insertAllComponents(List components, Container parent, ComponentFilter filter) { + int componentCount = parent.getComponentCount(); + for(int i = 0; i < componentCount; i++) { + Component component = parent.getComponent(i); + if(filter.checkComponent(component)) components.add(component); + if(component instanceof Container) { + insertAllComponents(components, (Container) component, filter); + } + } + } + + public static void premain(String args, Instrumentation inst) { + Toolkit.getDefaultToolkit() + .addAWTEventListener(new Agent(!"NPS".equals(args)), // No Profile Suppression + AWTEvent.WINDOW_EVENT_MASK); + } +} diff --git a/forge_installer/src/main/java/git/artdeell/forgeinstaller/ComponentFilter.java b/forge_installer/src/main/java/git/artdeell/forgeinstaller/ComponentFilter.java new file mode 100644 index 000000000..352128b56 --- /dev/null +++ b/forge_installer/src/main/java/git/artdeell/forgeinstaller/ComponentFilter.java @@ -0,0 +1,7 @@ +package git.artdeell.forgeinstaller; + +import java.awt.*; + +public interface ComponentFilter { + boolean checkComponent(Component component); +} diff --git a/forge_installer/src/main/java/git/artdeell/forgeinstaller/DialogFilter.java b/forge_installer/src/main/java/git/artdeell/forgeinstaller/DialogFilter.java new file mode 100644 index 000000000..d528b6946 --- /dev/null +++ b/forge_installer/src/main/java/git/artdeell/forgeinstaller/DialogFilter.java @@ -0,0 +1,11 @@ +package git.artdeell.forgeinstaller; + +import javax.swing.*; +import java.awt.*; + +public class DialogFilter implements ComponentFilter{ + @Override + public boolean checkComponent(Component component) { + return component instanceof JOptionPane; + } +} diff --git a/forge_installer/src/main/java/git/artdeell/forgeinstaller/MainWindowFilter.java b/forge_installer/src/main/java/git/artdeell/forgeinstaller/MainWindowFilter.java new file mode 100644 index 000000000..8f0921637 --- /dev/null +++ b/forge_installer/src/main/java/git/artdeell/forgeinstaller/MainWindowFilter.java @@ -0,0 +1,13 @@ +package git.artdeell.forgeinstaller; + +import javax.swing.*; +import java.awt.*; + +public class MainWindowFilter implements ComponentFilter{ + @Override + public boolean checkComponent(Component component) { + return component instanceof JRadioButton + || component instanceof JTextField + || component instanceof JButton; + } +} diff --git a/forge_installer/src/main/java/git/artdeell/forgeinstaller/ProfileFixer.java b/forge_installer/src/main/java/git/artdeell/forgeinstaller/ProfileFixer.java new file mode 100644 index 000000000..5228c0a2c --- /dev/null +++ b/forge_installer/src/main/java/git/artdeell/forgeinstaller/ProfileFixer.java @@ -0,0 +1,59 @@ +package git.artdeell.forgeinstaller; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.Random; + +public class ProfileFixer { + private static final Random random = new Random(); + private static final Path profilesPath = Paths.get(System.getProperty("user.home"), ".minecraft", "launcher_profiles.json"); + private static JSONObject oldProfile = null; + public static void storeProfile() { + try { + JSONObject minecraftProfiles = new JSONObject( + new String(Files.readAllBytes(profilesPath), + StandardCharsets.UTF_8) + ); + JSONObject profilesArray = minecraftProfiles.getJSONObject("profiles"); + oldProfile = profilesArray.optJSONObject("forge", null); + }catch (IOException | JSONException e) { + System.out.println("Failed to store Forge profile: "+e); + } + } + + private static String pickProfileName() { + return "forge"+random.nextInt(); + } + public static void reinsertProfile(boolean suppressProfileCreation) { + try { + JSONObject minecraftProfiles = new JSONObject( + new String(Files.readAllBytes(profilesPath), + StandardCharsets.UTF_8) + ); + JSONObject profilesArray = minecraftProfiles.getJSONObject("profiles"); + if(oldProfile != null) { + if(suppressProfileCreation) profilesArray.put("forge", oldProfile); // restore the old profile + else { + String name = pickProfileName(); + while(profilesArray.has(name)) name = pickProfileName(); + profilesArray.put(name, oldProfile); // restore the old profile under a new name + } + }else{ + if(suppressProfileCreation) profilesArray.remove("forge"); // remove the new profile + // otherwise it wont be removed + } + minecraftProfiles.put("profiles", profilesArray); + Files.write(profilesPath, minecraftProfiles.toString().getBytes(StandardCharsets.UTF_8), + StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); + }catch (IOException | JSONException e) { + System.out.println("Failed to restore old Forge profile: "+e); + } + } +} diff --git a/settings.gradle b/settings.gradle index 695595b9e..0f0ae5d15 100644 --- a/settings.gradle +++ b/settings.gradle @@ -19,3 +19,4 @@ include ':jre_lwjgl3glfw' include ':app_pojavlauncher' include ':arc_dns_injector' +include ':forge_installer' \ No newline at end of file