Add OptiFine automatic installation

Unlike other modloaders, it does require the game being installed beforehand, so it's also implemented here
This commit is contained in:
BuildTools 2023-06-30 12:49:01 +03:00
parent 3cfaec3b4c
commit 84a2bd24c7
10 changed files with 138 additions and 32 deletions

View File

@ -1 +1 @@
1687971356220 1688118307460

View File

@ -1,9 +1,11 @@
package net.kdt.pojavlaunch.fragments; package net.kdt.pojavlaunch.fragments;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.widget.ExpandableListAdapter; import android.widget.ExpandableListAdapter;
import net.kdt.pojavlaunch.JavaGUILauncherActivity;
import net.kdt.pojavlaunch.R; import net.kdt.pojavlaunch.R;
import net.kdt.pojavlaunch.Tools; import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.modloaders.ModloaderListenerProxy; import net.kdt.pojavlaunch.modloaders.ModloaderListenerProxy;
@ -55,6 +57,8 @@ public class OptiFineInstallFragment extends ModVersionListFragment<OptiFineUtil
@Override @Override
public void onDownloadFinished(Context context, File downloadedFile) { public void onDownloadFinished(Context context, File downloadedFile) {
Tools.dialog(context, "Not yet complete", "Installation of OptiFine is not yet implemented. To be done!"); Intent modInstallerStartIntent = new Intent(context, JavaGUILauncherActivity.class);
OptiFineUtils.addAutoInstallArgs(modInstallerStartIntent, downloadedFile);
context.startActivity(modInstallerStartIntent);
} }
} }

View File

@ -2,18 +2,25 @@ package net.kdt.pojavlaunch.modloaders;
import com.kdt.mcgui.ProgressLayout; import com.kdt.mcgui.ProgressLayout;
import net.kdt.pojavlaunch.JMinecraftVersionList;
import net.kdt.pojavlaunch.R; import net.kdt.pojavlaunch.R;
import net.kdt.pojavlaunch.Tools; import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper; import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper;
import net.kdt.pojavlaunch.tasks.AsyncMinecraftDownloader;
import net.kdt.pojavlaunch.utils.DownloadUtils; import net.kdt.pojavlaunch.utils.DownloadUtils;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class OptiFineDownloadTask implements Runnable, Tools.DownloaderFeedback{ public class OptiFineDownloadTask implements Runnable, Tools.DownloaderFeedback, AsyncMinecraftDownloader.DoneListener {
private static final Pattern sMcVersionPattern = Pattern.compile("([0-9]+)\\.([0-9]+)\\.?([0-9]+)?");
private final OptiFineUtils.OptiFineVersion mOptiFineVersion; private final OptiFineUtils.OptiFineVersion mOptiFineVersion;
private final File mDestinationFile; private final File mDestinationFile;
private final ModloaderDownloadListener mListener; private final ModloaderDownloadListener mListener;
private final Object mMinecraftDownloadLock = new Object();
private Throwable mDownloaderThrowable;
public OptiFineDownloadTask(OptiFineUtils.OptiFineVersion mOptiFineVersion, File mDestinationFile, ModloaderDownloadListener mListener) { public OptiFineDownloadTask(OptiFineUtils.OptiFineVersion mOptiFineVersion, File mDestinationFile, ModloaderDownloadListener mListener) {
this.mOptiFineVersion = mOptiFineVersion; this.mOptiFineVersion = mOptiFineVersion;
@ -23,7 +30,7 @@ public class OptiFineDownloadTask implements Runnable, Tools.DownloaderFeedback{
@Override @Override
public void run() { public void run() {
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.of_dl_progress, mOptiFineVersion.downloadUrl); ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, 0, R.string.of_dl_progress, mOptiFineVersion.versionName);
try { try {
if(runCatching()) mListener.onDownloadFinished(mDestinationFile); if(runCatching()) mListener.onDownloadFinished(mDestinationFile);
}catch (IOException e) { }catch (IOException e) {
@ -35,6 +42,17 @@ public class OptiFineDownloadTask implements Runnable, Tools.DownloaderFeedback{
public boolean runCatching() throws IOException { public boolean runCatching() throws IOException {
String downloadUrl = scrapeDownloadsPage(); String downloadUrl = scrapeDownloadsPage();
if(downloadUrl == null) return false; if(downloadUrl == null) return false;
String minecraftVersion = determineMinecraftVersion();
if(minecraftVersion == null) return false;
if(!downloadMinecraft(minecraftVersion)) {
if(mDownloaderThrowable instanceof Exception) {
mListener.onDownloadError((Exception) mDownloaderThrowable);
}else {
Exception exception = new Exception(mDownloaderThrowable);
mListener.onDownloadError(exception);
}
return false;
}
DownloadUtils.downloadFileMonitored(downloadUrl, mDestinationFile, new byte[8192], this); DownloadUtils.downloadFileMonitored(downloadUrl, mDestinationFile, new byte[8192], this);
return true; return true;
} }
@ -45,9 +63,59 @@ public class OptiFineDownloadTask implements Runnable, Tools.DownloaderFeedback{
return scrapeResult; return scrapeResult;
} }
public String determineMinecraftVersion() {
Matcher matcher = sMcVersionPattern.matcher(mOptiFineVersion.minecraftVersion);
if(matcher.find()) {
StringBuilder mcVersionBuilder = new StringBuilder();
mcVersionBuilder.append(matcher.group(1));
mcVersionBuilder.append('.');
mcVersionBuilder.append(matcher.group(2));
String thirdGroup = matcher.group(3);
if(thirdGroup != null && !thirdGroup.isEmpty() && !"0".equals(thirdGroup)) {
mcVersionBuilder.append('.');
mcVersionBuilder.append(thirdGroup);
}
return mcVersionBuilder.toString();
}else{
mListener.onDataNotAvailable();
return null;
}
}
public boolean downloadMinecraft(String minecraftVersion) {
// the string is always normalized
JMinecraftVersionList.Version minecraftJsonVersion = AsyncMinecraftDownloader.getListedVersion(minecraftVersion);
if(minecraftJsonVersion == null) return false;
try {
synchronized (mMinecraftDownloadLock) {
new AsyncMinecraftDownloader(null, minecraftJsonVersion, minecraftVersion, this);
mMinecraftDownloadLock.wait();
}
}catch (InterruptedException e) {
e.printStackTrace();
}
return mDownloaderThrowable == null;
}
@Override @Override
public void updateProgress(int curr, int max) { public void updateProgress(int curr, int max) {
int progress100 = (int)(((float)curr / (float)max)*100f); int progress100 = (int)(((float)curr / (float)max)*100f);
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, progress100, R.string.of_dl_progress, mOptiFineVersion.versionName); ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, progress100, R.string.of_dl_progress, mOptiFineVersion.versionName);
} }
@Override
public void onDownloadDone() {
synchronized (mMinecraftDownloadLock) {
mDownloaderThrowable = null;
mMinecraftDownloadLock.notifyAll();
}
}
@Override
public void onDownloadFailed(Throwable throwable) {
synchronized (mMinecraftDownloadLock) {
mDownloaderThrowable = throwable;
mMinecraftDownloadLock.notifyAll();
}
}
} }

View File

@ -55,6 +55,7 @@ public class OptiFineScraper implements DownloadUtils.ParseCallback<OptiFineUtil
private void traverseDownloadLine(TagNode tagNode) { private void traverseDownloadLine(TagNode tagNode) {
OptiFineUtils.OptiFineVersion optiFineVersion = new OptiFineUtils.OptiFineVersion(); OptiFineUtils.OptiFineVersion optiFineVersion = new OptiFineUtils.OptiFineVersion();
optiFineVersion.minecraftVersion = mMinecraftVersion;
for(TagNode subNode : tagNode.getChildTags()) { for(TagNode subNode : tagNode.getChildTags()) {
if(!subNode.getName().equals("td")) continue; if(!subNode.getName().equals("td")) continue;
switch(subNode.getAttributeByName("class")) { switch(subNode.getAttributeByName("class")) {

View File

@ -1,7 +1,11 @@
package net.kdt.pojavlaunch.modloaders; package net.kdt.pojavlaunch.modloaders;
import android.content.Intent;
import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.utils.DownloadUtils; import net.kdt.pojavlaunch.utils.DownloadUtils;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
@ -17,11 +21,19 @@ public class OptiFineUtils {
} }
} }
public static void addAutoInstallArgs(Intent intent, File modInstallerJar) {
intent.putExtra("javaArgs", "-javaagent:"+ Tools.DIR_DATA+"/forge_installer/forge_installer.jar"
+ "=OFNPS" +// No Profile Suppression
" -jar "+modInstallerJar.getAbsolutePath());
intent.putExtra("skipDetectMod", true);
}
public static class OptiFineVersions { public static class OptiFineVersions {
public List<String> minecraftVersions; public List<String> minecraftVersions;
public List<List<OptiFineVersion>> optifineVersions; public List<List<OptiFineVersion>> optifineVersions;
} }
public static class OptiFineVersion { public static class OptiFineVersion {
public String minecraftVersion;
public String versionName; public String versionName;
public String downloadUrl; public String downloadUrl;
} }

View File

@ -46,7 +46,7 @@ public class AsyncMinecraftDownloader {
/* Allows each downloading thread to have its own RECYCLED buffer */ /* Allows each downloading thread to have its own RECYCLED buffer */
private final ConcurrentHashMap<Thread, byte[]> mThreadBuffers = new ConcurrentHashMap<>(5); private final ConcurrentHashMap<Thread, byte[]> mThreadBuffers = new ConcurrentHashMap<>(5);
public AsyncMinecraftDownloader(@NonNull Activity activity, JMinecraftVersionList.Version version, String realVersion, public AsyncMinecraftDownloader(Activity activity, JMinecraftVersionList.Version version, String realVersion,
@NonNull DoneListener listener){ // this was there for a reason @NonNull DoneListener listener){ // this was there for a reason
sExecutorService.execute(() -> { sExecutorService.execute(() -> {
try { try {
@ -58,7 +58,7 @@ public class AsyncMinecraftDownloader {
}); });
} }
/* we do the throws DownloaderException thing to avoid blanket-catching Exception as a form of anti-lazy-developer protection */ /* we do the throws DownloaderException thing to avoid blanket-catching Exception as a form of anti-lazy-developer protection */
private void downloadGame(@NonNull Activity activity, JMinecraftVersionList.Version verInfo, String versionName) throws DownloaderException { private void downloadGame(Activity activity, JMinecraftVersionList.Version verInfo, String versionName) throws DownloaderException {
final String downVName = "/" + versionName + "/" + versionName; final String downVName = "/" + versionName + "/" + versionName;
//Downloading libraries //Downloading libraries
@ -88,7 +88,7 @@ public class AsyncMinecraftDownloader {
verInfo = Tools.getVersionInfo(versionName); verInfo = Tools.getVersionInfo(versionName);
// THIS one function need the activity in the case of an error // THIS one function need the activity in the case of an error
if(!JRE17Util.installNewJreIfNeeded(activity, verInfo)){ if(activity != null && !JRE17Util.installNewJreIfNeeded(activity, verInfo)){
ProgressKeeper.submitProgress(ProgressLayout.DOWNLOAD_MINECRAFT, -1, -1); ProgressKeeper.submitProgress(ProgressLayout.DOWNLOAD_MINECRAFT, -1, -1);
throw new DownloaderException(); throw new DownloaderException();
} }

View File

@ -392,7 +392,7 @@
<string name="create_profile_modded_versions">Modded versions</string> <string name="create_profile_modded_versions">Modded versions</string>
<string name="fabric_dl_loader_title">Select versions</string> <string name="fabric_dl_loader_title">Select versions</string>
<string name="of_dl_select_version">Select OptiFine version</string> <string name="of_dl_select_version">Select OptiFine version</string>
<string name="of_dl_failed_to_scrape">Failed to get the OptiFine download link</string> <string name="of_dl_failed_to_scrape">Failed to collect data for OptiFine installation</string>
<string name="of_dl_progress">Downloading %s</string> <string name="of_dl_progress">Downloading %s</string>
<string name="create_profile_optifine">Create OptiFine profile</string> <string name="create_profile_optifine">Create OptiFine profile</string>
</resources> </resources>

View File

@ -20,11 +20,12 @@ import javax.swing.JOptionPane;
public class Agent implements AWTEventListener { public class Agent implements AWTEventListener {
private boolean forgeWindowHandled = false; private boolean forgeWindowHandled = false;
private final boolean suppressProfileCreation; private final boolean suppressProfileCreation;
private final boolean optiFineInstallation;
private final Timer componentTimer = new Timer(); private final Timer componentTimer = new Timer();
public Agent(boolean ps) { public Agent(boolean nps, boolean of) {
this.suppressProfileCreation = ps; this.suppressProfileCreation = !nps;
this.optiFineInstallation = of;
} }
@Override @Override
@ -33,7 +34,7 @@ public class Agent implements AWTEventListener {
Window window = windowEvent.getWindow(); Window window = windowEvent.getWindow();
if(windowEvent.getID() == WindowEvent.WINDOW_OPENED) { if(windowEvent.getID() == WindowEvent.WINDOW_OPENED) {
if(!forgeWindowHandled) { // false at startup, so we will handle the first window as the Forge one if(!forgeWindowHandled) { // false at startup, so we will handle the first window as the Forge one
forgeWindowHandled = handleForgeWindow(window); forgeWindowHandled = handleMainWindow(window);
if(forgeWindowHandled) { if(forgeWindowHandled) {
componentTimer.cancel(); componentTimer.cancel();
componentTimer.purge(); componentTimer.purge();
@ -46,34 +47,47 @@ public class Agent implements AWTEventListener {
} }
} }
public boolean handleForgeWindow(Window window) { public boolean handleMainWindow(Window window) {
List<Component> components = new ArrayList<>(); List<Component> components = new ArrayList<>();
insertAllComponents(components, window, new MainWindowFilter()); insertAllComponents(components, window, new MainWindowFilter());
AbstractButton okButton = null; AbstractButton okButton = null;
for(Component component : components) { for(Component component : components) {
if(component instanceof AbstractButton) { if(component instanceof AbstractButton) {
AbstractButton abstractButton = (AbstractButton) component; AbstractButton abstractButton = (AbstractButton) component;
switch(abstractButton.getText()) { abstractButton = optiFineInstallation ?
case "OK": handleOptiFineButton(abstractButton) :
okButton = abstractButton; // store the button, so we can press it after processing other stuff handleForgeButton(abstractButton);
break; if(abstractButton != null) okButton = abstractButton;
case "Install client":
abstractButton.doClick(); // It should be the default, but let's make sure
}
} }
} }
if(okButton == null) { if(okButton == null) {
System.out.println("Failed to set all the UI components, wil try again in the next window"); System.out.println("Failed to set all the UI components, wil try again in the next window");
System.exit(17);
return false; return false;
}else{ }else{
ProfileFixer.storeProfile(); ProfileFixer.storeProfile(optiFineInstallation ? "OptiFine" : "forge");
EventQueue.invokeLater(okButton::doClick); // do that after forge actually builds its window, otherwise we set the path too fast EventQueue.invokeLater(okButton::doClick); // do that after forge actually builds its window, otherwise we set the path too fast
return true; return true;
} }
} }
public AbstractButton handleForgeButton(AbstractButton abstractButton) {
switch(abstractButton.getText()) {
case "OK":
return abstractButton; // return the button, so we can press it after processing other stuff
case "Install client":
abstractButton.doClick(); // It should be the default, but let's make sure
}
return null;
}
public AbstractButton handleOptiFineButton(AbstractButton abstractButton) {
if ("Install".equals(abstractButton.getText())) {
return abstractButton;
}
return null;
}
public void handleDialog(Window window) { public void handleDialog(Window window) {
List<Component> components = new ArrayList<>(); List<Component> components = new ArrayList<>();
insertAllComponents(components, window, new DialogFilter()); // ensure that it's a JOptionPane dialog insertAllComponents(components, window, new DialogFilter()); // ensure that it's a JOptionPane dialog
@ -84,7 +98,7 @@ public class Agent implements AWTEventListener {
JOptionPane optionPane = (JOptionPane) components.get(0); JOptionPane optionPane = (JOptionPane) components.get(0);
if(optionPane.getMessageType() == JOptionPane.INFORMATION_MESSAGE) { // forge doesn't emit information messages for other reasons yet if(optionPane.getMessageType() == JOptionPane.INFORMATION_MESSAGE) { // forge doesn't emit information messages for other reasons yet
System.out.println("The install was successful!"); System.out.println("The install was successful!");
ProfileFixer.reinsertProfile(suppressProfileCreation); ProfileFixer.reinsertProfile(optiFineInstallation ? "OptiFine" : "forge", suppressProfileCreation);
System.exit(0); // again, forge doesn't call exit for some reason, so we do that ourselves here System.exit(0); // again, forge doesn't call exit for some reason, so we do that ourselves here
} }
} }
@ -102,8 +116,15 @@ public class Agent implements AWTEventListener {
} }
public static void premain(String args, Instrumentation inst) { public static void premain(String args, Instrumentation inst) {
boolean noProfileSuppression = false;
boolean optifine = false;
if(args != null ) {
noProfileSuppression = args.contains("NPS"); // No Profile Suppression
optifine = args.contains("OF"); // OptiFine
}
Agent agent = new Agent(noProfileSuppression, optifine);
Toolkit.getDefaultToolkit() Toolkit.getDefaultToolkit()
.addAWTEventListener(new Agent(!"NPS".equals(args)), // No Profile Suppression .addAWTEventListener(agent,
AWTEvent.WINDOW_EVENT_MASK); AWTEvent.WINDOW_EVENT_MASK);
} }
} }

View File

@ -15,23 +15,23 @@ public class ProfileFixer {
private static final Random random = new Random(); private static final Random random = new Random();
private static final Path profilesPath = Paths.get(System.getProperty("user.home"), ".minecraft", "launcher_profiles.json"); private static final Path profilesPath = Paths.get(System.getProperty("user.home"), ".minecraft", "launcher_profiles.json");
private static JSONObject oldProfile = null; private static JSONObject oldProfile = null;
public static void storeProfile() { public static void storeProfile(String profileName) {
try { try {
JSONObject minecraftProfiles = new JSONObject( JSONObject minecraftProfiles = new JSONObject(
new String(Files.readAllBytes(profilesPath), new String(Files.readAllBytes(profilesPath),
StandardCharsets.UTF_8) StandardCharsets.UTF_8)
); );
JSONObject profilesArray = minecraftProfiles.getJSONObject("profiles"); JSONObject profilesArray = minecraftProfiles.getJSONObject("profiles");
oldProfile = profilesArray.optJSONObject("forge", null); oldProfile = profilesArray.optJSONObject(profileName, null);
}catch (IOException | JSONException e) { }catch (IOException | JSONException e) {
System.out.println("Failed to store Forge profile: "+e); System.out.println("Failed to store Forge profile: "+e);
} }
} }
private static String pickProfileName() { private static String pickProfileName(String profileName) {
return "forge"+random.nextInt(); return profileName+random.nextInt();
} }
public static void reinsertProfile(boolean suppressProfileCreation) { public static void reinsertProfile(String profileName, boolean suppressProfileCreation) {
try { try {
JSONObject minecraftProfiles = new JSONObject( JSONObject minecraftProfiles = new JSONObject(
new String(Files.readAllBytes(profilesPath), new String(Files.readAllBytes(profilesPath),
@ -41,8 +41,8 @@ public class ProfileFixer {
if(oldProfile != null) { if(oldProfile != null) {
if(suppressProfileCreation) profilesArray.put("forge", oldProfile); // restore the old profile if(suppressProfileCreation) profilesArray.put("forge", oldProfile); // restore the old profile
else { else {
String name = pickProfileName(); String name = pickProfileName(profileName);
while(profilesArray.has(name)) name = pickProfileName(); while(profilesArray.has(name)) name = pickProfileName(profileName);
profilesArray.put(name, oldProfile); // restore the old profile under a new name profilesArray.put(name, oldProfile); // restore the old profile under a new name
} }
}else{ }else{