mirror of
				https://github.com/packwiz/packwiz-installer.git
				synced 2025-11-04 12:34:31 +01:00 
			
		
		
		
	Rename .java to .kt
This commit is contained in:
		
							
								
								
									
										251
									
								
								src/main/kotlin/link/infra/packwiz/installer/DownloadTask.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										251
									
								
								src/main/kotlin/link/infra/packwiz/installer/DownloadTask.kt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,251 @@
 | 
			
		||||
package link.infra.packwiz.installer;
 | 
			
		||||
 | 
			
		||||
import link.infra.packwiz.installer.metadata.IndexFile;
 | 
			
		||||
import link.infra.packwiz.installer.metadata.ManifestFile;
 | 
			
		||||
import link.infra.packwiz.installer.metadata.ModFile;
 | 
			
		||||
import link.infra.packwiz.installer.metadata.SpaceSafeURI;
 | 
			
		||||
import link.infra.packwiz.installer.metadata.hash.GeneralHashingSource;
 | 
			
		||||
import link.infra.packwiz.installer.metadata.hash.Hash;
 | 
			
		||||
import link.infra.packwiz.installer.metadata.hash.HashUtils;
 | 
			
		||||
import link.infra.packwiz.installer.ui.IExceptionDetails;
 | 
			
		||||
import link.infra.packwiz.installer.ui.IOptionDetails;
 | 
			
		||||
import okio.Buffer;
 | 
			
		||||
import okio.Okio;
 | 
			
		||||
import okio.Source;
 | 
			
		||||
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.nio.file.Files;
 | 
			
		||||
import java.nio.file.Path;
 | 
			
		||||
import java.nio.file.Paths;
 | 
			
		||||
import java.nio.file.StandardCopyOption;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
 | 
			
		||||
class DownloadTask implements IOptionDetails, IExceptionDetails {
 | 
			
		||||
	final IndexFile.File metadata;
 | 
			
		||||
	ManifestFile.File cachedFile = null;
 | 
			
		||||
	private Exception failure = null;
 | 
			
		||||
	private boolean alreadyUpToDate = false;
 | 
			
		||||
	private boolean metadataRequired = true;
 | 
			
		||||
	private boolean invalidated = false;
 | 
			
		||||
	// If file is new or isOptional changed to true, the option needs to be presented again
 | 
			
		||||
	private boolean newOptional = true;
 | 
			
		||||
	private final UpdateManager.Options.Side downloadSide;
 | 
			
		||||
 | 
			
		||||
	private DownloadTask(IndexFile.File metadata, String defaultFormat, UpdateManager.Options.Side downloadSide) {
 | 
			
		||||
		this.metadata = metadata;
 | 
			
		||||
		if (metadata.getHashFormat() == null || metadata.getHashFormat().length() == 0) {
 | 
			
		||||
			metadata.setHashFormat(defaultFormat);
 | 
			
		||||
		}
 | 
			
		||||
		this.downloadSide = downloadSide;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	void invalidate() {
 | 
			
		||||
		invalidated = true;
 | 
			
		||||
		alreadyUpToDate = false;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	void updateFromCache(ManifestFile.File cachedFile) {
 | 
			
		||||
		if (failure != null) return;
 | 
			
		||||
		if (cachedFile == null) {
 | 
			
		||||
			this.cachedFile = new ManifestFile.File();
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		this.cachedFile = cachedFile;
 | 
			
		||||
 | 
			
		||||
		if (!invalidated) {
 | 
			
		||||
			Hash currHash;
 | 
			
		||||
			try {
 | 
			
		||||
				currHash = HashUtils.getHash(Objects.requireNonNull(metadata.getHashFormat()), Objects.requireNonNull(metadata.getHash()));
 | 
			
		||||
			} catch (Exception e) {
 | 
			
		||||
				failure = e;
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
			if (currHash.equals(cachedFile.getHash())) {
 | 
			
		||||
				// Already up to date
 | 
			
		||||
				alreadyUpToDate = true;
 | 
			
		||||
				metadataRequired = false;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if (cachedFile.isOptional()) {
 | 
			
		||||
			// Because option selection dialog might set this task to true/false, metadata is always needed to download
 | 
			
		||||
			// the file, and to show the description and name
 | 
			
		||||
			metadataRequired = true;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	void downloadMetadata(IndexFile parentIndexFile, SpaceSafeURI indexUri) {
 | 
			
		||||
		if (failure != null) return;
 | 
			
		||||
		if (metadataRequired) {
 | 
			
		||||
			try {
 | 
			
		||||
				metadata.downloadMeta(parentIndexFile, indexUri);
 | 
			
		||||
			} catch (Exception e) {
 | 
			
		||||
				failure = e;
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
			ModFile linkedFile = metadata.getLinkedFile();
 | 
			
		||||
			if (linkedFile != null) {
 | 
			
		||||
				ModFile.Option option = linkedFile.getOption();
 | 
			
		||||
				if (option != null) {
 | 
			
		||||
					if (option.getOptional()) {
 | 
			
		||||
						if (cachedFile.isOptional()) {
 | 
			
		||||
							// isOptional didn't change
 | 
			
		||||
							newOptional = false;
 | 
			
		||||
						} else {
 | 
			
		||||
							// isOptional false -> true, set option to it's default value
 | 
			
		||||
							// TODO: preserve previous option value, somehow??
 | 
			
		||||
							cachedFile.setOptionValue(option.getDefaultValue());
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				cachedFile.setOptional(isOptional());
 | 
			
		||||
				cachedFile.setOnlyOtherSide(!correctSide());
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	void download(String packFolder, SpaceSafeURI indexUri) {
 | 
			
		||||
		if (failure != null) return;
 | 
			
		||||
 | 
			
		||||
		// Ensure it is removed
 | 
			
		||||
		if (!cachedFile.getOptionValue() || !correctSide()) {
 | 
			
		||||
			if (cachedFile.getCachedLocation() == null) return;
 | 
			
		||||
			try {
 | 
			
		||||
				Files.deleteIfExists(Paths.get(packFolder, cachedFile.getCachedLocation()));
 | 
			
		||||
			} catch (IOException e) {
 | 
			
		||||
				// TODO: how much of a problem is this? use log4j/other log library to show warning?
 | 
			
		||||
				e.printStackTrace();
 | 
			
		||||
			}
 | 
			
		||||
			cachedFile.setCachedLocation(null);
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (alreadyUpToDate) return;
 | 
			
		||||
 | 
			
		||||
		Path destPath = Paths.get(packFolder, Objects.requireNonNull(metadata.getDestURI()).toString());
 | 
			
		||||
 | 
			
		||||
		// Don't update files marked with preserve if they already exist on disk
 | 
			
		||||
		if (metadata.getPreserve()) {
 | 
			
		||||
			if (destPath.toFile().exists()) {
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		try {
 | 
			
		||||
			Hash hash;
 | 
			
		||||
			String fileHashFormat;
 | 
			
		||||
			ModFile linkedFile = metadata.getLinkedFile();
 | 
			
		||||
			if (linkedFile != null) {
 | 
			
		||||
				hash = linkedFile.getHash();
 | 
			
		||||
				fileHashFormat = Objects.requireNonNull(linkedFile.getDownload()).getHashFormat();
 | 
			
		||||
			} else {
 | 
			
		||||
				hash = metadata.getHashObj();
 | 
			
		||||
				fileHashFormat = metadata.getHashFormat();
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			Source src = metadata.getSource(indexUri);
 | 
			
		||||
			assert fileHashFormat != null;
 | 
			
		||||
			GeneralHashingSource fileSource = HashUtils.getHasher(fileHashFormat).getHashingSource(src);
 | 
			
		||||
			Buffer data = new Buffer();
 | 
			
		||||
			Okio.buffer(fileSource).readAll(data);
 | 
			
		||||
 | 
			
		||||
			if (fileSource.hashIsEqual(hash)) {
 | 
			
		||||
				Files.createDirectories(destPath.getParent());
 | 
			
		||||
				Files.copy(data.inputStream(), destPath, StandardCopyOption.REPLACE_EXISTING);
 | 
			
		||||
			} else {
 | 
			
		||||
				// TODO: no more SYSOUT!!!!!!!!!
 | 
			
		||||
				System.out.println("Invalid hash for " + metadata.getDestURI().toString());
 | 
			
		||||
				System.out.println("Calculated: " + fileSource.getHash());
 | 
			
		||||
				System.out.println("Expected:   " + hash);
 | 
			
		||||
				failure = new Exception("Hash invalid!");
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (cachedFile.getCachedLocation() != null && !destPath.equals(Paths.get(packFolder, cachedFile.getCachedLocation()))) {
 | 
			
		||||
				// Delete old file if location changes
 | 
			
		||||
				Files.delete(Paths.get(packFolder, cachedFile.getCachedLocation()));
 | 
			
		||||
			}
 | 
			
		||||
		} catch (Exception e) {
 | 
			
		||||
			failure = e;
 | 
			
		||||
		}
 | 
			
		||||
		if (failure == null) {
 | 
			
		||||
			if (cachedFile == null) {
 | 
			
		||||
				cachedFile = new ManifestFile.File();
 | 
			
		||||
			}
 | 
			
		||||
			// Update the manifest file
 | 
			
		||||
			try {
 | 
			
		||||
				cachedFile.setHash(metadata.getHashObj());
 | 
			
		||||
			} catch (Exception e) {
 | 
			
		||||
				failure = e;
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
			cachedFile.setOptional(isOptional());
 | 
			
		||||
			cachedFile.setCachedLocation(metadata.getDestURI().toString());
 | 
			
		||||
			if (metadata.getLinkedFile() != null) {
 | 
			
		||||
				try {
 | 
			
		||||
					cachedFile.setLinkedFileHash(metadata.getLinkedFile().getHash());
 | 
			
		||||
				} catch (Exception e) {
 | 
			
		||||
					failure = e;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public Exception getException() {
 | 
			
		||||
		return failure;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	boolean isOptional() {
 | 
			
		||||
		if (metadata.getLinkedFile() != null) {
 | 
			
		||||
			return metadata.getLinkedFile().isOptional();
 | 
			
		||||
		}
 | 
			
		||||
		return false;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	boolean isNewOptional() {
 | 
			
		||||
		return isOptional() && this.newOptional;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	boolean correctSide() {
 | 
			
		||||
		if (metadata.getLinkedFile() != null && metadata.getLinkedFile().getSide() != null) {
 | 
			
		||||
			return metadata.getLinkedFile().getSide().hasSide(downloadSide);
 | 
			
		||||
		}
 | 
			
		||||
		return true;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public String getName() {
 | 
			
		||||
		return metadata.getName();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public boolean getOptionValue() {
 | 
			
		||||
		return cachedFile.getOptionValue();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public String getOptionDescription() {
 | 
			
		||||
		if (metadata.getLinkedFile() != null && metadata.getLinkedFile().getOption() != null) {
 | 
			
		||||
			return metadata.getLinkedFile().getOption().getDescription();
 | 
			
		||||
		}
 | 
			
		||||
		return null;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public void setOptionValue(boolean value) {
 | 
			
		||||
		if (value && !cachedFile.getOptionValue()) {
 | 
			
		||||
			// Ensure that an update is done if it changes from false to true, or from true to false
 | 
			
		||||
			alreadyUpToDate = false;
 | 
			
		||||
		}
 | 
			
		||||
		cachedFile.setOptionValue(value);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	static List<DownloadTask> createTasksFromIndex(IndexFile index, String defaultFormat, UpdateManager.Options.Side downloadSide) {
 | 
			
		||||
		ArrayList<DownloadTask> tasks = new ArrayList<>();
 | 
			
		||||
		for (IndexFile.File file : Objects.requireNonNull(index.getFiles())) {
 | 
			
		||||
			tasks.add(new DownloadTask(file, defaultFormat, downloadSide));
 | 
			
		||||
		}
 | 
			
		||||
		return tasks;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										495
									
								
								src/main/kotlin/link/infra/packwiz/installer/UpdateManager.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										495
									
								
								src/main/kotlin/link/infra/packwiz/installer/UpdateManager.kt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,495 @@
 | 
			
		||||
package link.infra.packwiz.installer;
 | 
			
		||||
 | 
			
		||||
import com.google.gson.Gson;
 | 
			
		||||
import com.google.gson.GsonBuilder;
 | 
			
		||||
import com.google.gson.JsonIOException;
 | 
			
		||||
import com.google.gson.JsonSyntaxException;
 | 
			
		||||
import com.google.gson.annotations.SerializedName;
 | 
			
		||||
import com.moandjiezana.toml.Toml;
 | 
			
		||||
import link.infra.packwiz.installer.metadata.IndexFile;
 | 
			
		||||
import link.infra.packwiz.installer.metadata.ManifestFile;
 | 
			
		||||
import link.infra.packwiz.installer.metadata.PackFile;
 | 
			
		||||
import link.infra.packwiz.installer.metadata.SpaceSafeURI;
 | 
			
		||||
import link.infra.packwiz.installer.metadata.hash.GeneralHashingSource;
 | 
			
		||||
import link.infra.packwiz.installer.metadata.hash.Hash;
 | 
			
		||||
import link.infra.packwiz.installer.metadata.hash.HashUtils;
 | 
			
		||||
import link.infra.packwiz.installer.request.HandlerManager;
 | 
			
		||||
import link.infra.packwiz.installer.ui.IExceptionDetails;
 | 
			
		||||
import link.infra.packwiz.installer.ui.IUserInterface;
 | 
			
		||||
import link.infra.packwiz.installer.ui.InputStateHandler;
 | 
			
		||||
import link.infra.packwiz.installer.ui.InstallProgress;
 | 
			
		||||
import okio.Okio;
 | 
			
		||||
import okio.Source;
 | 
			
		||||
 | 
			
		||||
import java.io.*;
 | 
			
		||||
import java.nio.file.Files;
 | 
			
		||||
import java.nio.file.Paths;
 | 
			
		||||
import java.util.*;
 | 
			
		||||
import java.util.concurrent.*;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
 | 
			
		||||
public class UpdateManager {
 | 
			
		||||
 | 
			
		||||
	private final Options opts;
 | 
			
		||||
	public final IUserInterface ui;
 | 
			
		||||
	private boolean cancelled;
 | 
			
		||||
	private boolean cancelledStartGame = false;
 | 
			
		||||
	private InputStateHandler stateHandler;
 | 
			
		||||
	private boolean errorsOccurred = false;
 | 
			
		||||
 | 
			
		||||
	public static class Options {
 | 
			
		||||
		SpaceSafeURI downloadURI = null;
 | 
			
		||||
		String manifestFile = "packwiz.json"; // TODO: make configurable
 | 
			
		||||
		String packFolder = ".";
 | 
			
		||||
		Side side = Side.CLIENT;
 | 
			
		||||
 | 
			
		||||
		public enum Side {
 | 
			
		||||
			@SerializedName("client")
 | 
			
		||||
			CLIENT("client"),
 | 
			
		||||
			@SerializedName("server")
 | 
			
		||||
			SERVER("server"),
 | 
			
		||||
			@SerializedName("both")
 | 
			
		||||
			BOTH("both", new Side[] { CLIENT, SERVER });
 | 
			
		||||
 | 
			
		||||
			private final String sideName;
 | 
			
		||||
			private final Side[] depSides;
 | 
			
		||||
 | 
			
		||||
			Side(String sideName) {
 | 
			
		||||
				this.sideName = sideName.toLowerCase();
 | 
			
		||||
				this.depSides = null;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			Side(String sideName, Side[] depSides) {
 | 
			
		||||
				this.sideName = sideName.toLowerCase();
 | 
			
		||||
				this.depSides = depSides;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			@Override
 | 
			
		||||
			public String toString() {
 | 
			
		||||
				return this.sideName;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			public boolean hasSide(Side tSide) {
 | 
			
		||||
				if (this.equals(tSide)) {
 | 
			
		||||
					return true;
 | 
			
		||||
				}
 | 
			
		||||
				if (this.depSides != null) {
 | 
			
		||||
					for (Side depSide : this.depSides) {
 | 
			
		||||
						if (depSide.equals(tSide)) {
 | 
			
		||||
							return true;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				return false;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			public static Side from(String name) {
 | 
			
		||||
				String lowerName = name.toLowerCase();
 | 
			
		||||
				for (Side side : Side.values()) {
 | 
			
		||||
					if (side.sideName.equals(lowerName)) {
 | 
			
		||||
						return side;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				return null;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	UpdateManager(Options opts, IUserInterface ui, InputStateHandler inputStateHandler) {
 | 
			
		||||
		this.opts = opts;
 | 
			
		||||
		this.ui = ui;
 | 
			
		||||
		this.stateHandler = inputStateHandler;
 | 
			
		||||
		this.start();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void start() {
 | 
			
		||||
		this.checkOptions();
 | 
			
		||||
 | 
			
		||||
		ui.submitProgress(new InstallProgress("Loading manifest file..."));
 | 
			
		||||
		Gson gson = new GsonBuilder().registerTypeAdapter(Hash.class, new Hash.TypeHandler()).create();
 | 
			
		||||
		ManifestFile manifest;
 | 
			
		||||
		try {
 | 
			
		||||
			manifest = gson.fromJson(new FileReader(Paths.get(opts.packFolder, opts.manifestFile).toString()),
 | 
			
		||||
					ManifestFile.class);
 | 
			
		||||
		} catch (FileNotFoundException e) {
 | 
			
		||||
			manifest = new ManifestFile();
 | 
			
		||||
		} catch (JsonSyntaxException | JsonIOException e) {
 | 
			
		||||
			ui.handleExceptionAndExit(e);
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (stateHandler.getCancelButton()) {
 | 
			
		||||
			showCancellationDialog();
 | 
			
		||||
			handleCancellation();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		ui.submitProgress(new InstallProgress("Loading pack file..."));
 | 
			
		||||
		GeneralHashingSource packFileSource;
 | 
			
		||||
		try {
 | 
			
		||||
			Source src = HandlerManager.getFileSource(opts.downloadURI);
 | 
			
		||||
			packFileSource = HashUtils.getHasher("sha256").getHashingSource(src);
 | 
			
		||||
		} catch (Exception e) {
 | 
			
		||||
			// TODO: still launch the game if updating doesn't work?
 | 
			
		||||
			// TODO: ask user if they want to launch the game, exit(1) if they don't
 | 
			
		||||
			ui.handleExceptionAndExit(e);
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
		PackFile pf;
 | 
			
		||||
		try {
 | 
			
		||||
			pf = new Toml().read(Okio.buffer(packFileSource).inputStream()).to(PackFile.class);
 | 
			
		||||
		} catch (IllegalStateException e) {
 | 
			
		||||
			ui.handleExceptionAndExit(e);
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (stateHandler.getCancelButton()) {
 | 
			
		||||
			showCancellationDialog();
 | 
			
		||||
			handleCancellation();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		ui.submitProgress(new InstallProgress("Checking local files..."));
 | 
			
		||||
 | 
			
		||||
		// Invalidation checking must be done here, as it must happen before pack/index hashes are checked
 | 
			
		||||
		List<SpaceSafeURI> invalidatedUris = new ArrayList<>();
 | 
			
		||||
		if (manifest.getCachedFiles() != null) {
 | 
			
		||||
			for (Map.Entry<SpaceSafeURI, ManifestFile.File> entry : manifest.getCachedFiles().entrySet()) {
 | 
			
		||||
				// ignore onlyOtherSide files
 | 
			
		||||
				if (entry.getValue().getOnlyOtherSide()) {
 | 
			
		||||
					continue;
 | 
			
		||||
				}
 | 
			
		||||
				boolean invalid = false;
 | 
			
		||||
				// if isn't optional, or is optional but optionValue == true
 | 
			
		||||
				if (!entry.getValue().isOptional() || entry.getValue().getOptionValue()) {
 | 
			
		||||
					if (entry.getValue().getCachedLocation() != null) {
 | 
			
		||||
						if (!Paths.get(opts.packFolder, entry.getValue().getCachedLocation()).toFile().exists()) {
 | 
			
		||||
							invalid = true;
 | 
			
		||||
						}
 | 
			
		||||
					} else {
 | 
			
		||||
						// if cachedLocation == null, should probably be installed!!
 | 
			
		||||
						invalid = true;
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				if (invalid) {
 | 
			
		||||
					SpaceSafeURI fileUri = entry.getKey();
 | 
			
		||||
					System.out.println("File " + fileUri.toString() + " invalidated, marked for redownloading");
 | 
			
		||||
					invalidatedUris.add(fileUri);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (manifest.getPackFileHash() != null && packFileSource.hashIsEqual(manifest.getPackFileHash()) && invalidatedUris.isEmpty()) {
 | 
			
		||||
			System.out.println("Modpack is already up to date!");
 | 
			
		||||
			// todo: --force?
 | 
			
		||||
			if (!stateHandler.getOptionsButton()) {
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		System.out.println("Modpack name: " + pf.getName());
 | 
			
		||||
 | 
			
		||||
		if (stateHandler.getCancelButton()) {
 | 
			
		||||
			showCancellationDialog();
 | 
			
		||||
			handleCancellation();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		try {
 | 
			
		||||
			// This is badly written, I'll probably heavily refactor it at some point
 | 
			
		||||
			processIndex(HandlerManager.getNewLoc(opts.downloadURI, Objects.requireNonNull(pf.getIndex()).getFile()),
 | 
			
		||||
					HashUtils.getHash(Objects.requireNonNull(pf.getIndex().getHashFormat()), Objects.requireNonNull(pf.getIndex().getHash())), pf.getIndex().getHashFormat(), manifest, invalidatedUris);
 | 
			
		||||
		} catch (Exception e1) {
 | 
			
		||||
			ui.handleExceptionAndExit(e1);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		handleCancellation();
 | 
			
		||||
 | 
			
		||||
		// TODO: update MMC params, java args etc
 | 
			
		||||
 | 
			
		||||
		// If there were errors, don't write the manifest/index hashes, to ensure they are rechecked later
 | 
			
		||||
		if (errorsOccurred) {
 | 
			
		||||
			manifest.setIndexFileHash(null);
 | 
			
		||||
			manifest.setPackFileHash(null);
 | 
			
		||||
		} else {
 | 
			
		||||
			manifest.setPackFileHash(packFileSource.getHash());
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		manifest.setCachedSide(opts.side);
 | 
			
		||||
		try (Writer writer = new FileWriter(Paths.get(opts.packFolder, opts.manifestFile).toString())) {
 | 
			
		||||
			gson.toJson(manifest, writer);
 | 
			
		||||
		} catch (IOException e) {
 | 
			
		||||
			// TODO: add message?
 | 
			
		||||
			ui.handleException(e);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void checkOptions() {
 | 
			
		||||
		// TODO: implement
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void processIndex(SpaceSafeURI indexUri, Hash indexHash, String hashFormat, ManifestFile manifest, List<SpaceSafeURI> invalidatedUris) {
 | 
			
		||||
		if (manifest.getIndexFileHash() != null && manifest.getIndexFileHash().equals(indexHash) && invalidatedUris.isEmpty()) {
 | 
			
		||||
			System.out.println("Modpack files are already up to date!");
 | 
			
		||||
			if (!stateHandler.getOptionsButton()) {
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		manifest.setIndexFileHash(indexHash);
 | 
			
		||||
 | 
			
		||||
		GeneralHashingSource indexFileSource;
 | 
			
		||||
		try {
 | 
			
		||||
			Source src = HandlerManager.getFileSource(indexUri);
 | 
			
		||||
			indexFileSource = HashUtils.getHasher(hashFormat).getHashingSource(src);
 | 
			
		||||
		} catch (Exception e) {
 | 
			
		||||
			// TODO: still launch the game if updating doesn't work?
 | 
			
		||||
			// TODO: ask user if they want to launch the game, exit(1) if they don't
 | 
			
		||||
			ui.handleExceptionAndExit(e);
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
		IndexFile indexFile;
 | 
			
		||||
		try {
 | 
			
		||||
			indexFile = new Toml().read(Okio.buffer(indexFileSource).inputStream()).to(IndexFile.class);
 | 
			
		||||
		} catch (IllegalStateException e) {
 | 
			
		||||
			ui.handleExceptionAndExit(e);
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (!indexFileSource.hashIsEqual(indexHash)) {
 | 
			
		||||
			// TODO: throw exception
 | 
			
		||||
			System.out.println("I was meant to put an error message here but I'll do that later");
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (stateHandler.getCancelButton()) {
 | 
			
		||||
			showCancellationDialog();
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		ui.submitProgress(new InstallProgress("Checking local files..."));
 | 
			
		||||
		Iterator<Map.Entry<SpaceSafeURI, ManifestFile.File>> it = manifest.getCachedFiles().entrySet().iterator();
 | 
			
		||||
		while (it.hasNext()) {
 | 
			
		||||
			Map.Entry<SpaceSafeURI, ManifestFile.File> entry = it.next();
 | 
			
		||||
			if (entry.getValue().getCachedLocation() != null) {
 | 
			
		||||
				boolean alreadyDeleted = false;
 | 
			
		||||
				// Delete if option value has been set to false
 | 
			
		||||
				if (entry.getValue().isOptional() && !entry.getValue().getOptionValue()) {
 | 
			
		||||
					try {
 | 
			
		||||
						Files.deleteIfExists(Paths.get(opts.packFolder, entry.getValue().getCachedLocation()));
 | 
			
		||||
					} catch (IOException e) {
 | 
			
		||||
						// TODO: should this be shown to the user in some way?
 | 
			
		||||
						e.printStackTrace();
 | 
			
		||||
					}
 | 
			
		||||
					// Set to null, as it doesn't exist anymore
 | 
			
		||||
					entry.getValue().setCachedLocation(null);
 | 
			
		||||
					alreadyDeleted = true;
 | 
			
		||||
				}
 | 
			
		||||
				if (indexFile.getFiles().stream().noneMatch(f -> Objects.equals(f.getFile(), entry.getKey()))) {
 | 
			
		||||
					// File has been removed from the index
 | 
			
		||||
					if (!alreadyDeleted) {
 | 
			
		||||
						try {
 | 
			
		||||
							Files.deleteIfExists(Paths.get(opts.packFolder, entry.getValue().getCachedLocation()));
 | 
			
		||||
						} catch (IOException e) {
 | 
			
		||||
							// TODO: should this be shown to the user in some way?
 | 
			
		||||
							e.printStackTrace();
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
					it.remove();
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (stateHandler.getCancelButton()) {
 | 
			
		||||
			showCancellationDialog();
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
		ui.submitProgress(new InstallProgress("Comparing new files..."));
 | 
			
		||||
 | 
			
		||||
		// TODO: progress bar?
 | 
			
		||||
		if (indexFile.getFiles().isEmpty()) {
 | 
			
		||||
			System.out.println("Warning: Index is empty!");
 | 
			
		||||
		}
 | 
			
		||||
		List<DownloadTask> tasks = DownloadTask.createTasksFromIndex(indexFile, indexFile.getHashFormat(), opts.side);
 | 
			
		||||
		// If the side changes, invalidate EVERYTHING just in case
 | 
			
		||||
		// Might not be needed, but done just to be safe
 | 
			
		||||
		boolean invalidateAll = !opts.side.equals(manifest.getCachedSide());
 | 
			
		||||
		if (invalidateAll) {
 | 
			
		||||
			System.out.println("Side changed, invalidating all mods");
 | 
			
		||||
		}
 | 
			
		||||
		tasks.forEach(f -> {
 | 
			
		||||
			// TODO: should linkedfile be checked as well? should this be done in the download section?
 | 
			
		||||
			if (invalidateAll) {
 | 
			
		||||
				f.invalidate();
 | 
			
		||||
			} else if (invalidatedUris.contains(f.metadata.getFile())) {
 | 
			
		||||
				f.invalidate();
 | 
			
		||||
			}
 | 
			
		||||
			ManifestFile.File file = manifest.getCachedFiles().get(f.metadata.getFile());
 | 
			
		||||
			if (file != null) {
 | 
			
		||||
				// Ensure the file can be reverted later if necessary - the DownloadTask modifies the file so if it fails we need the old version back
 | 
			
		||||
				file.backup();
 | 
			
		||||
			}
 | 
			
		||||
			// If it is null, the DownloadTask will make a new empty cachedFile
 | 
			
		||||
			f.updateFromCache(file);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (stateHandler.getCancelButton()) {
 | 
			
		||||
			showCancellationDialog();
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Let's hope downloadMetadata is a pure function!!!
 | 
			
		||||
		tasks.parallelStream().forEach(f -> f.downloadMetadata(indexFile, indexUri));
 | 
			
		||||
 | 
			
		||||
		List<IExceptionDetails> failedTasks = tasks.stream().filter(t -> t.getException() != null).collect(Collectors.toList());
 | 
			
		||||
		if (!failedTasks.isEmpty()) {
 | 
			
		||||
			errorsOccurred = true;
 | 
			
		||||
			IExceptionDetails.ExceptionListResult exceptionListResult;
 | 
			
		||||
			try {
 | 
			
		||||
				exceptionListResult = ui.showExceptions(failedTasks, tasks.size(), true).get();
 | 
			
		||||
			} catch (InterruptedException | ExecutionException e) {
 | 
			
		||||
				// Interrupted means cancelled???
 | 
			
		||||
				ui.handleExceptionAndExit(e);
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
			switch (exceptionListResult) {
 | 
			
		||||
				case CONTINUE:
 | 
			
		||||
					break;
 | 
			
		||||
				case CANCEL:
 | 
			
		||||
					cancelled = true;
 | 
			
		||||
					return;
 | 
			
		||||
				case IGNORE:
 | 
			
		||||
					cancelledStartGame = true;
 | 
			
		||||
					return;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (stateHandler.getCancelButton()) {
 | 
			
		||||
			showCancellationDialog();
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		List<DownloadTask> nonFailedFirstTasks = tasks.stream().filter(t -> t.getException() == null).collect(Collectors.toList());
 | 
			
		||||
		List<DownloadTask> optionTasks = nonFailedFirstTasks.stream().filter(DownloadTask::correctSide).filter(DownloadTask::isOptional).collect(Collectors.toList());
 | 
			
		||||
		// If options changed, present all options again
 | 
			
		||||
		if (stateHandler.getOptionsButton() || optionTasks.stream().anyMatch(DownloadTask::isNewOptional)) {
 | 
			
		||||
			// new ArrayList is requires so it's an IOptionDetails rather than a DownloadTask list
 | 
			
		||||
			Future<Boolean> cancelledResult = ui.showOptions(new ArrayList<>(optionTasks));
 | 
			
		||||
			try {
 | 
			
		||||
				if (cancelledResult.get()) {
 | 
			
		||||
					cancelled = true;
 | 
			
		||||
					// TODO: Should the UI be closed somehow??
 | 
			
		||||
					return;
 | 
			
		||||
				}
 | 
			
		||||
			} catch (InterruptedException | ExecutionException e) {
 | 
			
		||||
				// Interrupted means cancelled???
 | 
			
		||||
				ui.handleExceptionAndExit(e);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		ui.disableOptionsButton();
 | 
			
		||||
 | 
			
		||||
		// TODO: different thread pool type?
 | 
			
		||||
		ExecutorService threadPool = Executors.newFixedThreadPool(10);
 | 
			
		||||
		CompletionService<DownloadTask> completionService = new ExecutorCompletionService<>(threadPool);
 | 
			
		||||
 | 
			
		||||
		tasks.forEach(t -> completionService.submit(() -> {
 | 
			
		||||
			t.download(opts.packFolder, indexUri);
 | 
			
		||||
			return t;
 | 
			
		||||
		}));
 | 
			
		||||
 | 
			
		||||
		for (int i = 0; i < tasks.size(); i++) {
 | 
			
		||||
			DownloadTask task;
 | 
			
		||||
			try {
 | 
			
		||||
				task = completionService.take().get();
 | 
			
		||||
			} catch (InterruptedException | ExecutionException e) {
 | 
			
		||||
				ui.handleException(e);
 | 
			
		||||
				task = null;
 | 
			
		||||
			}
 | 
			
		||||
			// Update manifest - If there were no errors cachedFile has already been modified in place (good old pass by reference)
 | 
			
		||||
			if (task != null) {
 | 
			
		||||
				if (task.getException() != null) {
 | 
			
		||||
					ManifestFile.File file = task.cachedFile.getRevert();
 | 
			
		||||
					if (file != null) {
 | 
			
		||||
						manifest.getCachedFiles().putIfAbsent(task.metadata.getFile(), file);
 | 
			
		||||
					}
 | 
			
		||||
				} else {
 | 
			
		||||
					// idiot, if it wasn't there in the first place it won't magically appear there
 | 
			
		||||
					manifest.getCachedFiles().putIfAbsent(task.metadata.getFile(), task.cachedFile);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			String progress;
 | 
			
		||||
			if (task != null) {
 | 
			
		||||
				if (task.getException() != null) {
 | 
			
		||||
					progress = "Failed to download " + task.metadata.getName() + ": " + task.getException().getMessage();
 | 
			
		||||
					task.getException().printStackTrace();
 | 
			
		||||
				} else {
 | 
			
		||||
					// TODO: should this be revised for tasks that didn't actually download it?
 | 
			
		||||
					progress = "Downloaded " + task.metadata.getName();
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				progress = "Failed to download, unknown reason";
 | 
			
		||||
			}
 | 
			
		||||
			ui.submitProgress(new InstallProgress(progress, i + 1, tasks.size()));
 | 
			
		||||
 | 
			
		||||
			if (stateHandler.getCancelButton()) {
 | 
			
		||||
				// Stop all tasks, don't launch the game (it's in an invalid state!)
 | 
			
		||||
				threadPool.shutdown();
 | 
			
		||||
				cancelled = true;
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Shut down the thread pool when the update is done
 | 
			
		||||
		threadPool.shutdown();
 | 
			
		||||
 | 
			
		||||
		List<IExceptionDetails> failedTasks2ElectricBoogaloo = nonFailedFirstTasks.stream().filter(t -> t.getException() != null).collect(Collectors.toList());
 | 
			
		||||
		if (!failedTasks2ElectricBoogaloo.isEmpty()) {
 | 
			
		||||
			errorsOccurred = true;
 | 
			
		||||
			IExceptionDetails.ExceptionListResult exceptionListResult;
 | 
			
		||||
			try {
 | 
			
		||||
				exceptionListResult = ui.showExceptions(failedTasks2ElectricBoogaloo, tasks.size(), false).get();
 | 
			
		||||
			} catch (InterruptedException | ExecutionException e) {
 | 
			
		||||
				// Interrupted means cancelled???
 | 
			
		||||
				ui.handleExceptionAndExit(e);
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
			switch (exceptionListResult) {
 | 
			
		||||
				case CONTINUE:
 | 
			
		||||
					break;
 | 
			
		||||
				case CANCEL:
 | 
			
		||||
					cancelled = true;
 | 
			
		||||
					return;
 | 
			
		||||
				case IGNORE:
 | 
			
		||||
					cancelledStartGame = true;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void showCancellationDialog() {
 | 
			
		||||
		IExceptionDetails.ExceptionListResult exceptionListResult;
 | 
			
		||||
		try {
 | 
			
		||||
			exceptionListResult = ui.showCancellationDialog().get();
 | 
			
		||||
		} catch (InterruptedException | ExecutionException e) {
 | 
			
		||||
			// Interrupted means cancelled???
 | 
			
		||||
			ui.handleExceptionAndExit(e);
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
		switch (exceptionListResult) {
 | 
			
		||||
			case CONTINUE:
 | 
			
		||||
				throw new RuntimeException("Continuation not allowed here!");
 | 
			
		||||
			case CANCEL:
 | 
			
		||||
				cancelled = true;
 | 
			
		||||
				return;
 | 
			
		||||
			case IGNORE:
 | 
			
		||||
				cancelledStartGame = true;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void handleCancellation() {
 | 
			
		||||
		if (cancelled) {
 | 
			
		||||
			System.out.println("Update cancelled by user!");
 | 
			
		||||
			System.exit(1);
 | 
			
		||||
		} else if (cancelledStartGame) {
 | 
			
		||||
			System.out.println("Update cancelled by user! Continuing to start game...");
 | 
			
		||||
			System.exit(0);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user