mirror of
https://github.com/packwiz/packwiz-installer.git
synced 2025-04-19 13:06:30 +02:00
Complete Kotlin port
This commit is contained in:
parent
9d3587c72e
commit
a15489f5e4
@ -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.IndexFile
|
||||||
import link.infra.packwiz.installer.metadata.ManifestFile;
|
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.SpaceSafeURI;
|
import link.infra.packwiz.installer.metadata.hash.Hash
|
||||||
import link.infra.packwiz.installer.metadata.hash.GeneralHashingSource;
|
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHash
|
||||||
import link.infra.packwiz.installer.metadata.hash.Hash;
|
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHasher
|
||||||
import link.infra.packwiz.installer.metadata.hash.HashUtils;
|
import link.infra.packwiz.installer.ui.ExceptionDetails
|
||||||
import link.infra.packwiz.installer.ui.IExceptionDetails;
|
import link.infra.packwiz.installer.ui.IOptionDetails
|
||||||
import link.infra.packwiz.installer.ui.IOptionDetails;
|
import okio.Buffer
|
||||||
import okio.Buffer;
|
import okio.buffer
|
||||||
import okio.Okio;
|
import java.io.IOException
|
||||||
import okio.Source;
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.nio.file.StandardCopyOption
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
import java.io.IOException;
|
internal class DownloadTask private constructor(val metadata: IndexFile.File, defaultFormat: String, private val downloadSide: UpdateManager.Options.Side) : IOptionDetails {
|
||||||
import java.nio.file.Files;
|
var cachedFile: ManifestFile.File? = null
|
||||||
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 {
|
private var err: Exception? = null
|
||||||
final IndexFile.File metadata;
|
val exceptionDetails get() = err?.let { e -> ExceptionDetails(name, e) }
|
||||||
ManifestFile.File cachedFile = null;
|
|
||||||
private Exception failure = null;
|
fun failed() = err != null
|
||||||
private boolean alreadyUpToDate = false;
|
|
||||||
private boolean metadataRequired = true;
|
private var alreadyUpToDate = false
|
||||||
private boolean invalidated = 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
|
// If file is new or isOptional changed to true, the option needs to be presented again
|
||||||
private boolean newOptional = true;
|
private var newOptional = true
|
||||||
private final UpdateManager.Options.Side downloadSide;
|
|
||||||
|
|
||||||
private DownloadTask(IndexFile.File metadata, String defaultFormat, UpdateManager.Options.Side downloadSide) {
|
val isOptional get() = metadata.linkedFile?.isOptional ?: false
|
||||||
this.metadata = metadata;
|
|
||||||
if (metadata.getHashFormat() == null || metadata.getHashFormat().length() == 0) {
|
fun isNewOptional() = isOptional && newOptional
|
||||||
metadata.setHashFormat(defaultFormat);
|
|
||||||
|
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() {
|
override val optionDescription get() = metadata.linkedFile?.option?.description ?: ""
|
||||||
invalidated = true;
|
|
||||||
alreadyUpToDate = false;
|
init {
|
||||||
|
if (metadata.hashFormat?.isEmpty() != false) {
|
||||||
|
metadata.hashFormat = defaultFormat
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateFromCache(ManifestFile.File cachedFile) {
|
fun invalidate() {
|
||||||
if (failure != null) return;
|
invalidated = true
|
||||||
|
alreadyUpToDate = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateFromCache(cachedFile: ManifestFile.File?) {
|
||||||
|
if (err != null) return
|
||||||
|
|
||||||
if (cachedFile == null) {
|
if (cachedFile == null) {
|
||||||
this.cachedFile = new ManifestFile.File();
|
this.cachedFile = ManifestFile.File()
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
this.cachedFile = cachedFile
|
||||||
this.cachedFile = cachedFile;
|
|
||||||
|
|
||||||
if (!invalidated) {
|
if (!invalidated) {
|
||||||
Hash currHash;
|
val currHash = try {
|
||||||
try {
|
getHash(metadata.hashFormat!!, metadata.hash!!)
|
||||||
currHash = HashUtils.getHash(Objects.requireNonNull(metadata.getHashFormat()), Objects.requireNonNull(metadata.getHash()));
|
} catch (e: Exception) {
|
||||||
} catch (Exception e) {
|
err = e
|
||||||
failure = e;
|
return
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (currHash.equals(cachedFile.getHash())) {
|
if (currHash == cachedFile.hash) { // Already up to date
|
||||||
// Already up to date
|
alreadyUpToDate = true
|
||||||
alreadyUpToDate = true;
|
metadataRequired = false
|
||||||
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
|
// 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
|
// the file, and to show the description and name
|
||||||
metadataRequired = true;
|
metadataRequired = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void downloadMetadata(IndexFile parentIndexFile, SpaceSafeURI indexUri) {
|
fun downloadMetadata(parentIndexFile: IndexFile, indexUri: SpaceSafeURI) {
|
||||||
if (failure != null) return;
|
if (err != null) return
|
||||||
|
|
||||||
if (metadataRequired) {
|
if (metadataRequired) {
|
||||||
try {
|
try {
|
||||||
metadata.downloadMeta(parentIndexFile, indexUri);
|
// Retrieve the linked metadata file
|
||||||
} catch (Exception e) {
|
metadata.downloadMeta(parentIndexFile, indexUri)
|
||||||
failure = e;
|
} catch (e: Exception) {
|
||||||
return;
|
err = e
|
||||||
|
return
|
||||||
}
|
}
|
||||||
ModFile linkedFile = metadata.getLinkedFile();
|
cachedFile?.let { cachedFile ->
|
||||||
|
val linkedFile = metadata.linkedFile
|
||||||
if (linkedFile != null) {
|
if (linkedFile != null) {
|
||||||
ModFile.Option option = linkedFile.getOption();
|
linkedFile.option?.let { opt ->
|
||||||
if (option != null) {
|
if (opt.optional) {
|
||||||
if (option.getOptional()) {
|
if (cachedFile.isOptional) {
|
||||||
if (cachedFile.isOptional()) {
|
|
||||||
// isOptional didn't change
|
// isOptional didn't change
|
||||||
newOptional = false;
|
newOptional = false
|
||||||
} else {
|
} else {
|
||||||
// isOptional false -> true, set option to it's default value
|
// isOptional false -> true, set option to it's default value
|
||||||
// TODO: preserve previous option value, somehow??
|
// TODO: preserve previous option value, somehow??
|
||||||
cachedFile.setOptionValue(option.getDefaultValue());
|
cachedFile.optionValue = opt.defaultValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cachedFile.setOptional(isOptional());
|
cachedFile.isOptional = isOptional
|
||||||
cachedFile.setOnlyOtherSide(!correctSide());
|
cachedFile.onlyOtherSide = !correctSide()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void download(String packFolder, SpaceSafeURI indexUri) {
|
fun download(packFolder: String, indexUri: SpaceSafeURI) {
|
||||||
if (failure != null) return;
|
if (err != null) return
|
||||||
|
|
||||||
// Ensure it is removed
|
// Ensure it is removed
|
||||||
if (!cachedFile.getOptionValue() || !correctSide()) {
|
cachedFile?.let {
|
||||||
if (cachedFile.getCachedLocation() == null) return;
|
if (!it.optionValue || !correctSide()) {
|
||||||
|
if (it.cachedLocation == null) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Files.deleteIfExists(Paths.get(packFolder, cachedFile.getCachedLocation()));
|
Files.deleteIfExists(Paths.get(packFolder, it.cachedLocation))
|
||||||
} catch (IOException e) {
|
} catch (e: IOException) {
|
||||||
// TODO: how much of a problem is this? use log4j/other log library to show warning?
|
// TODO: how much of a problem is this? use log4j/other log library to show warning?
|
||||||
e.printStackTrace();
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
cachedFile.setCachedLocation(null);
|
it.cachedLocation = null
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (alreadyUpToDate) return
|
||||||
|
|
||||||
if (alreadyUpToDate) return;
|
// TODO: should I be validating JSON properly, or this fine!!!!!!!??
|
||||||
|
assert(metadata.destURI != null)
|
||||||
Path destPath = Paths.get(packFolder, Objects.requireNonNull(metadata.getDestURI()).toString());
|
val destPath = Paths.get(packFolder, metadata.destURI.toString())
|
||||||
|
|
||||||
// Don't update files marked with preserve if they already exist on disk
|
// Don't update files marked with preserve if they already exist on disk
|
||||||
if (metadata.getPreserve()) {
|
if (metadata.preserve) {
|
||||||
if (destPath.toFile().exists()) {
|
if (destPath.toFile().exists()) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Hash hash;
|
val hash: Hash
|
||||||
String fileHashFormat;
|
val fileHashFormat: String
|
||||||
ModFile linkedFile = metadata.getLinkedFile();
|
val linkedFile = metadata.linkedFile
|
||||||
|
|
||||||
if (linkedFile != null) {
|
if (linkedFile != null) {
|
||||||
hash = linkedFile.getHash();
|
hash = linkedFile.hash
|
||||||
fileHashFormat = Objects.requireNonNull(linkedFile.getDownload()).getHashFormat();
|
fileHashFormat = Objects.requireNonNull(Objects.requireNonNull(linkedFile.download)!!.hashFormat)!!
|
||||||
} else {
|
} else {
|
||||||
hash = metadata.getHashObj();
|
hash = metadata.getHashObj()
|
||||||
fileHashFormat = metadata.getHashFormat();
|
fileHashFormat = Objects.requireNonNull(metadata.hashFormat)!!
|
||||||
}
|
}
|
||||||
|
|
||||||
Source src = metadata.getSource(indexUri);
|
val src = metadata.getSource(indexUri)
|
||||||
assert fileHashFormat != null;
|
val fileSource = getHasher(fileHashFormat).getHashingSource(src)
|
||||||
GeneralHashingSource fileSource = HashUtils.getHasher(fileHashFormat).getHashingSource(src);
|
val data = Buffer()
|
||||||
Buffer data = new Buffer();
|
|
||||||
Okio.buffer(fileSource).readAll(data);
|
// 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)) {
|
if (fileSource.hashIsEqual(hash)) {
|
||||||
Files.createDirectories(destPath.getParent());
|
Files.createDirectories(destPath.parent)
|
||||||
Files.copy(data.inputStream(), destPath, StandardCopyOption.REPLACE_EXISTING);
|
Files.copy(data.inputStream(), destPath, StandardCopyOption.REPLACE_EXISTING)
|
||||||
|
data.clear()
|
||||||
} else {
|
} else {
|
||||||
// TODO: no more SYSOUT!!!!!!!!!
|
// TODO: no more PRINTLN!!!!!!!!!
|
||||||
System.out.println("Invalid hash for " + metadata.getDestURI().toString());
|
println("Invalid hash for " + metadata.destURI.toString())
|
||||||
System.out.println("Calculated: " + fileSource.getHash());
|
println("Calculated: " + fileSource.hash)
|
||||||
System.out.println("Expected: " + hash);
|
println("Expected: $hash")
|
||||||
failure = new Exception("Hash invalid!");
|
err = Exception("Hash invalid!")
|
||||||
}
|
}
|
||||||
|
cachedFile?.cachedLocation?.let {
|
||||||
if (cachedFile.getCachedLocation() != null && !destPath.equals(Paths.get(packFolder, cachedFile.getCachedLocation()))) {
|
if (destPath != Paths.get(packFolder, it)) {
|
||||||
// Delete old file if location changes
|
// 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) {
|
} catch (e: Exception) {
|
||||||
if (cachedFile == null) {
|
err = e
|
||||||
cachedFile = new ManifestFile.File();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (err == null) {
|
||||||
// Update the manifest file
|
// Update the manifest file
|
||||||
|
cachedFile = (cachedFile ?: ManifestFile.File()).also {
|
||||||
try {
|
try {
|
||||||
cachedFile.setHash(metadata.getHashObj());
|
it.hash = metadata.getHashObj()
|
||||||
} catch (Exception e) {
|
} catch (e: Exception) {
|
||||||
failure = e;
|
err = e
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
cachedFile.setOptional(isOptional());
|
it.isOptional = isOptional
|
||||||
cachedFile.setCachedLocation(metadata.getDestURI().toString());
|
it.cachedLocation = metadata.destURI.toString()
|
||||||
if (metadata.getLinkedFile() != null) {
|
metadata.linkedFile?.let { linked ->
|
||||||
try {
|
try {
|
||||||
cachedFile.setLinkedFileHash(metadata.getLinkedFile().getHash());
|
it.linkedFileHash = linked.hash
|
||||||
} catch (Exception e) {
|
} catch (e: Exception) {
|
||||||
failure = e;
|
err = e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Exception getException() {
|
companion object {
|
||||||
return failure;
|
@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))
|
||||||
}
|
}
|
||||||
|
return tasks
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -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.GsonBuilder;
|
import com.google.gson.JsonIOException
|
||||||
import com.google.gson.JsonIOException;
|
import com.google.gson.JsonSyntaxException
|
||||||
import com.google.gson.JsonSyntaxException;
|
import com.google.gson.annotations.SerializedName
|
||||||
import com.google.gson.annotations.SerializedName;
|
import com.moandjiezana.toml.Toml
|
||||||
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.IndexFile
|
||||||
import link.infra.packwiz.installer.metadata.ManifestFile;
|
import link.infra.packwiz.installer.metadata.ManifestFile
|
||||||
import link.infra.packwiz.installer.metadata.PackFile;
|
import link.infra.packwiz.installer.metadata.PackFile
|
||||||
import link.infra.packwiz.installer.metadata.SpaceSafeURI;
|
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.Hash;
|
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHash
|
||||||
import link.infra.packwiz.installer.metadata.hash.HashUtils;
|
import link.infra.packwiz.installer.metadata.hash.HashUtils.getHasher
|
||||||
import link.infra.packwiz.installer.request.HandlerManager;
|
import link.infra.packwiz.installer.request.HandlerManager.getFileSource
|
||||||
import link.infra.packwiz.installer.ui.IExceptionDetails;
|
import link.infra.packwiz.installer.request.HandlerManager.getNewLoc
|
||||||
import link.infra.packwiz.installer.ui.IUserInterface;
|
import link.infra.packwiz.installer.ui.IUserInterface
|
||||||
import link.infra.packwiz.installer.ui.InputStateHandler;
|
import link.infra.packwiz.installer.ui.IUserInterface.CancellationResult
|
||||||
import link.infra.packwiz.installer.ui.InstallProgress;
|
import link.infra.packwiz.installer.ui.IUserInterface.ExceptionListResult
|
||||||
import okio.Okio;
|
import link.infra.packwiz.installer.ui.InputStateHandler
|
||||||
import okio.Source;
|
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.*;
|
class UpdateManager internal constructor(private val opts: Options, val ui: IUserInterface, private val stateHandler: InputStateHandler) {
|
||||||
import java.nio.file.Files;
|
private var cancelled = false
|
||||||
import java.nio.file.Paths;
|
private var cancelledStartGame = false
|
||||||
import java.util.*;
|
private var errorsOccurred = false
|
||||||
import java.util.concurrent.*;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public class UpdateManager {
|
init {
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
|
||||||
private final Options opts;
|
data class Options(
|
||||||
public final IUserInterface ui;
|
var downloadURI: SpaceSafeURI? = null,
|
||||||
private boolean cancelled;
|
var manifestFile: String = "packwiz.json", // TODO: make configurable
|
||||||
private boolean cancelledStartGame = false;
|
var packFolder: String = ".",
|
||||||
private InputStateHandler stateHandler;
|
var side: Side = Side.CLIENT
|
||||||
private boolean errorsOccurred = false;
|
) {
|
||||||
|
enum class Side {
|
||||||
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")
|
@SerializedName("client")
|
||||||
CLIENT("client"),
|
CLIENT("client"),
|
||||||
@SerializedName("server")
|
@SerializedName("server")
|
||||||
SERVER("server"),
|
SERVER("server"),
|
||||||
@SerializedName("both")
|
@SerializedName("both")
|
||||||
BOTH("both", new Side[] { CLIENT, SERVER });
|
@Suppress("unused")
|
||||||
|
BOTH("both", arrayOf(CLIENT, SERVER));
|
||||||
|
|
||||||
private final String sideName;
|
private val sideName: String
|
||||||
private final Side[] depSides;
|
private val depSides: Array<Side>?
|
||||||
|
|
||||||
Side(String sideName) {
|
constructor(sideName: String) {
|
||||||
this.sideName = sideName.toLowerCase();
|
this.sideName = sideName.toLowerCase()
|
||||||
this.depSides = null;
|
depSides = null
|
||||||
}
|
}
|
||||||
|
|
||||||
Side(String sideName, Side[] depSides) {
|
constructor(sideName: String, depSides: Array<Side>) {
|
||||||
this.sideName = sideName.toLowerCase();
|
this.sideName = sideName.toLowerCase()
|
||||||
this.depSides = depSides;
|
this.depSides = depSides
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
override fun toString() = sideName
|
||||||
public String toString() {
|
|
||||||
return this.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) {
|
companion object {
|
||||||
if (this.equals(tSide)) {
|
fun from(name: String): Side? {
|
||||||
return true;
|
val lowerName = name.toLowerCase()
|
||||||
}
|
for (side in values()) {
|
||||||
if (this.depSides != null) {
|
if (side.sideName == lowerName) {
|
||||||
for (Side depSide : this.depSides) {
|
return side
|
||||||
if (depSide.equals(tSide)) {
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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) {
|
private fun start() {
|
||||||
this.opts = opts;
|
checkOptions()
|
||||||
this.ui = ui;
|
|
||||||
this.stateHandler = inputStateHandler;
|
ui.submitProgress(InstallProgress("Loading manifest file..."))
|
||||||
this.start();
|
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() {
|
if (stateHandler.cancelButton) {
|
||||||
this.checkOptions();
|
showCancellationDialog()
|
||||||
|
handleCancellation()
|
||||||
|
}
|
||||||
|
|
||||||
ui.submitProgress(new InstallProgress("Loading manifest file..."));
|
ui.submitProgress(InstallProgress("Loading pack file..."))
|
||||||
Gson gson = new GsonBuilder().registerTypeAdapter(Hash.class, new Hash.TypeHandler()).create();
|
val packFileSource = try {
|
||||||
ManifestFile manifest;
|
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 {
|
try {
|
||||||
manifest = gson.fromJson(new FileReader(Paths.get(opts.packFolder, opts.manifestFile).toString()),
|
Toml().read(it.inputStream()).to(PackFile::class.java)
|
||||||
ManifestFile.class);
|
} catch (e: IllegalStateException) {
|
||||||
} catch (FileNotFoundException e) {
|
ui.handleExceptionAndExit(e)
|
||||||
manifest = new ManifestFile();
|
return
|
||||||
} catch (JsonSyntaxException | JsonIOException e) {
|
}
|
||||||
ui.handleExceptionAndExit(e);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stateHandler.getCancelButton()) {
|
if (stateHandler.cancelButton) {
|
||||||
showCancellationDialog();
|
showCancellationDialog()
|
||||||
handleCancellation();
|
handleCancellation()
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.submitProgress(new InstallProgress("Loading pack file..."));
|
ui.submitProgress(InstallProgress("Checking local files..."))
|
||||||
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
|
// Invalidation checking must be done here, as it must happen before pack/index hashes are checked
|
||||||
List<SpaceSafeURI> invalidatedUris = new ArrayList<>();
|
val invalidatedUris: MutableList<SpaceSafeURI> = ArrayList()
|
||||||
if (manifest.getCachedFiles() != null) {
|
for ((fileUri, file) in manifest.cachedFiles) {
|
||||||
for (Map.Entry<SpaceSafeURI, ManifestFile.File> entry : manifest.getCachedFiles().entrySet()) {
|
|
||||||
// ignore onlyOtherSide files
|
// ignore onlyOtherSide files
|
||||||
if (entry.getValue().getOnlyOtherSide()) {
|
if (file.onlyOtherSide) {
|
||||||
continue;
|
continue
|
||||||
}
|
}
|
||||||
boolean invalid = false;
|
|
||||||
|
var invalid = false
|
||||||
// if isn't optional, or is optional but optionValue == true
|
// if isn't optional, or is optional but optionValue == true
|
||||||
if (!entry.getValue().isOptional() || entry.getValue().getOptionValue()) {
|
if (!file.isOptional || file.optionValue) {
|
||||||
if (entry.getValue().getCachedLocation() != null) {
|
if (file.cachedLocation != null) {
|
||||||
if (!Paths.get(opts.packFolder, entry.getValue().getCachedLocation()).toFile().exists()) {
|
if (!Paths.get(opts.packFolder, file.cachedLocation).toFile().exists()) {
|
||||||
invalid = true;
|
invalid = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// if cachedLocation == null, should probably be installed!!
|
// if cachedLocation == null, should probably be installed!!
|
||||||
invalid = true;
|
invalid = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (invalid) {
|
if (invalid) {
|
||||||
SpaceSafeURI fileUri = entry.getKey();
|
println("File $fileUri invalidated, marked for redownloading")
|
||||||
System.out.println("File " + fileUri.toString() + " invalidated, marked for redownloading");
|
invalidatedUris.add(fileUri)
|
||||||
invalidatedUris.add(fileUri);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (manifest.getPackFileHash() != null && packFileSource.hashIsEqual(manifest.getPackFileHash()) && invalidatedUris.isEmpty()) {
|
if (manifest.packFileHash?.let { packFileSource.hashIsEqual(it) } == true && invalidatedUris.isEmpty()) {
|
||||||
System.out.println("Modpack is already up to date!");
|
println("Modpack is already up to date!")
|
||||||
// todo: --force?
|
// todo: --force?
|
||||||
if (!stateHandler.getOptionsButton()) {
|
if (!stateHandler.optionsButton) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
System.out.println("Modpack name: " + pf.getName());
|
println("Modpack name: " + pf.name)
|
||||||
|
|
||||||
if (stateHandler.getCancelButton()) {
|
if (stateHandler.cancelButton) {
|
||||||
showCancellationDialog();
|
showCancellationDialog()
|
||||||
handleCancellation();
|
handleCancellation()
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// This is badly written, I'll probably heavily refactor it at some point
|
// This is badly written, I'll probably heavily refactor it at some point
|
||||||
processIndex(HandlerManager.getNewLoc(opts.downloadURI, Objects.requireNonNull(pf.getIndex()).getFile()),
|
// The port to Kotlin made this REALLY messy!!!!
|
||||||
HashUtils.getHash(Objects.requireNonNull(pf.getIndex().getHashFormat()), Objects.requireNonNull(pf.getIndex().getHash())), pf.getIndex().getHashFormat(), manifest, invalidatedUris);
|
getNewLoc(opts.downloadURI, Objects.requireNonNull(pf.index)!!.file)?.let {
|
||||||
} catch (Exception e1) {
|
pf.index!!.hashFormat?.let { it1 ->
|
||||||
ui.handleExceptionAndExit(e1);
|
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
|
// 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 there were errors, don't write the manifest/index hashes, to ensure they are rechecked later
|
||||||
if (errorsOccurred) {
|
if (errorsOccurred) {
|
||||||
manifest.setIndexFileHash(null);
|
manifest.indexFileHash = null
|
||||||
manifest.setPackFileHash(null);
|
manifest.packFileHash = null
|
||||||
} else {
|
} else {
|
||||||
manifest.setPackFileHash(packFileSource.getHash());
|
manifest.packFileHash = packFileSource.hash
|
||||||
}
|
}
|
||||||
|
|
||||||
manifest.setCachedSide(opts.side);
|
manifest.cachedSide = opts.side
|
||||||
try (Writer writer = new FileWriter(Paths.get(opts.packFolder, opts.manifestFile).toString())) {
|
try {
|
||||||
gson.toJson(manifest, writer);
|
FileWriter(Paths.get(opts.packFolder, opts.manifestFile).toString()).use { writer -> gson.toJson(manifest, writer) }
|
||||||
} catch (IOException e) {
|
} catch (e: IOException) {
|
||||||
// TODO: add message?
|
// TODO: add message?
|
||||||
ui.handleException(e);
|
ui.handleException(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
private fun checkOptions() {
|
||||||
|
|
||||||
private void checkOptions() {
|
|
||||||
// TODO: implement
|
// TODO: implement
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processIndex(SpaceSafeURI indexUri, Hash indexHash, String hashFormat, ManifestFile manifest, List<SpaceSafeURI> invalidatedUris) {
|
private fun processIndex(indexUri: SpaceSafeURI, indexHash: Hash, hashFormat: String, manifest: ManifestFile, invalidatedUris: List<SpaceSafeURI>) {
|
||||||
if (manifest.getIndexFileHash() != null && manifest.getIndexFileHash().equals(indexHash) && invalidatedUris.isEmpty()) {
|
if (manifest.indexFileHash == indexHash && invalidatedUris.isEmpty()) {
|
||||||
System.out.println("Modpack files are already up to date!");
|
println("Modpack files are already up to date!")
|
||||||
if (!stateHandler.getOptionsButton()) {
|
if (!stateHandler.optionsButton) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
manifest.setIndexFileHash(indexHash);
|
manifest.indexFileHash = indexHash
|
||||||
|
|
||||||
GeneralHashingSource indexFileSource;
|
val indexFileSource = try {
|
||||||
try {
|
val src = getFileSource(indexUri)
|
||||||
Source src = HandlerManager.getFileSource(indexUri);
|
getHasher(hashFormat).getHashingSource(src)
|
||||||
indexFileSource = HashUtils.getHasher(hashFormat).getHashingSource(src);
|
} catch (e: Exception) {
|
||||||
} catch (Exception e) {
|
// TODO: run cancellation window?
|
||||||
// TODO: still launch the game if updating doesn't work?
|
ui.handleExceptionAndExit(e)
|
||||||
// TODO: ask user if they want to launch the game, exit(1) if they don't
|
return
|
||||||
ui.handleExceptionAndExit(e);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
IndexFile indexFile;
|
val indexFile = try {
|
||||||
try {
|
Toml().read(indexFileSource.buffer().inputStream()).to(IndexFile::class.java)
|
||||||
indexFile = new Toml().read(Okio.buffer(indexFileSource).inputStream()).to(IndexFile.class);
|
} catch (e: IllegalStateException) {
|
||||||
} catch (IllegalStateException e) {
|
ui.handleExceptionAndExit(e)
|
||||||
ui.handleExceptionAndExit(e);
|
return
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!indexFileSource.hashIsEqual(indexHash)) {
|
if (!indexFileSource.hashIsEqual(indexHash)) {
|
||||||
// TODO: throw exception
|
// TODO: throw exception
|
||||||
System.out.println("I was meant to put an error message here but I'll do that later");
|
println("I was meant to put an error message here but I'll do that later")
|
||||||
return;
|
return
|
||||||
|
}
|
||||||
|
if (stateHandler.cancelButton) {
|
||||||
|
showCancellationDialog()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stateHandler.getCancelButton()) {
|
ui.submitProgress(InstallProgress("Checking local files..."))
|
||||||
showCancellationDialog();
|
// TODO: use kotlin filtering/FP rather than an iterator?
|
||||||
return;
|
val it: MutableIterator<Map.Entry<SpaceSafeURI, ManifestFile.File>> = manifest.cachedFiles.entries.iterator()
|
||||||
}
|
|
||||||
|
|
||||||
ui.submitProgress(new InstallProgress("Checking local files..."));
|
|
||||||
Iterator<Map.Entry<SpaceSafeURI, ManifestFile.File>> it = manifest.getCachedFiles().entrySet().iterator();
|
|
||||||
while (it.hasNext()) {
|
while (it.hasNext()) {
|
||||||
Map.Entry<SpaceSafeURI, ManifestFile.File> entry = it.next();
|
val (uri, file) = it.next()
|
||||||
if (entry.getValue().getCachedLocation() != null) {
|
if (file.cachedLocation != null) {
|
||||||
boolean alreadyDeleted = false;
|
var alreadyDeleted = false
|
||||||
// Delete if option value has been set to false
|
// Delete if option value has been set to false
|
||||||
if (entry.getValue().isOptional() && !entry.getValue().getOptionValue()) {
|
if (file.isOptional && !file.optionValue) {
|
||||||
try {
|
try {
|
||||||
Files.deleteIfExists(Paths.get(opts.packFolder, entry.getValue().getCachedLocation()));
|
Files.deleteIfExists(Paths.get(opts.packFolder, file.cachedLocation))
|
||||||
} catch (IOException e) {
|
} catch (e: IOException) {
|
||||||
// TODO: should this be shown to the user in some way?
|
// TODO: should this be shown to the user in some way?
|
||||||
e.printStackTrace();
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
// Set to null, as it doesn't exist anymore
|
// Set to null, as it doesn't exist anymore
|
||||||
entry.getValue().setCachedLocation(null);
|
file.cachedLocation = null
|
||||||
alreadyDeleted = true;
|
alreadyDeleted = true
|
||||||
}
|
}
|
||||||
if (indexFile.getFiles().stream().noneMatch(f -> Objects.equals(f.getFile(), entry.getKey()))) {
|
if (indexFile.files.none { it.file == uri }) { // File has been removed from the index
|
||||||
// File has been removed from the index
|
|
||||||
if (!alreadyDeleted) {
|
if (!alreadyDeleted) {
|
||||||
try {
|
try {
|
||||||
Files.deleteIfExists(Paths.get(opts.packFolder, entry.getValue().getCachedLocation()));
|
Files.deleteIfExists(Paths.get(opts.packFolder, file.cachedLocation))
|
||||||
} catch (IOException e) {
|
} catch (e: IOException) { // TODO: should this be shown to the user in some way?
|
||||||
// TODO: should this be shown to the user in some way?
|
e.printStackTrace()
|
||||||
e.printStackTrace();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
it.remove();
|
it.remove()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stateHandler.getCancelButton()) {
|
if (stateHandler.cancelButton) {
|
||||||
showCancellationDialog();
|
showCancellationDialog()
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
ui.submitProgress(new InstallProgress("Comparing new files..."));
|
ui.submitProgress(InstallProgress("Comparing new files..."))
|
||||||
|
|
||||||
// TODO: progress bar?
|
// TODO: progress bar?
|
||||||
if (indexFile.getFiles().isEmpty()) {
|
if (indexFile.files.isEmpty()) {
|
||||||
System.out.println("Warning: Index is empty!");
|
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
|
// If the side changes, invalidate EVERYTHING just in case
|
||||||
// Might not be needed, but done just to be safe
|
// 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) {
|
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?
|
// TODO: should linkedfile be checked as well? should this be done in the download section?
|
||||||
if (invalidateAll) {
|
if (invalidateAll) {
|
||||||
f.invalidate();
|
f.invalidate()
|
||||||
} else if (invalidatedUris.contains(f.metadata.getFile())) {
|
} else if (invalidatedUris.contains(f.metadata.file)) {
|
||||||
f.invalidate();
|
f.invalidate()
|
||||||
}
|
}
|
||||||
ManifestFile.File file = manifest.getCachedFiles().get(f.metadata.getFile());
|
val file = manifest.cachedFiles[f.metadata.file]
|
||||||
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
|
// 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
|
// If it is null, the DownloadTask will make a new empty cachedFile
|
||||||
f.updateFromCache(file);
|
f.updateFromCache(file)
|
||||||
});
|
}
|
||||||
|
|
||||||
if (stateHandler.getCancelButton()) {
|
if (stateHandler.cancelButton) {
|
||||||
showCancellationDialog();
|
showCancellationDialog()
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Let's hope downloadMetadata is a pure function!!!
|
// 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());
|
val failedTaskDetails = tasks.asSequence().map(DownloadTask::exceptionDetails).filterNotNull().toList()
|
||||||
if (!failedTasks.isEmpty()) {
|
if (failedTaskDetails.isNotEmpty()) {
|
||||||
errorsOccurred = true;
|
errorsOccurred = true
|
||||||
IExceptionDetails.ExceptionListResult exceptionListResult;
|
val exceptionListResult: ExceptionListResult
|
||||||
try {
|
exceptionListResult = try {
|
||||||
exceptionListResult = ui.showExceptions(failedTasks, tasks.size(), true).get();
|
ui.showExceptions(failedTaskDetails, tasks.size, true).get()
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
} catch (e: InterruptedException) { // Interrupted means cancelled???
|
||||||
// Interrupted means cancelled???
|
ui.handleExceptionAndExit(e)
|
||||||
ui.handleExceptionAndExit(e);
|
return
|
||||||
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()) {
|
if (stateHandler.cancelButton) {
|
||||||
showCancellationDialog();
|
showCancellationDialog()
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
List<DownloadTask> nonFailedFirstTasks = tasks.stream().filter(t -> t.getException() == null).collect(Collectors.toList());
|
// TODO: task failed function?
|
||||||
List<DownloadTask> optionTasks = nonFailedFirstTasks.stream().filter(DownloadTask::correctSide).filter(DownloadTask::isOptional).collect(Collectors.toList());
|
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 options changed, present all options again
|
||||||
if (stateHandler.getOptionsButton() || optionTasks.stream().anyMatch(DownloadTask::isNewOptional)) {
|
if (stateHandler.optionsButton || optionTasks.any(DownloadTask::isNewOptional)) {
|
||||||
// new ArrayList is requires so it's an IOptionDetails rather than a DownloadTask list
|
// new ArrayList is required so it's an IOptionDetails rather than a DownloadTask list
|
||||||
Future<Boolean> cancelledResult = ui.showOptions(new ArrayList<>(optionTasks));
|
val cancelledResult = ui.showOptions(ArrayList(optionTasks))
|
||||||
try {
|
try {
|
||||||
if (cancelledResult.get()) {
|
if (cancelledResult.get()) {
|
||||||
cancelled = true;
|
cancelled = true
|
||||||
// TODO: Should the UI be closed somehow??
|
// TODO: Should the UI be closed somehow??
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
} catch (e: InterruptedException) {
|
||||||
// Interrupted means cancelled???
|
// Interrupted means cancelled???
|
||||||
ui.handleExceptionAndExit(e);
|
ui.handleExceptionAndExit(e)
|
||||||
|
} catch (e: ExecutionException) {
|
||||||
|
ui.handleExceptionAndExit(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ui.disableOptionsButton();
|
ui.disableOptionsButton()
|
||||||
|
|
||||||
// TODO: different thread pool type?
|
// TODO: different thread pool type?
|
||||||
ExecutorService threadPool = Executors.newFixedThreadPool(10);
|
val threadPool = Executors.newFixedThreadPool(10)
|
||||||
CompletionService<DownloadTask> completionService = new ExecutorCompletionService<>(threadPool);
|
val completionService: CompletionService<DownloadTask> = ExecutorCompletionService(threadPool)
|
||||||
|
tasks.forEach { t ->
|
||||||
tasks.forEach(t -> completionService.submit(() -> {
|
completionService.submit {
|
||||||
t.download(opts.packFolder, indexUri);
|
t.download(opts.packFolder, indexUri)
|
||||||
return t;
|
t
|
||||||
}));
|
}
|
||||||
|
}
|
||||||
for (int i = 0; i < tasks.size(); i++) {
|
for (i in tasks.indices) {
|
||||||
DownloadTask task;
|
var task: DownloadTask?
|
||||||
try {
|
task = try {
|
||||||
task = completionService.take().get();
|
completionService.take().get()
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
} catch (e: InterruptedException) {
|
||||||
ui.handleException(e);
|
ui.handleException(e)
|
||||||
task = null;
|
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)
|
// Update manifest - If there were no errors cachedFile has already been modified in place (good old pass by reference)
|
||||||
if (task != null) {
|
task?.cachedFile?.let { file ->
|
||||||
if (task.getException() != null) {
|
if (task.failed()) {
|
||||||
ManifestFile.File file = task.cachedFile.getRevert();
|
val oldFile = file.revert
|
||||||
if (file != null) {
|
if (oldFile != null) {
|
||||||
manifest.getCachedFiles().putIfAbsent(task.metadata.getFile(), file);
|
task.metadata.file?.let { uri -> manifest.cachedFiles.putIfAbsent(uri, oldFile) }
|
||||||
}
|
} else { null }
|
||||||
} else {
|
} else {
|
||||||
// idiot, if it wasn't there in the first place it won't magically appear there
|
task.metadata.file?.let { uri -> manifest.cachedFiles.putIfAbsent(uri, file) }
|
||||||
manifest.getCachedFiles().putIfAbsent(task.metadata.getFile(), task.cachedFile);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String progress;
|
var progress: String
|
||||||
if (task != null) {
|
if (task != null) {
|
||||||
if (task.getException() != null) {
|
val exDetails = task.exceptionDetails
|
||||||
progress = "Failed to download " + task.metadata.getName() + ": " + task.getException().getMessage();
|
if (exDetails != null) {
|
||||||
task.getException().printStackTrace();
|
progress = "Failed to download ${exDetails.name}: ${exDetails.exception.message}"
|
||||||
|
exDetails.exception.printStackTrace()
|
||||||
} else {
|
} else {
|
||||||
// TODO: should this be revised for tasks that didn't actually download it?
|
progress = "Downloaded ${task.name}"
|
||||||
progress = "Downloaded " + task.metadata.getName();
|
|
||||||
}
|
}
|
||||||
} else {
|
} 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()) {
|
if (stateHandler.cancelButton) { // Stop all tasks, don't launch the game (it's in an invalid state!)
|
||||||
// Stop all tasks, don't launch the game (it's in an invalid state!)
|
threadPool.shutdown()
|
||||||
threadPool.shutdown();
|
cancelled = true
|
||||||
cancelled = true;
|
return
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shut down the thread pool when the update is done
|
// 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());
|
val failedTasks2ElectricBoogaloo = nonFailedFirstTasks.asSequence().map(DownloadTask::exceptionDetails).filterNotNull().toList()
|
||||||
if (!failedTasks2ElectricBoogaloo.isEmpty()) {
|
if (failedTasks2ElectricBoogaloo.isNotEmpty()) {
|
||||||
errorsOccurred = true;
|
errorsOccurred = true
|
||||||
IExceptionDetails.ExceptionListResult exceptionListResult;
|
val exceptionListResult: ExceptionListResult
|
||||||
try {
|
exceptionListResult = try {
|
||||||
exceptionListResult = ui.showExceptions(failedTasks2ElectricBoogaloo, tasks.size(), false).get();
|
ui.showExceptions(failedTasks2ElectricBoogaloo, tasks.size, false).get()
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
} catch (e: InterruptedException) {
|
||||||
// Interrupted means cancelled???
|
// Interrupted means cancelled???
|
||||||
ui.handleExceptionAndExit(e);
|
ui.handleExceptionAndExit(e)
|
||||||
return;
|
return
|
||||||
|
} catch (e: ExecutionException) {
|
||||||
|
ui.handleExceptionAndExit(e)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
switch (exceptionListResult) {
|
when (exceptionListResult) {
|
||||||
case CONTINUE:
|
ExceptionListResult.CONTINUE -> {}
|
||||||
break;
|
ExceptionListResult.CANCEL -> cancelled = true
|
||||||
case CANCEL:
|
ExceptionListResult.IGNORE -> cancelledStartGame = true
|
||||||
cancelled = true;
|
|
||||||
return;
|
|
||||||
case IGNORE:
|
|
||||||
cancelledStartGame = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showCancellationDialog() {
|
private fun showCancellationDialog() {
|
||||||
IExceptionDetails.ExceptionListResult exceptionListResult;
|
val cancellationResult: CancellationResult
|
||||||
try {
|
cancellationResult = try {
|
||||||
exceptionListResult = ui.showCancellationDialog().get();
|
ui.showCancellationDialog().get()
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
} catch (e: InterruptedException) {
|
||||||
// Interrupted means cancelled???
|
// Interrupted means cancelled???
|
||||||
ui.handleExceptionAndExit(e);
|
ui.handleExceptionAndExit(e)
|
||||||
return;
|
return
|
||||||
|
} catch (e: ExecutionException) {
|
||||||
|
ui.handleExceptionAndExit(e)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
switch (exceptionListResult) {
|
when (cancellationResult) {
|
||||||
case CONTINUE:
|
CancellationResult.QUIT -> cancelled = true
|
||||||
throw new RuntimeException("Continuation not allowed here!");
|
CancellationResult.CONTINUE -> cancelledStartGame = true
|
||||||
case CANCEL:
|
|
||||||
cancelled = true;
|
|
||||||
return;
|
|
||||||
case IGNORE:
|
|
||||||
cancelledStartGame = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleCancellation() {
|
private fun handleCancellation() {
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
System.out.println("Update cancelled by user!");
|
println("Update cancelled by user!")
|
||||||
System.exit(1);
|
exitProcess(1)
|
||||||
} else if (cancelledStartGame) {
|
} else if (cancelledStartGame) {
|
||||||
System.out.println("Update cancelled by user! Continuing to start game...");
|
println("Update cancelled by user! Continuing to start game...")
|
||||||
System.exit(0);
|
exitProcess(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -74,10 +74,11 @@ class IndexFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: throw some kind of exception?
|
// TODO: throw some kind of exception?
|
||||||
val name: String?
|
val name: String
|
||||||
get() {
|
get() {
|
||||||
if (metafile) {
|
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"
|
return file?.run { Paths.get(path).fileName.toString() } ?: "Invalid file"
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package link.infra.packwiz.installer.ui
|
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.CompletableFuture
|
||||||
import java.util.concurrent.Future
|
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>()
|
val future = CompletableFuture<ExceptionListResult>()
|
||||||
future.complete(ExceptionListResult.CANCEL)
|
future.complete(ExceptionListResult.CANCEL)
|
||||||
return future
|
return future
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
package link.infra.packwiz.installer.ui
|
||||||
|
|
||||||
|
data class ExceptionDetails(
|
||||||
|
val name: String,
|
||||||
|
val exception: Exception
|
||||||
|
)
|
@ -1,6 +1,5 @@
|
|||||||
package link.infra.packwiz.installer.ui
|
package link.infra.packwiz.installer.ui
|
||||||
|
|
||||||
import link.infra.packwiz.installer.ui.IExceptionDetails.ExceptionListResult
|
|
||||||
import java.awt.BorderLayout
|
import java.awt.BorderLayout
|
||||||
import java.awt.Desktop
|
import java.awt.Desktop
|
||||||
import java.awt.event.WindowAdapter
|
import java.awt.event.WindowAdapter
|
||||||
@ -14,10 +13,10 @@ import java.util.concurrent.CompletableFuture
|
|||||||
import javax.swing.*
|
import javax.swing.*
|
||||||
import javax.swing.border.EmptyBorder
|
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 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 getSize() = details.size
|
||||||
override fun getElementAt(index: Int) = details[index].name
|
override fun getElementAt(index: Int) = details[index].name
|
||||||
fun getExceptionAt(index: Int) = details[index].exception
|
fun getExceptionAt(index: Int) = details[index].exception
|
||||||
@ -90,7 +89,7 @@ internal class ExceptionListWindow(eList: List<IExceptionDetails>, future: Compl
|
|||||||
add(JButton("Continue").apply {
|
add(JButton("Continue").apply {
|
||||||
toolTipText = "Attempt to continue installing, excluding the failed downloads"
|
toolTipText = "Attempt to continue installing, excluding the failed downloads"
|
||||||
addActionListener {
|
addActionListener {
|
||||||
future.complete(ExceptionListResult.CONTINUE)
|
future.complete(IUserInterface.ExceptionListResult.CONTINUE)
|
||||||
this@ExceptionListWindow.dispose()
|
this@ExceptionListWindow.dispose()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -98,7 +97,7 @@ internal class ExceptionListWindow(eList: List<IExceptionDetails>, future: Compl
|
|||||||
add(JButton("Cancel launch").apply {
|
add(JButton("Cancel launch").apply {
|
||||||
toolTipText = "Stop launching the game"
|
toolTipText = "Stop launching the game"
|
||||||
addActionListener {
|
addActionListener {
|
||||||
future.complete(ExceptionListResult.CANCEL)
|
future.complete(IUserInterface.ExceptionListResult.CANCEL)
|
||||||
this@ExceptionListWindow.dispose()
|
this@ExceptionListWindow.dispose()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -107,7 +106,7 @@ internal class ExceptionListWindow(eList: List<IExceptionDetails>, future: Compl
|
|||||||
toolTipText = "Start the game without attempting to update"
|
toolTipText = "Start the game without attempting to update"
|
||||||
isEnabled = allowsIgnore
|
isEnabled = allowsIgnore
|
||||||
addActionListener {
|
addActionListener {
|
||||||
future.complete(ExceptionListResult.IGNORE)
|
future.complete(IUserInterface.ExceptionListResult.IGNORE)
|
||||||
this@ExceptionListWindow.dispose()
|
this@ExceptionListWindow.dispose()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -139,13 +138,13 @@ internal class ExceptionListWindow(eList: List<IExceptionDetails>, future: Compl
|
|||||||
|
|
||||||
addWindowListener(object : WindowAdapter() {
|
addWindowListener(object : WindowAdapter() {
|
||||||
override fun windowClosing(e: WindowEvent) {
|
override fun windowClosing(e: WindowEvent) {
|
||||||
future.complete(ExceptionListResult.CANCEL)
|
future.complete(IUserInterface.ExceptionListResult.CANCEL)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun windowClosed(e: WindowEvent) {
|
override fun windowClosed(e: WindowEvent) {
|
||||||
// Just in case closing didn't get triggered - if something else called dispose() the
|
// Just in case closing didn't get triggered - if something else called dispose() the
|
||||||
// future will have already completed
|
// future will have already completed
|
||||||
future.complete(ExceptionListResult.CANCEL)
|
future.complete(IUserInterface.ExceptionListResult.CANCEL)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.ui
|
|
||||||
|
|
||||||
interface IExceptionDetails {
|
|
||||||
val exception: Exception
|
|
||||||
val name: String
|
|
||||||
|
|
||||||
enum class ExceptionListResult {
|
|
||||||
CONTINUE, CANCEL, IGNORE
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +1,5 @@
|
|||||||
package link.infra.packwiz.installer.ui
|
package link.infra.packwiz.installer.ui
|
||||||
|
|
||||||
import link.infra.packwiz.installer.ui.IExceptionDetails.ExceptionListResult
|
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
import java.util.concurrent.Future
|
import java.util.concurrent.Future
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
@ -21,14 +20,22 @@ interface IUserInterface {
|
|||||||
// Return true if the installation was cancelled!
|
// Return true if the installation was cancelled!
|
||||||
fun showOptions(options: List<IOptionDetails>): Future<Boolean>
|
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
|
@JvmDefault
|
||||||
fun disableOptionsButton() {}
|
fun disableOptionsButton() {}
|
||||||
// Should not return CONTINUE
|
|
||||||
@JvmDefault
|
@JvmDefault
|
||||||
fun showCancellationDialog(): Future<ExceptionListResult> {
|
fun showCancellationDialog(): Future<CancellationResult> {
|
||||||
return CompletableFuture<ExceptionListResult>().apply {
|
return CompletableFuture<CancellationResult>().apply {
|
||||||
complete(ExceptionListResult.CANCEL)
|
complete(CancellationResult.QUIT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class ExceptionListResult {
|
||||||
|
CONTINUE, CANCEL, IGNORE
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class CancellationResult {
|
||||||
|
QUIT, CONTINUE
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
package link.infra.packwiz.installer.ui
|
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.awt.*
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
import java.util.concurrent.Future
|
import java.util.concurrent.Future
|
||||||
@ -186,7 +186,7 @@ class InstallWindow : IUserInterface {
|
|||||||
return future
|
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>()
|
val future = CompletableFuture<ExceptionListResult>()
|
||||||
EventQueue.invokeLater {
|
EventQueue.invokeLater {
|
||||||
ExceptionListWindow(exceptions, future, numTotal, allowsIgnore, frmPackwizlauncher).apply {
|
ExceptionListWindow(exceptions, future, numTotal, allowsIgnore, frmPackwizlauncher).apply {
|
||||||
@ -204,15 +204,15 @@ class InstallWindow : IUserInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showCancellationDialog(): Future<ExceptionListResult> {
|
override fun showCancellationDialog(): Future<IUserInterface.CancellationResult> {
|
||||||
val future = CompletableFuture<ExceptionListResult>()
|
val future = CompletableFuture<IUserInterface.CancellationResult>()
|
||||||
EventQueue.invokeLater {
|
EventQueue.invokeLater {
|
||||||
val buttons = arrayOf("Quit", "Ignore")
|
val buttons = arrayOf("Quit", "Ignore")
|
||||||
val result = JOptionPane.showOptionDialog(frmPackwizlauncher,
|
val result = JOptionPane.showOptionDialog(frmPackwizlauncher,
|
||||||
"The installation was cancelled. Would you like to quit the game, or ignore the update and start the game?",
|
"The installation was cancelled. Would you like to quit the game, or ignore the update and start the game?",
|
||||||
"Cancelled installation",
|
"Cancelled installation",
|
||||||
JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, buttons, buttons[0])
|
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
|
return future
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user