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;
import android.content.Context;
import android.content.Intent;
import android.view.LayoutInflater;
import android.widget.ExpandableListAdapter;
import net.kdt.pojavlaunch.JavaGUILauncherActivity;
import net.kdt.pojavlaunch.R;
import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.modloaders.ModloaderListenerProxy;
@ -55,6 +57,8 @@ public class OptiFineInstallFragment extends ModVersionListFragment<OptiFineUtil
@Override
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 net.kdt.pojavlaunch.JMinecraftVersionList;
import net.kdt.pojavlaunch.R;
import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper;
import net.kdt.pojavlaunch.tasks.AsyncMinecraftDownloader;
import net.kdt.pojavlaunch.utils.DownloadUtils;
import java.io.File;
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 File mDestinationFile;
private final ModloaderDownloadListener mListener;
private final Object mMinecraftDownloadLock = new Object();
private Throwable mDownloaderThrowable;
public OptiFineDownloadTask(OptiFineUtils.OptiFineVersion mOptiFineVersion, File mDestinationFile, ModloaderDownloadListener mListener) {
this.mOptiFineVersion = mOptiFineVersion;
@ -23,7 +30,7 @@ public class OptiFineDownloadTask implements Runnable, Tools.DownloaderFeedback{
@Override
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 {
if(runCatching()) mListener.onDownloadFinished(mDestinationFile);
}catch (IOException e) {
@ -35,6 +42,17 @@ public class OptiFineDownloadTask implements Runnable, Tools.DownloaderFeedback{
public boolean runCatching() throws IOException {
String downloadUrl = scrapeDownloadsPage();
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);
return true;
}
@ -45,9 +63,59 @@ public class OptiFineDownloadTask implements Runnable, Tools.DownloaderFeedback{
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
public void updateProgress(int curr, int max) {
int progress100 = (int)(((float)curr / (float)max)*100f);
ProgressKeeper.submitProgress(ProgressLayout.INSTALL_MODPACK, progress100, R.string.of_dl_progress, 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) {
OptiFineUtils.OptiFineVersion optiFineVersion = new OptiFineUtils.OptiFineVersion();
optiFineVersion.minecraftVersion = mMinecraftVersion;
for(TagNode subNode : tagNode.getChildTags()) {
if(!subNode.getName().equals("td")) continue;
switch(subNode.getAttributeByName("class")) {

View File

@ -1,7 +1,11 @@
package net.kdt.pojavlaunch.modloaders;
import android.content.Intent;
import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.utils.DownloadUtils;
import java.io.File;
import java.io.IOException;
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 List<String> minecraftVersions;
public List<List<OptiFineVersion>> optifineVersions;
}
public static class OptiFineVersion {
public String minecraftVersion;
public String versionName;
public String downloadUrl;
}

View File

@ -46,7 +46,7 @@ public class AsyncMinecraftDownloader {
/* Allows each downloading thread to have its own RECYCLED buffer */
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
sExecutorService.execute(() -> {
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 */
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;
//Downloading libraries
@ -88,7 +88,7 @@ public class AsyncMinecraftDownloader {
verInfo = Tools.getVersionInfo(versionName);
// 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);
throw new DownloaderException();
}

View File

@ -392,7 +392,7 @@
<string name="create_profile_modded_versions">Modded 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_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="create_profile_optifine">Create OptiFine profile</string>
</resources>

View File

@ -20,11 +20,12 @@ import javax.swing.JOptionPane;
public class Agent implements AWTEventListener {
private boolean forgeWindowHandled = false;
private final boolean suppressProfileCreation;
private final boolean optiFineInstallation;
private final Timer componentTimer = new Timer();
public Agent(boolean ps) {
this.suppressProfileCreation = ps;
public Agent(boolean nps, boolean of) {
this.suppressProfileCreation = !nps;
this.optiFineInstallation = of;
}
@Override
@ -33,7 +34,7 @@ public class Agent implements AWTEventListener {
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
forgeWindowHandled = handleForgeWindow(window);
forgeWindowHandled = handleMainWindow(window);
if(forgeWindowHandled) {
componentTimer.cancel();
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<>();
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
}
abstractButton = optiFineInstallation ?
handleOptiFineButton(abstractButton) :
handleForgeButton(abstractButton);
if(abstractButton != null) okButton = abstractButton;
}
}
if(okButton == null) {
System.out.println("Failed to set all the UI components, wil try again in the next window");
System.exit(17);
return false;
}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
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) {
List<Component> components = new ArrayList<>();
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);
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);
ProfileFixer.reinsertProfile(optiFineInstallation ? "OptiFine" : "forge", suppressProfileCreation);
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) {
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()
.addAWTEventListener(new Agent(!"NPS".equals(args)), // No Profile Suppression
.addAWTEventListener(agent,
AWTEvent.WINDOW_EVENT_MASK);
}
}

View File

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