Complete Kotlin port

This commit is contained in:
comp500 2019-12-21 02:04:10 +00:00
parent 9d3587c72e
commit a15489f5e4
9 changed files with 537 additions and 557 deletions

View File

@ -1,251 +1,230 @@
package link.infra.packwiz.installer;
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 link.infra.packwiz.installer.metadata.IndexFile
import link.infra.packwiz.installer.metadata.ManifestFile
import link.infra.packwiz.installer.metadata.SpaceSafeURI
import link.infra.packwiz.installer.metadata.hash.Hash
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHash
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHasher
import link.infra.packwiz.installer.ui.ExceptionDetails
import link.infra.packwiz.installer.ui.IOptionDetails
import okio.Buffer
import okio.buffer
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
import java.util.*
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;
internal class DownloadTask private constructor(val metadata: IndexFile.File, defaultFormat: String, private val downloadSide: UpdateManager.Options.Side) : IOptionDetails {
var cachedFile: ManifestFile.File? = null
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;
private var err: Exception? = null
val exceptionDetails get() = err?.let { e -> ExceptionDetails(name, e) }
fun failed() = err != null
private var alreadyUpToDate = false
private var metadataRequired = true
private var 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 var newOptional = true
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);
val isOptional get() = metadata.linkedFile?.isOptional ?: false
fun isNewOptional() = isOptional && newOptional
fun correctSide() = metadata.linkedFile?.side?.hasSide(downloadSide) ?: true
override val name get() = metadata.name
// Ensure that an update is done if it changes from false to true, or from true to false
override var optionValue: Boolean
get() = cachedFile?.optionValue ?: true
set(value) {
if (value && !optionValue) { // Ensure that an update is done if it changes from false to true, or from true to false
alreadyUpToDate = false
}
this.downloadSide = downloadSide;
cachedFile?.optionValue = value
}
void invalidate() {
invalidated = true;
alreadyUpToDate = false;
override val optionDescription get() = metadata.linkedFile?.option?.description ?: ""
init {
if (metadata.hashFormat?.isEmpty() != false) {
metadata.hashFormat = defaultFormat
}
}
void updateFromCache(ManifestFile.File cachedFile) {
if (failure != null) return;
fun invalidate() {
invalidated = true
alreadyUpToDate = false
}
fun updateFromCache(cachedFile: ManifestFile.File?) {
if (err != null) return
if (cachedFile == null) {
this.cachedFile = new ManifestFile.File();
return;
this.cachedFile = ManifestFile.File()
return
}
this.cachedFile = cachedFile;
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;
val currHash = try {
getHash(metadata.hashFormat!!, metadata.hash!!)
} catch (e: Exception) {
err = e
return
}
if (currHash.equals(cachedFile.getHash())) {
// Already up to date
alreadyUpToDate = true;
metadataRequired = false;
if (currHash == cachedFile.hash) { // Already up to date
alreadyUpToDate = true
metadataRequired = false
}
}
if (cachedFile.isOptional()) {
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;
metadataRequired = true
}
}
void downloadMetadata(IndexFile parentIndexFile, SpaceSafeURI indexUri) {
if (failure != null) return;
fun downloadMetadata(parentIndexFile: IndexFile, indexUri: SpaceSafeURI) {
if (err != null) return
if (metadataRequired) {
try {
metadata.downloadMeta(parentIndexFile, indexUri);
} catch (Exception e) {
failure = e;
return;
// Retrieve the linked metadata file
metadata.downloadMeta(parentIndexFile, indexUri)
} catch (e: Exception) {
err = e
return
}
ModFile linkedFile = metadata.getLinkedFile();
cachedFile?.let { cachedFile ->
val linkedFile = metadata.linkedFile
if (linkedFile != null) {
ModFile.Option option = linkedFile.getOption();
if (option != null) {
if (option.getOptional()) {
if (cachedFile.isOptional()) {
linkedFile.option?.let { opt ->
if (opt.optional) {
if (cachedFile.isOptional) {
// isOptional didn't change
newOptional = false;
newOptional = false
} else {
// isOptional false -> true, set option to it's default value
// TODO: preserve previous option value, somehow??
cachedFile.setOptionValue(option.getDefaultValue());
cachedFile.optionValue = opt.defaultValue
}
}
}
cachedFile.setOptional(isOptional());
cachedFile.setOnlyOtherSide(!correctSide());
cachedFile.isOptional = isOptional
cachedFile.onlyOtherSide = !correctSide()
}
}
}
}
void download(String packFolder, SpaceSafeURI indexUri) {
if (failure != null) return;
fun download(packFolder: String, indexUri: SpaceSafeURI) {
if (err != null) return
// Ensure it is removed
if (!cachedFile.getOptionValue() || !correctSide()) {
if (cachedFile.getCachedLocation() == null) return;
cachedFile?.let {
if (!it.optionValue || !correctSide()) {
if (it.cachedLocation == null) return
try {
Files.deleteIfExists(Paths.get(packFolder, cachedFile.getCachedLocation()));
} catch (IOException e) {
Files.deleteIfExists(Paths.get(packFolder, it.cachedLocation))
} catch (e: IOException) {
// TODO: how much of a problem is this? use log4j/other log library to show warning?
e.printStackTrace();
e.printStackTrace()
}
cachedFile.setCachedLocation(null);
return;
it.cachedLocation = null
}
}
if (alreadyUpToDate) return
if (alreadyUpToDate) return;
Path destPath = Paths.get(packFolder, Objects.requireNonNull(metadata.getDestURI()).toString());
// TODO: should I be validating JSON properly, or this fine!!!!!!!??
assert(metadata.destURI != null)
val destPath = Paths.get(packFolder, metadata.destURI.toString())
// Don't update files marked with preserve if they already exist on disk
if (metadata.getPreserve()) {
if (metadata.preserve) {
if (destPath.toFile().exists()) {
return;
return
}
}
try {
Hash hash;
String fileHashFormat;
ModFile linkedFile = metadata.getLinkedFile();
val hash: Hash
val fileHashFormat: String
val linkedFile = metadata.linkedFile
if (linkedFile != null) {
hash = linkedFile.getHash();
fileHashFormat = Objects.requireNonNull(linkedFile.getDownload()).getHashFormat();
hash = linkedFile.hash
fileHashFormat = Objects.requireNonNull(Objects.requireNonNull(linkedFile.download)!!.hashFormat)!!
} else {
hash = metadata.getHashObj();
fileHashFormat = metadata.getHashFormat();
hash = metadata.getHashObj()
fileHashFormat = Objects.requireNonNull(metadata.hashFormat)!!
}
Source src = metadata.getSource(indexUri);
assert fileHashFormat != null;
GeneralHashingSource fileSource = HashUtils.getHasher(fileHashFormat).getHashingSource(src);
Buffer data = new Buffer();
Okio.buffer(fileSource).readAll(data);
val src = metadata.getSource(indexUri)
val fileSource = getHasher(fileHashFormat).getHashingSource(src)
val data = Buffer()
// Read all the data into a buffer (very inefficient for large files! but allows rollback if hash check fails)
// TODO: should we instead rename the existing file, then stream straight to the file and rollback from the renamed file?
fileSource.buffer().use {
it.readAll(data)
}
if (fileSource.hashIsEqual(hash)) {
Files.createDirectories(destPath.getParent());
Files.copy(data.inputStream(), destPath, StandardCopyOption.REPLACE_EXISTING);
Files.createDirectories(destPath.parent)
Files.copy(data.inputStream(), destPath, StandardCopyOption.REPLACE_EXISTING)
data.clear()
} 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!");
// TODO: no more PRINTLN!!!!!!!!!
println("Invalid hash for " + metadata.destURI.toString())
println("Calculated: " + fileSource.hash)
println("Expected: $hash")
err = Exception("Hash invalid!")
}
if (cachedFile.getCachedLocation() != null && !destPath.equals(Paths.get(packFolder, cachedFile.getCachedLocation()))) {
cachedFile?.cachedLocation?.let {
if (destPath != Paths.get(packFolder, it)) {
// Delete old file if location changes
Files.delete(Paths.get(packFolder, cachedFile.getCachedLocation()));
Files.delete(Paths.get(packFolder, cachedFile!!.cachedLocation))
}
} catch (Exception e) {
failure = e;
}
if (failure == null) {
if (cachedFile == null) {
cachedFile = new ManifestFile.File();
} catch (e: Exception) {
err = e
}
if (err == null) {
// Update the manifest file
cachedFile = (cachedFile ?: ManifestFile.File()).also {
try {
cachedFile.setHash(metadata.getHashObj());
} catch (Exception e) {
failure = e;
return;
it.hash = metadata.getHashObj()
} catch (e: Exception) {
err = e
return
}
cachedFile.setOptional(isOptional());
cachedFile.setCachedLocation(metadata.getDestURI().toString());
if (metadata.getLinkedFile() != null) {
it.isOptional = isOptional
it.cachedLocation = metadata.destURI.toString()
metadata.linkedFile?.let { linked ->
try {
cachedFile.setLinkedFileHash(metadata.getLinkedFile().getHash());
} catch (Exception e) {
failure = e;
it.linkedFileHash = linked.hash
} catch (e: Exception) {
err = e
}
}
}
}
}
public Exception getException() {
return failure;
companion object {
@JvmStatic
fun createTasksFromIndex(index: IndexFile, defaultFormat: String, downloadSide: UpdateManager.Options.Side): List<DownloadTask> {
val tasks = ArrayList<DownloadTask>()
for (file in Objects.requireNonNull(index.files)) {
tasks.add(DownloadTask(file, defaultFormat, downloadSide))
}
boolean isOptional() {
if (metadata.getLinkedFile() != null) {
return metadata.getLinkedFile().isOptional();
return tasks
}
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;
}
}

View File

@ -1,495 +1,493 @@
package link.infra.packwiz.installer;
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 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.DownloadTask.Companion.createTasksFromIndex
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.Hash
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHash
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHasher
import link.infra.packwiz.installer.request.HandlerManager.getFileSource
import link.infra.packwiz.installer.request.HandlerManager.getNewLoc
import link.infra.packwiz.installer.ui.IUserInterface
import link.infra.packwiz.installer.ui.IUserInterface.CancellationResult
import link.infra.packwiz.installer.ui.IUserInterface.ExceptionListResult
import link.infra.packwiz.installer.ui.InputStateHandler
import link.infra.packwiz.installer.ui.InstallProgress
import okio.buffer
import java.io.FileNotFoundException
import java.io.FileReader
import java.io.FileWriter
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Paths
import java.util.*
import java.util.concurrent.CompletionService
import java.util.concurrent.ExecutionException
import java.util.concurrent.ExecutorCompletionService
import java.util.concurrent.Executors
import kotlin.system.exitProcess
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;
class UpdateManager internal constructor(private val opts: Options, val ui: IUserInterface, private val stateHandler: InputStateHandler) {
private var cancelled = false
private var cancelledStartGame = false
private var errorsOccurred = false
public class UpdateManager {
init {
start()
}
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 {
data class Options(
var downloadURI: SpaceSafeURI? = null,
var manifestFile: String = "packwiz.json", // TODO: make configurable
var packFolder: String = ".",
var side: Side = Side.CLIENT
) {
enum class Side {
@SerializedName("client")
CLIENT("client"),
@SerializedName("server")
SERVER("server"),
@SerializedName("both")
BOTH("both", new Side[] { CLIENT, SERVER });
@Suppress("unused")
BOTH("both", arrayOf(CLIENT, SERVER));
private final String sideName;
private final Side[] depSides;
private val sideName: String
private val depSides: Array<Side>?
Side(String sideName) {
this.sideName = sideName.toLowerCase();
this.depSides = null;
constructor(sideName: String) {
this.sideName = sideName.toLowerCase()
depSides = null
}
Side(String sideName, Side[] depSides) {
this.sideName = sideName.toLowerCase();
this.depSides = depSides;
constructor(sideName: String, depSides: Array<Side>) {
this.sideName = sideName.toLowerCase()
this.depSides = depSides
}
@Override
public String toString() {
return this.sideName;
override fun toString() = sideName
fun hasSide(tSide: Side): Boolean {
if (this == tSide) {
return true
}
if (depSides != null) {
for (depSide in depSides) {
if (depSide == tSide) {
return true
}
}
}
return false
}
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;
companion object {
fun from(name: String): Side? {
val lowerName = name.toLowerCase()
for (side in values()) {
if (side.sideName == lowerName) {
return side
}
}
return null
}
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 fun start() {
checkOptions()
ui.submitProgress(InstallProgress("Loading manifest file..."))
val gson = GsonBuilder().registerTypeAdapter(Hash::class.java, Hash.TypeHandler()).create()
val manifest = try {
gson.fromJson(FileReader(Paths.get(opts.packFolder, opts.manifestFile).toString()),
ManifestFile::class.java)
} catch (e: FileNotFoundException) {
ManifestFile()
} catch (e: JsonSyntaxException) {
ui.handleExceptionAndExit(e)
return
} catch (e: JsonIOException) {
ui.handleExceptionAndExit(e)
return
}
private void start() {
this.checkOptions();
if (stateHandler.cancelButton) {
showCancellationDialog()
handleCancellation()
}
ui.submitProgress(new InstallProgress("Loading manifest file..."));
Gson gson = new GsonBuilder().registerTypeAdapter(Hash.class, new Hash.TypeHandler()).create();
ManifestFile manifest;
ui.submitProgress(InstallProgress("Loading pack file..."))
val packFileSource = try {
Objects.requireNonNull(opts.downloadURI)
val src = getFileSource(opts.downloadURI!!)
getHasher("sha256").getHashingSource(src)
} catch (e: Exception) {
// TODO: run cancellation window?
ui.handleExceptionAndExit(e)
return
}
val pf = packFileSource.buffer().use {
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;
Toml().read(it.inputStream()).to(PackFile::class.java)
} catch (e: IllegalStateException) {
ui.handleExceptionAndExit(e)
return
}
}
if (stateHandler.getCancelButton()) {
showCancellationDialog();
handleCancellation();
if (stateHandler.cancelButton) {
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..."));
ui.submitProgress(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()) {
val invalidatedUris: MutableList<SpaceSafeURI> = ArrayList()
for ((fileUri, file) in manifest.cachedFiles) {
// ignore onlyOtherSide files
if (entry.getValue().getOnlyOtherSide()) {
continue;
if (file.onlyOtherSide) {
continue
}
boolean invalid = false;
var 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;
if (!file.isOptional || file.optionValue) {
if (file.cachedLocation != null) {
if (!Paths.get(opts.packFolder, file.cachedLocation).toFile().exists()) {
invalid = true
}
} else {
// if cachedLocation == null, should probably be installed!!
invalid = true;
invalid = true
}
}
if (invalid) {
SpaceSafeURI fileUri = entry.getKey();
System.out.println("File " + fileUri.toString() + " invalidated, marked for redownloading");
invalidatedUris.add(fileUri);
}
println("File $fileUri 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!");
if (manifest.packFileHash?.let { packFileSource.hashIsEqual(it) } == true && invalidatedUris.isEmpty()) {
println("Modpack is already up to date!")
// todo: --force?
if (!stateHandler.getOptionsButton()) {
return;
if (!stateHandler.optionsButton) {
return
}
}
System.out.println("Modpack name: " + pf.getName());
println("Modpack name: " + pf.name)
if (stateHandler.getCancelButton()) {
showCancellationDialog();
handleCancellation();
if (stateHandler.cancelButton) {
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);
// The port to Kotlin made this REALLY messy!!!!
getNewLoc(opts.downloadURI, Objects.requireNonNull(pf.index)!!.file)?.let {
pf.index!!.hashFormat?.let { it1 ->
processIndex(it,
getHash(Objects.requireNonNull(pf.index!!.hashFormat)!!, Objects.requireNonNull(pf.index!!.hash)!!), it1, manifest, invalidatedUris)
}
}
} catch (e1: Exception) {
ui.handleExceptionAndExit(e1)
}
handleCancellation();
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);
manifest.indexFileHash = null
manifest.packFileHash = null
} else {
manifest.setPackFileHash(packFileSource.getHash());
manifest.packFileHash = packFileSource.hash
}
manifest.setCachedSide(opts.side);
try (Writer writer = new FileWriter(Paths.get(opts.packFolder, opts.manifestFile).toString())) {
gson.toJson(manifest, writer);
} catch (IOException e) {
manifest.cachedSide = opts.side
try {
FileWriter(Paths.get(opts.packFolder, opts.manifestFile).toString()).use { writer -> gson.toJson(manifest, writer) }
} catch (e: IOException) {
// TODO: add message?
ui.handleException(e);
ui.handleException(e)
}
}
}
private void checkOptions() {
private fun 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;
private fun processIndex(indexUri: SpaceSafeURI, indexHash: Hash, hashFormat: String, manifest: ManifestFile, invalidatedUris: List<SpaceSafeURI>) {
if (manifest.indexFileHash == indexHash && invalidatedUris.isEmpty()) {
println("Modpack files are already up to date!")
if (!stateHandler.optionsButton) {
return
}
}
manifest.setIndexFileHash(indexHash);
manifest.indexFileHash = 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;
val indexFileSource = try {
val src = getFileSource(indexUri)
getHasher(hashFormat).getHashingSource(src)
} catch (e: Exception) {
// TODO: run cancellation window?
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;
val indexFile = try {
Toml().read(indexFileSource.buffer().inputStream()).to(IndexFile::class.java)
} catch (e: IllegalStateException) {
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;
println("I was meant to put an error message here but I'll do that later")
return
}
if (stateHandler.cancelButton) {
showCancellationDialog()
return
}
if (stateHandler.getCancelButton()) {
showCancellationDialog();
return;
}
ui.submitProgress(new InstallProgress("Checking local files..."));
Iterator<Map.Entry<SpaceSafeURI, ManifestFile.File>> it = manifest.getCachedFiles().entrySet().iterator();
ui.submitProgress(InstallProgress("Checking local files..."))
// TODO: use kotlin filtering/FP rather than an iterator?
val it: MutableIterator<Map.Entry<SpaceSafeURI, ManifestFile.File>> = manifest.cachedFiles.entries.iterator()
while (it.hasNext()) {
Map.Entry<SpaceSafeURI, ManifestFile.File> entry = it.next();
if (entry.getValue().getCachedLocation() != null) {
boolean alreadyDeleted = false;
val (uri, file) = it.next()
if (file.cachedLocation != null) {
var alreadyDeleted = false
// Delete if option value has been set to false
if (entry.getValue().isOptional() && !entry.getValue().getOptionValue()) {
if (file.isOptional && !file.optionValue) {
try {
Files.deleteIfExists(Paths.get(opts.packFolder, entry.getValue().getCachedLocation()));
} catch (IOException e) {
Files.deleteIfExists(Paths.get(opts.packFolder, file.cachedLocation))
} catch (e: IOException) {
// TODO: should this be shown to the user in some way?
e.printStackTrace();
e.printStackTrace()
}
// Set to null, as it doesn't exist anymore
entry.getValue().setCachedLocation(null);
alreadyDeleted = true;
file.cachedLocation = null
alreadyDeleted = true
}
if (indexFile.getFiles().stream().noneMatch(f -> Objects.equals(f.getFile(), entry.getKey()))) {
// File has been removed from the index
if (indexFile.files.none { it.file == uri }) { // 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();
Files.deleteIfExists(Paths.get(opts.packFolder, file.cachedLocation))
} catch (e: IOException) { // TODO: should this be shown to the user in some way?
e.printStackTrace()
}
}
it.remove();
it.remove()
}
}
}
if (stateHandler.getCancelButton()) {
showCancellationDialog();
return;
if (stateHandler.cancelButton) {
showCancellationDialog()
return
}
ui.submitProgress(new InstallProgress("Comparing new files..."));
ui.submitProgress(InstallProgress("Comparing new files..."))
// TODO: progress bar?
if (indexFile.getFiles().isEmpty()) {
System.out.println("Warning: Index is empty!");
if (indexFile.files.isEmpty()) {
println("Warning: Index is empty!")
}
List<DownloadTask> tasks = DownloadTask.createTasksFromIndex(indexFile, indexFile.getHashFormat(), opts.side);
val tasks = createTasksFromIndex(indexFile, indexFile.hashFormat, 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());
val invalidateAll = opts.side != manifest.cachedSide
if (invalidateAll) {
System.out.println("Side changed, invalidating all mods");
println("Side changed, invalidating all mods")
}
tasks.forEach(f -> {
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();
f.invalidate()
} else if (invalidatedUris.contains(f.metadata.file)) {
f.invalidate()
}
ManifestFile.File file = manifest.getCachedFiles().get(f.metadata.getFile());
if (file != null) {
val file = manifest.cachedFiles[f.metadata.file]
// 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();
}
file?.backup()
// If it is null, the DownloadTask will make a new empty cachedFile
f.updateFromCache(file);
});
f.updateFromCache(file)
}
if (stateHandler.getCancelButton()) {
showCancellationDialog();
return;
if (stateHandler.cancelButton) {
showCancellationDialog()
return
}
// Let's hope downloadMetadata is a pure function!!!
tasks.parallelStream().forEach(f -> f.downloadMetadata(indexFile, indexUri));
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;
val failedTaskDetails = tasks.asSequence().map(DownloadTask::exceptionDetails).filterNotNull().toList()
if (failedTaskDetails.isNotEmpty()) {
errorsOccurred = true
val exceptionListResult: ExceptionListResult
exceptionListResult = try {
ui.showExceptions(failedTaskDetails, tasks.size, true).get()
} catch (e: InterruptedException) { // Interrupted means cancelled???
ui.handleExceptionAndExit(e)
return
} catch (e: ExecutionException) {
ui.handleExceptionAndExit(e)
return
}
when (exceptionListResult) {
ExceptionListResult.CONTINUE -> {}
ExceptionListResult.CANCEL -> {
cancelled = true
return
}
ExceptionListResult.IGNORE -> {
cancelledStartGame = true
return
}
switch (exceptionListResult) {
case CONTINUE:
break;
case CANCEL:
cancelled = true;
return;
case IGNORE:
cancelledStartGame = true;
return;
}
}
if (stateHandler.getCancelButton()) {
showCancellationDialog();
return;
if (stateHandler.cancelButton) {
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());
// TODO: task failed function?
val nonFailedFirstTasks = tasks.filter { t -> !t.failed() }.toList()
val optionTasks = nonFailedFirstTasks.filter(DownloadTask::correctSide).filter(DownloadTask::isOptional).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));
if (stateHandler.optionsButton || optionTasks.any(DownloadTask::isNewOptional)) {
// new ArrayList is required so it's an IOptionDetails rather than a DownloadTask list
val cancelledResult = ui.showOptions(ArrayList(optionTasks))
try {
if (cancelledResult.get()) {
cancelled = true;
cancelled = true
// TODO: Should the UI be closed somehow??
return;
return
}
} catch (InterruptedException | ExecutionException e) {
} catch (e: InterruptedException) {
// Interrupted means cancelled???
ui.handleExceptionAndExit(e);
ui.handleExceptionAndExit(e)
} catch (e: ExecutionException) {
ui.handleExceptionAndExit(e)
}
}
ui.disableOptionsButton();
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;
val threadPool = Executors.newFixedThreadPool(10)
val completionService: CompletionService<DownloadTask> = ExecutorCompletionService(threadPool)
tasks.forEach { t ->
completionService.submit {
t.download(opts.packFolder, indexUri)
t
}
}
for (i in tasks.indices) {
var task: DownloadTask?
task = try {
completionService.take().get()
} catch (e: InterruptedException) {
ui.handleException(e)
null
} catch (e: ExecutionException) {
ui.handleException(e)
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);
}
task?.cachedFile?.let { file ->
if (task.failed()) {
val oldFile = file.revert
if (oldFile != null) {
task.metadata.file?.let { uri -> manifest.cachedFiles.putIfAbsent(uri, oldFile) }
} else { null }
} 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);
task.metadata.file?.let { uri -> manifest.cachedFiles.putIfAbsent(uri, file) }
}
}
String progress;
var progress: String
if (task != null) {
if (task.getException() != null) {
progress = "Failed to download " + task.metadata.getName() + ": " + task.getException().getMessage();
task.getException().printStackTrace();
val exDetails = task.exceptionDetails
if (exDetails != null) {
progress = "Failed to download ${exDetails.name}: ${exDetails.exception.message}"
exDetails.exception.printStackTrace()
} else {
// TODO: should this be revised for tasks that didn't actually download it?
progress = "Downloaded " + task.metadata.getName();
progress = "Downloaded ${task.name}"
}
} else {
progress = "Failed to download, unknown reason";
progress = "Failed to download, unknown reason"
}
ui.submitProgress(new InstallProgress(progress, i + 1, tasks.size()));
ui.submitProgress(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;
if (stateHandler.cancelButton) { // 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();
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) {
val failedTasks2ElectricBoogaloo = nonFailedFirstTasks.asSequence().map(DownloadTask::exceptionDetails).filterNotNull().toList()
if (failedTasks2ElectricBoogaloo.isNotEmpty()) {
errorsOccurred = true
val exceptionListResult: ExceptionListResult
exceptionListResult = try {
ui.showExceptions(failedTasks2ElectricBoogaloo, tasks.size, false).get()
} catch (e: InterruptedException) {
// Interrupted means cancelled???
ui.handleExceptionAndExit(e);
return;
ui.handleExceptionAndExit(e)
return
} catch (e: ExecutionException) {
ui.handleExceptionAndExit(e)
return
}
switch (exceptionListResult) {
case CONTINUE:
break;
case CANCEL:
cancelled = true;
return;
case IGNORE:
cancelledStartGame = true;
when (exceptionListResult) {
ExceptionListResult.CONTINUE -> {}
ExceptionListResult.CANCEL -> cancelled = true
ExceptionListResult.IGNORE -> cancelledStartGame = true
}
}
}
private void showCancellationDialog() {
IExceptionDetails.ExceptionListResult exceptionListResult;
try {
exceptionListResult = ui.showCancellationDialog().get();
} catch (InterruptedException | ExecutionException e) {
private fun showCancellationDialog() {
val cancellationResult: CancellationResult
cancellationResult = try {
ui.showCancellationDialog().get()
} catch (e: InterruptedException) {
// Interrupted means cancelled???
ui.handleExceptionAndExit(e);
return;
ui.handleExceptionAndExit(e)
return
} catch (e: ExecutionException) {
ui.handleExceptionAndExit(e)
return
}
switch (exceptionListResult) {
case CONTINUE:
throw new RuntimeException("Continuation not allowed here!");
case CANCEL:
cancelled = true;
return;
case IGNORE:
cancelledStartGame = true;
when (cancellationResult) {
CancellationResult.QUIT -> cancelled = true
CancellationResult.CONTINUE -> cancelledStartGame = true
}
}
private void handleCancellation() {
private fun handleCancellation() {
if (cancelled) {
System.out.println("Update cancelled by user!");
System.exit(1);
println("Update cancelled by user!")
exitProcess(1)
} else if (cancelledStartGame) {
System.out.println("Update cancelled by user! Continuing to start game...");
System.exit(0);
println("Update cancelled by user! Continuing to start game...")
exitProcess(0)
}
}
}

View File

@ -74,10 +74,11 @@ class IndexFile {
}
// TODO: throw some kind of exception?
val name: String?
val name: String
get() {
if (metafile) {
return linkedFile?.name ?: linkedFile?.filename
return linkedFile?.name ?: linkedFile?.filename ?:
file?.run { Paths.get(path).fileName.toString() } ?: "Invalid file"
}
return file?.run { Paths.get(path).fileName.toString() } ?: "Invalid file"
}

View File

@ -1,6 +1,6 @@
package link.infra.packwiz.installer.ui
import link.infra.packwiz.installer.ui.IExceptionDetails.ExceptionListResult
import link.infra.packwiz.installer.ui.IUserInterface.ExceptionListResult
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Future
@ -39,7 +39,7 @@ class CLIHandler : IUserInterface {
}
}
override fun showExceptions(exceptions: List<IExceptionDetails>, numTotal: Int, allowsIgnore: Boolean): Future<ExceptionListResult> {
override fun showExceptions(exceptions: List<ExceptionDetails>, numTotal: Int, allowsIgnore: Boolean): Future<ExceptionListResult> {
val future = CompletableFuture<ExceptionListResult>()
future.complete(ExceptionListResult.CANCEL)
return future

View File

@ -0,0 +1,6 @@
package link.infra.packwiz.installer.ui
data class ExceptionDetails(
val name: String,
val exception: Exception
)

View File

@ -1,6 +1,5 @@
package link.infra.packwiz.installer.ui
import link.infra.packwiz.installer.ui.IExceptionDetails.ExceptionListResult
import java.awt.BorderLayout
import java.awt.Desktop
import java.awt.event.WindowAdapter
@ -14,10 +13,10 @@ import java.util.concurrent.CompletableFuture
import javax.swing.*
import javax.swing.border.EmptyBorder
internal class ExceptionListWindow(eList: List<IExceptionDetails>, future: CompletableFuture<ExceptionListResult>, numTotal: Int, allowsIgnore: Boolean, parentWindow: JFrame?) : JDialog(parentWindow, "Failed file downloads", true) {
class ExceptionListWindow(eList: List<ExceptionDetails>, future: CompletableFuture<IUserInterface.ExceptionListResult>, numTotal: Int, allowsIgnore: Boolean, parentWindow: JFrame?) : JDialog(parentWindow, "Failed file downloads", true) {
private val lblExceptionStacktrace: JTextArea
private class ExceptionListModel internal constructor(private val details: List<IExceptionDetails>) : AbstractListModel<String>() {
private class ExceptionListModel internal constructor(private val details: List<ExceptionDetails>) : AbstractListModel<String>() {
override fun getSize() = details.size
override fun getElementAt(index: Int) = details[index].name
fun getExceptionAt(index: Int) = details[index].exception
@ -90,7 +89,7 @@ internal class ExceptionListWindow(eList: List<IExceptionDetails>, future: Compl
add(JButton("Continue").apply {
toolTipText = "Attempt to continue installing, excluding the failed downloads"
addActionListener {
future.complete(ExceptionListResult.CONTINUE)
future.complete(IUserInterface.ExceptionListResult.CONTINUE)
this@ExceptionListWindow.dispose()
}
})
@ -98,7 +97,7 @@ internal class ExceptionListWindow(eList: List<IExceptionDetails>, future: Compl
add(JButton("Cancel launch").apply {
toolTipText = "Stop launching the game"
addActionListener {
future.complete(ExceptionListResult.CANCEL)
future.complete(IUserInterface.ExceptionListResult.CANCEL)
this@ExceptionListWindow.dispose()
}
})
@ -107,7 +106,7 @@ internal class ExceptionListWindow(eList: List<IExceptionDetails>, future: Compl
toolTipText = "Start the game without attempting to update"
isEnabled = allowsIgnore
addActionListener {
future.complete(ExceptionListResult.IGNORE)
future.complete(IUserInterface.ExceptionListResult.IGNORE)
this@ExceptionListWindow.dispose()
}
})
@ -139,13 +138,13 @@ internal class ExceptionListWindow(eList: List<IExceptionDetails>, future: Compl
addWindowListener(object : WindowAdapter() {
override fun windowClosing(e: WindowEvent) {
future.complete(ExceptionListResult.CANCEL)
future.complete(IUserInterface.ExceptionListResult.CANCEL)
}
override fun windowClosed(e: WindowEvent) {
// Just in case closing didn't get triggered - if something else called dispose() the
// future will have already completed
future.complete(ExceptionListResult.CANCEL)
future.complete(IUserInterface.ExceptionListResult.CANCEL)
}
})
}

View File

@ -1,10 +0,0 @@
package link.infra.packwiz.installer.ui
interface IExceptionDetails {
val exception: Exception
val name: String
enum class ExceptionListResult {
CONTINUE, CANCEL, IGNORE
}
}

View File

@ -1,6 +1,5 @@
package link.infra.packwiz.installer.ui
import link.infra.packwiz.installer.ui.IExceptionDetails.ExceptionListResult
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Future
import kotlin.system.exitProcess
@ -21,14 +20,22 @@ interface IUserInterface {
// Return true if the installation was cancelled!
fun showOptions(options: List<IOptionDetails>): Future<Boolean>
fun showExceptions(exceptions: List<IExceptionDetails>, numTotal: Int, allowsIgnore: Boolean): Future<ExceptionListResult>
fun showExceptions(exceptions: List<ExceptionDetails>, numTotal: Int, allowsIgnore: Boolean): Future<ExceptionListResult>
@JvmDefault
fun disableOptionsButton() {}
// Should not return CONTINUE
@JvmDefault
fun showCancellationDialog(): Future<ExceptionListResult> {
return CompletableFuture<ExceptionListResult>().apply {
complete(ExceptionListResult.CANCEL)
fun showCancellationDialog(): Future<CancellationResult> {
return CompletableFuture<CancellationResult>().apply {
complete(CancellationResult.QUIT)
}
}
enum class ExceptionListResult {
CONTINUE, CANCEL, IGNORE
}
enum class CancellationResult {
QUIT, CONTINUE
}
}

View File

@ -1,6 +1,6 @@
package link.infra.packwiz.installer.ui
import link.infra.packwiz.installer.ui.IExceptionDetails.ExceptionListResult
import link.infra.packwiz.installer.ui.IUserInterface.ExceptionListResult
import java.awt.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Future
@ -186,7 +186,7 @@ class InstallWindow : IUserInterface {
return future
}
override fun showExceptions(exceptions: List<IExceptionDetails>, numTotal: Int, allowsIgnore: Boolean): Future<ExceptionListResult> {
override fun showExceptions(exceptions: List<ExceptionDetails>, numTotal: Int, allowsIgnore: Boolean): Future<ExceptionListResult> {
val future = CompletableFuture<ExceptionListResult>()
EventQueue.invokeLater {
ExceptionListWindow(exceptions, future, numTotal, allowsIgnore, frmPackwizlauncher).apply {
@ -204,15 +204,15 @@ class InstallWindow : IUserInterface {
}
}
override fun showCancellationDialog(): Future<ExceptionListResult> {
val future = CompletableFuture<ExceptionListResult>()
override fun showCancellationDialog(): Future<IUserInterface.CancellationResult> {
val future = CompletableFuture<IUserInterface.CancellationResult>()
EventQueue.invokeLater {
val buttons = arrayOf("Quit", "Ignore")
val result = JOptionPane.showOptionDialog(frmPackwizlauncher,
"The installation was cancelled. Would you like to quit the game, or ignore the update and start the game?",
"Cancelled installation",
JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, buttons, buttons[0])
future.complete(if (result == 0) ExceptionListResult.CANCEL else ExceptionListResult.IGNORE)
future.complete(if (result == 0) IUserInterface.CancellationResult.QUIT else IUserInterface.CancellationResult.CONTINUE)
}
return future
}