mirror of
https://github.com/packwiz/packwiz-installer.git
synced 2025-04-20 13:36:30 +02:00
Compare commits
No commits in common. "main" and "v0.1.2" have entirely different histories.
27
.github/workflows/pr.yml
vendored
27
.github/workflows/pr.yml
vendored
@ -1,27 +0,0 @@
|
|||||||
name: Java Gradle Build
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Set up JDK 8
|
|
||||||
uses: actions/setup-java@v2
|
|
||||||
with:
|
|
||||||
java-version: '8'
|
|
||||||
distribution: 'temurin'
|
|
||||||
cache: gradle
|
|
||||||
- name: Build with Gradle
|
|
||||||
run: ./gradlew build
|
|
||||||
- name: Cleanup Gradle Cache
|
|
||||||
# Remove some files from the Gradle cache, so they aren't cached by GitHub Actions.
|
|
||||||
# Restoring these files from a GitHub Actions cache might cause problems for future builds.
|
|
||||||
run: |
|
|
||||||
rm -f ~/.gradle/caches/modules-2/modules-2.lock
|
|
||||||
rm -f ~/.gradle/caches/modules-2/gc.properties
|
|
29
.github/workflows/release.yml
vendored
29
.github/workflows/release.yml
vendored
@ -1,29 +0,0 @@
|
|||||||
name: Java Gradle Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v\d+.\d+.\d+'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Set up JDK 8
|
|
||||||
uses: actions/setup-java@v2
|
|
||||||
with:
|
|
||||||
java-version: '8'
|
|
||||||
distribution: 'temurin'
|
|
||||||
cache: gradle
|
|
||||||
- name: Publish with Gradle
|
|
||||||
run: ./gradlew publish -Pgithub.token="${{ secrets.GITHUB_TOKEN }}" -Pbunnycdn.token="${{ secrets.BUNNYCDN_TOKEN }}" -Prelease=true
|
|
||||||
- name: Cleanup Gradle Cache
|
|
||||||
# Remove some files from the Gradle cache, so they aren't cached by GitHub Actions.
|
|
||||||
# Restoring these files from a GitHub Actions cache might cause problems for future builds.
|
|
||||||
run: |
|
|
||||||
rm -f ~/.gradle/caches/modules-2/modules-2.lock
|
|
||||||
rm -f ~/.gradle/caches/modules-2/gc.properties
|
|
29
.github/workflows/snapshot.yml
vendored
29
.github/workflows/snapshot.yml
vendored
@ -1,29 +0,0 @@
|
|||||||
name: Java Gradle Snapshot
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- 'main'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Set up JDK 8
|
|
||||||
uses: actions/setup-java@v2
|
|
||||||
with:
|
|
||||||
java-version: '8'
|
|
||||||
distribution: 'temurin'
|
|
||||||
cache: gradle
|
|
||||||
- name: Publish with Gradle
|
|
||||||
run: ./gradlew publish -Pgithub.token="${{ secrets.GITHUB_TOKEN }}" -Pbunnycdn.token="${{ secrets.BUNNYCDN_TOKEN }}"
|
|
||||||
- name: Cleanup Gradle Cache
|
|
||||||
# Remove some files from the Gradle cache, so they aren't cached by GitHub Actions.
|
|
||||||
# Restoring these files from a GitHub Actions cache might cause problems for future builds.
|
|
||||||
run: |
|
|
||||||
rm -f ~/.gradle/caches/modules-2/modules-2.lock
|
|
||||||
rm -f ~/.gradle/caches/modules-2/gc.properties
|
|
30
.gitignore
vendored
30
.gitignore
vendored
@ -1,9 +1,9 @@
|
|||||||
|
|
||||||
# Created by https://www.toptal.com/developers/gitignore/api/java,gradle,intellij+all
|
# Created by https://www.gitignore.io/api/java,gradle,intellij
|
||||||
# Edit at https://www.toptal.com/developers/gitignore?templates=java,gradle,intellij+all
|
# Edit at https://www.gitignore.io/?templates=java,gradle,intellij
|
||||||
|
|
||||||
### Intellij+all ###
|
### Intellij ###
|
||||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
|
||||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
# User-specific stuff
|
# User-specific stuff
|
||||||
@ -33,9 +33,6 @@
|
|||||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||||
# since they will be recreated, and may cause churn. Uncomment if using
|
# since they will be recreated, and may cause churn. Uncomment if using
|
||||||
# auto-import.
|
# auto-import.
|
||||||
# .idea/artifacts
|
|
||||||
# .idea/compiler.xml
|
|
||||||
# .idea/jarRepositories.xml
|
|
||||||
# .idea/modules.xml
|
# .idea/modules.xml
|
||||||
# .idea/*.iml
|
# .idea/*.iml
|
||||||
# .idea/modules
|
# .idea/modules
|
||||||
@ -75,18 +72,13 @@ fabric.properties
|
|||||||
# Android studio 3.1+ serialized cache file
|
# Android studio 3.1+ serialized cache file
|
||||||
.idea/caches/build_file_checksums.ser
|
.idea/caches/build_file_checksums.ser
|
||||||
|
|
||||||
### Intellij+all Patch ###
|
### Intellij Patch ###
|
||||||
# Ignores the whole .idea folder and all .iml files
|
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
|
||||||
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
|
|
||||||
|
|
||||||
.idea/
|
# *.iml
|
||||||
|
# modules.xml
|
||||||
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
|
# .idea/misc.xml
|
||||||
|
# *.ipr
|
||||||
*.iml
|
|
||||||
modules.xml
|
|
||||||
.idea/misc.xml
|
|
||||||
*.ipr
|
|
||||||
|
|
||||||
# Sonarlint plugin
|
# Sonarlint plugin
|
||||||
.idea/sonarlint
|
.idea/sonarlint
|
||||||
@ -135,4 +127,4 @@ gradle-app.setting
|
|||||||
### Gradle Patch ###
|
### Gradle Patch ###
|
||||||
**/build/
|
**/build/
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/java,gradle,intellij+all
|
# End of https://www.gitignore.io/api/java,gradle,intellij
|
36
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
36
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="JavaDoc" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="TOP_LEVEL_CLASS_OPTIONS">
|
||||||
|
<value>
|
||||||
|
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
|
||||||
|
<option name="REQUIRED_TAGS" value="" />
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
<option name="INNER_CLASS_OPTIONS">
|
||||||
|
<value>
|
||||||
|
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
|
||||||
|
<option name="REQUIRED_TAGS" value="" />
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
<option name="METHOD_OPTIONS">
|
||||||
|
<value>
|
||||||
|
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
|
||||||
|
<option name="REQUIRED_TAGS" value="@return@param@throws or @exception" />
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
<option name="FIELD_OPTIONS">
|
||||||
|
<value>
|
||||||
|
<option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" />
|
||||||
|
<option name="REQUIRED_TAGS" value="" />
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
<option name="IGNORE_DEPRECATED" value="false" />
|
||||||
|
<option name="IGNORE_JAVADOC_PERIOD" value="true" />
|
||||||
|
<option name="IGNORE_DUPLICATED_THROWS" value="false" />
|
||||||
|
<option name="IGNORE_POINT_TO_ITSELF" value="false" />
|
||||||
|
<option name="myAdditionalJavadocTags" value="wbp.parser.entryPoint" />
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
10
.idea/misc.xml
generated
Normal file
10
.idea/misc.xml
generated
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="EntryPointsManager">
|
||||||
|
<list size="1">
|
||||||
|
<item index="0" class="java.lang.String" itemvalue="com.google.gson.annotations.SerializedName" />
|
||||||
|
</list>
|
||||||
|
</component>
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="false" project-jdk-name="11" project-jdk-type="JavaSDK" />
|
||||||
|
</project>
|
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2021 comp500
|
Copyright (c) 2019
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
# packwiz-installer
|
# packwiz-installer
|
||||||
An installer for launching packwiz modpacks with MultiMC. You'll need [the bootstrapper](https://github.com/comp500/packwiz-installer-bootstrap/releases) to actually use this.
|
An installer for launching packwiz modpacks with MultiMC.
|
70
build.gradle
Normal file
70
build.gradle
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
plugins {
|
||||||
|
id 'java'
|
||||||
|
id 'application'
|
||||||
|
id 'com.github.johnrengelman.shadow' version '5.0.0'
|
||||||
|
id 'com.palantir.git-version' version '0.11.0'
|
||||||
|
id 'com.github.breadmoirai.github-release' version '2.2.9'
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceCompatibility = 1.8
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation 'commons-cli:commons-cli:1.4'
|
||||||
|
implementation 'com.moandjiezana.toml:toml4j:0.7.2'
|
||||||
|
// TODO: Implement tests
|
||||||
|
//testImplementation 'junit:junit:4.12'
|
||||||
|
implementation 'com.google.code.gson:gson:2.8.1'
|
||||||
|
implementation 'com.squareup.okio:okio:2.2.2'
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
jcenter()
|
||||||
|
}
|
||||||
|
|
||||||
|
mainClassName = 'link.infra.packwiz.installer.RequiresBootstrap'
|
||||||
|
version gitVersion()
|
||||||
|
|
||||||
|
jar {
|
||||||
|
manifest {
|
||||||
|
attributes(
|
||||||
|
'Main-Class': 'link.infra.packwiz.installer.RequiresBootstrap',
|
||||||
|
'Implementation-Version': project.version
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commons CLI and Minimal JSON are already included in packwiz-installer-bootstrap
|
||||||
|
shadowJar {
|
||||||
|
dependencies {
|
||||||
|
exclude(dependency('commons-cli:commons-cli:1.4'))
|
||||||
|
exclude(dependency('com.eclipsesource.minimal-json:minimal-json:0.9.5'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used for vscode launch.json
|
||||||
|
task copyJar(type: Copy) {
|
||||||
|
from shadowJar
|
||||||
|
rename "packwiz-installer-(.*)\\.jar", "packwiz-installer.jar"
|
||||||
|
into "build/libs/"
|
||||||
|
}
|
||||||
|
|
||||||
|
build.dependsOn copyJar
|
||||||
|
|
||||||
|
githubRelease {
|
||||||
|
// IntelliJ u ok?
|
||||||
|
//noinspection GroovyAssignabilityCheck
|
||||||
|
owner "comp500"
|
||||||
|
//noinspection GroovyAssignabilityCheck
|
||||||
|
repo "packwiz-installer"
|
||||||
|
//noinspection GroovyAssignabilityCheck
|
||||||
|
tagName "${project.version}"
|
||||||
|
//noinspection GroovyAssignabilityCheck
|
||||||
|
releaseName "Release ${project.version}"
|
||||||
|
//noinspection GroovyAssignabilityCheck
|
||||||
|
draft true
|
||||||
|
//noinspection GroovyAssignabilityCheck
|
||||||
|
token getProperty("github.token")
|
||||||
|
releaseAssets = [jar.destinationDirectory.file("packwiz-installer.jar").get()]
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.githubRelease.dependsOn(build)
|
206
build.gradle.kts
206
build.gradle.kts
@ -1,206 +0,0 @@
|
|||||||
plugins {
|
|
||||||
java
|
|
||||||
application
|
|
||||||
id("com.github.johnrengelman.shadow") version "7.1.2"
|
|
||||||
id("com.palantir.git-version") version "0.13.0"
|
|
||||||
id("com.github.breadmoirai.github-release") version "2.4.1"
|
|
||||||
kotlin("jvm") version "1.7.10"
|
|
||||||
id("com.github.jk1.dependency-license-report") version "2.0"
|
|
||||||
`maven-publish`
|
|
||||||
}
|
|
||||||
|
|
||||||
java {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
google()
|
|
||||||
maven {
|
|
||||||
url = uri("https://jitpack.io")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val r8 by configurations.creating
|
|
||||||
val distJarOutput by configurations.creating {
|
|
||||||
isCanBeResolved = false
|
|
||||||
isCanBeConsumed = true
|
|
||||||
|
|
||||||
attributes {
|
|
||||||
attribute(Usage.USAGE_ATTRIBUTE, project.objects.named(Usage::class.java, Usage.JAVA_RUNTIME))
|
|
||||||
attribute(Bundling.BUNDLING_ATTRIBUTE, project.objects.named(Bundling::class.java, Bundling.EMBEDDED))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation("commons-cli:commons-cli:1.5.0")
|
|
||||||
implementation("com.google.code.gson:gson:2.9.0")
|
|
||||||
implementation("com.squareup.okio:okio:3.1.0")
|
|
||||||
implementation(kotlin("stdlib-jdk8"))
|
|
||||||
implementation("com.squareup.okhttp3:okhttp:4.10.0")
|
|
||||||
implementation("cc.ekblad:4koma:1.1.0")
|
|
||||||
|
|
||||||
r8("com.android.tools:r8:3.3.28")
|
|
||||||
}
|
|
||||||
|
|
||||||
application {
|
|
||||||
mainClass.set("link.infra.packwiz.installer.RequiresBootstrap")
|
|
||||||
}
|
|
||||||
|
|
||||||
val gitVersion: groovy.lang.Closure<*> by extra
|
|
||||||
version = gitVersion()
|
|
||||||
|
|
||||||
tasks.jar {
|
|
||||||
manifest {
|
|
||||||
attributes["Main-Class"] = "link.infra.packwiz.installer.RequiresBootstrap"
|
|
||||||
attributes["Implementation-Version"] = project.version
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
licenseReport {
|
|
||||||
renderers = arrayOf<com.github.jk1.license.render.ReportRenderer>(
|
|
||||||
com.github.jk1.license.render.InventoryMarkdownReportRenderer("licenses.md", "packwiz-installer")
|
|
||||||
)
|
|
||||||
filters = arrayOf<com.github.jk1.license.filter.DependencyFilter>(com.github.jk1.license.filter.LicenseBundleNormalizer())
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.shadowJar {
|
|
||||||
// 4koma uses kotlin-reflect; requires Kotlin metadata
|
|
||||||
//exclude("**/*.kotlin_metadata")
|
|
||||||
//exclude("**/*.kotlin_builtins")
|
|
||||||
exclude("META-INF/maven/**/*")
|
|
||||||
exclude("META-INF/proguard/**/*")
|
|
||||||
|
|
||||||
// Relocate Commons CLI, so that it doesn't clash with old packwiz-installer-bootstrap (that shades it)
|
|
||||||
relocate("org.apache.commons.cli", "link.infra.packwiz.installer.deps.commons-cli")
|
|
||||||
|
|
||||||
// from Commons CLI
|
|
||||||
exclude("META-INF/LICENSE.txt")
|
|
||||||
exclude("META-INF/NOTICE.txt")
|
|
||||||
}
|
|
||||||
|
|
||||||
val shrinkJar by tasks.registering(JavaExec::class) {
|
|
||||||
val rules = file("src/main/proguard.txt")
|
|
||||||
val r8File = base.libsDirectory.file(provider {
|
|
||||||
base.archivesName.get() + "-" + project.version + "-all-shrink.jar"
|
|
||||||
})
|
|
||||||
dependsOn(configurations.named("runtimeClasspath"))
|
|
||||||
inputs.files(tasks.shadowJar, rules)
|
|
||||||
outputs.file(r8File)
|
|
||||||
|
|
||||||
classpath(r8)
|
|
||||||
mainClass.set("com.android.tools.r8.R8")
|
|
||||||
args = mutableListOf(
|
|
||||||
"--release",
|
|
||||||
"--classfile",
|
|
||||||
"--output", r8File.get().toString(),
|
|
||||||
"--pg-conf", rules.toString(),
|
|
||||||
"--lib", System.getProperty("java.home"),
|
|
||||||
*(if (System.getProperty("java.version").startsWith("1.")) {
|
|
||||||
// javax.crypto, necessary on <1.9 for compiling Okio
|
|
||||||
arrayOf("--lib", System.getProperty("java.home") + "/lib/jce.jar")
|
|
||||||
} else { arrayOf() }),
|
|
||||||
tasks.shadowJar.get().archiveFile.get().asFile.toString()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MANIFEST.MF must be one of the first 2 entries in the zip for JarInputStream to see it
|
|
||||||
// Gradle's JAR creation handles this whereas R8 doesn't, so the dist JAR is repacked
|
|
||||||
val distJar by tasks.registering(Jar::class) {
|
|
||||||
from(shrinkJar.map { zipTree(it.outputs.files.singleFile) })
|
|
||||||
archiveClassifier.set("all-repacked")
|
|
||||||
manifest {
|
|
||||||
from(shrinkJar.map { zipTree(it.outputs.files.singleFile).matching {
|
|
||||||
include("META-INF/MANIFEST.MF")
|
|
||||||
}.singleFile })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
artifacts {
|
|
||||||
add("distJarOutput", distJar) {
|
|
||||||
classifier = "dist"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used for vscode launch.json
|
|
||||||
val copyJar by tasks.registering(Copy::class) {
|
|
||||||
from(distJar)
|
|
||||||
rename("packwiz-installer-(.*)\\.jar", "packwiz-installer.jar")
|
|
||||||
into(layout.buildDirectory.dir("dist"))
|
|
||||||
outputs.file(layout.buildDirectory.dir("dist").map { it.file("packwiz-installer.jar") })
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.build {
|
|
||||||
dependsOn(copyJar)
|
|
||||||
}
|
|
||||||
|
|
||||||
githubRelease {
|
|
||||||
owner("comp500")
|
|
||||||
repo("packwiz-installer")
|
|
||||||
tagName("${project.version}")
|
|
||||||
releaseName("Release ${project.version}")
|
|
||||||
draft(true)
|
|
||||||
token(findProperty("github.token") as String?)
|
|
||||||
releaseAssets(layout.buildDirectory.dir("dist").map { it.file("packwiz-installer.jar") }.get())
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.githubRelease {
|
|
||||||
dependsOn(copyJar)
|
|
||||||
enabled = project.hasProperty("github.token") && project.findProperty("release") == "true"
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.publish {
|
|
||||||
dependsOn(tasks.githubRelease)
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.compileKotlin {
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "1.8"
|
|
||||||
freeCompilerArgs = listOf("-Xjvm-default=all", "-Xallow-result-return-type", "-opt-in=kotlin.io.path.ExperimentalPathApi", "-Xlambdas=indy")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tasks.compileTestKotlin {
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "1.8"
|
|
||||||
freeCompilerArgs = listOf("-Xjvm-default=all", "-Xallow-result-return-type", "-opt-in=kotlin.io.path.ExperimentalPathApi", "-Xlambdas=indy")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val javaComponent = components["java"] as AdhocComponentWithVariants
|
|
||||||
javaComponent.addVariantsFromConfiguration(distJarOutput) {
|
|
||||||
mapToMavenScope("runtime")
|
|
||||||
mapToOptional()
|
|
||||||
}
|
|
||||||
javaComponent.withVariantsFromConfiguration(configurations["shadowRuntimeElements"]) {
|
|
||||||
skip()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (project.hasProperty("bunnycdn.token")) {
|
|
||||||
publishing {
|
|
||||||
publications {
|
|
||||||
create<MavenPublication>("maven") {
|
|
||||||
groupId = "link.infra.packwiz"
|
|
||||||
artifactId = "packwiz-installer"
|
|
||||||
|
|
||||||
from(components["java"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
repositories {
|
|
||||||
maven {
|
|
||||||
url = if (project.findProperty("release") == "true") {
|
|
||||||
uri("https://storage.bunnycdn.com/comp-maven/repository/release")
|
|
||||||
} else {
|
|
||||||
uri("https://storage.bunnycdn.com/comp-maven/repository/snapshot")
|
|
||||||
}
|
|
||||||
credentials(HttpHeaderCredentials::class) {
|
|
||||||
name = "AccessKey"
|
|
||||||
value = findProperty("bunnycdn.token") as String?
|
|
||||||
}
|
|
||||||
authentication {
|
|
||||||
create<HttpHeaderAuthentication>("header")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
268
gradlew
vendored
Executable file → Normal file
268
gradlew
vendored
Executable file → Normal file
@ -1,13 +1,13 @@
|
|||||||
#!/bin/sh
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
#
|
#
|
||||||
# Copyright © 2015-2021 the original authors.
|
# Copyright 2015 the original author or authors.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
# You may obtain a copy of the License at
|
# You may obtain a copy of the License at
|
||||||
#
|
#
|
||||||
# https://www.apache.org/licenses/LICENSE-2.0
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
#
|
#
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
@ -17,113 +17,78 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
##
|
||||||
# Gradle start up script for POSIX generated by Gradle.
|
## Gradle start up script for UN*X
|
||||||
#
|
##
|
||||||
# Important for running:
|
|
||||||
#
|
|
||||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
|
||||||
# noncompliant, but you have some other compliant shell such as ksh or
|
|
||||||
# bash, then to run this script, type that shell name before the whole
|
|
||||||
# command line, like:
|
|
||||||
#
|
|
||||||
# ksh Gradle
|
|
||||||
#
|
|
||||||
# Busybox and similar reduced shells will NOT work, because this script
|
|
||||||
# requires all of these POSIX shell features:
|
|
||||||
# * functions;
|
|
||||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
|
||||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
|
||||||
# * compound commands having a testable exit status, especially «case»;
|
|
||||||
# * various built-in commands including «command», «set», and «ulimit».
|
|
||||||
#
|
|
||||||
# Important for patching:
|
|
||||||
#
|
|
||||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
|
||||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
|
||||||
#
|
|
||||||
# The "traditional" practice of packing multiple parameters into a
|
|
||||||
# space-separated string is a well documented source of bugs and security
|
|
||||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
|
||||||
# options in "$@", and eventually passing that to Java.
|
|
||||||
#
|
|
||||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
|
||||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
|
||||||
# see the in-line comments for details.
|
|
||||||
#
|
|
||||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
|
||||||
# Darwin, MinGW, and NonStop.
|
|
||||||
#
|
|
||||||
# (3) This script is generated from the Groovy template
|
|
||||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
|
||||||
# within the Gradle project.
|
|
||||||
#
|
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
|
||||||
#
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
# Attempt to set APP_HOME
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
# Resolve links: $0 may be a link
|
# Resolve links: $0 may be a link
|
||||||
app_path=$0
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
# Need this for daisy-chained symlinks.
|
while [ -h "$PRG" ] ; do
|
||||||
while
|
ls=`ls -ld "$PRG"`
|
||||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
[ -h "$app_path" ]
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
do
|
PRG="$link"
|
||||||
ls=$( ls -ld "$app_path" )
|
else
|
||||||
link=${ls#*' -> '}
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
case $link in #(
|
fi
|
||||||
/*) app_path=$link ;; #(
|
|
||||||
*) app_path=$APP_HOME$link ;;
|
|
||||||
esac
|
|
||||||
done
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
APP_NAME="Gradle"
|
APP_NAME="Gradle"
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD="maximum"
|
||||||
|
|
||||||
warn () {
|
warn () {
|
||||||
echo "$*"
|
echo "$*"
|
||||||
} >&2
|
}
|
||||||
|
|
||||||
die () {
|
die () {
|
||||||
echo
|
echo
|
||||||
echo "$*"
|
echo "$*"
|
||||||
echo
|
echo
|
||||||
exit 1
|
exit 1
|
||||||
} >&2
|
}
|
||||||
|
|
||||||
# OS specific support (must be 'true' or 'false').
|
# OS specific support (must be 'true' or 'false').
|
||||||
cygwin=false
|
cygwin=false
|
||||||
msys=false
|
msys=false
|
||||||
darwin=false
|
darwin=false
|
||||||
nonstop=false
|
nonstop=false
|
||||||
case "$( uname )" in #(
|
case "`uname`" in
|
||||||
CYGWIN* ) cygwin=true ;; #(
|
CYGWIN* )
|
||||||
Darwin* ) darwin=true ;; #(
|
cygwin=true
|
||||||
MSYS* | MINGW* ) msys=true ;; #(
|
;;
|
||||||
NONSTOP* ) nonstop=true ;;
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
NONSTOP* )
|
||||||
|
nonstop=true
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
# Determine the Java command to use to start the JVM.
|
||||||
if [ -n "$JAVA_HOME" ] ; then
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
else
|
else
|
||||||
JAVACMD=$JAVA_HOME/bin/java
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
fi
|
fi
|
||||||
if [ ! -x "$JAVACMD" ] ; then
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
@ -132,7 +97,7 @@ Please set the JAVA_HOME variable in your environment to match the
|
|||||||
location of your Java installation."
|
location of your Java installation."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
JAVACMD=java
|
JAVACMD="java"
|
||||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
@ -140,95 +105,84 @@ location of your Java installation."
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
# Increase the maximum file descriptors if we can.
|
||||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||||
case $MAX_FD in #(
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
max*)
|
if [ $? -eq 0 ] ; then
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
warn "Could not query maximum file descriptor limit"
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
esac
|
fi
|
||||||
case $MAX_FD in #(
|
ulimit -n $MAX_FD
|
||||||
'' | soft) :;; #(
|
if [ $? -ne 0 ] ; then
|
||||||
*)
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
ulimit -n "$MAX_FD" ||
|
fi
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
else
|
||||||
esac
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Collect all arguments for the java command, stacking in reverse order:
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
# * args from the command line
|
if $darwin; then
|
||||||
# * the main class name
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
# * -classpath
|
|
||||||
# * -D...appname settings
|
|
||||||
# * --module-path (only if needed)
|
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
|
||||||
|
|
||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
|
||||||
if "$cygwin" || "$msys" ; then
|
|
||||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
|
||||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
|
||||||
|
|
||||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
|
||||||
|
|
||||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
|
||||||
for arg do
|
|
||||||
if
|
|
||||||
case $arg in #(
|
|
||||||
-*) false ;; # don't mess with options #(
|
|
||||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
|
||||||
[ -e "$t" ] ;; #(
|
|
||||||
*) false ;;
|
|
||||||
esac
|
|
||||||
then
|
|
||||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
|
||||||
fi
|
fi
|
||||||
# Roll the args list around exactly as many times as the number of
|
|
||||||
# args, so each arg winds up back in the position where it started, but
|
# For Cygwin, switch paths to Windows format before running java
|
||||||
# possibly modified.
|
if $cygwin ; then
|
||||||
#
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
# NB: a `for` loop captures its iteration list before it begins, so
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
# changing the positional parameters here affects neither the number of
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
# iterations, nor the values presented in `arg`.
|
|
||||||
shift # remove old arg
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
set -- "$@" "$arg" # push replacement arg
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
done
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=$((i+1))
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
(0) set -- ;;
|
||||||
|
(1) set -- "$args0" ;;
|
||||||
|
(2) set -- "$args0" "$args1" ;;
|
||||||
|
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Collect all arguments for the java command;
|
# Escape application args
|
||||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
save () {
|
||||||
# shell script including quotes and variable substitutions, so put them in
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
# double quotes to make sure that they get re-expanded; and
|
echo " "
|
||||||
# * put everything else in single quotes, so that it's not re-expanded.
|
}
|
||||||
|
APP_ARGS=$(save "$@")
|
||||||
|
|
||||||
set -- \
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
-classpath "$CLASSPATH" \
|
|
||||||
org.gradle.wrapper.GradleWrapperMain \
|
|
||||||
"$@"
|
|
||||||
|
|
||||||
# Use "xargs" to parse quoted args.
|
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||||
#
|
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
cd "$(dirname "$0")"
|
||||||
#
|
fi
|
||||||
# In Bash we could simply go:
|
|
||||||
#
|
|
||||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
|
||||||
# set -- "${ARGS[@]}" "$@"
|
|
||||||
#
|
|
||||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
|
||||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
|
||||||
# character that might be a shell metacharacter, then use eval to reverse
|
|
||||||
# that process (while maintaining the separation between arguments), and wrap
|
|
||||||
# the whole thing up as a single "set" statement.
|
|
||||||
#
|
|
||||||
# This will of course break if any of these variables contains a newline or
|
|
||||||
# an unmatched quote.
|
|
||||||
#
|
|
||||||
|
|
||||||
eval "set -- $(
|
|
||||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
|
||||||
xargs -n1 |
|
|
||||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
|
||||||
tr '\n' ' '
|
|
||||||
)" '"$@"'
|
|
||||||
|
|
||||||
exec "$JAVACMD" "$@"
|
exec "$JAVACMD" "$@"
|
||||||
|
27
gradlew.bat
vendored
27
gradlew.bat
vendored
@ -5,7 +5,7 @@
|
|||||||
@rem you may not use this file except in compliance with the License.
|
@rem you may not use this file except in compliance with the License.
|
||||||
@rem You may obtain a copy of the License at
|
@rem You may obtain a copy of the License at
|
||||||
@rem
|
@rem
|
||||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
@rem http://www.apache.org/licenses/LICENSE-2.0
|
||||||
@rem
|
@rem
|
||||||
@rem Unless required by applicable law or agreed to in writing, software
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
@ -29,9 +29,6 @@ if "%DIRNAME%" == "" set DIRNAME=.
|
|||||||
set APP_BASE_NAME=%~n0
|
set APP_BASE_NAME=%~n0
|
||||||
set APP_HOME=%DIRNAME%
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
|
||||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
@ -40,7 +37,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
|||||||
|
|
||||||
set JAVA_EXE=java.exe
|
set JAVA_EXE=java.exe
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if "%ERRORLEVEL%" == "0" goto execute
|
if "%ERRORLEVEL%" == "0" goto init
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
@ -54,7 +51,7 @@ goto fail
|
|||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
if exist "%JAVA_EXE%" goto init
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
@ -64,14 +61,28 @@ echo location of your Java installation.
|
|||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
|
:init
|
||||||
|
@rem Get command-line arguments, handling Windows variants
|
||||||
|
|
||||||
|
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||||
|
|
||||||
|
:win9xME_args
|
||||||
|
@rem Slurp the command line arguments.
|
||||||
|
set CMD_LINE_ARGS=
|
||||||
|
set _SKIP=2
|
||||||
|
|
||||||
|
:win9xME_args_slurp
|
||||||
|
if "x%~1" == "x" goto execute
|
||||||
|
|
||||||
|
set CMD_LINE_ARGS=%*
|
||||||
|
|
||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
244
src/main/java/link/infra/packwiz/installer/DownloadTask.java
Normal file
244
src/main/java/link/infra/packwiz/installer/DownloadTask.java
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
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.SpaceSafeURI;
|
||||||
|
import link.infra.packwiz.installer.metadata.hash.GeneralHashingSource;
|
||||||
|
import link.infra.packwiz.installer.metadata.hash.Hash;
|
||||||
|
import link.infra.packwiz.installer.metadata.hash.HashUtils;
|
||||||
|
import link.infra.packwiz.installer.ui.IExceptionDetails;
|
||||||
|
import link.infra.packwiz.installer.ui.IOptionDetails;
|
||||||
|
import okio.Buffer;
|
||||||
|
import okio.Okio;
|
||||||
|
import okio.Source;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
class DownloadTask implements IOptionDetails, IExceptionDetails {
|
||||||
|
final IndexFile.File metadata;
|
||||||
|
ManifestFile.File cachedFile = null;
|
||||||
|
private Exception failure = null;
|
||||||
|
private boolean alreadyUpToDate = false;
|
||||||
|
private boolean metadataRequired = true;
|
||||||
|
private boolean invalidated = false;
|
||||||
|
// If file is new or isOptional changed to true, the option needs to be presented again
|
||||||
|
private boolean newOptional = true;
|
||||||
|
private final UpdateManager.Options.Side downloadSide;
|
||||||
|
|
||||||
|
private DownloadTask(IndexFile.File metadata, String defaultFormat, UpdateManager.Options.Side downloadSide) {
|
||||||
|
this.metadata = metadata;
|
||||||
|
if (metadata.hashFormat == null || metadata.hashFormat.length() == 0) {
|
||||||
|
metadata.hashFormat = defaultFormat;
|
||||||
|
}
|
||||||
|
this.downloadSide = downloadSide;
|
||||||
|
}
|
||||||
|
|
||||||
|
void invalidate() {
|
||||||
|
invalidated = true;
|
||||||
|
alreadyUpToDate = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateFromCache(ManifestFile.File cachedFile) {
|
||||||
|
if (failure != null) return;
|
||||||
|
if (cachedFile == null) {
|
||||||
|
this.cachedFile = new ManifestFile.File();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cachedFile = cachedFile;
|
||||||
|
|
||||||
|
if (!invalidated) {
|
||||||
|
Hash currHash;
|
||||||
|
try {
|
||||||
|
currHash = HashUtils.getHash(metadata.hashFormat, metadata.hash);
|
||||||
|
} catch (Exception e) {
|
||||||
|
failure = e;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currHash != null && currHash.equals(cachedFile.hash)) {
|
||||||
|
// Already up to date
|
||||||
|
alreadyUpToDate = true;
|
||||||
|
metadataRequired = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cachedFile.isOptional) {
|
||||||
|
// Because option selection dialog might set this task to true/false, metadata is always needed to download
|
||||||
|
// the file, and to show the description and name
|
||||||
|
metadataRequired = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void downloadMetadata(IndexFile parentIndexFile, SpaceSafeURI indexUri) {
|
||||||
|
if (failure != null) return;
|
||||||
|
if (metadataRequired) {
|
||||||
|
try {
|
||||||
|
metadata.downloadMeta(parentIndexFile, indexUri);
|
||||||
|
} catch (Exception e) {
|
||||||
|
failure = e;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (metadata.linkedFile != null) {
|
||||||
|
if (metadata.linkedFile.option != null) {
|
||||||
|
if (metadata.linkedFile.option.optional) {
|
||||||
|
if (cachedFile.isOptional) {
|
||||||
|
// isOptional didn't change
|
||||||
|
newOptional = false;
|
||||||
|
} else {
|
||||||
|
// isOptional false -> true, set option to it's default value
|
||||||
|
// TODO: preserve previous option value, somehow??
|
||||||
|
cachedFile.optionValue = this.metadata.linkedFile.option.defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cachedFile.isOptional = isOptional();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void download(String packFolder, SpaceSafeURI indexUri) {
|
||||||
|
if (failure != null) return;
|
||||||
|
|
||||||
|
// Ensure it is removed
|
||||||
|
if (!cachedFile.optionValue || !correctSide()) {
|
||||||
|
if (cachedFile.cachedLocation == null) return;
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(Paths.get(packFolder, cachedFile.cachedLocation));
|
||||||
|
} catch (IOException e) {
|
||||||
|
// TODO: how much of a problem is this? use log4j/other log library to show warning?
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
cachedFile.cachedLocation = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alreadyUpToDate) return;
|
||||||
|
|
||||||
|
Path destPath = Paths.get(packFolder, metadata.getDestURI().toString());
|
||||||
|
|
||||||
|
// Don't update files marked with preserve if they already exist on disk
|
||||||
|
if (metadata.preserve) {
|
||||||
|
if (Files.exists(destPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Hash hash;
|
||||||
|
String fileHashFormat;
|
||||||
|
if (metadata.linkedFile != null) {
|
||||||
|
hash = metadata.linkedFile.getHash();
|
||||||
|
fileHashFormat = metadata.linkedFile.download.hashFormat;
|
||||||
|
} else {
|
||||||
|
hash = metadata.getHash();
|
||||||
|
fileHashFormat = metadata.hashFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
Source src = metadata.getSource(indexUri);
|
||||||
|
GeneralHashingSource fileSource = HashUtils.getHasher(fileHashFormat).getHashingSource(src);
|
||||||
|
Buffer data = new Buffer();
|
||||||
|
Okio.buffer(fileSource).readAll(data);
|
||||||
|
|
||||||
|
if (fileSource.hashIsEqual(hash)) {
|
||||||
|
Files.createDirectories(destPath.getParent());
|
||||||
|
Files.copy(data.inputStream(), destPath, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
} else {
|
||||||
|
// TODO: no more SYSOUT!!!!!!!!!
|
||||||
|
System.out.println("Invalid hash for " + metadata.getDestURI().toString());
|
||||||
|
System.out.println("Calculated: " + fileSource.getHash());
|
||||||
|
System.out.println("Expected: " + hash);
|
||||||
|
failure = new Exception("Hash invalid!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cachedFile.cachedLocation != null && !destPath.equals(Paths.get(packFolder, cachedFile.cachedLocation))) {
|
||||||
|
// Delete old file if location changes
|
||||||
|
Files.delete(Paths.get(packFolder, cachedFile.cachedLocation));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
failure = e;
|
||||||
|
}
|
||||||
|
if (failure == null) {
|
||||||
|
if (cachedFile == null) {
|
||||||
|
cachedFile = new ManifestFile.File();
|
||||||
|
}
|
||||||
|
// Update the manifest file
|
||||||
|
try {
|
||||||
|
cachedFile.hash = metadata.getHash();
|
||||||
|
} catch (Exception e) {
|
||||||
|
failure = e;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cachedFile.isOptional = isOptional();
|
||||||
|
cachedFile.cachedLocation = metadata.getDestURI().toString();
|
||||||
|
if (metadata.linkedFile != null) {
|
||||||
|
try {
|
||||||
|
cachedFile.linkedFileHash = metadata.linkedFile.getHash();
|
||||||
|
} catch (Exception e) {
|
||||||
|
failure = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Exception getException() {
|
||||||
|
return failure;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isOptional() {
|
||||||
|
if (metadata.linkedFile != null) {
|
||||||
|
return metadata.linkedFile.isOptional();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isNewOptional() {
|
||||||
|
return isOptional() && this.newOptional;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean correctSide() {
|
||||||
|
if (metadata.linkedFile != null) {
|
||||||
|
return metadata.linkedFile.side.hasSide(downloadSide);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return metadata.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getOptionValue() {
|
||||||
|
return cachedFile.optionValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getOptionDescription() {
|
||||||
|
if (metadata.linkedFile != null) {
|
||||||
|
return metadata.linkedFile.option.description;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOptionValue(boolean value) {
|
||||||
|
if (value && !cachedFile.optionValue) {
|
||||||
|
// Ensure that an update is done if it changes from false to true, or from true to false
|
||||||
|
alreadyUpToDate = false;
|
||||||
|
}
|
||||||
|
cachedFile.optionValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<DownloadTask> createTasksFromIndex(IndexFile index, String defaultFormat, UpdateManager.Options.Side downloadSide) {
|
||||||
|
ArrayList<DownloadTask> tasks = new ArrayList<>();
|
||||||
|
for (IndexFile.File file : index.files) {
|
||||||
|
tasks.add(new DownloadTask(file, defaultFormat, downloadSide));
|
||||||
|
}
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
148
src/main/java/link/infra/packwiz/installer/Main.java
Normal file
148
src/main/java/link/infra/packwiz/installer/Main.java
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
package link.infra.packwiz.installer;
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.metadata.SpaceSafeURI;
|
||||||
|
import link.infra.packwiz.installer.ui.CLIHandler;
|
||||||
|
import link.infra.packwiz.installer.ui.IUserInterface;
|
||||||
|
import link.infra.packwiz.installer.ui.InputStateHandler;
|
||||||
|
import link.infra.packwiz.installer.ui.InstallWindow;
|
||||||
|
import org.apache.commons.cli.*;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class Main {
|
||||||
|
|
||||||
|
// Actual main() is in RequiresBootstrap!
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public Main(String[] args) {
|
||||||
|
// Big overarching try/catch just in case everything breaks
|
||||||
|
try {
|
||||||
|
this.startup(args);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
EventQueue.invokeLater(() -> {
|
||||||
|
JOptionPane.showMessageDialog(null,
|
||||||
|
"A fatal error occurred: \n" + e.getClass().getCanonicalName() + ": " + e.getMessage(),
|
||||||
|
"packwiz-installer", JOptionPane.ERROR_MESSAGE);
|
||||||
|
System.exit(1);
|
||||||
|
});
|
||||||
|
// In case the eventqueue is broken, exit after 1 minute
|
||||||
|
try {
|
||||||
|
Thread.sleep(60 * 1000);
|
||||||
|
} catch (InterruptedException e1) {
|
||||||
|
// Good, it was already called?
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startup(String[] args) {
|
||||||
|
Options options = new Options();
|
||||||
|
addNonBootstrapOptions(options);
|
||||||
|
addBootstrapOptions(options);
|
||||||
|
|
||||||
|
CommandLineParser parser = new DefaultParser();
|
||||||
|
CommandLine cmd = null;
|
||||||
|
try {
|
||||||
|
cmd = parser.parse(options, args);
|
||||||
|
} catch (ParseException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
try {
|
||||||
|
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||||
|
} catch (Exception e1) {
|
||||||
|
// Ignore the exceptions, just continue using the ugly L&F
|
||||||
|
}
|
||||||
|
JOptionPane.showMessageDialog(null, e.getMessage(), "packwiz-installer", JOptionPane.ERROR_MESSAGE);
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
IUserInterface ui;
|
||||||
|
// if "headless", GUI creation will fail anyway!
|
||||||
|
if (cmd.hasOption("no-gui") || GraphicsEnvironment.isHeadless()) {
|
||||||
|
ui = new CLIHandler();
|
||||||
|
} else {
|
||||||
|
ui = new InstallWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] unparsedArgs = cmd.getArgs();
|
||||||
|
if (unparsedArgs.length > 1) {
|
||||||
|
ui.handleExceptionAndExit(new RuntimeException("Too many arguments specified!"));
|
||||||
|
return;
|
||||||
|
} else if (unparsedArgs.length < 1) {
|
||||||
|
ui.handleExceptionAndExit(new RuntimeException("URI to install from must be specified!"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String title = cmd.getOptionValue("title");
|
||||||
|
if (title != null) {
|
||||||
|
ui.setTitle(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
InputStateHandler inputStateHandler = new InputStateHandler();
|
||||||
|
ui.show(inputStateHandler);
|
||||||
|
|
||||||
|
UpdateManager.Options uOptions = new UpdateManager.Options();
|
||||||
|
|
||||||
|
String side = cmd.getOptionValue("side");
|
||||||
|
if (side != null) {
|
||||||
|
uOptions.side = UpdateManager.Options.Side.from(side);
|
||||||
|
}
|
||||||
|
|
||||||
|
String packFolder = cmd.getOptionValue("pack-folder");
|
||||||
|
if (packFolder != null) {
|
||||||
|
uOptions.packFolder = packFolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
String metaFile = cmd.getOptionValue("meta-file");
|
||||||
|
if (metaFile != null) {
|
||||||
|
uOptions.manifestFile = metaFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
uOptions.downloadURI = new SpaceSafeURI(unparsedArgs[0]);
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
// TODO: better error message?
|
||||||
|
ui.handleExceptionAndExit(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start update process!
|
||||||
|
// TODO: start in SwingWorker?
|
||||||
|
try {
|
||||||
|
ui.executeManager(() -> {
|
||||||
|
try {
|
||||||
|
new UpdateManager(uOptions, ui, inputStateHandler);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// TODO: better error message?
|
||||||
|
ui.handleExceptionAndExit(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
// TODO: better error message?
|
||||||
|
ui.handleExceptionAndExit(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called by packwiz-installer-bootstrap to set up the help command
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
|
public static void addNonBootstrapOptions(Options options) {
|
||||||
|
options.addOption("s", "side", true, "Side to install mods from (client/server, defaults to client)");
|
||||||
|
options.addOption(null, "title", true, "Title of the installer window");
|
||||||
|
options.addOption(null, "pack-folder", true, "Folder to install the pack to (defaults to the JAR directory)");
|
||||||
|
options.addOption(null, "meta-file", true, "JSON file to store pack metadata, relative to the pack folder (defaults to packwiz.json)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: link these somehow so they're only defined once?
|
||||||
|
private static void addBootstrapOptions(Options options) {
|
||||||
|
options.addOption(null, "bootstrap-update-url", true, "Github API URL for checking for updates");
|
||||||
|
options.addOption(null, "bootstrap-update-token", true, "Github API Access Token, for private repositories");
|
||||||
|
options.addOption(null, "bootstrap-no-update", false, "Don't update packwiz-installer");
|
||||||
|
options.addOption(null, "bootstrap-main-jar", true, "Location of the packwiz-installer JAR file");
|
||||||
|
options.addOption("g", "no-gui", false, "Don't display a GUI to show update progress");
|
||||||
|
options.addOption("h", "help", false, "Display this message"); // Implemented in packwiz-installer-bootstrap!
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -8,6 +8,8 @@ public class RequiresBootstrap {
|
|||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
// Very small CLI implementation, because Commons CLI complains on unexpected
|
// Very small CLI implementation, because Commons CLI complains on unexpected
|
||||||
// options
|
// options
|
||||||
|
// Also so that Commons CLI can be excluded from the shaded JAR, as it is
|
||||||
|
// included in the bootstrap
|
||||||
if (Arrays.stream(args).map(str -> {
|
if (Arrays.stream(args).map(str -> {
|
||||||
if (str == null) return "";
|
if (str == null) return "";
|
||||||
if (str.startsWith("--")) {
|
if (str.startsWith("--")) {
|
||||||
|
493
src/main/java/link/infra/packwiz/installer/UpdateManager.java
Normal file
493
src/main/java/link/infra/packwiz/installer/UpdateManager.java
Normal file
@ -0,0 +1,493 @@
|
|||||||
|
package link.infra.packwiz.installer;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.google.gson.JsonIOException;
|
||||||
|
import com.google.gson.JsonSyntaxException;
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
import com.moandjiezana.toml.Toml;
|
||||||
|
import link.infra.packwiz.installer.metadata.IndexFile;
|
||||||
|
import link.infra.packwiz.installer.metadata.ManifestFile;
|
||||||
|
import link.infra.packwiz.installer.metadata.PackFile;
|
||||||
|
import link.infra.packwiz.installer.metadata.SpaceSafeURI;
|
||||||
|
import link.infra.packwiz.installer.metadata.hash.GeneralHashingSource;
|
||||||
|
import link.infra.packwiz.installer.metadata.hash.Hash;
|
||||||
|
import link.infra.packwiz.installer.metadata.hash.HashUtils;
|
||||||
|
import link.infra.packwiz.installer.request.HandlerManager;
|
||||||
|
import link.infra.packwiz.installer.ui.IExceptionDetails;
|
||||||
|
import link.infra.packwiz.installer.ui.IUserInterface;
|
||||||
|
import link.infra.packwiz.installer.ui.InputStateHandler;
|
||||||
|
import link.infra.packwiz.installer.ui.InstallProgress;
|
||||||
|
import okio.Okio;
|
||||||
|
import okio.Source;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class UpdateManager {
|
||||||
|
|
||||||
|
private final Options opts;
|
||||||
|
public final IUserInterface ui;
|
||||||
|
private boolean cancelled;
|
||||||
|
private boolean cancelledStartGame = false;
|
||||||
|
private InputStateHandler stateHandler;
|
||||||
|
private boolean errorsOccurred = false;
|
||||||
|
|
||||||
|
public static class Options {
|
||||||
|
SpaceSafeURI downloadURI = null;
|
||||||
|
String manifestFile = "packwiz.json"; // TODO: make configurable
|
||||||
|
String packFolder = ".";
|
||||||
|
Side side = Side.CLIENT;
|
||||||
|
|
||||||
|
public enum Side {
|
||||||
|
@SerializedName("client")
|
||||||
|
CLIENT("client"),
|
||||||
|
@SerializedName("server")
|
||||||
|
SERVER("server"),
|
||||||
|
@SerializedName("both")
|
||||||
|
BOTH("both", new Side[] { CLIENT, SERVER });
|
||||||
|
|
||||||
|
private final String sideName;
|
||||||
|
private final Side[] depSides;
|
||||||
|
|
||||||
|
Side(String sideName) {
|
||||||
|
this.sideName = sideName.toLowerCase();
|
||||||
|
this.depSides = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Side(String sideName, Side[] depSides) {
|
||||||
|
this.sideName = sideName.toLowerCase();
|
||||||
|
this.depSides = depSides;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this.sideName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasSide(Side tSide) {
|
||||||
|
if (this.equals(tSide)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this.depSides != null) {
|
||||||
|
for (Side depSide : this.depSides) {
|
||||||
|
if (depSide.equals(tSide)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Side from(String name) {
|
||||||
|
String lowerName = name.toLowerCase();
|
||||||
|
for (Side side : Side.values()) {
|
||||||
|
if (side.sideName.equals(lowerName)) {
|
||||||
|
return side;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateManager(Options opts, IUserInterface ui, InputStateHandler inputStateHandler) {
|
||||||
|
this.opts = opts;
|
||||||
|
this.ui = ui;
|
||||||
|
this.stateHandler = inputStateHandler;
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void start() {
|
||||||
|
this.checkOptions();
|
||||||
|
|
||||||
|
ui.submitProgress(new InstallProgress("Loading manifest file..."));
|
||||||
|
Gson gson = new GsonBuilder().registerTypeAdapter(Hash.class, new Hash.TypeHandler()).create();
|
||||||
|
ManifestFile manifest;
|
||||||
|
try {
|
||||||
|
manifest = gson.fromJson(new FileReader(Paths.get(opts.packFolder, opts.manifestFile).toString()),
|
||||||
|
ManifestFile.class);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
manifest = new ManifestFile();
|
||||||
|
} catch (JsonSyntaxException | JsonIOException e) {
|
||||||
|
ui.handleExceptionAndExit(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateHandler.getCancelButton()) {
|
||||||
|
showCancellationDialog();
|
||||||
|
handleCancellation();
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.submitProgress(new InstallProgress("Loading pack file..."));
|
||||||
|
GeneralHashingSource packFileSource;
|
||||||
|
try {
|
||||||
|
Source src = HandlerManager.getFileSource(opts.downloadURI);
|
||||||
|
packFileSource = HashUtils.getHasher("sha256").getHashingSource(src);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// TODO: still launch the game if updating doesn't work?
|
||||||
|
// TODO: ask user if they want to launch the game, exit(1) if they don't
|
||||||
|
ui.handleExceptionAndExit(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PackFile pf;
|
||||||
|
try {
|
||||||
|
pf = new Toml().read(Okio.buffer(packFileSource).inputStream()).to(PackFile.class);
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
ui.handleExceptionAndExit(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateHandler.getCancelButton()) {
|
||||||
|
showCancellationDialog();
|
||||||
|
handleCancellation();
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.submitProgress(new InstallProgress("Checking local files..."));
|
||||||
|
|
||||||
|
// Invalidation checking must be done here, as it must happen before pack/index hashes are checked
|
||||||
|
List<SpaceSafeURI> invalidatedUris = new ArrayList<>();
|
||||||
|
if (manifest.cachedFiles != null) {
|
||||||
|
for (Map.Entry<SpaceSafeURI, ManifestFile.File> entry : manifest.cachedFiles.entrySet()) {
|
||||||
|
boolean invalid = false;
|
||||||
|
// if isn't optional, or is optional but optionValue == true
|
||||||
|
if (!entry.getValue().isOptional || entry.getValue().optionValue) {
|
||||||
|
if (entry.getValue().cachedLocation != null) {
|
||||||
|
if (!Files.exists(Paths.get(opts.packFolder, entry.getValue().cachedLocation))) {
|
||||||
|
invalid = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if cachedLocation == null, should probably be installed!!
|
||||||
|
invalid = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (invalid) {
|
||||||
|
SpaceSafeURI fileUri = entry.getKey();
|
||||||
|
System.out.println("File " + fileUri.toString() + " invalidated, marked for redownloading");
|
||||||
|
invalidatedUris.add(fileUri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manifest.packFileHash != null && packFileSource.hashIsEqual(manifest.packFileHash) && invalidatedUris.isEmpty()) {
|
||||||
|
System.out.println("Modpack is already up to date!");
|
||||||
|
// todo: --force?
|
||||||
|
if (!stateHandler.getOptionsButton()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("Modpack name: " + pf.name);
|
||||||
|
|
||||||
|
if (stateHandler.getCancelButton()) {
|
||||||
|
showCancellationDialog();
|
||||||
|
handleCancellation();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// This is badly written, I'll probably heavily refactor it at some point
|
||||||
|
processIndex(HandlerManager.getNewLoc(opts.downloadURI, pf.index.file),
|
||||||
|
HashUtils.getHash(pf.index.hashFormat, pf.index.hash), pf.index.hashFormat, manifest, invalidatedUris);
|
||||||
|
} catch (Exception e1) {
|
||||||
|
ui.handleExceptionAndExit(e1);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCancellation();
|
||||||
|
|
||||||
|
// TODO: update MMC params, java args etc
|
||||||
|
|
||||||
|
// If there were errors, don't write the manifest/index hashes, to ensure they are rechecked later
|
||||||
|
if (errorsOccurred) {
|
||||||
|
manifest.indexFileHash = null;
|
||||||
|
manifest.packFileHash = null;
|
||||||
|
} else {
|
||||||
|
manifest.packFileHash = packFileSource.getHash();
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest.cachedSide = opts.side;
|
||||||
|
try (Writer writer = new FileWriter(Paths.get(opts.packFolder, opts.manifestFile).toString())) {
|
||||||
|
gson.toJson(manifest, writer);
|
||||||
|
} catch (IOException e) {
|
||||||
|
// TODO: add message?
|
||||||
|
ui.handleException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkOptions() {
|
||||||
|
// TODO: implement
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processIndex(SpaceSafeURI indexUri, Hash indexHash, String hashFormat, ManifestFile manifest, List<SpaceSafeURI> invalidatedUris) {
|
||||||
|
if (manifest.indexFileHash != null && manifest.indexFileHash.equals(indexHash) && invalidatedUris.isEmpty()) {
|
||||||
|
System.out.println("Modpack files are already up to date!");
|
||||||
|
if (!stateHandler.getOptionsButton()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
IndexFile indexFile;
|
||||||
|
try {
|
||||||
|
indexFile = new Toml().read(Okio.buffer(indexFileSource).inputStream()).to(IndexFile.class);
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
ui.handleExceptionAndExit(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!indexFileSource.hashIsEqual(indexHash)) {
|
||||||
|
// TODO: throw exception
|
||||||
|
System.out.println("I was meant to put an error message here but I'll do that later");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateHandler.getCancelButton()) {
|
||||||
|
showCancellationDialog();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manifest.cachedFiles == null) {
|
||||||
|
manifest.cachedFiles = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.submitProgress(new InstallProgress("Checking local files..."));
|
||||||
|
Iterator<Map.Entry<SpaceSafeURI, ManifestFile.File>> it = manifest.cachedFiles.entrySet().iterator();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
Map.Entry<SpaceSafeURI, ManifestFile.File> entry = it.next();
|
||||||
|
if (entry.getValue().cachedLocation != null) {
|
||||||
|
boolean alreadyDeleted = false;
|
||||||
|
// Delete if option value has been set to false
|
||||||
|
if (entry.getValue().isOptional && !entry.getValue().optionValue) {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(Paths.get(opts.packFolder, entry.getValue().cachedLocation));
|
||||||
|
} catch (IOException e) {
|
||||||
|
// TODO: should this be shown to the user in some way?
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
// Set to null, as it doesn't exist anymore
|
||||||
|
entry.getValue().cachedLocation = null;
|
||||||
|
alreadyDeleted = true;
|
||||||
|
}
|
||||||
|
if (indexFile.files.stream().noneMatch(f -> f.file.equals(entry.getKey()))) {
|
||||||
|
// File has been removed from the index
|
||||||
|
if (!alreadyDeleted) {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(Paths.get(opts.packFolder, entry.getValue().cachedLocation));
|
||||||
|
} catch (IOException e) {
|
||||||
|
// TODO: should this be shown to the user in some way?
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateHandler.getCancelButton()) {
|
||||||
|
showCancellationDialog();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ui.submitProgress(new InstallProgress("Comparing new files..."));
|
||||||
|
|
||||||
|
// TODO: progress bar?
|
||||||
|
if (indexFile.files == null || indexFile.files.size() == 0) {
|
||||||
|
System.out.println("Warning: Index is empty!");
|
||||||
|
indexFile.files = new ArrayList<>();
|
||||||
|
}
|
||||||
|
List<DownloadTask> tasks = DownloadTask.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.cachedSide);
|
||||||
|
if (invalidateAll) {
|
||||||
|
System.out.println("Side changed, invalidating all mods");
|
||||||
|
}
|
||||||
|
tasks.forEach(f -> {
|
||||||
|
// TODO: should linkedfile be checked as well? should this be done in the download section?
|
||||||
|
if (invalidateAll) {
|
||||||
|
f.invalidate();
|
||||||
|
} else if (invalidatedUris.contains(f.metadata.file)) {
|
||||||
|
f.invalidate();
|
||||||
|
}
|
||||||
|
ManifestFile.File file = manifest.cachedFiles.get(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
|
||||||
|
file.backup();
|
||||||
|
}
|
||||||
|
// If it is null, the DownloadTask will make a new empty cachedFile
|
||||||
|
f.updateFromCache(file);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (stateHandler.getCancelButton()) {
|
||||||
|
showCancellationDialog();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let's hope downloadMetadata is a pure function!!!
|
||||||
|
tasks.parallelStream().forEach(f -> f.downloadMetadata(indexFile, indexUri));
|
||||||
|
|
||||||
|
List<IExceptionDetails> failedTasks = tasks.stream().filter(t -> t.getException() != null).collect(Collectors.toList());
|
||||||
|
if (!failedTasks.isEmpty()) {
|
||||||
|
errorsOccurred = true;
|
||||||
|
IExceptionDetails.ExceptionListResult exceptionListResult;
|
||||||
|
try {
|
||||||
|
exceptionListResult = ui.showExceptions(failedTasks, tasks.size(), true).get();
|
||||||
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
|
// Interrupted means cancelled???
|
||||||
|
ui.handleExceptionAndExit(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (exceptionListResult) {
|
||||||
|
case CONTINUE:
|
||||||
|
break;
|
||||||
|
case CANCEL:
|
||||||
|
cancelled = true;
|
||||||
|
return;
|
||||||
|
case IGNORE:
|
||||||
|
cancelledStartGame = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateHandler.getCancelButton()) {
|
||||||
|
showCancellationDialog();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DownloadTask> nonFailedFirstTasks = tasks.stream().filter(t -> t.getException() == null).collect(Collectors.toList());
|
||||||
|
List<DownloadTask> optionTasks = nonFailedFirstTasks.stream().filter(DownloadTask::correctSide).filter(DownloadTask::isOptional).collect(Collectors.toList());
|
||||||
|
// If options changed, present all options again
|
||||||
|
if (stateHandler.getOptionsButton() || optionTasks.stream().anyMatch(DownloadTask::isNewOptional)) {
|
||||||
|
// new ArrayList is requires so it's an IOptionDetails rather than a DownloadTask list
|
||||||
|
Future<Boolean> cancelledResult = ui.showOptions(new ArrayList<>(optionTasks));
|
||||||
|
try {
|
||||||
|
if (cancelledResult.get()) {
|
||||||
|
cancelled = true;
|
||||||
|
// TODO: Should the UI be closed somehow??
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
|
// Interrupted means cancelled???
|
||||||
|
ui.handleExceptionAndExit(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ui.disableOptionsButton();
|
||||||
|
|
||||||
|
// TODO: different thread pool type?
|
||||||
|
ExecutorService threadPool = Executors.newFixedThreadPool(10);
|
||||||
|
CompletionService<DownloadTask> completionService = new ExecutorCompletionService<>(threadPool);
|
||||||
|
|
||||||
|
tasks.forEach(t -> completionService.submit(() -> {
|
||||||
|
t.download(opts.packFolder, indexUri);
|
||||||
|
return t;
|
||||||
|
}));
|
||||||
|
|
||||||
|
for (int i = 0; i < tasks.size(); i++) {
|
||||||
|
DownloadTask task;
|
||||||
|
try {
|
||||||
|
task = completionService.take().get();
|
||||||
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
|
ui.handleException(e);
|
||||||
|
task = null;
|
||||||
|
}
|
||||||
|
// Update manifest - If there were no errors cachedFile has already been modified in place (good old pass by reference)
|
||||||
|
if (task != null) {
|
||||||
|
if (task.getException() != null) {
|
||||||
|
ManifestFile.File file = task.cachedFile.getRevert();
|
||||||
|
if (file != null) {
|
||||||
|
manifest.cachedFiles.putIfAbsent(task.metadata.file, file);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// idiot, if it wasn't there in the first place it won't magically appear there
|
||||||
|
manifest.cachedFiles.putIfAbsent(task.metadata.file, task.cachedFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String progress;
|
||||||
|
if (task != null) {
|
||||||
|
if (task.getException() != null) {
|
||||||
|
progress = "Failed to download " + task.metadata.getName() + ": " + task.getException().getMessage();
|
||||||
|
task.getException().printStackTrace();
|
||||||
|
} else {
|
||||||
|
// TODO: should this be revised for tasks that didn't actually download it?
|
||||||
|
progress = "Downloaded " + task.metadata.getName();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
progress = "Failed to download, unknown reason";
|
||||||
|
}
|
||||||
|
ui.submitProgress(new InstallProgress(progress, i + 1, tasks.size()));
|
||||||
|
|
||||||
|
if (stateHandler.getCancelButton()) {
|
||||||
|
// Stop all tasks, don't launch the game (it's in an invalid state!)
|
||||||
|
threadPool.shutdown();
|
||||||
|
cancelled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<IExceptionDetails> failedTasks2ElectricBoogaloo = nonFailedFirstTasks.stream().filter(t -> t.getException() != null).collect(Collectors.toList());
|
||||||
|
if (!failedTasks2ElectricBoogaloo.isEmpty()) {
|
||||||
|
errorsOccurred = true;
|
||||||
|
IExceptionDetails.ExceptionListResult exceptionListResult;
|
||||||
|
try {
|
||||||
|
exceptionListResult = ui.showExceptions(failedTasks2ElectricBoogaloo, tasks.size(), false).get();
|
||||||
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
|
// Interrupted means cancelled???
|
||||||
|
ui.handleExceptionAndExit(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (exceptionListResult) {
|
||||||
|
case CONTINUE:
|
||||||
|
break;
|
||||||
|
case CANCEL:
|
||||||
|
cancelled = true;
|
||||||
|
return;
|
||||||
|
case IGNORE:
|
||||||
|
cancelledStartGame = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showCancellationDialog() {
|
||||||
|
IExceptionDetails.ExceptionListResult exceptionListResult;
|
||||||
|
try {
|
||||||
|
exceptionListResult = ui.showCancellationDialog().get();
|
||||||
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
|
// Interrupted means cancelled???
|
||||||
|
ui.handleExceptionAndExit(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (exceptionListResult) {
|
||||||
|
case CONTINUE:
|
||||||
|
throw new RuntimeException("Continuation not allowed here!");
|
||||||
|
case CANCEL:
|
||||||
|
cancelled = true;
|
||||||
|
return;
|
||||||
|
case IGNORE:
|
||||||
|
cancelledStartGame = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleCancellation() {
|
||||||
|
if (cancelled) {
|
||||||
|
System.out.println("Update cancelled by user!");
|
||||||
|
System.exit(1);
|
||||||
|
} else if (cancelledStartGame) {
|
||||||
|
System.out.println("Update cancelled by user! Continuing to start game...");
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,105 @@
|
|||||||
|
package link.infra.packwiz.installer.metadata;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
import com.moandjiezana.toml.Toml;
|
||||||
|
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 okio.Okio;
|
||||||
|
import okio.Source;
|
||||||
|
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class IndexFile {
|
||||||
|
@SerializedName("hash-format")
|
||||||
|
public String hashFormat;
|
||||||
|
public List<File> files;
|
||||||
|
|
||||||
|
public static class File {
|
||||||
|
public SpaceSafeURI file;
|
||||||
|
@SerializedName("hash-format")
|
||||||
|
public String hashFormat;
|
||||||
|
public String hash;
|
||||||
|
public SpaceSafeURI alias;
|
||||||
|
public boolean metafile;
|
||||||
|
public boolean preserve;
|
||||||
|
|
||||||
|
public transient ModFile linkedFile;
|
||||||
|
public transient SpaceSafeURI linkedFileURI;
|
||||||
|
|
||||||
|
public void downloadMeta(IndexFile parentIndexFile, SpaceSafeURI indexUri) throws Exception {
|
||||||
|
if (!metafile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hashFormat == null || hashFormat.length() == 0) {
|
||||||
|
hashFormat = parentIndexFile.hashFormat;
|
||||||
|
}
|
||||||
|
Hash fileHash = HashUtils.getHash(hashFormat, hash);
|
||||||
|
linkedFileURI = HandlerManager.getNewLoc(indexUri, file);
|
||||||
|
Source src = HandlerManager.getFileSource(linkedFileURI);
|
||||||
|
GeneralHashingSource fileStream = HashUtils.getHasher(hashFormat).getHashingSource(src);
|
||||||
|
|
||||||
|
linkedFile = new Toml().read(Okio.buffer(fileStream).inputStream()).to(ModFile.class);
|
||||||
|
if (!fileStream.hashIsEqual(fileHash)) {
|
||||||
|
throw new Exception("Invalid mod file hash");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Source getSource(SpaceSafeURI indexUri) throws Exception {
|
||||||
|
if (metafile) {
|
||||||
|
if (linkedFile == null) {
|
||||||
|
throw new Exception("Linked file doesn't exist!");
|
||||||
|
}
|
||||||
|
return linkedFile.getSource(linkedFileURI);
|
||||||
|
} else {
|
||||||
|
SpaceSafeURI newLoc = HandlerManager.getNewLoc(indexUri, file);
|
||||||
|
if (newLoc == null) {
|
||||||
|
throw new Exception("Index file URI is invalid");
|
||||||
|
}
|
||||||
|
return HandlerManager.getFileSource(newLoc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Hash getHash() throws Exception {
|
||||||
|
if (hash == null) {
|
||||||
|
// TODO: should these be more specific exceptions (e.g. IndexFileException?!)
|
||||||
|
throw new Exception("Index file doesn't have a hash");
|
||||||
|
}
|
||||||
|
if (hashFormat == null) {
|
||||||
|
throw new Exception("Index file doesn't have a hash format");
|
||||||
|
}
|
||||||
|
return HashUtils.getHash(hashFormat, hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
if (metafile) {
|
||||||
|
if (linkedFile != null) {
|
||||||
|
if (linkedFile.name != null) {
|
||||||
|
return linkedFile.name;
|
||||||
|
} else if (linkedFile.filename != null) {
|
||||||
|
return linkedFile.filename;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (file != null) {
|
||||||
|
return Paths.get(file.getPath()).getFileName().toString();
|
||||||
|
}
|
||||||
|
// TODO: throw some kind of exception?
|
||||||
|
return "Invalid file";
|
||||||
|
}
|
||||||
|
|
||||||
|
public SpaceSafeURI getDestURI() {
|
||||||
|
if (alias != null) {
|
||||||
|
return alias;
|
||||||
|
}
|
||||||
|
if (metafile && linkedFile != null) {
|
||||||
|
// TODO: URIs are bad
|
||||||
|
return file.resolve(linkedFile.filename);
|
||||||
|
} else {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
package link.infra.packwiz.installer.metadata;
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.UpdateManager;
|
||||||
|
import link.infra.packwiz.installer.metadata.hash.Hash;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class ManifestFile {
|
||||||
|
public Hash packFileHash = null;
|
||||||
|
public Hash indexFileHash = null;
|
||||||
|
public Map<SpaceSafeURI, File> cachedFiles;
|
||||||
|
// If the side changes, EVERYTHING invalidates. FUN!!!
|
||||||
|
public UpdateManager.Options.Side cachedSide = UpdateManager.Options.Side.CLIENT;
|
||||||
|
|
||||||
|
public static class File {
|
||||||
|
private transient File revert;
|
||||||
|
|
||||||
|
public Hash hash = null;
|
||||||
|
public Hash linkedFileHash = null;
|
||||||
|
public String cachedLocation = null;
|
||||||
|
|
||||||
|
public boolean isOptional = false;
|
||||||
|
public boolean optionValue = true;
|
||||||
|
|
||||||
|
// When an error occurs, the state needs to be reverted. To do this, I have a crude revert system.
|
||||||
|
public void backup() {
|
||||||
|
revert = new File();
|
||||||
|
revert.hash = hash;
|
||||||
|
revert.linkedFileHash = linkedFileHash;
|
||||||
|
revert.cachedLocation = cachedLocation;
|
||||||
|
revert.isOptional = isOptional;
|
||||||
|
revert.optionValue = optionValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getRevert() {
|
||||||
|
return revert;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
package link.infra.packwiz.installer.metadata;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
import link.infra.packwiz.installer.UpdateManager.Options.Side;
|
||||||
|
import link.infra.packwiz.installer.metadata.hash.Hash;
|
||||||
|
import link.infra.packwiz.installer.metadata.hash.HashUtils;
|
||||||
|
import link.infra.packwiz.installer.request.HandlerManager;
|
||||||
|
import okio.Source;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class ModFile {
|
||||||
|
public String name;
|
||||||
|
public String filename;
|
||||||
|
public Side side;
|
||||||
|
|
||||||
|
public Download download;
|
||||||
|
public static class Download {
|
||||||
|
public SpaceSafeURI url;
|
||||||
|
@SerializedName("hash-format")
|
||||||
|
public String hashFormat;
|
||||||
|
public String hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> update;
|
||||||
|
|
||||||
|
public Option option;
|
||||||
|
public static class Option {
|
||||||
|
public boolean optional;
|
||||||
|
public String description;
|
||||||
|
@SerializedName("default")
|
||||||
|
public boolean defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Source getSource(SpaceSafeURI baseLoc) throws Exception {
|
||||||
|
if (download == null) {
|
||||||
|
throw new Exception("Metadata file doesn't have download");
|
||||||
|
}
|
||||||
|
if (download.url == null) {
|
||||||
|
throw new Exception("Metadata file doesn't have a download URI");
|
||||||
|
}
|
||||||
|
SpaceSafeURI newLoc = HandlerManager.getNewLoc(baseLoc, download.url);
|
||||||
|
if (newLoc == null) {
|
||||||
|
throw new Exception("Metadata file URI is invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
return HandlerManager.getFileSource(newLoc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Hash getHash() throws Exception {
|
||||||
|
if (download == null) {
|
||||||
|
throw new Exception("Metadata file doesn't have download");
|
||||||
|
}
|
||||||
|
if (download.hash == null) {
|
||||||
|
throw new Exception("Metadata file doesn't have a hash");
|
||||||
|
}
|
||||||
|
if (download.hashFormat == null) {
|
||||||
|
throw new Exception("Metadata file doesn't have a hash format");
|
||||||
|
}
|
||||||
|
return HashUtils.getHash(download.hashFormat, download.hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOptional() {
|
||||||
|
if (option != null) {
|
||||||
|
return option.optional;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package link.infra.packwiz.installer.metadata;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class PackFile {
|
||||||
|
public String name;
|
||||||
|
|
||||||
|
public IndexFileLoc index;
|
||||||
|
public static class IndexFileLoc {
|
||||||
|
public SpaceSafeURI file;
|
||||||
|
@SerializedName("hash-format")
|
||||||
|
public String hashFormat;
|
||||||
|
public String hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> versions;
|
||||||
|
public Map<String, Object> client;
|
||||||
|
public Map<String, Object> server;
|
||||||
|
}
|
@ -0,0 +1,88 @@
|
|||||||
|
package link.infra.packwiz.installer.metadata;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.JsonAdapter;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
// The world's worst URI wrapper
|
||||||
|
@JsonAdapter(SpaceSafeURIParser.class)
|
||||||
|
public class SpaceSafeURI implements Comparable<SpaceSafeURI>, Serializable {
|
||||||
|
private final URI u;
|
||||||
|
|
||||||
|
public SpaceSafeURI(String str) throws URISyntaxException {
|
||||||
|
u = new URI(str.replace(" ", "%20"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public SpaceSafeURI(URI uri) {
|
||||||
|
this.u = uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SpaceSafeURI(String scheme, String authority, String path, String query, String fragment) throws URISyntaxException {
|
||||||
|
// TODO: do all components need to be replaced?
|
||||||
|
scheme = scheme.replace(" ", "%20");
|
||||||
|
authority = authority.replace(" ", "%20");
|
||||||
|
path = path.replace(" ", "%20");
|
||||||
|
query = query.replace(" ", "%20");
|
||||||
|
fragment = fragment.replace(" ", "%20");
|
||||||
|
u = new URI(scheme, authority, path, query, fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPath() {
|
||||||
|
return u.getPath().replace("%20", " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
return u.toString().replace("%20", " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
|
public SpaceSafeURI resolve(String path) {
|
||||||
|
return new SpaceSafeURI(u.resolve(path.replace(" ", "%20")));
|
||||||
|
}
|
||||||
|
|
||||||
|
public SpaceSafeURI resolve(SpaceSafeURI loc) {
|
||||||
|
return new SpaceSafeURI(u.resolve(loc.u));
|
||||||
|
}
|
||||||
|
|
||||||
|
public SpaceSafeURI relativize(SpaceSafeURI loc) {
|
||||||
|
return new SpaceSafeURI(u.relativize(loc.u));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (obj instanceof SpaceSafeURI) {
|
||||||
|
return u.equals(((SpaceSafeURI) obj).u);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return u.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(SpaceSafeURI uri) {
|
||||||
|
return u.compareTo(uri.u);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getScheme() {
|
||||||
|
return u.getScheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAuthority() {
|
||||||
|
return u.getAuthority();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getHost() {
|
||||||
|
return u.getHost();
|
||||||
|
}
|
||||||
|
|
||||||
|
public URL toURL() throws MalformedURLException {
|
||||||
|
return u.toURL();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package link.infra.packwiz.installer.metadata;
|
||||||
|
|
||||||
|
import com.google.gson.JsonDeserializationContext;
|
||||||
|
import com.google.gson.JsonDeserializer;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonParseException;
|
||||||
|
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class encodes spaces before parsing the URI, so the URI can actually be
|
||||||
|
* parsed.
|
||||||
|
*/
|
||||||
|
class SpaceSafeURIParser implements JsonDeserializer<SpaceSafeURI> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SpaceSafeURI deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
|
||||||
|
throws JsonParseException {
|
||||||
|
try {
|
||||||
|
return new SpaceSafeURI(json.getAsString());
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
throw new JsonParseException("Failed to parse URI", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: replace this with a better solution?
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
package link.infra.packwiz.installer.metadata.hash;
|
||||||
|
|
||||||
|
import okio.ForwardingSource;
|
||||||
|
import okio.Source;
|
||||||
|
|
||||||
|
public abstract class GeneralHashingSource extends ForwardingSource {
|
||||||
|
|
||||||
|
public GeneralHashingSource(Source delegate) {
|
||||||
|
super(delegate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract Hash getHash();
|
||||||
|
|
||||||
|
public boolean hashIsEqual(Object compareTo) {
|
||||||
|
return compareTo.equals(getHash());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
package link.infra.packwiz.installer.metadata.hash;
|
||||||
|
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
|
||||||
|
import com.google.gson.JsonDeserializationContext;
|
||||||
|
import com.google.gson.JsonDeserializer;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParseException;
|
||||||
|
import com.google.gson.JsonPrimitive;
|
||||||
|
import com.google.gson.JsonSerializationContext;
|
||||||
|
import com.google.gson.JsonSerializer;
|
||||||
|
|
||||||
|
public abstract class Hash {
|
||||||
|
protected abstract String getStringValue();
|
||||||
|
|
||||||
|
protected abstract String getType();
|
||||||
|
|
||||||
|
public static class TypeHandler implements JsonDeserializer<Hash>, JsonSerializer<Hash> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JsonElement serialize(Hash src, Type typeOfSrc, JsonSerializationContext context) {
|
||||||
|
JsonObject out = new JsonObject();
|
||||||
|
out.add("type", new JsonPrimitive(src.getType()));
|
||||||
|
out.add("value", new JsonPrimitive(src.getStringValue()));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Hash deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
|
||||||
|
throws JsonParseException {
|
||||||
|
JsonObject obj = json.getAsJsonObject();
|
||||||
|
String type, value;
|
||||||
|
try {
|
||||||
|
type = obj.get("type").getAsString();
|
||||||
|
value = obj.get("value").getAsString();
|
||||||
|
} catch (NullPointerException e) {
|
||||||
|
throw new JsonParseException("Invalid hash JSON data");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return HashUtils.getHash(type, value);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new JsonParseException("Failed to create hash object", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package link.infra.packwiz.installer.metadata.hash;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class HashUtils {
|
||||||
|
private static final Map<String, IHasher> hashTypeConversion = new HashMap<String, IHasher>();
|
||||||
|
static {
|
||||||
|
hashTypeConversion.put("sha256", new HashingSourceHasher("sha256"));
|
||||||
|
hashTypeConversion.put("murmur2", new Murmur2Hasher());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IHasher getHasher(String type) throws Exception {
|
||||||
|
IHasher hasher = hashTypeConversion.get(type);
|
||||||
|
if (hasher == null) {
|
||||||
|
throw new Exception("Hash type not supported: " + type);
|
||||||
|
}
|
||||||
|
return hasher;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Hash getHash(String type, String value) throws Exception {
|
||||||
|
if (hashTypeConversion.containsKey(type)) {
|
||||||
|
return hashTypeConversion.get(type).getHash(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception("Hash type not supported: " + type);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
package link.infra.packwiz.installer.metadata.hash;
|
||||||
|
|
||||||
|
import okio.HashingSource;
|
||||||
|
import okio.Source;
|
||||||
|
|
||||||
|
public class HashingSourceHasher implements IHasher {
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
HashingSourceHasher(String type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
// i love naming things
|
||||||
|
private class HashingSourceGeneralHashingSource extends GeneralHashingSource {
|
||||||
|
HashingSource delegateHashing;
|
||||||
|
HashingSourceHash value;
|
||||||
|
|
||||||
|
HashingSourceGeneralHashingSource(HashingSource delegate) {
|
||||||
|
super(delegate);
|
||||||
|
delegateHashing = delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Hash getHash() {
|
||||||
|
if (value == null) {
|
||||||
|
value = new HashingSourceHash(delegateHashing.hash().hex());
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// this some funky inner class stuff
|
||||||
|
// each of these classes is specific to the instance of the HasherHashingSource
|
||||||
|
// therefore HashingSourceHashes from different parent instances will be not instanceof each other
|
||||||
|
private class HashingSourceHash extends Hash {
|
||||||
|
String value;
|
||||||
|
private HashingSourceHash(String value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (!(obj instanceof HashingSourceHash)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
HashingSourceHash objHash = (HashingSourceHash) obj;
|
||||||
|
if (value != null) {
|
||||||
|
return value.equalsIgnoreCase(objHash.value);
|
||||||
|
} else {
|
||||||
|
return objHash.value == null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return type + ": " + value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getStringValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GeneralHashingSource getHashingSource(Source delegate) {
|
||||||
|
switch (type) {
|
||||||
|
case "md5":
|
||||||
|
return new HashingSourceGeneralHashingSource(HashingSource.md5(delegate));
|
||||||
|
case "sha256":
|
||||||
|
return new HashingSourceGeneralHashingSource(HashingSource.sha256(delegate));
|
||||||
|
case "sha512":
|
||||||
|
return new HashingSourceGeneralHashingSource(HashingSource.sha512(delegate));
|
||||||
|
}
|
||||||
|
throw new RuntimeException("Invalid hash type provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Hash getHash(String value) {
|
||||||
|
return new HashingSourceHash(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package link.infra.packwiz.installer.metadata.hash;
|
||||||
|
|
||||||
|
import okio.Source;
|
||||||
|
|
||||||
|
public interface IHasher {
|
||||||
|
public GeneralHashingSource getHashingSource(Source delegate);
|
||||||
|
public Hash getHash(String value);
|
||||||
|
}
|
@ -0,0 +1,103 @@
|
|||||||
|
package link.infra.packwiz.installer.metadata.hash;
|
||||||
|
|
||||||
|
import okio.Buffer;
|
||||||
|
import okio.Source;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class Murmur2Hasher implements IHasher {
|
||||||
|
private class Murmur2GeneralHashingSource extends GeneralHashingSource {
|
||||||
|
Murmur2Hash value;
|
||||||
|
Buffer internalBuffer = new Buffer();
|
||||||
|
Buffer tempBuffer = new Buffer();
|
||||||
|
Source delegate;
|
||||||
|
|
||||||
|
public Murmur2GeneralHashingSource(Source delegate) {
|
||||||
|
super(delegate);
|
||||||
|
this.delegate = delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long read(Buffer sink, long byteCount) throws IOException {
|
||||||
|
long out = delegate.read(tempBuffer, byteCount);
|
||||||
|
if (out > -1) {
|
||||||
|
sink.write(tempBuffer.clone(), out);
|
||||||
|
internalBuffer.write(tempBuffer, out);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Hash getHash() {
|
||||||
|
if (value == null) {
|
||||||
|
byte[] data = computeNormalizedArray(internalBuffer.readByteArray());
|
||||||
|
value = new Murmur2Hash(Murmur2Lib.hash32(data, data.length, 1));
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Credit to https://github.com/modmuss50/CAV2/blob/master/murmur.go
|
||||||
|
private byte[] computeNormalizedArray(byte[] input) {
|
||||||
|
byte[] output = new byte[input.length];
|
||||||
|
int num = 0;
|
||||||
|
for (byte b : input) {
|
||||||
|
if (!(b == 9 || b == 10 || b == 13 || b == 32)) {
|
||||||
|
output[num] = b;
|
||||||
|
num++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
byte[] outputTrimmed = new byte[num];
|
||||||
|
System.arraycopy(output, 0, outputTrimmed, 0, num);
|
||||||
|
return outputTrimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Murmur2Hash extends Hash {
|
||||||
|
int value;
|
||||||
|
private Murmur2Hash(String value) {
|
||||||
|
// Parsing as long then casting to int converts values gt int max value but lt uint max value
|
||||||
|
// into negatives. I presume this is how the murmur2 code handles this.
|
||||||
|
this.value = (int)Long.parseLong(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Murmur2Hash(int value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (!(obj instanceof Murmur2Hash)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Murmur2Hash objHash = (Murmur2Hash) obj;
|
||||||
|
return value == objHash.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "murmur2: " + value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getStringValue() {
|
||||||
|
return Integer.toString(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getType() {
|
||||||
|
return "murmur2";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GeneralHashingSource getHashingSource(Source delegate) {
|
||||||
|
return new Murmur2GeneralHashingSource(delegate);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Hash getHash(String value) {
|
||||||
|
return new Murmur2Hash(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -74,13 +74,13 @@ public class Murmur2Lib {
|
|||||||
int left = length - len_m;
|
int left = length - len_m;
|
||||||
if (left != 0) {
|
if (left != 0) {
|
||||||
if (left >= 3) {
|
if (left >= 3) {
|
||||||
h ^= (int) data[length - (left - 2)] << 16;
|
h ^= (int) data[length - 3] << 16;
|
||||||
}
|
}
|
||||||
if (left >= 2) {
|
if (left >= 2) {
|
||||||
h ^= (int) data[length - (left - 1)] << 8;
|
h ^= (int) data[length - 2] << 8;
|
||||||
}
|
}
|
||||||
if (left >= 1) {
|
if (left >= 1) {
|
||||||
h ^= data[length - left];
|
h ^= (int) data[length - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
h *= M_32;
|
h *= M_32;
|
||||||
@ -152,7 +152,7 @@ public class Murmur2Lib {
|
|||||||
case 2:
|
case 2:
|
||||||
h ^= (long) (data[tailStart + 1] & 0xff) << 8;
|
h ^= (long) (data[tailStart + 1] & 0xff) << 8;
|
||||||
case 1:
|
case 1:
|
||||||
h ^= data[tailStart] & 0xff;
|
h ^= (long) (data[tailStart] & 0xff);
|
||||||
h *= M_64;
|
h *= M_64;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,59 @@
|
|||||||
|
package link.infra.packwiz.installer.request;
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.metadata.SpaceSafeURI;
|
||||||
|
import link.infra.packwiz.installer.request.handlers.RequestHandlerGithub;
|
||||||
|
import link.infra.packwiz.installer.request.handlers.RequestHandlerHTTP;
|
||||||
|
import okio.Source;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public abstract class HandlerManager {
|
||||||
|
|
||||||
|
private static List<IRequestHandler> handlers = new ArrayList<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
handlers.add(new RequestHandlerGithub());
|
||||||
|
handlers.add(new RequestHandlerHTTP());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SpaceSafeURI getNewLoc(SpaceSafeURI base, SpaceSafeURI loc) {
|
||||||
|
if (loc == null) return null;
|
||||||
|
if (base != null) {
|
||||||
|
loc = base.resolve(loc);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (IRequestHandler handler : handlers) {
|
||||||
|
if (handler.matchesHandler(loc)) {
|
||||||
|
return handler.getNewLoc(loc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return loc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: What if files are read multiple times??
|
||||||
|
// Zip handler discards once read, requesting multiple times on other handlers would cause multiple downloads
|
||||||
|
// Caching system? Copy from already downloaded files?
|
||||||
|
|
||||||
|
public static Source getFileSource(SpaceSafeURI loc) throws Exception {
|
||||||
|
for (IRequestHandler handler : handlers) {
|
||||||
|
if (handler.matchesHandler(loc)) {
|
||||||
|
Source src = handler.getFileSource(loc);
|
||||||
|
if (src == null) {
|
||||||
|
throw new Exception("Couldn't find URI: " + loc.toString());
|
||||||
|
} else {
|
||||||
|
return src;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: specialised exception classes??
|
||||||
|
throw new Exception("No handler available for URI: " + loc.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// github toml resolution
|
||||||
|
// e.g. https://github.com/comp500/Demagnetize -> demagnetize.toml
|
||||||
|
// https://github.com/comp500/Demagnetize/blob/master/demagnetize.toml
|
||||||
|
|
||||||
|
// To handle "progress", just count tasks, rather than individual progress
|
||||||
|
// It'll look bad, especially for zip-based things, but it should work fine
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package link.infra.packwiz.installer.request;
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.metadata.SpaceSafeURI;
|
||||||
|
import okio.Source;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IRequestHandler handles requests for locations specified in modpack metadata.
|
||||||
|
*/
|
||||||
|
public interface IRequestHandler {
|
||||||
|
|
||||||
|
boolean matchesHandler(SpaceSafeURI loc);
|
||||||
|
|
||||||
|
default SpaceSafeURI getNewLoc(SpaceSafeURI loc) {
|
||||||
|
return loc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the Source for a location. Must be threadsafe.
|
||||||
|
* It is assumed that each location is read only once for the duration of an IRequestHandler.
|
||||||
|
* @param loc The location to be read
|
||||||
|
* @return The Source containing the data of the file
|
||||||
|
* @throws Exception Exception if it failed to download a file!!!
|
||||||
|
*/
|
||||||
|
Source getFileSource(SpaceSafeURI loc) throws Exception;
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,90 @@
|
|||||||
|
package link.infra.packwiz.installer.request.handlers;
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.metadata.SpaceSafeURI;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class RequestHandlerGithub extends RequestHandlerZip {
|
||||||
|
|
||||||
|
public RequestHandlerGithub() {
|
||||||
|
super(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SpaceSafeURI getNewLoc(SpaceSafeURI loc) {
|
||||||
|
return loc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: is caching really needed, if HTTPURLConnection follows redirects correctly?
|
||||||
|
private Map<String, SpaceSafeURI> zipUriMap = new HashMap<>();
|
||||||
|
private final ReentrantReadWriteLock zipUriLock = new ReentrantReadWriteLock();
|
||||||
|
private static Pattern repoMatcherPattern = Pattern.compile("/([\\w.-]+/[\\w.-]+).*");
|
||||||
|
|
||||||
|
private String getRepoName(SpaceSafeURI loc) {
|
||||||
|
Matcher matcher = repoMatcherPattern.matcher(loc.getPath());
|
||||||
|
if (matcher.matches()) {
|
||||||
|
return matcher.group(1);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected SpaceSafeURI getZipUri(SpaceSafeURI loc) throws Exception {
|
||||||
|
String repoName = getRepoName(loc);
|
||||||
|
String branchName = getBranch(loc);
|
||||||
|
zipUriLock.readLock().lock();
|
||||||
|
SpaceSafeURI zipUri = zipUriMap.get(repoName + "/" + branchName);
|
||||||
|
zipUriLock.readLock().unlock();
|
||||||
|
if (zipUri != null) {
|
||||||
|
return zipUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
zipUri = new SpaceSafeURI("https://api.github.com/repos/" + repoName + "/zipball/" + branchName);
|
||||||
|
|
||||||
|
zipUriLock.writeLock().lock();
|
||||||
|
// If another thread sets the value concurrently, use the value of the
|
||||||
|
// thread that first acquired the lock.
|
||||||
|
SpaceSafeURI zipUriInserted = zipUriMap.putIfAbsent(repoName + "/" + branchName, zipUri);
|
||||||
|
if (zipUriInserted != null) {
|
||||||
|
zipUri = zipUriInserted;
|
||||||
|
}
|
||||||
|
zipUriLock.writeLock().unlock();
|
||||||
|
return zipUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Pattern branchMatcherPattern = Pattern.compile("/[\\w.-]+/[\\w.-]+/blob/([\\w.-]+).*");
|
||||||
|
|
||||||
|
private String getBranch(SpaceSafeURI loc) {
|
||||||
|
Matcher matcher = branchMatcherPattern.matcher(loc.getPath());
|
||||||
|
if (matcher.matches()) {
|
||||||
|
return matcher.group(1);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected SpaceSafeURI getLocationInZip(SpaceSafeURI loc) throws Exception {
|
||||||
|
String path = "/" + getRepoName(loc) + "/blob/" + getBranch(loc);
|
||||||
|
return new SpaceSafeURI(loc.getScheme(), loc.getAuthority(), path, null, null).relativize(loc);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matchesHandler(SpaceSafeURI loc) {
|
||||||
|
String scheme = loc.getScheme();
|
||||||
|
if (!("http".equals(scheme) || "https".equals(scheme))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!"github.com".equals(loc.getHost())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// TODO: sanity checks, support for more github urls
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package link.infra.packwiz.installer.request.handlers;
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.metadata.SpaceSafeURI;
|
||||||
|
import link.infra.packwiz.installer.request.IRequestHandler;
|
||||||
|
import okio.Okio;
|
||||||
|
import okio.Source;
|
||||||
|
|
||||||
|
import java.net.URLConnection;
|
||||||
|
|
||||||
|
public class RequestHandlerHTTP implements IRequestHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matchesHandler(SpaceSafeURI loc) {
|
||||||
|
String scheme = loc.getScheme();
|
||||||
|
return "http".equals(scheme) || "https".equals(scheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Source getFileSource(SpaceSafeURI loc) throws Exception {
|
||||||
|
URLConnection conn = loc.toURL().openConnection();
|
||||||
|
// TODO: when do we send specific headers??? should there be a way to signal this?
|
||||||
|
// github *sometimes* requires it, sometimes not!
|
||||||
|
//conn.addRequestProperty("Accept", "application/octet-stream");
|
||||||
|
// 30 second read timeout
|
||||||
|
conn.setReadTimeout(30 * 1000);
|
||||||
|
return Okio.source(conn.getInputStream());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,169 @@
|
|||||||
|
package link.infra.packwiz.installer.request.handlers;
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.metadata.SpaceSafeURI;
|
||||||
|
import okio.Buffer;
|
||||||
|
import okio.BufferedSource;
|
||||||
|
import okio.Okio;
|
||||||
|
import okio.Source;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipInputStream;
|
||||||
|
|
||||||
|
public abstract class RequestHandlerZip extends RequestHandlerHTTP {
|
||||||
|
|
||||||
|
private final boolean modeHasFolder;
|
||||||
|
|
||||||
|
public RequestHandlerZip(boolean modeHasFolder) {
|
||||||
|
this.modeHasFolder = modeHasFolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String removeFolder(String name) {
|
||||||
|
if (modeHasFolder) {
|
||||||
|
return name.substring(name.indexOf("/")+1);
|
||||||
|
} else {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ZipReader {
|
||||||
|
|
||||||
|
private final ZipInputStream zis;
|
||||||
|
private final Map<SpaceSafeURI, Buffer> readFiles = new HashMap<>();
|
||||||
|
// Write lock implies access to ZipInputStream - only 1 thread must read at a time!
|
||||||
|
final ReentrantLock filesLock = new ReentrantLock();
|
||||||
|
private ZipEntry entry;
|
||||||
|
|
||||||
|
private final BufferedSource zipSource;
|
||||||
|
|
||||||
|
ZipReader(Source zip) {
|
||||||
|
zis = new ZipInputStream(Okio.buffer(zip).inputStream());
|
||||||
|
zipSource = Okio.buffer(Okio.source(zis));
|
||||||
|
}
|
||||||
|
|
||||||
|
// File lock must be obtained before calling this function
|
||||||
|
private Buffer readCurrFile() throws IOException {
|
||||||
|
Buffer fileBuffer = new Buffer();
|
||||||
|
zipSource.readFully(fileBuffer, entry.getSize());
|
||||||
|
return fileBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// File lock must be obtained before calling this function
|
||||||
|
private Buffer findFile(SpaceSafeURI loc) throws IOException, URISyntaxException {
|
||||||
|
while (true) {
|
||||||
|
entry = zis.getNextEntry();
|
||||||
|
if (entry == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Buffer data = readCurrFile();
|
||||||
|
SpaceSafeURI fileLoc = new SpaceSafeURI(removeFolder(entry.getName()));
|
||||||
|
if (loc.equals(fileLoc)) {
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
readFiles.put(fileLoc, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Source getFileSource(SpaceSafeURI loc) throws Exception {
|
||||||
|
filesLock.lock();
|
||||||
|
// Assume files are only read once, allow GC by removing
|
||||||
|
Buffer file = readFiles.remove(loc);
|
||||||
|
if (file != null) {
|
||||||
|
filesLock.unlock();
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
file = findFile(loc);
|
||||||
|
filesLock.unlock();
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
SpaceSafeURI findInZip(Predicate<SpaceSafeURI> matches) throws Exception {
|
||||||
|
filesLock.lock();
|
||||||
|
for (SpaceSafeURI file : readFiles.keySet()) {
|
||||||
|
if (matches.test(file)) {
|
||||||
|
filesLock.unlock();
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
entry = zis.getNextEntry();
|
||||||
|
if (entry == null) {
|
||||||
|
filesLock.unlock();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Buffer data = readCurrFile();
|
||||||
|
SpaceSafeURI fileLoc = new SpaceSafeURI(removeFolder(entry.getName()));
|
||||||
|
readFiles.put(fileLoc, data);
|
||||||
|
if (matches.test(fileLoc)) {
|
||||||
|
filesLock.unlock();
|
||||||
|
return fileLoc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Map<SpaceSafeURI, ZipReader> cache = new HashMap<>();
|
||||||
|
private final ReentrantReadWriteLock cacheLock = new ReentrantReadWriteLock();
|
||||||
|
|
||||||
|
protected abstract SpaceSafeURI getZipUri(SpaceSafeURI loc) throws Exception;
|
||||||
|
|
||||||
|
protected abstract SpaceSafeURI getLocationInZip(SpaceSafeURI loc) throws Exception;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public abstract boolean matchesHandler(SpaceSafeURI loc);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Source getFileSource(SpaceSafeURI loc) throws Exception {
|
||||||
|
SpaceSafeURI zipUri = getZipUri(loc);
|
||||||
|
cacheLock.readLock().lock();
|
||||||
|
ZipReader zr = cache.get(zipUri);
|
||||||
|
cacheLock.readLock().unlock();
|
||||||
|
if (zr == null) {
|
||||||
|
cacheLock.writeLock().lock();
|
||||||
|
// Recheck, because unlocking read lock allows another thread to modify it
|
||||||
|
zr = cache.get(zipUri);
|
||||||
|
if (zr == null) {
|
||||||
|
Source src = super.getFileSource(zipUri);
|
||||||
|
if (src == null) {
|
||||||
|
cacheLock.writeLock().unlock();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
zr = new ZipReader(src);
|
||||||
|
cache.put(zipUri, zr);
|
||||||
|
}
|
||||||
|
cacheLock.writeLock().unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
return zr.getFileSource(getLocationInZip(loc));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected SpaceSafeURI findInZip(SpaceSafeURI loc, Predicate<SpaceSafeURI> matches) throws Exception {
|
||||||
|
SpaceSafeURI zipUri = getZipUri(loc);
|
||||||
|
cacheLock.readLock().lock();
|
||||||
|
ZipReader zr = cache.get(zipUri);
|
||||||
|
cacheLock.readLock().unlock();
|
||||||
|
if (zr == null) {
|
||||||
|
cacheLock.writeLock().lock();
|
||||||
|
// Recheck, because unlocking read lock allows another thread to modify it
|
||||||
|
zr = cache.get(zipUri);
|
||||||
|
if (zr == null) {
|
||||||
|
zr = new ZipReader(super.getFileSource(zipUri));
|
||||||
|
cache.put(zipUri, zr);
|
||||||
|
}
|
||||||
|
cacheLock.writeLock().unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
return zr.findInZip(matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
package link.infra.packwiz.installer.ui;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
|
public class CLIHandler implements IUserInterface {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleException(Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void show(InputStateHandler h) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void submitProgress(InstallProgress progress) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
if (progress.hasProgress) {
|
||||||
|
sb.append('(');
|
||||||
|
sb.append(progress.progress);
|
||||||
|
sb.append('/');
|
||||||
|
sb.append(progress.progressTotal);
|
||||||
|
sb.append(") ");
|
||||||
|
}
|
||||||
|
sb.append(progress.message);
|
||||||
|
System.out.println(sb.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void executeManager(Runnable task) {
|
||||||
|
task.run();
|
||||||
|
System.out.println("Finished successfully!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Future<Boolean> showOptions(List<IOptionDetails> option) {
|
||||||
|
throw new RuntimeException("Optional mods not implemented for CLI! Make sure your optional mods are only on the client side!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Future<IExceptionDetails.ExceptionListResult> showExceptions(List<IExceptionDetails> opts, int numTotal, boolean allowsIgnore) {
|
||||||
|
CompletableFuture<IExceptionDetails.ExceptionListResult> future = new CompletableFuture<>();
|
||||||
|
future.complete(IExceptionDetails.ExceptionListResult.CANCEL);
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,183 @@
|
|||||||
|
package link.infra.packwiz.installer.ui;
|
||||||
|
|
||||||
|
import link.infra.packwiz.installer.ui.IExceptionDetails.ExceptionListResult;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import javax.swing.border.EmptyBorder;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.event.WindowAdapter;
|
||||||
|
import java.awt.event.WindowEvent;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
class ExceptionListWindow extends JDialog {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
private final JTextArea lblExceptionStacktrace;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the dialog.
|
||||||
|
*/
|
||||||
|
ExceptionListWindow(List<IExceptionDetails> eList, CompletableFuture<ExceptionListResult> future, int numTotal, boolean allowsIgnore, JFrame parentWindow) {
|
||||||
|
super(parentWindow, "Failed file downloads", true);
|
||||||
|
|
||||||
|
setBounds(100, 100, 540, 340);
|
||||||
|
setLocationRelativeTo(parentWindow);
|
||||||
|
getContentPane().setLayout(new BorderLayout());
|
||||||
|
{
|
||||||
|
JPanel errorPanel = new JPanel();
|
||||||
|
getContentPane().add(errorPanel, BorderLayout.NORTH);
|
||||||
|
{
|
||||||
|
JLabel lblWarning = new JLabel("One or more errors were encountered while installing the modpack!");
|
||||||
|
lblWarning.setIcon(UIManager.getIcon("OptionPane.warningIcon"));
|
||||||
|
errorPanel.add(lblWarning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JPanel contentPanel = new JPanel();
|
||||||
|
contentPanel.setBorder(new EmptyBorder(5, 5, 5, 5));
|
||||||
|
getContentPane().add(contentPanel, BorderLayout.CENTER);
|
||||||
|
contentPanel.setLayout(new BorderLayout(0, 0));
|
||||||
|
{
|
||||||
|
JSplitPane splitPane = new JSplitPane();
|
||||||
|
splitPane.setResizeWeight(0.3);
|
||||||
|
contentPanel.add(splitPane);
|
||||||
|
{
|
||||||
|
lblExceptionStacktrace = new JTextArea("Select a file");
|
||||||
|
lblExceptionStacktrace.setBackground(UIManager.getColor("List.background"));
|
||||||
|
lblExceptionStacktrace.setOpaque(true);
|
||||||
|
lblExceptionStacktrace.setWrapStyleWord(true);
|
||||||
|
lblExceptionStacktrace.setLineWrap(true);
|
||||||
|
lblExceptionStacktrace.setEditable(false);
|
||||||
|
lblExceptionStacktrace.setFocusable(true);
|
||||||
|
lblExceptionStacktrace.setFont(UIManager.getFont("Label.font"));
|
||||||
|
lblExceptionStacktrace.setBorder(new EmptyBorder(5, 5, 5, 5));
|
||||||
|
JScrollPane scrollPane = new JScrollPane(lblExceptionStacktrace);
|
||||||
|
scrollPane.setBorder(new EmptyBorder(0, 0, 0, 0));
|
||||||
|
splitPane.setRightComponent(scrollPane);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
JList<String> list = new JList<>();
|
||||||
|
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||||
|
list.setBorder(new EmptyBorder(5, 5, 5, 5));
|
||||||
|
ExceptionListModel listModel = new ExceptionListModel(eList);
|
||||||
|
list.setModel(listModel);
|
||||||
|
list.addListSelectionListener(e -> {
|
||||||
|
int i = list.getSelectedIndex();
|
||||||
|
if (i > -1) {
|
||||||
|
StringWriter sw = new StringWriter();
|
||||||
|
listModel.getExceptionAt(i).printStackTrace(new PrintWriter(sw));
|
||||||
|
lblExceptionStacktrace.setText(sw.toString());
|
||||||
|
// Scroll to the top
|
||||||
|
lblExceptionStacktrace.setCaretPosition(0);
|
||||||
|
} else {
|
||||||
|
lblExceptionStacktrace.setText("Select a file");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
JScrollPane scrollPane = new JScrollPane(list);
|
||||||
|
scrollPane.setBorder(new EmptyBorder(0, 0, 0, 0));
|
||||||
|
splitPane.setLeftComponent(scrollPane);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
JPanel buttonPane = new JPanel();
|
||||||
|
getContentPane().add(buttonPane, BorderLayout.SOUTH);
|
||||||
|
buttonPane.setLayout(new BorderLayout(0, 0));
|
||||||
|
{
|
||||||
|
JPanel rightButtons = new JPanel();
|
||||||
|
buttonPane.add(rightButtons, BorderLayout.EAST);
|
||||||
|
{
|
||||||
|
JButton btnContinue = new JButton("Continue");
|
||||||
|
btnContinue.setToolTipText("Attempt to continue installing, excluding the failed downloads");
|
||||||
|
btnContinue.addActionListener(e -> {
|
||||||
|
future.complete(ExceptionListResult.CONTINUE);
|
||||||
|
ExceptionListWindow.this.dispose();
|
||||||
|
});
|
||||||
|
rightButtons.add(btnContinue);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
JButton btnCancelLaunch = new JButton("Cancel launch");
|
||||||
|
btnCancelLaunch.setToolTipText("Stop launching the game");
|
||||||
|
btnCancelLaunch.addActionListener(e -> {
|
||||||
|
future.complete(ExceptionListResult.CANCEL);
|
||||||
|
ExceptionListWindow.this.dispose();
|
||||||
|
});
|
||||||
|
rightButtons.add(btnCancelLaunch);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
JButton btnIgnoreUpdate = new JButton("Ignore update");
|
||||||
|
btnIgnoreUpdate.setEnabled(allowsIgnore);
|
||||||
|
btnIgnoreUpdate.setToolTipText("Start the game without attempting to update");
|
||||||
|
btnIgnoreUpdate.addActionListener(e -> {
|
||||||
|
future.complete(ExceptionListResult.IGNORE);
|
||||||
|
ExceptionListWindow.this.dispose();
|
||||||
|
});
|
||||||
|
rightButtons.add(btnIgnoreUpdate);
|
||||||
|
{
|
||||||
|
JLabel lblErrored = new JLabel(eList.size() + "/" + numTotal + " errored");
|
||||||
|
lblErrored.setHorizontalAlignment(SwingConstants.CENTER);
|
||||||
|
buttonPane.add(lblErrored, BorderLayout.CENTER);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
JPanel leftButtons = new JPanel();
|
||||||
|
buttonPane.add(leftButtons, BorderLayout.WEST);
|
||||||
|
{
|
||||||
|
JButton btnReportIssue = new JButton("Report issue");
|
||||||
|
boolean supported = Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE);
|
||||||
|
btnReportIssue.setEnabled(supported);
|
||||||
|
if (supported) {
|
||||||
|
btnReportIssue.addActionListener(e -> {
|
||||||
|
try {
|
||||||
|
Desktop.getDesktop().browse(new URI("https://github.com/comp500/packwiz-installer/issues/new"));
|
||||||
|
} catch (IOException | URISyntaxException e1) {
|
||||||
|
// lol the button just won't work i guess
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
leftButtons.add(btnReportIssue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addWindowListener(new WindowAdapter() {
|
||||||
|
@Override
|
||||||
|
public void windowClosing(WindowEvent e) {
|
||||||
|
future.complete(ExceptionListResult.CANCEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void windowClosed(WindowEvent e) {
|
||||||
|
// Just in case closing didn't get triggered - if something else called dispose() the
|
||||||
|
// future will have already completed
|
||||||
|
future.complete(ExceptionListResult.CANCEL);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ExceptionListModel extends AbstractListModel<String> {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
private final List<IExceptionDetails> details;
|
||||||
|
|
||||||
|
ExceptionListModel(List<IExceptionDetails> details) {
|
||||||
|
this.details = details;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSize() {
|
||||||
|
return details.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getElementAt(int index) {
|
||||||
|
return details.get(index).getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
Exception getExceptionAt(int index) {
|
||||||
|
return details.get(index).getException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package link.infra.packwiz.installer.ui;
|
||||||
|
|
||||||
|
public interface IExceptionDetails {
|
||||||
|
Exception getException();
|
||||||
|
String getName();
|
||||||
|
|
||||||
|
enum ExceptionListResult {
|
||||||
|
CONTINUE, CANCEL, IGNORE
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package link.infra.packwiz.installer.ui;
|
||||||
|
|
||||||
|
public interface IOptionDetails {
|
||||||
|
String getName();
|
||||||
|
boolean getOptionValue();
|
||||||
|
String getOptionDescription();
|
||||||
|
void setOptionValue(boolean value);
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
package link.infra.packwiz.installer.ui;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
|
public interface IUserInterface {
|
||||||
|
|
||||||
|
void show(InputStateHandler handler);
|
||||||
|
|
||||||
|
void handleException(Exception e);
|
||||||
|
|
||||||
|
default void handleExceptionAndExit(Exception e) {
|
||||||
|
handleException(e);
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
default void setTitle(String title) {}
|
||||||
|
|
||||||
|
void submitProgress(InstallProgress progress);
|
||||||
|
|
||||||
|
void executeManager(Runnable task);
|
||||||
|
|
||||||
|
// Return true if the installation was cancelled!
|
||||||
|
Future<Boolean> showOptions(List<IOptionDetails> option);
|
||||||
|
|
||||||
|
Future<IExceptionDetails.ExceptionListResult> showExceptions(List<IExceptionDetails> opts, int numTotal, boolean allowsIgnore);
|
||||||
|
|
||||||
|
default void disableOptionsButton() {}
|
||||||
|
|
||||||
|
// Should not return CONTINUE
|
||||||
|
default Future<IExceptionDetails.ExceptionListResult> showCancellationDialog() {
|
||||||
|
CompletableFuture<IExceptionDetails.ExceptionListResult> future = new CompletableFuture<>();
|
||||||
|
future.complete(IExceptionDetails.ExceptionListResult.CANCEL);
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
package link.infra.packwiz.installer.ui;
|
||||||
|
|
||||||
|
public class InputStateHandler {
|
||||||
|
private boolean optionsButtonPressed = false;
|
||||||
|
private boolean cancelButtonPressed = false;
|
||||||
|
|
||||||
|
synchronized void pressCancelButton() {
|
||||||
|
this.cancelButtonPressed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized void pressOptionsButton() {
|
||||||
|
this.optionsButtonPressed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean getCancelButton() {
|
||||||
|
return cancelButtonPressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean getOptionsButton() {
|
||||||
|
return optionsButtonPressed;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
package link.infra.packwiz.installer.ui;
|
||||||
|
|
||||||
|
public class InstallProgress {
|
||||||
|
public final String message;
|
||||||
|
public final boolean hasProgress;
|
||||||
|
public final int progress;
|
||||||
|
public final int progressTotal;
|
||||||
|
|
||||||
|
public InstallProgress(String message) {
|
||||||
|
this.message = message;
|
||||||
|
hasProgress = false;
|
||||||
|
progress = 0;
|
||||||
|
progressTotal = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InstallProgress(String message, int progress, int progressTotal) {
|
||||||
|
this.message = message;
|
||||||
|
hasProgress = true;
|
||||||
|
this.progress = progress;
|
||||||
|
this.progressTotal = progressTotal;
|
||||||
|
}
|
||||||
|
}
|
228
src/main/java/link/infra/packwiz/installer/ui/InstallWindow.java
Normal file
228
src/main/java/link/infra/packwiz/installer/ui/InstallWindow.java
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
package link.infra.packwiz.installer.ui;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import javax.swing.border.EmptyBorder;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
public class InstallWindow implements IUserInterface {
|
||||||
|
|
||||||
|
private JFrame frmPackwizlauncher;
|
||||||
|
private JLabel lblProgresslabel;
|
||||||
|
private JProgressBar progressBar;
|
||||||
|
private InputStateHandler inputStateHandler;
|
||||||
|
|
||||||
|
private String title = "Updating modpack...";
|
||||||
|
private SwingWorkerButWithPublicPublish<Void, InstallProgress> worker;
|
||||||
|
private AtomicBoolean aboutToCrash = new AtomicBoolean();
|
||||||
|
private JButton btnOptions;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void show(InputStateHandler handler) {
|
||||||
|
this.inputStateHandler = handler;
|
||||||
|
EventQueue.invokeLater(() -> {
|
||||||
|
try {
|
||||||
|
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||||
|
initialize();
|
||||||
|
frmPackwizlauncher.setVisible(true);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the contents of the frame.
|
||||||
|
* @wbp.parser.entryPoint
|
||||||
|
*/
|
||||||
|
private void initialize() {
|
||||||
|
frmPackwizlauncher = new JFrame();
|
||||||
|
frmPackwizlauncher.setTitle(title);
|
||||||
|
frmPackwizlauncher.setBounds(100, 100, 493, 95);
|
||||||
|
frmPackwizlauncher.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||||
|
frmPackwizlauncher.setLocationRelativeTo(null);
|
||||||
|
|
||||||
|
JPanel panel = new JPanel();
|
||||||
|
panel.setBorder(new EmptyBorder(10, 10, 10, 10));
|
||||||
|
frmPackwizlauncher.getContentPane().add(panel, BorderLayout.CENTER);
|
||||||
|
panel.setLayout(new BorderLayout(0, 0));
|
||||||
|
|
||||||
|
progressBar = new JProgressBar();
|
||||||
|
progressBar.setIndeterminate(true);
|
||||||
|
panel.add(progressBar, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
lblProgresslabel = new JLabel("Loading...");
|
||||||
|
panel.add(lblProgresslabel, BorderLayout.SOUTH);
|
||||||
|
|
||||||
|
JPanel panel_1 = new JPanel();
|
||||||
|
panel_1.setBorder(new EmptyBorder(0, 5, 0, 5));
|
||||||
|
frmPackwizlauncher.getContentPane().add(panel_1, BorderLayout.EAST);
|
||||||
|
GridBagLayout gbl_panel_1 = new GridBagLayout();
|
||||||
|
panel_1.setLayout(gbl_panel_1);
|
||||||
|
|
||||||
|
btnOptions = new JButton("Optional mods...");
|
||||||
|
btnOptions.addActionListener(e -> {
|
||||||
|
btnOptions.setText("Loading...");
|
||||||
|
btnOptions.setEnabled(false);
|
||||||
|
inputStateHandler.pressOptionsButton();
|
||||||
|
});
|
||||||
|
btnOptions.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||||
|
GridBagConstraints gbc_btnOptions = new GridBagConstraints();
|
||||||
|
gbc_btnOptions.gridx = 0;
|
||||||
|
gbc_btnOptions.gridy = 0;
|
||||||
|
panel_1.add(btnOptions, gbc_btnOptions);
|
||||||
|
|
||||||
|
JButton btnCancel = new JButton("Cancel");
|
||||||
|
btnCancel.addActionListener(e -> {
|
||||||
|
btnCancel.setEnabled(false);
|
||||||
|
inputStateHandler.pressCancelButton();
|
||||||
|
});
|
||||||
|
btnCancel.setAlignmentX(Component.CENTER_ALIGNMENT);
|
||||||
|
GridBagConstraints gbc_btnCancel = new GridBagConstraints();
|
||||||
|
gbc_btnCancel.gridx = 0;
|
||||||
|
gbc_btnCancel.gridy = 1;
|
||||||
|
panel_1.add(btnCancel, gbc_btnCancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleException(Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
EventQueue.invokeLater(() -> {
|
||||||
|
JOptionPane.showMessageDialog(null, "An error occurred: \n" + e.getClass().getCanonicalName() + ": " + e.getMessage(), title, JOptionPane.ERROR_MESSAGE);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleExceptionAndExit(Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
// Used to prevent the done() handler of SwingWorker executing if the invokeLater hasn't happened yet
|
||||||
|
aboutToCrash.set(true);
|
||||||
|
EventQueue.invokeLater(() -> {
|
||||||
|
JOptionPane.showMessageDialog(null, "A fatal error occurred: \n" + e.getClass().getCanonicalName() + ": " + e.getMessage(), title, JOptionPane.ERROR_MESSAGE);
|
||||||
|
System.exit(1);
|
||||||
|
});
|
||||||
|
// Pause forever, so it blocks while we wait for System.exit to take effect
|
||||||
|
try {
|
||||||
|
Thread.currentThread().join();
|
||||||
|
} catch (InterruptedException ex) {
|
||||||
|
// no u
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
if (frmPackwizlauncher != null) {
|
||||||
|
EventQueue.invokeLater(() -> frmPackwizlauncher.setTitle(title));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void submitProgress(InstallProgress progress) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
if (progress.hasProgress) {
|
||||||
|
sb.append('(');
|
||||||
|
sb.append(progress.progress);
|
||||||
|
sb.append('/');
|
||||||
|
sb.append(progress.progressTotal);
|
||||||
|
sb.append(") ");
|
||||||
|
}
|
||||||
|
sb.append(progress.message);
|
||||||
|
// TODO: better logging library?
|
||||||
|
System.out.println(sb.toString());
|
||||||
|
if (worker != null) {
|
||||||
|
worker.publishPublic(progress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void executeManager(Runnable task) {
|
||||||
|
EventQueue.invokeLater(() -> {
|
||||||
|
worker = new SwingWorkerButWithPublicPublish<Void, InstallProgress>() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Void doInBackground() {
|
||||||
|
task.run();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void process(List<InstallProgress> chunks) {
|
||||||
|
// Only process last chunk
|
||||||
|
if (chunks.size() > 0) {
|
||||||
|
InstallProgress prog = chunks.get(chunks.size() - 1);
|
||||||
|
if (prog.hasProgress) {
|
||||||
|
progressBar.setIndeterminate(false);
|
||||||
|
progressBar.setValue(prog.progress);
|
||||||
|
progressBar.setMaximum(prog.progressTotal);
|
||||||
|
} else {
|
||||||
|
progressBar.setIndeterminate(true);
|
||||||
|
progressBar.setValue(0);
|
||||||
|
}
|
||||||
|
lblProgresslabel.setText(prog.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void done() {
|
||||||
|
if (aboutToCrash.get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// TODO: a better way to do this?
|
||||||
|
frmPackwizlauncher.dispose();
|
||||||
|
System.out.println("Finished successfully!");
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
worker.execute();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Future<Boolean> showOptions(List<IOptionDetails> opts) {
|
||||||
|
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
||||||
|
EventQueue.invokeLater(() -> {
|
||||||
|
OptionsSelectWindow dialog = new OptionsSelectWindow(opts, future, frmPackwizlauncher);
|
||||||
|
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
|
||||||
|
dialog.setVisible(true);
|
||||||
|
});
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Future<IExceptionDetails.ExceptionListResult> showExceptions(List<IExceptionDetails> opts, int numTotal, boolean allowsIgnore) {
|
||||||
|
CompletableFuture<IExceptionDetails.ExceptionListResult> future = new CompletableFuture<>();
|
||||||
|
EventQueue.invokeLater(() -> {
|
||||||
|
ExceptionListWindow dialog = new ExceptionListWindow(opts, future, numTotal, allowsIgnore, frmPackwizlauncher);
|
||||||
|
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
|
||||||
|
dialog.setVisible(true);
|
||||||
|
});
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disableOptionsButton() {
|
||||||
|
if (btnOptions != null) {
|
||||||
|
btnOptions.setText("Optional mods...");
|
||||||
|
btnOptions.setEnabled(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Future<IExceptionDetails.ExceptionListResult> showCancellationDialog() {
|
||||||
|
CompletableFuture<IExceptionDetails.ExceptionListResult> future = new CompletableFuture<>();
|
||||||
|
EventQueue.invokeLater(() -> {
|
||||||
|
Object[] buttons = {"Quit", "Ignore"};
|
||||||
|
int 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(result == 0 ? IExceptionDetails.ExceptionListResult.CANCEL : IExceptionDetails.ExceptionListResult.IGNORE);
|
||||||
|
});
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package link.infra.packwiz.installer.ui;
|
||||||
|
|
||||||
|
// Serves as a proxy for IOptionDetails, so that setOptionValue isn't called until OK is clicked
|
||||||
|
class OptionTempHandler implements IOptionDetails {
|
||||||
|
private final IOptionDetails opt;
|
||||||
|
private boolean tempValue;
|
||||||
|
|
||||||
|
OptionTempHandler(IOptionDetails opt) {
|
||||||
|
this.opt = opt;
|
||||||
|
tempValue = opt.getOptionValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return opt.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOptionDescription() {
|
||||||
|
return opt.getOptionDescription();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getOptionValue() {
|
||||||
|
return tempValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOptionValue(boolean value) {
|
||||||
|
tempValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void finalise() {
|
||||||
|
opt.setOptionValue(tempValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,205 @@
|
|||||||
|
package link.infra.packwiz.installer.ui;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import javax.swing.border.EmptyBorder;
|
||||||
|
import javax.swing.event.ListSelectionEvent;
|
||||||
|
import javax.swing.event.ListSelectionListener;
|
||||||
|
import javax.swing.event.TableModelListener;
|
||||||
|
import javax.swing.table.TableModel;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.event.ActionEvent;
|
||||||
|
import java.awt.event.ActionListener;
|
||||||
|
import java.awt.event.WindowAdapter;
|
||||||
|
import java.awt.event.WindowEvent;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public class OptionsSelectWindow extends JDialog implements ActionListener {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
private final JTextArea lblOptionDescription;
|
||||||
|
private final OptionTableModel tableModel;
|
||||||
|
private final CompletableFuture<Boolean> future;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the dialog.
|
||||||
|
*/
|
||||||
|
OptionsSelectWindow(List<IOptionDetails> optList, CompletableFuture<Boolean> future, JFrame parentWindow) {
|
||||||
|
super(parentWindow, "Select optional mods...", true);
|
||||||
|
|
||||||
|
tableModel = new OptionTableModel(optList);
|
||||||
|
this.future = future;
|
||||||
|
|
||||||
|
setBounds(100, 100, 450, 300);
|
||||||
|
setLocationRelativeTo(parentWindow);
|
||||||
|
getContentPane().setLayout(new BorderLayout());
|
||||||
|
JPanel contentPanel = new JPanel();
|
||||||
|
contentPanel.setBorder(new EmptyBorder(5, 5, 5, 5));
|
||||||
|
getContentPane().add(contentPanel, BorderLayout.CENTER);
|
||||||
|
contentPanel.setLayout(new BorderLayout(0, 0));
|
||||||
|
{
|
||||||
|
JSplitPane splitPane = new JSplitPane();
|
||||||
|
splitPane.setResizeWeight(0.5);
|
||||||
|
contentPanel.add(splitPane);
|
||||||
|
{
|
||||||
|
JTable table = new JTable();
|
||||||
|
table.setShowVerticalLines(false);
|
||||||
|
table.setShowHorizontalLines(false);
|
||||||
|
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||||
|
table.setShowGrid(false);
|
||||||
|
table.setModel(tableModel);
|
||||||
|
table.getColumnModel().getColumn(0).setResizable(false);
|
||||||
|
table.getColumnModel().getColumn(0).setPreferredWidth(15);
|
||||||
|
table.getColumnModel().getColumn(0).setMaxWidth(15);
|
||||||
|
table.getColumnModel().getColumn(1).setResizable(false);
|
||||||
|
table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
|
||||||
|
@Override
|
||||||
|
public void valueChanged(ListSelectionEvent e) {
|
||||||
|
int i = table.getSelectedRow();
|
||||||
|
if (i > -1) {
|
||||||
|
lblOptionDescription.setText(tableModel.getDescription(i));
|
||||||
|
} else {
|
||||||
|
lblOptionDescription.setText("Select an option...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
table.setTableHeader(null);
|
||||||
|
JScrollPane scrollPane = new JScrollPane(table);
|
||||||
|
scrollPane.getViewport().setBackground(UIManager.getColor("List.background"));
|
||||||
|
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
|
||||||
|
scrollPane.setBorder(new EmptyBorder(0, 0, 0, 0));
|
||||||
|
splitPane.setLeftComponent(scrollPane);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
lblOptionDescription = new JTextArea("Select an option...");
|
||||||
|
lblOptionDescription.setBackground(UIManager.getColor("List.background"));
|
||||||
|
lblOptionDescription.setOpaque(true);
|
||||||
|
lblOptionDescription.setWrapStyleWord(true);
|
||||||
|
lblOptionDescription.setLineWrap(true);
|
||||||
|
lblOptionDescription.setEditable(false);
|
||||||
|
lblOptionDescription.setFocusable(false);
|
||||||
|
lblOptionDescription.setFont(UIManager.getFont("Label.font"));
|
||||||
|
lblOptionDescription.setBorder(new EmptyBorder(10, 10, 10, 10));
|
||||||
|
JScrollPane scrollPane = new JScrollPane(lblOptionDescription);
|
||||||
|
scrollPane.setBorder(new EmptyBorder(0, 0, 0, 0));
|
||||||
|
splitPane.setRightComponent(scrollPane);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
JPanel buttonPane = new JPanel();
|
||||||
|
buttonPane.setLayout(new FlowLayout(FlowLayout.RIGHT));
|
||||||
|
getContentPane().add(buttonPane, BorderLayout.SOUTH);
|
||||||
|
{
|
||||||
|
JButton okButton = new JButton("OK");
|
||||||
|
okButton.setActionCommand("OK");
|
||||||
|
okButton.addActionListener(this);
|
||||||
|
buttonPane.add(okButton);
|
||||||
|
getRootPane().setDefaultButton(okButton);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
JButton cancelButton = new JButton("Cancel");
|
||||||
|
cancelButton.setActionCommand("Cancel");
|
||||||
|
cancelButton.addActionListener(this);
|
||||||
|
buttonPane.add(cancelButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addWindowListener(new WindowAdapter() {
|
||||||
|
@Override
|
||||||
|
public void windowClosing(WindowEvent e) {
|
||||||
|
future.complete(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void windowClosed(WindowEvent e) {
|
||||||
|
// Just in case closing didn't get triggered - if something else called dispose() the
|
||||||
|
// future will have already completed
|
||||||
|
future.complete(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class OptionTableModel implements TableModel {
|
||||||
|
private List<OptionTempHandler> opts = new ArrayList<>();
|
||||||
|
|
||||||
|
OptionTableModel(List<IOptionDetails> givenOpts) {
|
||||||
|
for (IOptionDetails opt : givenOpts) {
|
||||||
|
opts.add(new OptionTempHandler(opt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getRowCount() {
|
||||||
|
return opts.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getColumnCount() {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final String[] columnNames = {"Enabled", "Mod name"};
|
||||||
|
private final Class<?>[] columnTypes = {Boolean.class, String.class};
|
||||||
|
private final boolean[] columnEditables = {true, false};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getColumnName(int columnIndex) {
|
||||||
|
return columnNames[columnIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<?> getColumnClass(int columnIndex) {
|
||||||
|
return columnTypes[columnIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isCellEditable(int rowIndex, int columnIndex) {
|
||||||
|
return columnEditables[columnIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getValueAt(int rowIndex, int columnIndex) {
|
||||||
|
OptionTempHandler opt = opts.get(rowIndex);
|
||||||
|
return columnIndex == 0 ? opt.getOptionValue() : opt.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
|
||||||
|
if (columnIndex == 0) {
|
||||||
|
OptionTempHandler opt = opts.get(rowIndex);
|
||||||
|
opt.setOptionValue((boolean) aValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Noop, the table model doesn't change!
|
||||||
|
@Override
|
||||||
|
public void addTableModelListener(TableModelListener l) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeTableModelListener(TableModelListener l) {}
|
||||||
|
|
||||||
|
String getDescription(int rowIndex) {
|
||||||
|
return opts.get(rowIndex).getOptionDescription();
|
||||||
|
}
|
||||||
|
|
||||||
|
void finalise() {
|
||||||
|
for (OptionTempHandler opt : opts) {
|
||||||
|
opt.finalise();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void actionPerformed(ActionEvent e) {
|
||||||
|
if (e.getActionCommand().equals("OK")) {
|
||||||
|
tableModel.finalise();
|
||||||
|
future.complete(false);
|
||||||
|
dispose();
|
||||||
|
} else if (e.getActionCommand().equals("Cancel")) {
|
||||||
|
future.complete(true);
|
||||||
|
dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package link.infra.packwiz.installer.ui;
|
||||||
|
|
||||||
|
import javax.swing.SwingWorker;
|
||||||
|
|
||||||
|
// Q: AAA WHAT HAVE YOU DONE THIS IS DISGUSTING
|
||||||
|
// A: it just makes things easier, so i can easily have one interface for CLI/GUI
|
||||||
|
// if someone has a better way to do this please PR it
|
||||||
|
public abstract class SwingWorkerButWithPublicPublish<T,V> extends SwingWorker<T,V> {
|
||||||
|
@SafeVarargs
|
||||||
|
public final void publishPublic(V... chunks) {
|
||||||
|
publish(chunks);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +0,0 @@
|
|||||||
package link.infra.packwiz.installer
|
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
|
||||||
Main(args)
|
|
||||||
}
|
|
@ -1,325 +0,0 @@
|
|||||||
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.hash.Hash
|
|
||||||
import link.infra.packwiz.installer.metadata.hash.HashFormat
|
|
||||||
import link.infra.packwiz.installer.request.RequestException
|
|
||||||
import link.infra.packwiz.installer.target.ClientHolder
|
|
||||||
import link.infra.packwiz.installer.target.Side
|
|
||||||
import link.infra.packwiz.installer.target.path.PackwizFilePath
|
|
||||||
import link.infra.packwiz.installer.ui.data.ExceptionDetails
|
|
||||||
import link.infra.packwiz.installer.ui.data.IOptionDetails
|
|
||||||
import link.infra.packwiz.installer.util.Log
|
|
||||||
import okio.Buffer
|
|
||||||
import okio.HashingSink
|
|
||||||
import okio.blackholeSink
|
|
||||||
import okio.buffer
|
|
||||||
import java.io.IOException
|
|
||||||
import java.nio.file.Files
|
|
||||||
import java.nio.file.StandardCopyOption
|
|
||||||
|
|
||||||
internal class DownloadTask private constructor(val metadata: IndexFile.File, val index: IndexFile, private val downloadSide: Side) : IOptionDetails {
|
|
||||||
var cachedFile: ManifestFile.File? = null
|
|
||||||
private set
|
|
||||||
|
|
||||||
private var err: Exception? = null
|
|
||||||
val exceptionDetails get() = err?.let { e -> ExceptionDetails(name, e) }
|
|
||||||
|
|
||||||
fun failed() = err != null
|
|
||||||
|
|
||||||
var alreadyUpToDate = false
|
|
||||||
private set
|
|
||||||
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 var newOptional = true
|
|
||||||
var completionStatus = CompletionStatus.INCOMPLETE
|
|
||||||
private set
|
|
||||||
|
|
||||||
enum class CompletionStatus {
|
|
||||||
INCOMPLETE,
|
|
||||||
DOWNLOADED,
|
|
||||||
ALREADY_EXISTS_CACHED,
|
|
||||||
ALREADY_EXISTS_VALIDATED,
|
|
||||||
SKIPPED_DISABLED,
|
|
||||||
SKIPPED_WRONG_SIDE,
|
|
||||||
DELETED_DISABLED,
|
|
||||||
DELETED_WRONG_SIDE;
|
|
||||||
}
|
|
||||||
|
|
||||||
val isOptional get() = metadata.linkedFile?.option?.optional ?: false
|
|
||||||
|
|
||||||
fun isNewOptional() = isOptional && newOptional
|
|
||||||
|
|
||||||
fun correctSide() = metadata.linkedFile?.side?.let { downloadSide.hasSide(it) } ?: 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
|
|
||||||
}
|
|
||||||
cachedFile?.optionValue = value
|
|
||||||
}
|
|
||||||
|
|
||||||
override val optionDescription get() = metadata.linkedFile?.option?.description ?: ""
|
|
||||||
|
|
||||||
fun invalidate() {
|
|
||||||
invalidated = true
|
|
||||||
alreadyUpToDate = false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateFromCache(cachedFile: ManifestFile.File?) {
|
|
||||||
if (err != null) return
|
|
||||||
|
|
||||||
if (cachedFile == null) {
|
|
||||||
this.cachedFile = ManifestFile.File()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.cachedFile = cachedFile
|
|
||||||
if (!invalidated) {
|
|
||||||
val currHash = try {
|
|
||||||
metadata.getHashObj(index)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
err = e
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (currHash == cachedFile.hash) { // Already up to date
|
|
||||||
alreadyUpToDate = true
|
|
||||||
metadataRequired = false
|
|
||||||
completionStatus = CompletionStatus.ALREADY_EXISTS_CACHED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun downloadMetadata(clientHolder: ClientHolder) {
|
|
||||||
if (err != null) return
|
|
||||||
|
|
||||||
if (metadataRequired) {
|
|
||||||
try {
|
|
||||||
// Retrieve the linked metadata file
|
|
||||||
metadata.downloadMeta(index, clientHolder)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
err = e
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cachedFile?.let { cachedFile ->
|
|
||||||
val linkedFile = metadata.linkedFile
|
|
||||||
if (linkedFile != null) {
|
|
||||||
if (linkedFile.option.optional) {
|
|
||||||
if (cachedFile.isOptional) {
|
|
||||||
// isOptional didn't change
|
|
||||||
newOptional = false
|
|
||||||
} else {
|
|
||||||
// isOptional false -> true, set option to it's default value
|
|
||||||
// TODO: preserve previous option value, somehow??
|
|
||||||
cachedFile.optionValue = linkedFile.option.defaultValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cachedFile.isOptional = isOptional
|
|
||||||
cachedFile.onlyOtherSide = !correctSide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the file in the destination location is already valid
|
|
||||||
* Must be done after metadata retrieval
|
|
||||||
*/
|
|
||||||
fun validateExistingFile(packFolder: PackwizFilePath, clientHolder: ClientHolder) {
|
|
||||||
if (!alreadyUpToDate) {
|
|
||||||
try {
|
|
||||||
// TODO: only do this for files that didn't exist before or have been modified since last full update?
|
|
||||||
val destPath = metadata.destURI.rebase(packFolder)
|
|
||||||
destPath.source(clientHolder).use { src ->
|
|
||||||
// TODO: clean up duplicated code
|
|
||||||
val hash: Hash<*>
|
|
||||||
val fileHashFormat: HashFormat<*>
|
|
||||||
val linkedFile = metadata.linkedFile
|
|
||||||
|
|
||||||
if (linkedFile != null) {
|
|
||||||
hash = linkedFile.hash
|
|
||||||
fileHashFormat = linkedFile.download.hashFormat
|
|
||||||
} else {
|
|
||||||
hash = metadata.getHashObj(index)
|
|
||||||
fileHashFormat = metadata.hashFormat(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
val fileSource = fileHashFormat.source(src)
|
|
||||||
fileSource.buffer().readAll(blackholeSink())
|
|
||||||
if (hash == fileSource.hash) {
|
|
||||||
alreadyUpToDate = true
|
|
||||||
completionStatus = CompletionStatus.ALREADY_EXISTS_VALIDATED
|
|
||||||
|
|
||||||
// Update the manifest file
|
|
||||||
cachedFile = (cachedFile ?: ManifestFile.File()).also {
|
|
||||||
try {
|
|
||||||
it.hash = metadata.getHashObj(index)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
err = e
|
|
||||||
return
|
|
||||||
}
|
|
||||||
it.isOptional = isOptional
|
|
||||||
it.cachedLocation = metadata.destURI.rebase(packFolder)
|
|
||||||
metadata.linkedFile?.let { linked ->
|
|
||||||
try {
|
|
||||||
it.linkedFileHash = linked.hash
|
|
||||||
} catch (e: Exception) {
|
|
||||||
err = e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: RequestException) {
|
|
||||||
// Ignore exceptions; if the file doesn't exist we'll be downloading it
|
|
||||||
} catch (e: IOException) {
|
|
||||||
// Ignore exceptions; if the file doesn't exist we'll be downloading it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun download(packFolder: PackwizFilePath, clientHolder: ClientHolder) {
|
|
||||||
if (err != null) return
|
|
||||||
|
|
||||||
// Exclude wrong-side and optional false files
|
|
||||||
cachedFile?.let {
|
|
||||||
if ((it.isOptional && !it.optionValue) || !correctSide()) {
|
|
||||||
if (it.cachedLocation != null) {
|
|
||||||
// Ensure wrong-side or optional false files are removed
|
|
||||||
try {
|
|
||||||
completionStatus = if (Files.deleteIfExists(it.cachedLocation!!.nioPath)) {
|
|
||||||
if (correctSide()) { CompletionStatus.DELETED_DISABLED } else { CompletionStatus.DELETED_WRONG_SIDE }
|
|
||||||
} else {
|
|
||||||
if (correctSide()) { CompletionStatus.SKIPPED_DISABLED } else { CompletionStatus.SKIPPED_WRONG_SIDE }
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.warn("Failed to delete file", e)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
completionStatus =
|
|
||||||
if (correctSide()) { CompletionStatus.SKIPPED_DISABLED }
|
|
||||||
else { CompletionStatus.SKIPPED_WRONG_SIDE }
|
|
||||||
}
|
|
||||||
it.cachedLocation = null
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (alreadyUpToDate) return
|
|
||||||
|
|
||||||
val destPath = metadata.destURI.rebase(packFolder)
|
|
||||||
|
|
||||||
// Don't update files marked with preserve if they already exist on disk
|
|
||||||
if (metadata.preserve) {
|
|
||||||
if (destPath.nioPath.toFile().exists()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: add .disabled support?
|
|
||||||
|
|
||||||
try {
|
|
||||||
val hash: Hash<*>
|
|
||||||
val fileHashFormat: HashFormat<*>
|
|
||||||
val linkedFile = metadata.linkedFile
|
|
||||||
|
|
||||||
if (linkedFile != null) {
|
|
||||||
hash = linkedFile.hash
|
|
||||||
fileHashFormat = linkedFile.download.hashFormat
|
|
||||||
} else {
|
|
||||||
hash = metadata.getHashObj(index)
|
|
||||||
fileHashFormat = metadata.hashFormat(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
val src = metadata.getSource(clientHolder)
|
|
||||||
val fileSource = fileHashFormat.source(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 (hash == fileSource.hash) {
|
|
||||||
// isDirectory follows symlinks, but createDirectories doesn't
|
|
||||||
try {
|
|
||||||
Files.createDirectories(destPath.parent.nioPath)
|
|
||||||
} catch (e: java.nio.file.FileAlreadyExistsException) {
|
|
||||||
if (!Files.isDirectory(destPath.parent.nioPath)) {
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Files.copy(data.inputStream(), destPath.nioPath, StandardCopyOption.REPLACE_EXISTING)
|
|
||||||
data.clear()
|
|
||||||
} else {
|
|
||||||
// TODO: move println to something visible in the error window
|
|
||||||
println("Invalid hash for " + metadata.destURI.toString())
|
|
||||||
println("Calculated: " + fileSource.hash)
|
|
||||||
println("Expected: $hash")
|
|
||||||
// Attempt to get the SHA256 hash
|
|
||||||
val sha256 = HashingSink.sha256(blackholeSink())
|
|
||||||
data.readAll(sha256)
|
|
||||||
println("SHA256 hash value: " + sha256.hash)
|
|
||||||
err = Exception("Hash invalid!")
|
|
||||||
data.clear()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cachedFile?.cachedLocation?.let {
|
|
||||||
if (destPath != it) {
|
|
||||||
// Delete old file if location changes
|
|
||||||
try {
|
|
||||||
Files.delete(cachedFile!!.cachedLocation!!.nioPath)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
// Continue, as it was probably already deleted?
|
|
||||||
// TODO: log it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
err = e
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the manifest file
|
|
||||||
cachedFile = (cachedFile ?: ManifestFile.File()).also {
|
|
||||||
try {
|
|
||||||
it.hash = metadata.getHashObj(index)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
err = e
|
|
||||||
return
|
|
||||||
}
|
|
||||||
it.isOptional = isOptional
|
|
||||||
it.cachedLocation = metadata.destURI.rebase(packFolder)
|
|
||||||
metadata.linkedFile?.let { linked ->
|
|
||||||
try {
|
|
||||||
it.linkedFileHash = linked.hash
|
|
||||||
} catch (e: Exception) {
|
|
||||||
err = e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
completionStatus = CompletionStatus.DOWNLOADED
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun createTasksFromIndex(index: IndexFile, downloadSide: Side): MutableList<DownloadTask> {
|
|
||||||
val tasks = ArrayList<DownloadTask>()
|
|
||||||
for (file in index.files) {
|
|
||||||
tasks.add(DownloadTask(file, index, downloadSide))
|
|
||||||
}
|
|
||||||
return tasks
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,142 +0,0 @@
|
|||||||
package link.infra.packwiz.installer
|
|
||||||
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.JsonIOException
|
|
||||||
import com.google.gson.JsonParser
|
|
||||||
import com.google.gson.JsonSyntaxException
|
|
||||||
import link.infra.packwiz.installer.metadata.PackFile
|
|
||||||
import link.infra.packwiz.installer.ui.IUserInterface
|
|
||||||
import link.infra.packwiz.installer.util.Log
|
|
||||||
import kotlin.io.path.reader
|
|
||||||
import kotlin.io.path.writeText
|
|
||||||
|
|
||||||
class LauncherUtils internal constructor(private val opts: UpdateManager.Options, val ui: IUserInterface) {
|
|
||||||
enum class LauncherStatus {
|
|
||||||
SUCCESSFUL,
|
|
||||||
NO_CHANGES,
|
|
||||||
CANCELLED,
|
|
||||||
NOT_FOUND, // When there is no mmc-pack.json file found (i.e. MultiMC is not being used)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleMultiMC(pf: PackFile, gson: Gson): LauncherStatus {
|
|
||||||
// MultiMC MC and loader version checker
|
|
||||||
val manifestPath = opts.multimcFolder / "mmc-pack.json"
|
|
||||||
|
|
||||||
if (!manifestPath.nioPath.toFile().exists()) {
|
|
||||||
return LauncherStatus.NOT_FOUND
|
|
||||||
}
|
|
||||||
|
|
||||||
val multimcManifest = manifestPath.nioPath.reader().use {
|
|
||||||
try {
|
|
||||||
JsonParser.parseReader(it)
|
|
||||||
} catch (e: JsonIOException) {
|
|
||||||
throw Exception("Cannot read the MultiMC pack file", e)
|
|
||||||
} catch (e: JsonSyntaxException) {
|
|
||||||
throw Exception("Invalid MultiMC pack file", e)
|
|
||||||
}.asJsonObject
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.info("Loaded MultiMC config")
|
|
||||||
|
|
||||||
// We only support format 1, if it gets updated in the future we'll have to handle that
|
|
||||||
// There's only version 1 for now tho, so that's good
|
|
||||||
if (multimcManifest["formatVersion"]?.asInt != 1) {
|
|
||||||
throw Exception("Unsupported MultiMC format version ${multimcManifest["formatVersion"]}")
|
|
||||||
}
|
|
||||||
|
|
||||||
var manifestModified = false
|
|
||||||
val modLoaders = hashMapOf(
|
|
||||||
"net.minecraft" to "minecraft",
|
|
||||||
"net.minecraftforge" to "forge",
|
|
||||||
"net.neoforged" to "neoforge",
|
|
||||||
"net.fabricmc.fabric-loader" to "fabric",
|
|
||||||
"org.quiltmc.quilt-loader" to "quilt",
|
|
||||||
"com.mumfrey.liteloader" to "liteloader"
|
|
||||||
)
|
|
||||||
// MultiMC requires components to be sorted; this is defined in the MultiMC meta repo, but they seem to
|
|
||||||
// be the same for every version so they are just used directly here
|
|
||||||
val componentOrders = mapOf(
|
|
||||||
"net.minecraft" to -2,
|
|
||||||
"org.lwjgl" to -1,
|
|
||||||
"org.lwjgl3" to -1,
|
|
||||||
"net.minecraftforge" to 5,
|
|
||||||
"net.neoforged" to 5,
|
|
||||||
"net.fabricmc.fabric-loader" to 10,
|
|
||||||
"org.quiltmc.quilt-loader" to 10,
|
|
||||||
"com.mumfrey.liteloader" to 10,
|
|
||||||
"net.fabricmc.intermediary" to 11
|
|
||||||
)
|
|
||||||
val modLoadersClasses = modLoaders.entries.associate{(k,v)-> v to k}
|
|
||||||
val loaderVersionsFound = HashMap<String, String?>()
|
|
||||||
val outdatedLoaders = mutableSetOf<String>()
|
|
||||||
val components = multimcManifest["components"]?.asJsonArray ?: throw Exception("Invalid mmc-pack.json: no components key")
|
|
||||||
components.removeAll {
|
|
||||||
val component = it.asJsonObject
|
|
||||||
|
|
||||||
val version = component["version"]?.asString
|
|
||||||
// If we find any of the modloaders we support, we save it and check the version
|
|
||||||
if (modLoaders.containsKey(component["uid"]?.asString)) {
|
|
||||||
val modLoader = modLoaders.getValue(component["uid"]!!.asString)
|
|
||||||
loaderVersionsFound[modLoader] = version
|
|
||||||
if (version != pf.versions[modLoader]) {
|
|
||||||
outdatedLoaders.add(modLoader)
|
|
||||||
true // Delete component; cached metadata is invalid and will be re-added
|
|
||||||
} else {
|
|
||||||
false // Already up to date; cached metadata is valid
|
|
||||||
}
|
|
||||||
} else { false } // Not a known loader / MC
|
|
||||||
}
|
|
||||||
|
|
||||||
for ((_, loader) in modLoaders
|
|
||||||
.filter {
|
|
||||||
(!loaderVersionsFound.containsKey(it.value) || outdatedLoaders.contains(it.value)) && pf.versions.containsKey(it.value)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
manifestModified = true
|
|
||||||
components.add(gson.toJsonTree(
|
|
||||||
hashMapOf("uid" to modLoadersClasses[loader], "version" to pf.versions[loader]))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If inconsistent Intermediary mappings version is found, delete it - MultiMC will add and re-dl the correct one
|
|
||||||
components.find { it.isJsonObject && it.asJsonObject["uid"]?.asString == "net.fabricmc.intermediary" }?.let {
|
|
||||||
if (it.asJsonObject["version"]?.asString != pf.versions["minecraft"]) {
|
|
||||||
components.remove(it)
|
|
||||||
manifestModified = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (manifestModified) {
|
|
||||||
// Sort manifest by component order
|
|
||||||
val sortedComponents = components.sortedWith(nullsLast(compareBy {
|
|
||||||
if (it.isJsonObject) {
|
|
||||||
componentOrders[it.asJsonObject["uid"]?.asString]
|
|
||||||
} else { null }
|
|
||||||
}))
|
|
||||||
components.removeAll { true }
|
|
||||||
sortedComponents.forEach { components.add(it) }
|
|
||||||
|
|
||||||
// The manifest has been modified, so before saving it we'll ask the user
|
|
||||||
// if they wanna update it, continue without updating it, or exit
|
|
||||||
val oldVers = loaderVersionsFound.map { Pair(it.key, it.value) }
|
|
||||||
val newVers = pf.versions.map { Pair(it.key, it.value) }
|
|
||||||
|
|
||||||
when (ui.showUpdateConfirmationDialog(oldVers, newVers)) {
|
|
||||||
IUserInterface.UpdateConfirmationResult.CANCELLED -> {
|
|
||||||
return LauncherStatus.CANCELLED
|
|
||||||
}
|
|
||||||
IUserInterface.UpdateConfirmationResult.CONTINUE -> {
|
|
||||||
return LauncherStatus.SUCCESSFUL
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
|
|
||||||
manifestPath.nioPath.writeText(gson.toJson(multimcManifest))
|
|
||||||
Log.info("Successfully updated mmc-pack.json based on version metadata")
|
|
||||||
|
|
||||||
return LauncherStatus.SUCCESSFUL
|
|
||||||
}
|
|
||||||
|
|
||||||
return LauncherStatus.NO_CHANGES
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,170 +0,0 @@
|
|||||||
@file:JvmName("Main")
|
|
||||||
|
|
||||||
package link.infra.packwiz.installer
|
|
||||||
|
|
||||||
import link.infra.packwiz.installer.target.Side
|
|
||||||
import link.infra.packwiz.installer.target.path.HttpUrlPath
|
|
||||||
import link.infra.packwiz.installer.target.path.PackwizFilePath
|
|
||||||
import link.infra.packwiz.installer.ui.cli.CLIHandler
|
|
||||||
import link.infra.packwiz.installer.ui.gui.GUIHandler
|
|
||||||
import link.infra.packwiz.installer.ui.wrap
|
|
||||||
import link.infra.packwiz.installer.util.Log
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okio.Path.Companion.toOkioPath
|
|
||||||
import okio.Path.Companion.toPath
|
|
||||||
import org.apache.commons.cli.DefaultParser
|
|
||||||
import org.apache.commons.cli.Options
|
|
||||||
import org.apache.commons.cli.ParseException
|
|
||||||
import java.awt.EventQueue
|
|
||||||
import java.awt.GraphicsEnvironment
|
|
||||||
import java.net.URI
|
|
||||||
import java.nio.file.Paths
|
|
||||||
import javax.swing.JOptionPane
|
|
||||||
import javax.swing.UIManager
|
|
||||||
import kotlin.system.exitProcess
|
|
||||||
|
|
||||||
@Suppress("unused")
|
|
||||||
class Main(args: Array<String>) {
|
|
||||||
// Don't attempt to start a GUI if we are headless
|
|
||||||
private var guiEnabled = !GraphicsEnvironment.isHeadless()
|
|
||||||
|
|
||||||
private fun startup(args: Array<String>) {
|
|
||||||
val options = Options()
|
|
||||||
addNonBootstrapOptions(options)
|
|
||||||
addBootstrapOptions(options)
|
|
||||||
|
|
||||||
val parser = DefaultParser()
|
|
||||||
val cmd = try {
|
|
||||||
parser.parse(options, args)
|
|
||||||
} catch (e: ParseException) {
|
|
||||||
Log.fatal("Failed to parse command line arguments", e)
|
|
||||||
if (guiEnabled) {
|
|
||||||
EventQueue.invokeAndWait {
|
|
||||||
try {
|
|
||||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
|
|
||||||
} catch (ignored: Exception) {
|
|
||||||
// Ignore the exceptions, just continue using the ugly L&F
|
|
||||||
}
|
|
||||||
JOptionPane.showMessageDialog(null, "Failed to parse command line arguments: $e",
|
|
||||||
"packwiz-installer", JOptionPane.ERROR_MESSAGE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exitProcess(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (guiEnabled && cmd.hasOption("no-gui")) {
|
|
||||||
guiEnabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
val ui = if (guiEnabled) GUIHandler() else CLIHandler()
|
|
||||||
|
|
||||||
val unparsedArgs = cmd.args
|
|
||||||
if (unparsedArgs.size > 1) {
|
|
||||||
ui.showErrorAndExit("Too many arguments specified!")
|
|
||||||
} else if (unparsedArgs.isEmpty()) {
|
|
||||||
ui.showErrorAndExit("pack.toml URI to install from must be specified!")
|
|
||||||
}
|
|
||||||
|
|
||||||
val title = cmd.getOptionValue("title")
|
|
||||||
if (title != null) {
|
|
||||||
ui.title = title
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.show()
|
|
||||||
|
|
||||||
val packFileRaw = unparsedArgs[0]
|
|
||||||
|
|
||||||
val packFile = when {
|
|
||||||
// HTTP(s) URLs
|
|
||||||
Regex("^https?://", RegexOption.IGNORE_CASE).containsMatchIn(packFileRaw) -> ui.wrap("Invalid HTTP/HTTPS URL for pack file: $packFileRaw") {
|
|
||||||
HttpUrlPath(packFileRaw.toHttpUrl().resolve(".")!!, packFileRaw.toHttpUrl().pathSegments.last())
|
|
||||||
}
|
|
||||||
// File URIs (uses same logic as old packwiz-installer, for backwards compat)
|
|
||||||
Regex("^file:", RegexOption.IGNORE_CASE).containsMatchIn(packFileRaw) -> {
|
|
||||||
ui.wrap("Failed to parse file path for pack file: $packFileRaw") {
|
|
||||||
val path = Paths.get(URI(packFileRaw)).toOkioPath()
|
|
||||||
PackwizFilePath(path.parent ?: ui.showErrorAndExit("Invalid pack file path: $packFileRaw"), path.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Other URIs (unsupported)
|
|
||||||
Regex("^[a-z][a-z\\d+\\-.]*://", RegexOption.IGNORE_CASE).containsMatchIn(packFileRaw) -> ui.showErrorAndExit("Unsupported scheme for pack file: $packFileRaw")
|
|
||||||
// None of the above matches -> interpret as file path
|
|
||||||
else -> PackwizFilePath(packFileRaw.toPath().parent ?: ui.showErrorAndExit("Invalid pack file path: $packFileRaw"), packFileRaw.toPath().name)
|
|
||||||
}
|
|
||||||
val side = cmd.getOptionValue("side")?.let {
|
|
||||||
Side.from(it) ?: ui.showErrorAndExit("Unknown side name: $it")
|
|
||||||
} ?: Side.CLIENT
|
|
||||||
val packFolder = ui.wrap("Invalid pack folder path") {
|
|
||||||
cmd.getOptionValue("pack-folder")?.let{ PackwizFilePath(it.toPath()) } ?: PackwizFilePath(".".toPath())
|
|
||||||
}
|
|
||||||
val multimcFolder = ui.wrap("Invalid MultiMC folder path") {
|
|
||||||
cmd.getOptionValue("multimc-folder")?.let{ PackwizFilePath(it.toPath()) } ?: PackwizFilePath("..".toPath())
|
|
||||||
}
|
|
||||||
val manifestFile = ui.wrap("Invalid manifest file path") {
|
|
||||||
packFolder / (cmd.getOptionValue("meta-file") ?: "packwiz.json")
|
|
||||||
}
|
|
||||||
val timeout = ui.wrap("Invalid timeout value") {
|
|
||||||
cmd.getOptionValue("timeout")?.toLong() ?: 10
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start update process!
|
|
||||||
try {
|
|
||||||
UpdateManager(UpdateManager.Options(packFile, manifestFile, packFolder, multimcFolder, side, timeout), ui)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
ui.showErrorAndExit("Update process failed", e)
|
|
||||||
}
|
|
||||||
println("Finished successfully!")
|
|
||||||
ui.dispose()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
// Called by packwiz-installer-bootstrap to set up the help command
|
|
||||||
@JvmStatic
|
|
||||||
fun addNonBootstrapOptions(options: Options) {
|
|
||||||
options.addOption("s", "side", true, "Side to install mods from (client/server, defaults to client)")
|
|
||||||
options.addOption(null, "title", true, "Title of the installer window")
|
|
||||||
options.addOption(null, "pack-folder", true, "Folder to install the pack to (defaults to the JAR directory)")
|
|
||||||
options.addOption(null, "multimc-folder", true, "The MultiMC pack folder (defaults to the parent of the pack directory)")
|
|
||||||
options.addOption(null, "meta-file", true, "JSON file to store pack metadata, relative to the pack folder (defaults to packwiz.json)")
|
|
||||||
options.addOption("t", "timeout", true, "Seconds to wait before automatically launching when asking about optional mods (defaults to 10)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: link these somehow so they're only defined once?
|
|
||||||
@JvmStatic
|
|
||||||
private fun addBootstrapOptions(options: Options) {
|
|
||||||
options.addOption(null, "bootstrap-update-url", true, "Github API URL for checking for updates")
|
|
||||||
options.addOption(null, "bootstrap-update-token", true, "Github API Access Token, for private repositories")
|
|
||||||
options.addOption(null, "bootstrap-no-update", false, "Don't update packwiz-installer")
|
|
||||||
options.addOption(null, "bootstrap-main-jar", true, "Location of the packwiz-installer JAR file")
|
|
||||||
options.addOption("g", "no-gui", false, "Don't display a GUI to show update progress")
|
|
||||||
options.addOption("h", "help", false, "Display this message") // Implemented in packwiz-installer-bootstrap!
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun main(args: Array<String>) {
|
|
||||||
Log.info("packwiz-installer was started without packwiz-installer-bootstrap. Use the bootstrapper for automatic updates! (Disregard this message if you have your own update mechanism)")
|
|
||||||
Main(args)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actual main() is in RequiresBootstrap!
|
|
||||||
init {
|
|
||||||
// Big overarching try/catch just in case everything breaks
|
|
||||||
try {
|
|
||||||
startup(args)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.fatal("Error from main", e)
|
|
||||||
if (guiEnabled) {
|
|
||||||
EventQueue.invokeLater {
|
|
||||||
JOptionPane.showMessageDialog(null,
|
|
||||||
"A fatal error occurred: \n$e",
|
|
||||||
"packwiz-installer", JOptionPane.ERROR_MESSAGE)
|
|
||||||
exitProcess(1)
|
|
||||||
}
|
|
||||||
// In case the EventQueue is broken, exit after 1 minute
|
|
||||||
Thread.sleep(60 * 1000.toLong())
|
|
||||||
}
|
|
||||||
exitProcess(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,487 +0,0 @@
|
|||||||
package link.infra.packwiz.installer
|
|
||||||
|
|
||||||
import cc.ekblad.toml.decode
|
|
||||||
import com.google.gson.GsonBuilder
|
|
||||||
import com.google.gson.JsonIOException
|
|
||||||
import com.google.gson.JsonSyntaxException
|
|
||||||
import link.infra.packwiz.installer.DownloadTask.Companion.createTasksFromIndex
|
|
||||||
import link.infra.packwiz.installer.metadata.DownloadMode
|
|
||||||
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.curseforge.resolveCfMetadata
|
|
||||||
import link.infra.packwiz.installer.metadata.hash.Hash
|
|
||||||
import link.infra.packwiz.installer.metadata.hash.HashFormat
|
|
||||||
import link.infra.packwiz.installer.request.RequestException
|
|
||||||
import link.infra.packwiz.installer.target.ClientHolder
|
|
||||||
import link.infra.packwiz.installer.target.Side
|
|
||||||
import link.infra.packwiz.installer.target.path.PackwizFilePath
|
|
||||||
import link.infra.packwiz.installer.target.path.PackwizPath
|
|
||||||
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.data.InstallProgress
|
|
||||||
import link.infra.packwiz.installer.util.Log
|
|
||||||
import okio.buffer
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.InputStreamReader
|
|
||||||
import java.nio.charset.StandardCharsets
|
|
||||||
import java.nio.file.Files
|
|
||||||
import java.util.concurrent.CompletionService
|
|
||||||
import java.util.concurrent.ExecutionException
|
|
||||||
import java.util.concurrent.ExecutorCompletionService
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import kotlin.system.exitProcess
|
|
||||||
|
|
||||||
class UpdateManager internal constructor(private val opts: Options, val ui: IUserInterface) {
|
|
||||||
private var cancelled = false
|
|
||||||
private var cancelledStartGame = false
|
|
||||||
private var errorsOccurred = false
|
|
||||||
|
|
||||||
init {
|
|
||||||
start()
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Options(
|
|
||||||
val packFile: PackwizPath<*>,
|
|
||||||
val manifestFile: PackwizFilePath,
|
|
||||||
val packFolder: PackwizFilePath,
|
|
||||||
val multimcFolder: PackwizFilePath,
|
|
||||||
val side: Side,
|
|
||||||
val timeout: Long,
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO: make this return a value based on results?
|
|
||||||
private fun start() {
|
|
||||||
val clientHolder = ClientHolder()
|
|
||||||
ui.cancelCallback = {
|
|
||||||
clientHolder.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.submitProgress(InstallProgress("Loading manifest file..."))
|
|
||||||
val gson = GsonBuilder()
|
|
||||||
.registerTypeAdapter(Hash::class.java, Hash.TypeHandler())
|
|
||||||
.registerTypeAdapter(PackwizFilePath::class.java, PackwizPath.adapterRelativeTo(opts.packFolder))
|
|
||||||
.enableComplexMapKeySerialization()
|
|
||||||
.create()
|
|
||||||
val manifest = try {
|
|
||||||
// TODO: kotlinx.serialisation?
|
|
||||||
InputStreamReader(opts.manifestFile.source(clientHolder).inputStream(), StandardCharsets.UTF_8).use { reader ->
|
|
||||||
gson.fromJson(reader, ManifestFile::class.java)
|
|
||||||
}
|
|
||||||
} catch (e: RequestException.Response.File.FileNotFound) {
|
|
||||||
ui.firstInstall = true
|
|
||||||
ManifestFile()
|
|
||||||
} catch (e: JsonSyntaxException) {
|
|
||||||
ui.showErrorAndExit("Invalid local manifest file, try deleting ${opts.manifestFile}", e)
|
|
||||||
} catch (e: JsonIOException) {
|
|
||||||
ui.showErrorAndExit("Failed to read local manifest file, try deleting ${opts.manifestFile}", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ui.cancelButtonPressed) {
|
|
||||||
showCancellationDialog()
|
|
||||||
handleCancellation()
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.submitProgress(InstallProgress("Loading pack file..."))
|
|
||||||
val packFileSource = try {
|
|
||||||
val src = opts.packFile.source(clientHolder)
|
|
||||||
HashFormat.SHA256.source(src)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// TODO: ensure suppressed/caused exceptions are shown?
|
|
||||||
ui.showErrorAndExit("Failed to download pack.toml", e)
|
|
||||||
}
|
|
||||||
val pf = packFileSource.buffer().use {
|
|
||||||
try {
|
|
||||||
PackFile.mapper(opts.packFile).decode<PackFile>(it.inputStream())
|
|
||||||
} catch (e: IllegalStateException) {
|
|
||||||
ui.showErrorAndExit("Failed to parse pack.toml", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ui.cancelButtonPressed) {
|
|
||||||
showCancellationDialog()
|
|
||||||
handleCancellation()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Launcher checks
|
|
||||||
val lu = LauncherUtils(opts, ui)
|
|
||||||
|
|
||||||
// MultiMC MC and loader version checker
|
|
||||||
ui.submitProgress(InstallProgress("Loading MultiMC pack file..."))
|
|
||||||
try {
|
|
||||||
when (lu.handleMultiMC(pf, gson)) {
|
|
||||||
LauncherUtils.LauncherStatus.CANCELLED -> cancelled = true
|
|
||||||
LauncherUtils.LauncherStatus.NOT_FOUND -> Log.info("MultiMC not detected")
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
handleCancellation()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
ui.showErrorAndExit(e.message!!, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ui.cancelButtonPressed) {
|
|
||||||
showCancellationDialog()
|
|
||||||
handleCancellation()
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.submitProgress(InstallProgress("Checking local files..."))
|
|
||||||
|
|
||||||
// If the side changes, invalidate EVERYTHING (even when the index hasn't changed)
|
|
||||||
val invalidateAll = opts.side != manifest.cachedSide
|
|
||||||
val invalidatedUris: MutableList<PackwizFilePath> = ArrayList()
|
|
||||||
if (!invalidateAll) {
|
|
||||||
// Invalidation checking must be done here, as it must happen before pack/index hashes are checked
|
|
||||||
for ((fileUri, file) in manifest.cachedFiles) {
|
|
||||||
// ignore onlyOtherSide files
|
|
||||||
if (file.onlyOtherSide) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var invalid = false
|
|
||||||
// if isn't optional, or is optional but optionValue == true
|
|
||||||
if (!file.isOptional || file.optionValue) {
|
|
||||||
if (file.cachedLocation != null) {
|
|
||||||
if (!file.cachedLocation!!.nioPath.toFile().exists()) {
|
|
||||||
invalid = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// if cachedLocation == null, should probably be installed!!
|
|
||||||
invalid = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (invalid) {
|
|
||||||
Log.info("File ${fileUri.filename} invalidated, marked for redownloading")
|
|
||||||
invalidatedUris.add(fileUri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (manifest.packFileHash?.let { it == packFileSource.hash } == true && invalidatedUris.isEmpty()) {
|
|
||||||
// todo: --force?
|
|
||||||
ui.submitProgress(InstallProgress("Modpack is already up to date!", 1, 1))
|
|
||||||
if (manifest.cachedFiles.any { it.value.isOptional }) {
|
|
||||||
ui.awaitOptionalButton(false, opts.timeout)
|
|
||||||
}
|
|
||||||
if (!ui.optionsButtonPressed) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.info("Modpack name: ${pf.name}")
|
|
||||||
|
|
||||||
if (ui.cancelButtonPressed) {
|
|
||||||
showCancellationDialog()
|
|
||||||
handleCancellation()
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
processIndex(
|
|
||||||
pf.index.file,
|
|
||||||
pf.index.hashFormat.fromString(pf.index.hash),
|
|
||||||
pf.index.hashFormat,
|
|
||||||
manifest,
|
|
||||||
invalidatedUris,
|
|
||||||
invalidateAll,
|
|
||||||
clientHolder
|
|
||||||
)
|
|
||||||
} catch (e1: Exception) {
|
|
||||||
ui.showErrorAndExit("Failed to process index file", e1)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCancellation()
|
|
||||||
|
|
||||||
|
|
||||||
// If there were errors, don't write the manifest/index hashes, to ensure they are rechecked later
|
|
||||||
if (errorsOccurred) {
|
|
||||||
manifest.indexFileHash = null
|
|
||||||
manifest.packFileHash = null
|
|
||||||
} else {
|
|
||||||
manifest.packFileHash = packFileSource.hash
|
|
||||||
}
|
|
||||||
|
|
||||||
manifest.cachedSide = opts.side
|
|
||||||
try {
|
|
||||||
Files.newBufferedWriter(opts.manifestFile.nioPath, StandardCharsets.UTF_8).use { writer -> gson.toJson(manifest, writer) }
|
|
||||||
} catch (e: IOException) {
|
|
||||||
ui.showErrorAndExit("Failed to save local manifest file", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun processIndex(indexUri: PackwizPath<*>, indexHash: Hash<*>, hashFormat: HashFormat<*>, manifest: ManifestFile, invalidatedFiles: List<PackwizFilePath>, invalidateAll: Boolean, clientHolder: ClientHolder) {
|
|
||||||
if (!invalidateAll) {
|
|
||||||
if (manifest.indexFileHash == indexHash && invalidatedFiles.isEmpty()) {
|
|
||||||
ui.submitProgress(InstallProgress("Modpack files are already up to date!", 1, 1))
|
|
||||||
if (manifest.cachedFiles.any { it.value.isOptional }) {
|
|
||||||
ui.awaitOptionalButton(false, opts.timeout)
|
|
||||||
}
|
|
||||||
if (!ui.optionsButtonPressed) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (ui.cancelButtonPressed) {
|
|
||||||
showCancellationDialog()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
manifest.indexFileHash = indexHash
|
|
||||||
|
|
||||||
val indexFileSource = try {
|
|
||||||
val src = indexUri.source(clientHolder)
|
|
||||||
hashFormat.source(src)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
ui.showErrorAndExit("Failed to download index file", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
val indexFile = try {
|
|
||||||
IndexFile.mapper(indexUri).decode<IndexFile>(indexFileSource.buffer().inputStream())
|
|
||||||
} catch (e: IllegalStateException) {
|
|
||||||
ui.showErrorAndExit("Failed to parse index file", e)
|
|
||||||
}
|
|
||||||
if (indexHash != indexFileSource.hash) {
|
|
||||||
ui.showErrorAndExit("Your index file hash is invalid! The pack developer should packwiz refresh on the pack again")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ui.cancelButtonPressed) {
|
|
||||||
showCancellationDialog()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.submitProgress(InstallProgress("Checking local files..."))
|
|
||||||
val it: MutableIterator<Map.Entry<PackwizFilePath, ManifestFile.File>> = manifest.cachedFiles.entries.iterator()
|
|
||||||
while (it.hasNext()) {
|
|
||||||
val (uri, file) = it.next()
|
|
||||||
if (file.cachedLocation != null) {
|
|
||||||
if (indexFile.files.none { it.file.rebase(opts.packFolder) == uri }) { // File has been removed from the index
|
|
||||||
try {
|
|
||||||
Files.deleteIfExists(file.cachedLocation!!.nioPath)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.warn("Failed to delete file removed from index", e)
|
|
||||||
}
|
|
||||||
Log.info("Deleted ${file.cachedLocation!!.filename} (removed from pack)")
|
|
||||||
it.remove()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ui.cancelButtonPressed) {
|
|
||||||
showCancellationDialog()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ui.submitProgress(InstallProgress("Comparing new files..."))
|
|
||||||
|
|
||||||
// TODO: progress bar?
|
|
||||||
if (indexFile.files.isEmpty()) {
|
|
||||||
Log.warn("Index is empty!")
|
|
||||||
}
|
|
||||||
val tasks = createTasksFromIndex(indexFile, opts.side)
|
|
||||||
if (invalidateAll) {
|
|
||||||
Log.info("Side changed, invalidating all mods")
|
|
||||||
}
|
|
||||||
tasks.forEach{ f ->
|
|
||||||
// TODO: should linkedfile be checked as well? should this be done in the download section?
|
|
||||||
if (invalidateAll) {
|
|
||||||
f.invalidate()
|
|
||||||
} else if (invalidatedFiles.contains(f.metadata.file.rebase(opts.packFolder))) {
|
|
||||||
f.invalidate()
|
|
||||||
}
|
|
||||||
val file = manifest.cachedFiles[f.metadata.file.rebase(opts.packFolder)]
|
|
||||||
// Ensure the file can be reverted later if necessary - the DownloadTask modifies the file so if it fails we need the old version back
|
|
||||||
file?.backup()
|
|
||||||
// If it is null, the DownloadTask will make a new empty cachedFile
|
|
||||||
f.updateFromCache(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ui.cancelButtonPressed) {
|
|
||||||
showCancellationDialog()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let's hope downloadMetadata is a pure function!!!
|
|
||||||
tasks.parallelStream().forEach { f -> f.downloadMetadata(clientHolder) }
|
|
||||||
|
|
||||||
val failedTaskDetails = tasks.asSequence().map(DownloadTask::exceptionDetails).filterNotNull().toList()
|
|
||||||
if (failedTaskDetails.isNotEmpty()) {
|
|
||||||
errorsOccurred = true
|
|
||||||
when (ui.showExceptions(failedTaskDetails, tasks.size, true)) {
|
|
||||||
ExceptionListResult.CONTINUE -> {}
|
|
||||||
ExceptionListResult.CANCEL -> {
|
|
||||||
cancelled = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ExceptionListResult.IGNORE -> {
|
|
||||||
cancelledStartGame = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ui.cancelButtonPressed) {
|
|
||||||
showCancellationDialog()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: task failed function?
|
|
||||||
tasks.removeAll { it.failed() }
|
|
||||||
val optionTasks = tasks.filter(DownloadTask::correctSide).filter(DownloadTask::isOptional).toList()
|
|
||||||
val optionsChanged = optionTasks.any(DownloadTask::isNewOptional)
|
|
||||||
if (optionTasks.isNotEmpty() && !optionsChanged) {
|
|
||||||
if (!ui.optionsButtonPressed) {
|
|
||||||
// TODO: this is so ugly
|
|
||||||
ui.submitProgress(InstallProgress("Reconfigure optional mods?", 0,1))
|
|
||||||
ui.awaitOptionalButton(true, opts.timeout)
|
|
||||||
if (ui.cancelButtonPressed) {
|
|
||||||
showCancellationDialog()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If options changed, present all options again
|
|
||||||
if (ui.optionsButtonPressed || optionsChanged) {
|
|
||||||
// new ArrayList is required so it's an IOptionDetails rather than a DownloadTask list
|
|
||||||
if (ui.showOptions(ArrayList(optionTasks))) {
|
|
||||||
cancelled = true
|
|
||||||
handleCancellation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO: keep this enabled? then apply changes after download process?
|
|
||||||
ui.disableOptionsButton(optionTasks.isNotEmpty())
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
when (validateAndResolve(tasks, clientHolder)) {
|
|
||||||
ResolveResult.RETRY -> {}
|
|
||||||
ResolveResult.QUIT -> return
|
|
||||||
ResolveResult.SUCCESS -> break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: different thread pool type?
|
|
||||||
val threadPool = Executors.newFixedThreadPool(10)
|
|
||||||
val completionService: CompletionService<DownloadTask> = ExecutorCompletionService(threadPool)
|
|
||||||
tasks.forEach { t ->
|
|
||||||
completionService.submit {
|
|
||||||
t.download(opts.packFolder, clientHolder)
|
|
||||||
t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (i in tasks.indices) {
|
|
||||||
val task: DownloadTask = try {
|
|
||||||
completionService.take().get()
|
|
||||||
} catch (e: InterruptedException) {
|
|
||||||
ui.showErrorAndExit("Interrupted when consuming download tasks", e)
|
|
||||||
} catch (e: ExecutionException) {
|
|
||||||
ui.showErrorAndExit("Failed to execute download task", e)
|
|
||||||
}
|
|
||||||
// Update manifest - If there were no errors cachedFile has already been modified in place (good old pass by reference)
|
|
||||||
task.cachedFile?.let { file ->
|
|
||||||
if (task.failed()) {
|
|
||||||
val oldFile = file.revert
|
|
||||||
if (oldFile != null) {
|
|
||||||
manifest.cachedFiles.putIfAbsent(task.metadata.file.rebase(opts.packFolder), oldFile)
|
|
||||||
} else { null }
|
|
||||||
} else {
|
|
||||||
manifest.cachedFiles.putIfAbsent(task.metadata.file.rebase(opts.packFolder), file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val exDetails = task.exceptionDetails
|
|
||||||
val progress = if (exDetails != null) {
|
|
||||||
"Failed to download ${exDetails.name}: ${exDetails.exception.message}"
|
|
||||||
} else {
|
|
||||||
when (task.completionStatus) {
|
|
||||||
DownloadTask.CompletionStatus.INCOMPLETE -> "${task.name} pending (you should never see this...)"
|
|
||||||
DownloadTask.CompletionStatus.DOWNLOADED -> "Downloaded ${task.name}"
|
|
||||||
DownloadTask.CompletionStatus.ALREADY_EXISTS_CACHED -> "${task.name} already exists (cached)"
|
|
||||||
DownloadTask.CompletionStatus.ALREADY_EXISTS_VALIDATED -> "${task.name} already exists (validated)"
|
|
||||||
DownloadTask.CompletionStatus.SKIPPED_DISABLED -> "Skipped ${task.name} (disabled)"
|
|
||||||
DownloadTask.CompletionStatus.SKIPPED_WRONG_SIDE -> "Skipped ${task.name} (wrong side)"
|
|
||||||
DownloadTask.CompletionStatus.DELETED_DISABLED -> "Deleted ${task.name} (disabled)"
|
|
||||||
DownloadTask.CompletionStatus.DELETED_WRONG_SIDE -> "Deleted ${task.name} (wrong side)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ui.submitProgress(InstallProgress(progress, i + 1, tasks.size))
|
|
||||||
|
|
||||||
if (ui.cancelButtonPressed) { // Stop all tasks, don't launch the game (it's in an invalid state!)
|
|
||||||
// TODO: close client holder in more places?
|
|
||||||
clientHolder.close()
|
|
||||||
threadPool.shutdown()
|
|
||||||
cancelled = true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shut down the thread pool when the update is done
|
|
||||||
threadPool.shutdown()
|
|
||||||
|
|
||||||
val failedTasks2ElectricBoogaloo = tasks.asSequence().map(DownloadTask::exceptionDetails).filterNotNull().toList()
|
|
||||||
if (failedTasks2ElectricBoogaloo.isNotEmpty()) {
|
|
||||||
errorsOccurred = true
|
|
||||||
when (ui.showExceptions(failedTasks2ElectricBoogaloo, tasks.size, false)) {
|
|
||||||
ExceptionListResult.CONTINUE -> {}
|
|
||||||
ExceptionListResult.CANCEL -> cancelled = true
|
|
||||||
ExceptionListResult.IGNORE -> cancelledStartGame = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class ResolveResult {
|
|
||||||
RETRY,
|
|
||||||
QUIT,
|
|
||||||
SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun validateAndResolve(nonFailedFirstTasks: List<DownloadTask>, clientHolder: ClientHolder): ResolveResult {
|
|
||||||
ui.submitProgress(InstallProgress("Validating existing files..."))
|
|
||||||
|
|
||||||
// Validate existing files
|
|
||||||
for (downloadTask in nonFailedFirstTasks.filter(DownloadTask::correctSide)) {
|
|
||||||
downloadTask.validateExistingFile(opts.packFolder, clientHolder)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve CurseForge metadata
|
|
||||||
val cfFiles = nonFailedFirstTasks.asSequence().filter { !it.alreadyUpToDate }
|
|
||||||
.filter(DownloadTask::correctSide)
|
|
||||||
.map { it.metadata }
|
|
||||||
.filter { it.linkedFile != null }
|
|
||||||
.filter { it.linkedFile!!.download.mode == DownloadMode.CURSEFORGE }.toList()
|
|
||||||
if (cfFiles.isNotEmpty()) {
|
|
||||||
ui.submitProgress(InstallProgress("Resolving CurseForge metadata..."))
|
|
||||||
val resolveFailures = resolveCfMetadata(cfFiles, opts.packFolder, clientHolder)
|
|
||||||
if (resolveFailures.isNotEmpty()) {
|
|
||||||
errorsOccurred = true
|
|
||||||
return when (ui.showExceptions(resolveFailures, cfFiles.size, true)) {
|
|
||||||
ExceptionListResult.CONTINUE -> {
|
|
||||||
ResolveResult.RETRY
|
|
||||||
}
|
|
||||||
ExceptionListResult.CANCEL -> {
|
|
||||||
cancelled = true
|
|
||||||
ResolveResult.QUIT
|
|
||||||
}
|
|
||||||
ExceptionListResult.IGNORE -> {
|
|
||||||
cancelledStartGame = true
|
|
||||||
ResolveResult.QUIT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ResolveResult.SUCCESS
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showCancellationDialog() {
|
|
||||||
when (ui.showCancellationDialog()) {
|
|
||||||
CancellationResult.QUIT -> cancelled = true
|
|
||||||
CancellationResult.CONTINUE -> cancelledStartGame = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: move to UI?
|
|
||||||
private fun handleCancellation() {
|
|
||||||
if (cancelled) {
|
|
||||||
println("Update cancelled by user!")
|
|
||||||
exitProcess(1)
|
|
||||||
} else if (cancelledStartGame) {
|
|
||||||
println("Update cancelled by user! Continuing to start game...")
|
|
||||||
exitProcess(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.metadata
|
|
||||||
|
|
||||||
import cc.ekblad.toml.model.TomlValue
|
|
||||||
import cc.ekblad.toml.tomlMapper
|
|
||||||
|
|
||||||
enum class DownloadMode {
|
|
||||||
URL,
|
|
||||||
CURSEFORGE;
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun mapper() = tomlMapper {
|
|
||||||
decoder { it: TomlValue.String -> when (it.value) {
|
|
||||||
"", "url" -> URL
|
|
||||||
"metadata:curseforge" -> CURSEFORGE
|
|
||||||
else -> throw Exception("Unsupported download mode ${it.value}")
|
|
||||||
} }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.metadata
|
|
||||||
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import com.google.gson.stream.JsonReader
|
|
||||||
import com.google.gson.stream.JsonToken
|
|
||||||
import com.google.gson.stream.JsonWriter
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
class EfficientBooleanAdapter : TypeAdapter<Boolean?>() {
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun write(out: JsonWriter, value: Boolean?) {
|
|
||||||
if (value == null || !value) {
|
|
||||||
out.nullValue()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
out.value(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun read(reader: JsonReader): Boolean {
|
|
||||||
if (reader.peek() == JsonToken.NULL) {
|
|
||||||
reader.nextNull()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return reader.nextBoolean()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,97 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.metadata
|
|
||||||
|
|
||||||
import cc.ekblad.toml.decode
|
|
||||||
import cc.ekblad.toml.tomlMapper
|
|
||||||
import link.infra.packwiz.installer.metadata.hash.Hash
|
|
||||||
import link.infra.packwiz.installer.metadata.hash.HashFormat
|
|
||||||
import link.infra.packwiz.installer.target.ClientHolder
|
|
||||||
import link.infra.packwiz.installer.target.path.PackwizPath
|
|
||||||
import link.infra.packwiz.installer.util.delegateTransitive
|
|
||||||
import okio.Source
|
|
||||||
import okio.buffer
|
|
||||||
|
|
||||||
data class IndexFile(
|
|
||||||
val hashFormat: HashFormat<*>,
|
|
||||||
val files: List<File> = listOf()
|
|
||||||
) {
|
|
||||||
data class File(
|
|
||||||
val file: PackwizPath<*>,
|
|
||||||
private val hashFormat: HashFormat<*>? = null,
|
|
||||||
val hash: String,
|
|
||||||
val alias: PackwizPath<*>?,
|
|
||||||
val metafile: Boolean = false,
|
|
||||||
val preserve: Boolean = false,
|
|
||||||
) {
|
|
||||||
var linkedFile: ModFile? = null
|
|
||||||
|
|
||||||
fun hashFormat(index: IndexFile) = hashFormat ?: index.hashFormat
|
|
||||||
@Throws(Exception::class)
|
|
||||||
fun getHashObj(index: IndexFile): Hash<*> {
|
|
||||||
// TODO: more specific exceptions?
|
|
||||||
return hashFormat(index).fromString(hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(Exception::class)
|
|
||||||
fun downloadMeta(index: IndexFile, clientHolder: ClientHolder) {
|
|
||||||
if (!metafile) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val fileHash = getHashObj(index)
|
|
||||||
val src = file.source(clientHolder)
|
|
||||||
val fileStream = hashFormat(index).source(src)
|
|
||||||
linkedFile = ModFile.mapper(file).decode<ModFile>(fileStream.buffer().inputStream())
|
|
||||||
if (fileHash != fileStream.hash) {
|
|
||||||
// TODO: propagate details about hash, and show better error!
|
|
||||||
throw Exception("Invalid mod file hash")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(Exception::class)
|
|
||||||
fun getSource(clientHolder: ClientHolder): Source {
|
|
||||||
return if (metafile) {
|
|
||||||
if (linkedFile == null) {
|
|
||||||
throw Exception("Linked file doesn't exist!")
|
|
||||||
}
|
|
||||||
linkedFile!!.getSource(clientHolder)
|
|
||||||
} else {
|
|
||||||
file.source(clientHolder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val name: String
|
|
||||||
get() {
|
|
||||||
if (metafile) {
|
|
||||||
return linkedFile?.name ?: file.filename
|
|
||||||
}
|
|
||||||
return file.filename
|
|
||||||
}
|
|
||||||
|
|
||||||
val destURI: PackwizPath<*>
|
|
||||||
get() {
|
|
||||||
if (alias != null) {
|
|
||||||
return alias
|
|
||||||
}
|
|
||||||
return if (metafile) {
|
|
||||||
linkedFile!!.filename
|
|
||||||
} else {
|
|
||||||
file
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun mapper(base: PackwizPath<*>) = tomlMapper {
|
|
||||||
mapping<File>("hash-format" to "hashFormat")
|
|
||||||
delegateTransitive<HashFormat<*>>(HashFormat.mapper())
|
|
||||||
delegateTransitive<PackwizPath<*>>(PackwizPath.mapperRelativeTo(base))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun mapper(base: PackwizPath<*>) = tomlMapper {
|
|
||||||
mapping<IndexFile>("hash-format" to "hashFormat")
|
|
||||||
delegateTransitive<HashFormat<*>>(HashFormat.mapper())
|
|
||||||
delegateTransitive<File>(File.mapper(base))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.metadata
|
|
||||||
|
|
||||||
import com.google.gson.annotations.JsonAdapter
|
|
||||||
import link.infra.packwiz.installer.metadata.hash.Hash
|
|
||||||
import link.infra.packwiz.installer.target.Side
|
|
||||||
import link.infra.packwiz.installer.target.path.PackwizFilePath
|
|
||||||
|
|
||||||
class ManifestFile {
|
|
||||||
var packFileHash: Hash<*>? = null
|
|
||||||
var indexFileHash: Hash<*>? = null
|
|
||||||
var cachedFiles: MutableMap<PackwizFilePath, File> = HashMap()
|
|
||||||
// If the side changes, EVERYTHING invalidates. FUN!!!
|
|
||||||
var cachedSide = Side.CLIENT
|
|
||||||
|
|
||||||
class File {
|
|
||||||
@Transient
|
|
||||||
var revert: File? = null
|
|
||||||
private set
|
|
||||||
|
|
||||||
var hash: Hash<*>? = null
|
|
||||||
var linkedFileHash: Hash<*>? = null
|
|
||||||
var cachedLocation: PackwizFilePath? = null
|
|
||||||
|
|
||||||
@JsonAdapter(EfficientBooleanAdapter::class)
|
|
||||||
var isOptional = false
|
|
||||||
var optionValue = true
|
|
||||||
|
|
||||||
@JsonAdapter(EfficientBooleanAdapter::class)
|
|
||||||
var onlyOtherSide = false
|
|
||||||
|
|
||||||
// When an error occurs, the state needs to be reverted. To do this, I have a crude revert system.
|
|
||||||
fun backup() {
|
|
||||||
revert = File().also {
|
|
||||||
it.hash = hash
|
|
||||||
it.linkedFileHash = linkedFileHash
|
|
||||||
it.cachedLocation = cachedLocation
|
|
||||||
it.isOptional = isOptional
|
|
||||||
it.optionValue = optionValue
|
|
||||||
it.onlyOtherSide = onlyOtherSide
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,96 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.metadata
|
|
||||||
|
|
||||||
import cc.ekblad.toml.delegate
|
|
||||||
import cc.ekblad.toml.model.TomlValue
|
|
||||||
import cc.ekblad.toml.tomlMapper
|
|
||||||
import link.infra.packwiz.installer.metadata.curseforge.UpdateData
|
|
||||||
import link.infra.packwiz.installer.metadata.hash.Hash
|
|
||||||
import link.infra.packwiz.installer.metadata.hash.HashFormat
|
|
||||||
import link.infra.packwiz.installer.target.ClientHolder
|
|
||||||
import link.infra.packwiz.installer.target.Side
|
|
||||||
import link.infra.packwiz.installer.target.path.HttpUrlPath
|
|
||||||
import link.infra.packwiz.installer.target.path.PackwizPath
|
|
||||||
import link.infra.packwiz.installer.util.delegateTransitive
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okio.Source
|
|
||||||
import kotlin.reflect.KType
|
|
||||||
|
|
||||||
data class ModFile(
|
|
||||||
val name: String,
|
|
||||||
val filename: PackwizPath<*>,
|
|
||||||
val side: Side = Side.BOTH,
|
|
||||||
val download: Download,
|
|
||||||
val update: Map<String, UpdateData> = mapOf(),
|
|
||||||
val option: Option = Option(false)
|
|
||||||
) {
|
|
||||||
data class Download(
|
|
||||||
val url: PackwizPath<*>?,
|
|
||||||
val hashFormat: HashFormat<*>,
|
|
||||||
val hash: String,
|
|
||||||
val mode: DownloadMode = DownloadMode.URL
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
fun mapper() = tomlMapper {
|
|
||||||
decoder<TomlValue.String, PackwizPath<*>> { it -> HttpUrlPath(it.value.toHttpUrl()) }
|
|
||||||
mapping<Download>("hash-format" to "hashFormat")
|
|
||||||
|
|
||||||
delegateTransitive<HashFormat<*>>(HashFormat.mapper())
|
|
||||||
delegate<DownloadMode>(DownloadMode.mapper())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transient
|
|
||||||
val resolvedUpdateData = mutableMapOf<String, PackwizPath<*>>()
|
|
||||||
|
|
||||||
data class Option(
|
|
||||||
val optional: Boolean,
|
|
||||||
val description: String = "",
|
|
||||||
val defaultValue: Boolean = false
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
fun mapper() = tomlMapper {
|
|
||||||
mapping<Option>("default" to "defaultValue")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(Exception::class)
|
|
||||||
fun getSource(clientHolder: ClientHolder): Source {
|
|
||||||
return when (download.mode) {
|
|
||||||
DownloadMode.URL -> {
|
|
||||||
(download.url ?: throw Exception("No download URL provided")).source(clientHolder)
|
|
||||||
}
|
|
||||||
DownloadMode.CURSEFORGE -> {
|
|
||||||
if (!resolvedUpdateData.contains("curseforge")) {
|
|
||||||
throw Exception("Metadata file specifies CurseForge mode, but is missing metadata")
|
|
||||||
}
|
|
||||||
return resolvedUpdateData["curseforge"]!!.source(clientHolder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@get:Throws(Exception::class)
|
|
||||||
val hash: Hash<*>
|
|
||||||
get() = download.hashFormat.fromString(download.hash)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun mapper(base: PackwizPath<*>) = tomlMapper {
|
|
||||||
delegateTransitive<PackwizPath<*>>(PackwizPath.mapperRelativeTo(base))
|
|
||||||
|
|
||||||
delegateTransitive<Option>(Option.mapper())
|
|
||||||
delegateTransitive<Download>(Download.mapper())
|
|
||||||
|
|
||||||
delegateTransitive<Side>(Side.mapper())
|
|
||||||
|
|
||||||
val updateDataMapper = UpdateData.mapper()
|
|
||||||
decoder { type: KType, it: TomlValue.Map ->
|
|
||||||
if (type.arguments[1].type?.classifier == UpdateData::class) {
|
|
||||||
updateDataMapper.decode<Map<String, UpdateData>>(it)
|
|
||||||
} else {
|
|
||||||
pass()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.metadata
|
|
||||||
|
|
||||||
import cc.ekblad.toml.model.TomlValue
|
|
||||||
import cc.ekblad.toml.tomlMapper
|
|
||||||
import link.infra.packwiz.installer.metadata.hash.HashFormat
|
|
||||||
import link.infra.packwiz.installer.target.path.PackwizPath
|
|
||||||
import link.infra.packwiz.installer.util.delegateTransitive
|
|
||||||
|
|
||||||
data class PackFile(
|
|
||||||
val name: String,
|
|
||||||
val packFormat: PackFormat = PackFormat.DEFAULT,
|
|
||||||
val index: IndexFileLoc,
|
|
||||||
val versions: Map<String, String> = mapOf()
|
|
||||||
) {
|
|
||||||
data class IndexFileLoc(
|
|
||||||
val file: PackwizPath<*>,
|
|
||||||
val hashFormat: HashFormat<*>,
|
|
||||||
val hash: String,
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
fun mapper(base: PackwizPath<*>) = tomlMapper {
|
|
||||||
mapping<IndexFileLoc>("hash-format" to "hashFormat")
|
|
||||||
delegateTransitive<PackwizPath<*>>(PackwizPath.mapperRelativeTo(base))
|
|
||||||
delegateTransitive<HashFormat<*>>(HashFormat.mapper())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun mapper(base: PackwizPath<*>) = tomlMapper {
|
|
||||||
mapping<PackFile>("pack-format" to "packFormat")
|
|
||||||
decoder { it: TomlValue.String -> PackFormat(it.value) }
|
|
||||||
encoder { it: PackFormat -> TomlValue.String(it.format) }
|
|
||||||
delegateTransitive<IndexFileLoc>(IndexFileLoc.mapper(base))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.metadata
|
|
||||||
|
|
||||||
@JvmInline
|
|
||||||
value class PackFormat(val format: String) {
|
|
||||||
companion object {
|
|
||||||
val DEFAULT = PackFormat("packwiz:1.0.0")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: implement validation, errors for too new / invalid versions
|
|
||||||
}
|
|
@ -1,159 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.metadata.curseforge
|
|
||||||
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.JsonIOException
|
|
||||||
import com.google.gson.JsonSyntaxException
|
|
||||||
import link.infra.packwiz.installer.metadata.IndexFile
|
|
||||||
import link.infra.packwiz.installer.target.ClientHolder
|
|
||||||
import link.infra.packwiz.installer.target.path.HttpUrlPath
|
|
||||||
import link.infra.packwiz.installer.target.path.PackwizFilePath
|
|
||||||
import link.infra.packwiz.installer.ui.data.ExceptionDetails
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
|
||||||
import okhttp3.internal.closeQuietly
|
|
||||||
import okio.ByteString.Companion.decodeBase64
|
|
||||||
import java.nio.charset.StandardCharsets
|
|
||||||
import kotlin.io.path.absolute
|
|
||||||
|
|
||||||
private class GetFilesRequest(val fileIds: List<Int>)
|
|
||||||
private class GetModsRequest(val modIds: List<Int>)
|
|
||||||
|
|
||||||
private class GetFilesResponse {
|
|
||||||
class CfFile {
|
|
||||||
var id = 0
|
|
||||||
var modId = 0
|
|
||||||
var downloadUrl: String? = null
|
|
||||||
}
|
|
||||||
val data = mutableListOf<CfFile>()
|
|
||||||
}
|
|
||||||
|
|
||||||
private class GetModsResponse {
|
|
||||||
class CfMod {
|
|
||||||
var id = 0
|
|
||||||
var name = ""
|
|
||||||
var links: CfLinks? = null
|
|
||||||
}
|
|
||||||
class CfLinks {
|
|
||||||
var websiteUrl = ""
|
|
||||||
}
|
|
||||||
val data = mutableListOf<CfMod>()
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val APIServer = "api.curseforge.com"
|
|
||||||
// If you fork/derive from packwiz, I request that you obtain your own API key.
|
|
||||||
private val APIKey = "JDJhJDEwJHNBWVhqblU1N0EzSmpzcmJYM3JVdk92UWk2NHBLS3BnQ2VpbGc1TUM1UGNKL0RYTmlGWWxh".decodeBase64()!!
|
|
||||||
.string(StandardCharsets.UTF_8)
|
|
||||||
|
|
||||||
@Throws(JsonSyntaxException::class, JsonIOException::class)
|
|
||||||
fun resolveCfMetadata(mods: List<IndexFile.File>, packFolder: PackwizFilePath, clientHolder: ClientHolder): List<ExceptionDetails> {
|
|
||||||
val failures = mutableListOf<ExceptionDetails>()
|
|
||||||
val fileIdMap = mutableMapOf<Int, List<IndexFile.File>>()
|
|
||||||
|
|
||||||
for (mod in mods) {
|
|
||||||
if (!mod.linkedFile!!.update.contains("curseforge")) {
|
|
||||||
failures.add(ExceptionDetails(mod.linkedFile!!.name, Exception("Failed to resolve CurseForge metadata: no CurseForge update section")))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
val fileId = (mod.linkedFile!!.update["curseforge"] as CurseForgeUpdateData).fileId
|
|
||||||
fileIdMap[fileId] = (fileIdMap[fileId] ?: listOf()) + mod
|
|
||||||
}
|
|
||||||
|
|
||||||
val reqData = GetFilesRequest(fileIdMap.keys.toList())
|
|
||||||
val req = Request.Builder()
|
|
||||||
.url("https://${APIServer}/v1/mods/files")
|
|
||||||
.header("Accept", "application/json")
|
|
||||||
.header("User-Agent", "packwiz-installer")
|
|
||||||
.header("X-API-Key", APIKey)
|
|
||||||
.post(Gson().toJson(reqData, GetFilesRequest::class.java).toRequestBody("application/json".toMediaType()))
|
|
||||||
.build()
|
|
||||||
val res = clientHolder.okHttpClient.newCall(req).execute()
|
|
||||||
if (!res.isSuccessful || res.body == null) {
|
|
||||||
res.closeQuietly()
|
|
||||||
failures.add(ExceptionDetails("Other", Exception("Failed to resolve CurseForge metadata for file data: error code ${res.code}")))
|
|
||||||
return failures
|
|
||||||
}
|
|
||||||
|
|
||||||
val resData = Gson().fromJson(res.body!!.charStream(), GetFilesResponse::class.java)
|
|
||||||
res.closeQuietly()
|
|
||||||
|
|
||||||
val manualDownloadMods = mutableMapOf<Int, List<Int>>()
|
|
||||||
for (file in resData.data) {
|
|
||||||
if (!fileIdMap.contains(file.id)) {
|
|
||||||
failures.add(ExceptionDetails(file.id.toString(),
|
|
||||||
Exception("Failed to find file from result: ID ${file.id}, Project ID ${file.modId}")))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (file.downloadUrl == null) {
|
|
||||||
manualDownloadMods[file.modId] = (manualDownloadMods[file.modId] ?: listOf()) + file.id
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
for (indexFile in fileIdMap[file.id]!!) {
|
|
||||||
indexFile.linkedFile!!.resolvedUpdateData["curseforge"] =
|
|
||||||
HttpUrlPath(file.downloadUrl!!.toHttpUrl())
|
|
||||||
}
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
failures.add(ExceptionDetails(file.id.toString(),
|
|
||||||
Exception("Failed to parse URL: ${file.downloadUrl} for ID ${file.id}, Project ID ${file.modId}", e)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some file types don't show up in the API at all! (e.g. shaderpacks)
|
|
||||||
// Add unresolved files to manualDownloadMods
|
|
||||||
for ((fileId, indexFiles) in fileIdMap) {
|
|
||||||
for (file in indexFiles) {
|
|
||||||
if (file.linkedFile != null) {
|
|
||||||
if (file.linkedFile!!.resolvedUpdateData["curseforge"] == null) {
|
|
||||||
val projectId = (file.linkedFile!!.update["curseforge"] as CurseForgeUpdateData).projectId
|
|
||||||
manualDownloadMods[projectId] = (manualDownloadMods[projectId] ?: listOf()) + fileId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (manualDownloadMods.isNotEmpty()) {
|
|
||||||
val reqModsData = GetModsRequest(manualDownloadMods.keys.toList())
|
|
||||||
val reqMods = Request.Builder()
|
|
||||||
.url("https://${APIServer}/v1/mods")
|
|
||||||
.header("Accept", "application/json")
|
|
||||||
.header("User-Agent", "packwiz-installer")
|
|
||||||
.header("X-API-Key", APIKey)
|
|
||||||
.post(Gson().toJson(reqModsData, GetModsRequest::class.java).toRequestBody("application/json".toMediaType()))
|
|
||||||
.build()
|
|
||||||
val resMods = clientHolder.okHttpClient.newCall(reqMods).execute()
|
|
||||||
if (!resMods.isSuccessful || resMods.body == null) {
|
|
||||||
resMods.closeQuietly()
|
|
||||||
failures.add(ExceptionDetails("Other", Exception("Failed to resolve CurseForge metadata for mod data: error code ${resMods.code}")))
|
|
||||||
return failures
|
|
||||||
}
|
|
||||||
|
|
||||||
val resModsData = Gson().fromJson(resMods.body!!.charStream(), GetModsResponse::class.java)
|
|
||||||
resMods.closeQuietly()
|
|
||||||
|
|
||||||
for (mod in resModsData.data) {
|
|
||||||
if (!manualDownloadMods.contains(mod.id)) {
|
|
||||||
failures.add(ExceptionDetails(mod.name,
|
|
||||||
Exception("Failed to find project from result: ID ${mod.id}")))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for (fileId in manualDownloadMods[mod.id]!!) {
|
|
||||||
if (!fileIdMap.contains(fileId)) {
|
|
||||||
failures.add(ExceptionDetails(mod.name,
|
|
||||||
Exception("Failed to find file from result: file ID $fileId")))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for (indexFile in fileIdMap[fileId]!!) {
|
|
||||||
var modUrl = "${mod.links?.websiteUrl}/files/${fileId}"
|
|
||||||
failures.add(ExceptionDetails(indexFile.name, Exception("This mod is excluded from the CurseForge API and must be downloaded manually.\n" +
|
|
||||||
"Please go to ${modUrl} and save this file to ${indexFile.destURI.rebase(packFolder).nioPath.absolute()}"), modUrl))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return failures
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.metadata.curseforge
|
|
||||||
|
|
||||||
import cc.ekblad.toml.tomlMapper
|
|
||||||
|
|
||||||
data class CurseForgeUpdateData(
|
|
||||||
val fileId: Int,
|
|
||||||
val projectId: Int,
|
|
||||||
): UpdateData {
|
|
||||||
companion object {
|
|
||||||
fun mapper() = tomlMapper {
|
|
||||||
mapping<CurseForgeUpdateData>("file-id" to "fileId", "project-id" to "projectId")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.metadata.curseforge
|
|
||||||
|
|
||||||
import cc.ekblad.toml.model.TomlValue
|
|
||||||
import cc.ekblad.toml.tomlMapper
|
|
||||||
|
|
||||||
interface UpdateData {
|
|
||||||
companion object {
|
|
||||||
fun mapper() = tomlMapper {
|
|
||||||
val cfMapper = CurseForgeUpdateData.mapper()
|
|
||||||
decoder { it: TomlValue.Map ->
|
|
||||||
if (it.properties.contains("curseforge")) {
|
|
||||||
mapOf("curseforge" to cfMapper.decode<CurseForgeUpdateData>(it.properties["curseforge"]!!))
|
|
||||||
} else { mapOf() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.metadata.curseforge
|
|
||||||
|
|
||||||
import com.google.gson.JsonDeserializationContext
|
|
||||||
import com.google.gson.JsonDeserializer
|
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import java.lang.reflect.Type
|
|
||||||
|
|
||||||
class UpdateDeserializer: JsonDeserializer<Map<String, UpdateData>> {
|
|
||||||
override fun deserialize(
|
|
||||||
json: JsonElement?,
|
|
||||||
typeOfT: Type?,
|
|
||||||
context: JsonDeserializationContext?
|
|
||||||
): Map<String, UpdateData> {
|
|
||||||
val out = mutableMapOf<String, UpdateData>()
|
|
||||||
for ((k, v) in json!!.asJsonObject.entrySet()) {
|
|
||||||
if (k == "curseforge") {
|
|
||||||
out[k] = context!!.deserialize(v, CurseForgeUpdateData::class.java)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,76 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.metadata.hash
|
|
||||||
|
|
||||||
import com.google.gson.*
|
|
||||||
import link.infra.packwiz.installer.metadata.hash.Hash.SourceProvider
|
|
||||||
import okio.ByteString
|
|
||||||
import okio.ByteString.Companion.decodeHex
|
|
||||||
import okio.ForwardingSource
|
|
||||||
import okio.HashingSource
|
|
||||||
import okio.Source
|
|
||||||
import java.lang.reflect.Type
|
|
||||||
|
|
||||||
data class Hash<T>(val type: HashFormat<T>, val value: T) {
|
|
||||||
interface Encoding<T> {
|
|
||||||
fun encodeToString(value: T): String
|
|
||||||
fun decodeFromString(str: String): T
|
|
||||||
|
|
||||||
object Hex: Encoding<ByteString> {
|
|
||||||
override fun encodeToString(value: ByteString) = value.hex()
|
|
||||||
override fun decodeFromString(str: String) = str.decodeHex()
|
|
||||||
}
|
|
||||||
|
|
||||||
object UInt: Encoding<kotlin.UInt> {
|
|
||||||
override fun encodeToString(value: kotlin.UInt) = value.toString()
|
|
||||||
override fun decodeFromString(str: String) =
|
|
||||||
try {
|
|
||||||
str.toUInt()
|
|
||||||
} catch (e: NumberFormatException) {
|
|
||||||
// Old packwiz.json values are signed; if they are negative they should be parsed as signed integers
|
|
||||||
// and reinterpreted as unsigned integers
|
|
||||||
str.toInt().toUInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun interface SourceProvider<T> {
|
|
||||||
fun source(type: HashFormat<T>, delegate: Source): HasherSource<T>
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromOkio(provider: ((Source) -> HashingSource)): SourceProvider<ByteString> {
|
|
||||||
return SourceProvider { type, delegate ->
|
|
||||||
val delegateHashing = provider.invoke(delegate)
|
|
||||||
object : ForwardingSource(delegateHashing), HasherSource<ByteString> {
|
|
||||||
override val hash: Hash<ByteString> by lazy(LazyThreadSafetyMode.NONE) { Hash(type, delegateHashing.hash) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TypeHandler : JsonDeserializer<Hash<*>>, JsonSerializer<Hash<*>> {
|
|
||||||
override fun serialize(src: Hash<*>, typeOfSrc: Type, context: JsonSerializationContext): JsonElement = JsonObject().apply {
|
|
||||||
add("type", JsonPrimitive(src.type.formatName))
|
|
||||||
// Local function for generics
|
|
||||||
fun <T> addValue(src: Hash<T>) = add("value", JsonPrimitive(src.type.encodeToString(src.value)))
|
|
||||||
addValue(src)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(JsonParseException::class)
|
|
||||||
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Hash<*> {
|
|
||||||
val obj = json.asJsonObject
|
|
||||||
val type: String
|
|
||||||
val value: String
|
|
||||||
try {
|
|
||||||
type = obj["type"].asString
|
|
||||||
value = obj["value"].asString
|
|
||||||
} catch (e: NullPointerException) {
|
|
||||||
throw JsonParseException("Invalid hash JSON data")
|
|
||||||
}
|
|
||||||
return try {
|
|
||||||
(HashFormat.fromName(type) ?: throw JsonParseException("Unknown hash type $type")).fromString(value)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw JsonParseException("Failed to create hash object", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.metadata.hash
|
|
||||||
|
|
||||||
import cc.ekblad.toml.model.TomlValue
|
|
||||||
import cc.ekblad.toml.tomlMapper
|
|
||||||
import link.infra.packwiz.installer.metadata.hash.Hash.Encoding
|
|
||||||
import link.infra.packwiz.installer.metadata.hash.Hash.SourceProvider
|
|
||||||
import link.infra.packwiz.installer.metadata.hash.Hash.SourceProvider.Companion.fromOkio
|
|
||||||
import okio.ByteString
|
|
||||||
import okio.Source
|
|
||||||
import okio.HashingSource.Companion as OkHashes
|
|
||||||
|
|
||||||
sealed class HashFormat<T>(val formatName: String): Encoding<T>, SourceProvider<T> {
|
|
||||||
object SHA1: HashFormat<ByteString>("sha1"),
|
|
||||||
Encoding<ByteString> by Encoding.Hex, SourceProvider<ByteString> by fromOkio(OkHashes::sha1)
|
|
||||||
object SHA256: HashFormat<ByteString>("sha256"),
|
|
||||||
Encoding<ByteString> by Encoding.Hex, SourceProvider<ByteString> by fromOkio(OkHashes::sha256)
|
|
||||||
object SHA512: HashFormat<ByteString>("sha512"),
|
|
||||||
Encoding<ByteString> by Encoding.Hex, SourceProvider<ByteString> by fromOkio(OkHashes::sha512)
|
|
||||||
object MD5: HashFormat<ByteString>("md5"),
|
|
||||||
Encoding<ByteString> by Encoding.Hex, SourceProvider<ByteString> by fromOkio(OkHashes::md5)
|
|
||||||
object MURMUR2: HashFormat<UInt>("murmur2"),
|
|
||||||
Encoding<UInt> by Encoding.UInt, SourceProvider<UInt> by SourceProvider(::Murmur2HasherSource)
|
|
||||||
|
|
||||||
fun source(delegate: Source): HasherSource<T> = source(this, delegate)
|
|
||||||
fun fromString(str: String) = Hash(this, decodeFromString(str))
|
|
||||||
override fun toString() = formatName
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
// lazy used to prevent initialisation issues!
|
|
||||||
private val values by lazy { listOf(SHA1, SHA256, SHA512, MD5, MURMUR2) }
|
|
||||||
fun fromName(formatName: String) = values.find { formatName == it.formatName }
|
|
||||||
|
|
||||||
fun mapper() = tomlMapper {
|
|
||||||
// TODO: better exception?
|
|
||||||
decoder { it: TomlValue.String -> fromName(it.value) ?: throw Exception("Hash format ${it.value} not supported") }
|
|
||||||
encoder { it: HashFormat<*> -> TomlValue.String(it.formatName) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.metadata.hash
|
|
||||||
|
|
||||||
import okio.Source
|
|
||||||
|
|
||||||
interface HasherSource<T>: Source {
|
|
||||||
val hash: Hash<T>
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.metadata.hash
|
|
||||||
|
|
||||||
import okio.Buffer
|
|
||||||
import okio.ForwardingSource
|
|
||||||
import okio.Source
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
class Murmur2HasherSource(type: HashFormat<UInt>, delegate: Source) : ForwardingSource(delegate), HasherSource<UInt> {
|
|
||||||
private val internalBuffer = Buffer()
|
|
||||||
private val tempBuffer = Buffer()
|
|
||||||
|
|
||||||
override val hash: Hash<UInt> by lazy(LazyThreadSafetyMode.NONE) {
|
|
||||||
// TODO: remove internal buffering?
|
|
||||||
val data = internalBuffer.readByteArray()
|
|
||||||
Hash(type, Murmur2Lib.hash32(data, data.size, 1).toUInt())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun read(sink: Buffer, byteCount: Long): Long {
|
|
||||||
val out = delegate.read(tempBuffer, byteCount)
|
|
||||||
if (out > -1) {
|
|
||||||
sink.write(tempBuffer.clone(), out)
|
|
||||||
computeNormalizedBufferFaster(tempBuffer, internalBuffer)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// Credit to https://github.com/modmuss50/CAV2/blob/master/murmur.go
|
|
||||||
private fun computeNormalizedBufferFaster(input: Buffer, output: Buffer) {
|
|
||||||
var index = 0
|
|
||||||
val arr = input.readByteArray()
|
|
||||||
for (b in arr) {
|
|
||||||
when (b) {
|
|
||||||
9.toByte(), 10.toByte(), 13.toByte(), 32.toByte() -> {}
|
|
||||||
else -> {
|
|
||||||
arr[index] = b
|
|
||||||
index++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output.write(arr, 0, index)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,67 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.request
|
|
||||||
|
|
||||||
import okio.IOException
|
|
||||||
|
|
||||||
sealed class RequestException: Exception {
|
|
||||||
constructor(message: String, cause: Throwable) : super(message, cause)
|
|
||||||
constructor(message: String) : super(message)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal errors that should not be shown to the user when the code is correct
|
|
||||||
*/
|
|
||||||
sealed class Internal: RequestException {
|
|
||||||
constructor(message: String, cause: Throwable) : super(message, cause)
|
|
||||||
constructor(message: String) : super(message)
|
|
||||||
|
|
||||||
sealed class HTTP: Internal {
|
|
||||||
constructor(message: String, cause: Throwable) : super(message, cause)
|
|
||||||
constructor(message: String) : super(message)
|
|
||||||
|
|
||||||
class NoResponseBody: HTTP("HTTP response in onResponse must have a response body")
|
|
||||||
class RequestFailed(cause: IOException): HTTP("HTTP request failed", cause)
|
|
||||||
class IllegalState(cause: IllegalStateException): HTTP("Internal fatal HTTP request error", cause)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Errors indicating that the request is malformed
|
|
||||||
*/
|
|
||||||
sealed class Validation: RequestException {
|
|
||||||
constructor(message: String, cause: Throwable) : super(message, cause)
|
|
||||||
constructor(message: String) : super(message)
|
|
||||||
|
|
||||||
// TODO: move out of RequestException?
|
|
||||||
class PathContainsNUL(path: String): Validation("Invalid path; contains NUL bytes: ${path.replace("\u0000", "")}")
|
|
||||||
class PathContainsVolumeLetter(path: String): Validation("Invalid path; contains volume letter: $path")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Errors relating to the response from the server
|
|
||||||
*/
|
|
||||||
sealed class Response: RequestException {
|
|
||||||
constructor(message: String, cause: Throwable) : super(message, cause)
|
|
||||||
constructor(message: String) : super(message)
|
|
||||||
|
|
||||||
// TODO: fancier way of displaying this?
|
|
||||||
sealed class HTTP: Response {
|
|
||||||
val res: okhttp3.Response
|
|
||||||
|
|
||||||
constructor(req: okhttp3.Request, res: okhttp3.Response, message: String, cause: Throwable) : super("Failed to make HTTP request to ${req.url}: $message", cause) {
|
|
||||||
this.res = res
|
|
||||||
}
|
|
||||||
constructor(req: okhttp3.Request, res: okhttp3.Response, message: String) : super("Failed to make HTTP request to ${req.url}: $message") {
|
|
||||||
this.res = res
|
|
||||||
}
|
|
||||||
|
|
||||||
class ErrorCode(req: okhttp3.Request, res: okhttp3.Response): HTTP(req, res, "Non-successful error code from HTTP request: ${res.code}")
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class File: RequestException {
|
|
||||||
constructor(message: String, cause: Throwable) : super(message, cause)
|
|
||||||
constructor(message: String) : super(message)
|
|
||||||
|
|
||||||
class FileNotFound(file: String): File("File path not found: $file")
|
|
||||||
class Other(cause: Throwable): File("Failed to read file", cause)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.target
|
|
||||||
|
|
||||||
import link.infra.packwiz.installer.metadata.hash.Hash
|
|
||||||
import java.nio.file.Path
|
|
||||||
|
|
||||||
data class CachedTarget(
|
|
||||||
/**
|
|
||||||
* @see Target.name
|
|
||||||
*/
|
|
||||||
val name: String,
|
|
||||||
/**
|
|
||||||
* The location where the target was last downloaded to.
|
|
||||||
* This is used for removing old files when the destination path changes.
|
|
||||||
* This shouldn't be set to the .disabled path (that is manually appended and checked)
|
|
||||||
*/
|
|
||||||
val cachedLocation: Path,
|
|
||||||
val enabled: Boolean,
|
|
||||||
val hash: Hash<*>,
|
|
||||||
/**
|
|
||||||
* For detecting when a target transitions non-optional -> optional and showing the option selection screen
|
|
||||||
*/
|
|
||||||
val isOptional: Boolean
|
|
||||||
)
|
|
@ -1,95 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.target
|
|
||||||
|
|
||||||
import java.io.IOException
|
|
||||||
import java.nio.file.*
|
|
||||||
import java.nio.file.attribute.BasicFileAttributes
|
|
||||||
import kotlin.io.path.relativeTo
|
|
||||||
|
|
||||||
data class CachedTargetStatus(val target: CachedTarget, var isValid: Boolean, var markDisabled: Boolean)
|
|
||||||
|
|
||||||
fun validate(targets: List<CachedTarget>, baseDir: Path) = runCatching {
|
|
||||||
val results = targets.map {
|
|
||||||
CachedTargetStatus(it, isValid = false, markDisabled = false)
|
|
||||||
}
|
|
||||||
val tree = buildTree(results, baseDir)
|
|
||||||
|
|
||||||
// Efficient file exists checking using directory listing, several orders of magnitude faster than Files.exists calls
|
|
||||||
Files.walkFileTree(baseDir, setOf(FileVisitOption.FOLLOW_LINKS), Int.MAX_VALUE, object : FileVisitor<Path> {
|
|
||||||
var currentNode: PathNode<CachedTargetStatus> = tree
|
|
||||||
|
|
||||||
override fun preVisitDirectory(dir: Path?, attrs: BasicFileAttributes?): FileVisitResult {
|
|
||||||
if (dir == null) {
|
|
||||||
return FileVisitResult.SKIP_SUBTREE
|
|
||||||
}
|
|
||||||
val subdirNode = currentNode.subdirs[dir.getName(dir.nameCount - 1)]
|
|
||||||
return if (subdirNode != null) {
|
|
||||||
currentNode = subdirNode
|
|
||||||
FileVisitResult.CONTINUE
|
|
||||||
} else {
|
|
||||||
FileVisitResult.SKIP_SUBTREE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun visitFile(file: Path?, attrs: BasicFileAttributes?): FileVisitResult {
|
|
||||||
if (file == null) {
|
|
||||||
return FileVisitResult.CONTINUE
|
|
||||||
}
|
|
||||||
// TODO: these are relative paths to baseDir
|
|
||||||
// TODO: strip the .disabled for lookup
|
|
||||||
val target = currentNode.files[file.getName(file.nameCount - 1)]
|
|
||||||
if (target != null) {
|
|
||||||
val disabledFile = file.endsWith(".disabled")
|
|
||||||
// If a .disabled file and the actual file both exist, mark as invalid if the target is disabled
|
|
||||||
if ((disabledFile )) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return FileVisitResult.CONTINUE
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun visitFileFailed(file: Path?, exc: IOException?): FileVisitResult {
|
|
||||||
if (exc != null) {
|
|
||||||
throw exc
|
|
||||||
}
|
|
||||||
throw IOException("visitFileFailed called with no exception")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun postVisitDirectory(dir: Path?, exc: IOException?): FileVisitResult {
|
|
||||||
if (exc != null) {
|
|
||||||
throw exc
|
|
||||||
} else {
|
|
||||||
val parent = currentNode.parent
|
|
||||||
if (parent != null) {
|
|
||||||
currentNode = parent
|
|
||||||
} else {
|
|
||||||
throw IOException("Invalid visitor tree structure")
|
|
||||||
}
|
|
||||||
return FileVisitResult.CONTINUE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
results
|
|
||||||
}
|
|
||||||
|
|
||||||
fun buildTree(targets: List<CachedTargetStatus>, baseDir: Path): PathNode<CachedTargetStatus> {
|
|
||||||
val root = PathNode<CachedTargetStatus>()
|
|
||||||
for (target in targets) {
|
|
||||||
val relPath = target.target.cachedLocation.relativeTo(baseDir)
|
|
||||||
var node = root
|
|
||||||
// Traverse all the directory components, except for the last one
|
|
||||||
for (i in 0 until (relPath.nameCount - 1)) {
|
|
||||||
node = node.createSubdir(relPath.getName(i))
|
|
||||||
}
|
|
||||||
node.files[relPath.getName(relPath.nameCount - 1)] = target
|
|
||||||
}
|
|
||||||
return root
|
|
||||||
}
|
|
||||||
|
|
||||||
data class PathNode<T>(val subdirs: MutableMap<Path, PathNode<T>>, val files: MutableMap<Path, T>, val parent: PathNode<T>?) {
|
|
||||||
constructor() : this(mutableMapOf(), mutableMapOf(), null)
|
|
||||||
|
|
||||||
fun createSubdir(nextComponent: Path) = subdirs.getOrPut(nextComponent, { PathNode(mutableMapOf(), mutableMapOf(), this) })
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.target
|
|
||||||
|
|
||||||
import link.infra.packwiz.installer.util.Log
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Response
|
|
||||||
import okio.FileSystem
|
|
||||||
import java.net.SocketTimeoutException
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class ClientHolder {
|
|
||||||
// Tries 10s timeouts (default), then 15s timeouts, then 60s timeouts
|
|
||||||
private val retryTimes = arrayOf(15, 60)
|
|
||||||
|
|
||||||
// TODO: a button to increase timeouts temporarily when retrying? manual retry button?
|
|
||||||
val okHttpClient by lazy { OkHttpClient.Builder()
|
|
||||||
// Retry requests according to retryTimes list
|
|
||||||
.addInterceptor {
|
|
||||||
val req = it.request()
|
|
||||||
|
|
||||||
var lastException: SocketTimeoutException? = null
|
|
||||||
var res: Response? = null
|
|
||||||
|
|
||||||
try {
|
|
||||||
res = it.proceed(req)
|
|
||||||
} catch (e: SocketTimeoutException) {
|
|
||||||
lastException = e
|
|
||||||
}
|
|
||||||
|
|
||||||
var tryCount = 0
|
|
||||||
while (res == null && tryCount < retryTimes.size) {
|
|
||||||
Log.info("OkHttp connection to ${req.url} timed out; retrying... (${tryCount + 1}/${retryTimes.size})")
|
|
||||||
|
|
||||||
val longerTimeoutChain = it
|
|
||||||
.withConnectTimeout(retryTimes[tryCount], TimeUnit.SECONDS)
|
|
||||||
.withReadTimeout(retryTimes[tryCount], TimeUnit.SECONDS)
|
|
||||||
.withWriteTimeout(retryTimes[tryCount], TimeUnit.SECONDS)
|
|
||||||
try {
|
|
||||||
res = longerTimeoutChain.proceed(req)
|
|
||||||
} catch (e: SocketTimeoutException) {
|
|
||||||
lastException = e
|
|
||||||
}
|
|
||||||
|
|
||||||
tryCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
res ?: throw lastException!!
|
|
||||||
}
|
|
||||||
.build() }
|
|
||||||
|
|
||||||
val fileSystem = FileSystem.SYSTEM
|
|
||||||
|
|
||||||
fun close() {
|
|
||||||
okHttpClient.dispatcher.cancelAll()
|
|
||||||
okHttpClient.dispatcher.executorService.shutdown()
|
|
||||||
okHttpClient.connectionPool.evictAll()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.target
|
|
||||||
|
|
||||||
enum class OverwriteMode {
|
|
||||||
/**
|
|
||||||
* Overwrite the destination with the source file, if the source file has changed.
|
|
||||||
*/
|
|
||||||
IF_SRC_CHANGED,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Never overwrite the destination; if it exists, it should not be written to.
|
|
||||||
*/
|
|
||||||
NEVER
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.target
|
|
||||||
|
|
||||||
import cc.ekblad.toml.model.TomlValue
|
|
||||||
import cc.ekblad.toml.tomlMapper
|
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
|
|
||||||
enum class Side(sideName: String) {
|
|
||||||
@SerializedName("client")
|
|
||||||
CLIENT("client"),
|
|
||||||
@SerializedName("server")
|
|
||||||
SERVER("server"),
|
|
||||||
@SerializedName("both")
|
|
||||||
@Suppress("unused")
|
|
||||||
BOTH("both") {
|
|
||||||
override fun hasSide(tSide: Side): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private val sideName: String
|
|
||||||
|
|
||||||
init {
|
|
||||||
this.sideName = sideName.lowercase()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString() = sideName
|
|
||||||
|
|
||||||
open fun hasSide(tSide: Side): Boolean {
|
|
||||||
return this == tSide || tSide == BOTH
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun from(name: String): Side? {
|
|
||||||
val lowerName = name.lowercase()
|
|
||||||
for (side in values()) {
|
|
||||||
if (side.sideName == lowerName) {
|
|
||||||
return side
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun mapper() = tomlMapper {
|
|
||||||
encoder { it: Side -> TomlValue.String(it.sideName) }
|
|
||||||
decoder { it: TomlValue.String -> from(it.value) ?: throw Exception("Invalid side name ${it.value}") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.target
|
|
||||||
|
|
||||||
import link.infra.packwiz.installer.target.path.PackwizPath
|
|
||||||
|
|
||||||
// TODO: rename to avoid conflicting with @Target
|
|
||||||
interface Target {
|
|
||||||
val src: PackwizPath<*>
|
|
||||||
val dest: PackwizPath<*>
|
|
||||||
val validityToken: ValidityToken
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Token interface for types used to compare Target identity. Implementations should use equals to indicate that
|
|
||||||
* these tokens represent the same file; used to preserve optional target choices between file renames.
|
|
||||||
*/
|
|
||||||
interface IdentityToken
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default implementation of IdentityToken that assumes files are not renamed; so optional choices do not need to
|
|
||||||
* be preserved across renames.
|
|
||||||
*/
|
|
||||||
@JvmInline
|
|
||||||
value class PathIdentityToken(val path: PackwizPath<*>): IdentityToken
|
|
||||||
|
|
||||||
val ident: IdentityToken
|
|
||||||
get() = PathIdentityToken(dest) // TODO: should use local-rebased path?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A user-friendly name; defaults to the destination path of the file.
|
|
||||||
*/
|
|
||||||
val name: String
|
|
||||||
get() = dest.filename
|
|
||||||
val side: Side
|
|
||||||
get() = Side.BOTH
|
|
||||||
val overwriteMode: OverwriteMode
|
|
||||||
get() = OverwriteMode.IF_SRC_CHANGED
|
|
||||||
|
|
||||||
interface Optional: Target {
|
|
||||||
val optional: Boolean
|
|
||||||
val optionalDefaultValue: Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OptionalDescribed: Optional {
|
|
||||||
val optionalDescription: String
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.target
|
|
||||||
|
|
||||||
import link.infra.packwiz.installer.metadata.hash.Hash
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A token used to determine if the source or destination file is changed, and ensure that the destination file is valid.
|
|
||||||
*/
|
|
||||||
interface ValidityToken {
|
|
||||||
// TODO: functions to allow validating this from file metadata, from file, or during the download process
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default implementation of ValidityToken based on a single hash.
|
|
||||||
*/
|
|
||||||
@JvmInline
|
|
||||||
value class HashValidityToken(val hash: Hash<*>): ValidityToken
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.target.path
|
|
||||||
|
|
||||||
import link.infra.packwiz.installer.request.RequestException
|
|
||||||
import link.infra.packwiz.installer.target.ClientHolder
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import okhttp3.Request
|
|
||||||
import okio.BufferedSource
|
|
||||||
import okio.IOException
|
|
||||||
|
|
||||||
class HttpUrlPath(private val url: HttpUrl, path: String? = null): PackwizPath<HttpUrlPath>(path) {
|
|
||||||
private fun build() = if (path == null) { url } else { url.newBuilder().addPathSegments(path).build() }
|
|
||||||
|
|
||||||
@Throws(RequestException::class)
|
|
||||||
override fun source(clientHolder: ClientHolder): BufferedSource {
|
|
||||||
val req = Request.Builder()
|
|
||||||
.url(build())
|
|
||||||
.header("Accept", "application/octet-stream")
|
|
||||||
.header("User-Agent", "packwiz-installer")
|
|
||||||
.get()
|
|
||||||
.build()
|
|
||||||
try {
|
|
||||||
val res = clientHolder.okHttpClient.newCall(req).execute()
|
|
||||||
// Can't use .use since it would close the response body before returning it to the caller
|
|
||||||
try {
|
|
||||||
if (!res.isSuccessful) {
|
|
||||||
throw RequestException.Response.HTTP.ErrorCode(req, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
val body = res.body ?: throw RequestException.Internal.HTTP.NoResponseBody()
|
|
||||||
return body.source()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// If an exception is thrown, close the response and rethrow
|
|
||||||
res.close()
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw RequestException.Internal.HTTP.RequestFailed(e)
|
|
||||||
} catch (e: IllegalStateException) {
|
|
||||||
throw RequestException.Internal.HTTP.IllegalState(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun construct(path: String): HttpUrlPath = HttpUrlPath(url, path)
|
|
||||||
|
|
||||||
override val folder: Boolean
|
|
||||||
get() = pathFolder ?: (url.pathSegments.last() == "")
|
|
||||||
override val filename: String
|
|
||||||
get() = pathFilename ?: url.pathSegments.last()
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
if (!super.equals(other)) return false
|
|
||||||
|
|
||||||
other as HttpUrlPath
|
|
||||||
|
|
||||||
if (url != other.url) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = super.hashCode()
|
|
||||||
result = 31 * result + url.hashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString() = build().toString()
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.target.path
|
|
||||||
|
|
||||||
import link.infra.packwiz.installer.request.RequestException
|
|
||||||
import link.infra.packwiz.installer.target.ClientHolder
|
|
||||||
import okio.*
|
|
||||||
|
|
||||||
class PackwizFilePath(private val base: Path, path: String? = null): PackwizPath<PackwizFilePath>(path) {
|
|
||||||
@Throws(RequestException::class)
|
|
||||||
override fun source(clientHolder: ClientHolder): BufferedSource {
|
|
||||||
val resolved = if (path == null) { base } else { this.base.resolve(path, true) }
|
|
||||||
try {
|
|
||||||
return clientHolder.fileSystem.source(resolved).buffer()
|
|
||||||
} catch (e: FileNotFoundException) {
|
|
||||||
throw RequestException.Response.File.FileNotFound(resolved.toString())
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw RequestException.Response.File.Other(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val nioPath: java.nio.file.Path get() {
|
|
||||||
val resolved = if (path == null) { base } else { this.base.resolve(path, true) }
|
|
||||||
return resolved.toNioPath()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun construct(path: String): PackwizFilePath = PackwizFilePath(base, path)
|
|
||||||
|
|
||||||
override val folder: Boolean
|
|
||||||
get() = pathFolder ?: (base.segments.last() == "")
|
|
||||||
override val filename: String
|
|
||||||
get() = pathFilename ?: base.segments.last()
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
if (!super.equals(other)) return false
|
|
||||||
|
|
||||||
other as PackwizFilePath
|
|
||||||
|
|
||||||
if (base != other.base) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = super.hashCode()
|
|
||||||
result = 31 * result + base.hashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString() = nioPath.toString()
|
|
||||||
}
|
|
@ -1,128 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.target.path
|
|
||||||
|
|
||||||
import cc.ekblad.toml.model.TomlValue
|
|
||||||
import cc.ekblad.toml.tomlMapper
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import com.google.gson.stream.JsonReader
|
|
||||||
import com.google.gson.stream.JsonWriter
|
|
||||||
import link.infra.packwiz.installer.request.RequestException
|
|
||||||
import link.infra.packwiz.installer.target.ClientHolder
|
|
||||||
import okio.BufferedSource
|
|
||||||
|
|
||||||
abstract class PackwizPath<T: PackwizPath<T>>(path: String? = null) {
|
|
||||||
protected val path: String?
|
|
||||||
|
|
||||||
init {
|
|
||||||
if (path != null) {
|
|
||||||
// Check for NUL bytes
|
|
||||||
if (path.contains('\u0000')) { throw RequestException.Validation.PathContainsNUL(path) }
|
|
||||||
// Normalise separator, to prevent differences between Unix/Windows
|
|
||||||
val pathNorm = path.replace('\\', '/')
|
|
||||||
// Split, create new lists for output
|
|
||||||
val split = pathNorm.split('/')
|
|
||||||
val canonicalised = mutableListOf<String>()
|
|
||||||
|
|
||||||
// Backward pass: collapse ".." components, remove "." components and empty components (except an empty component at the end; indicating a folder)
|
|
||||||
var parentComponentCount = 0
|
|
||||||
var first = true
|
|
||||||
for (component in split.asReversed()) {
|
|
||||||
if (first) {
|
|
||||||
first = false
|
|
||||||
if (component == "") {
|
|
||||||
canonicalised += component
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// URL-encoded . is normalised
|
|
||||||
val componentNorm = component.replace("%2e", ".")
|
|
||||||
if (componentNorm == "." || componentNorm == "") {
|
|
||||||
// Do nothing
|
|
||||||
} else if (componentNorm == "..") {
|
|
||||||
parentComponentCount++
|
|
||||||
} else if (parentComponentCount > 0) {
|
|
||||||
parentComponentCount--
|
|
||||||
} else {
|
|
||||||
canonicalised += componentNorm
|
|
||||||
// Don't allow volume letters (allows traversal to the root on Windows)
|
|
||||||
if (componentNorm.length == 2) {
|
|
||||||
if (componentNorm[0] in 'a'..'z' || componentNorm[0] in 'A'..'Z') {
|
|
||||||
if (componentNorm[1] == ':') {
|
|
||||||
throw RequestException.Validation.PathContainsVolumeLetter(path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (canonicalised.isEmpty()) {
|
|
||||||
this.path = null
|
|
||||||
} else {
|
|
||||||
// Join path
|
|
||||||
this.path = canonicalised.asReversed().joinToString("/")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.path = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract fun construct(path: String): T
|
|
||||||
|
|
||||||
protected val pathFolder: Boolean? get() = path?.endsWith("/")
|
|
||||||
abstract val folder: Boolean
|
|
||||||
protected val pathFilename: String? get() = path?.split("/")?.last()
|
|
||||||
abstract val filename: String
|
|
||||||
|
|
||||||
fun resolve(path: String): T {
|
|
||||||
return if (path.startsWith('/') || path.startsWith('\\')) {
|
|
||||||
// Absolute (but still relative to base of pack)
|
|
||||||
construct(path)
|
|
||||||
} else if (folder) {
|
|
||||||
// File in folder; append
|
|
||||||
construct((this.path ?: "") + path)
|
|
||||||
} else {
|
|
||||||
// File in parent folder; append with parent component
|
|
||||||
construct((this.path ?: "") + "/../" + path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun div(path: String) = resolve(path)
|
|
||||||
|
|
||||||
fun <U: PackwizPath<U>> rebase(path: U) = path.resolve(this.path ?: "")
|
|
||||||
|
|
||||||
val parent: T get() = resolve(if (folder) { ".." } else { "." })
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtain a BufferedSource for this path
|
|
||||||
* @throws RequestException When resolving the file failed
|
|
||||||
*/
|
|
||||||
@Throws(RequestException::class)
|
|
||||||
abstract fun source(clientHolder: ClientHolder): BufferedSource
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as PackwizPath<*>
|
|
||||||
|
|
||||||
if (path != other.path) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode() = path.hashCode()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun mapperRelativeTo(base: PackwizPath<*>) = tomlMapper {
|
|
||||||
encoder { it: PackwizPath<*> -> TomlValue.String(it.path ?: "") }
|
|
||||||
decoder { it: TomlValue.String -> base.resolve(it.value) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T: PackwizPath<T>> adapterRelativeTo(base: T) = object : TypeAdapter<T>() {
|
|
||||||
override fun write(writer: JsonWriter, value: T?) {
|
|
||||||
writer.value(value?.path)
|
|
||||||
}
|
|
||||||
override fun read(reader: JsonReader) = base.resolve(reader.nextString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString() = "(Unknown base) $path"
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.task
|
|
||||||
|
|
||||||
data class CacheKey<T>(val key: String, val version: Int)
|
|
@ -1,22 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.task
|
|
||||||
|
|
||||||
import kotlin.reflect.KProperty
|
|
||||||
|
|
||||||
class CacheManager {
|
|
||||||
class CacheValue<T> {
|
|
||||||
operator fun getValue(thisVal: Any?, property: KProperty<*>): T {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun setValue(thisVal: Any?, property: KProperty<*>, value: T) {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun <T> get(cacheKey: CacheKey<T>): CacheValue<T> {
|
|
||||||
return CacheValue()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.task
|
|
||||||
|
|
||||||
import kotlin.reflect.KMutableProperty0
|
|
||||||
|
|
||||||
// TODO: task processing on 1 background thread; actual resolving of values calls out to a thread group
|
|
||||||
// TODO: progress bar is updated from each of these tasks
|
|
||||||
// TODO: have everything be lazy so there's no need to determine task ordering upfront? a bit like rust async - task results must be queried to occur
|
|
||||||
|
|
||||||
abstract class Task<T>(protected val ctx: TaskContext): TaskInput<T> {
|
|
||||||
// TODO: lazy wrapper for fallible results
|
|
||||||
// TODO: multithreaded fanout subclass/helper
|
|
||||||
|
|
||||||
protected fun <T> wasUpdated(value: KMutableProperty0<T>, newValue: T): Boolean {
|
|
||||||
if (value.get() == newValue) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
value.set(newValue)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.task
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An object for storing results where result and upToDate are calculated simultaneously
|
|
||||||
*/
|
|
||||||
data class TaskCombinedResult<T>(val result: T, val upToDate: Boolean)
|
|
@ -1,12 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.task
|
|
||||||
|
|
||||||
import link.infra.packwiz.installer.target.ClientHolder
|
|
||||||
|
|
||||||
class TaskContext {
|
|
||||||
// TODO: thread pools, protocol roots
|
|
||||||
// TODO: cache management
|
|
||||||
|
|
||||||
val cache = CacheManager()
|
|
||||||
|
|
||||||
val clients = ClientHolder()
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.task
|
|
||||||
|
|
||||||
import kotlin.reflect.KProperty
|
|
||||||
|
|
||||||
interface TaskInput<T> {
|
|
||||||
/**
|
|
||||||
* The value of this task input. May be lazily evaluated; must be threadsafe.
|
|
||||||
*/
|
|
||||||
val value: T
|
|
||||||
|
|
||||||
/**
|
|
||||||
* True if the effective value of this input has changed since the task was last run.
|
|
||||||
* Doesn't require evaluation of the input value; should use cached data if possible.
|
|
||||||
* May be lazily evaluated; must be threadsafe.
|
|
||||||
*/
|
|
||||||
val upToDate: Boolean
|
|
||||||
|
|
||||||
operator fun getValue(thisVal: Any?, property: KProperty<*>): T = value
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun <T> raw(value: T): TaskInput<T> {
|
|
||||||
return object: TaskInput<T> {
|
|
||||||
override val value = value
|
|
||||||
override val upToDate: Boolean
|
|
||||||
get() = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.task.formats.packwizv1
|
|
||||||
|
|
||||||
import link.infra.packwiz.installer.metadata.hash.Hash
|
|
||||||
import link.infra.packwiz.installer.target.path.PackwizPath
|
|
||||||
|
|
||||||
data class PackwizV1PackFile(val name: String, val indexPath: PackwizPath<*>, val indexHash: Hash<*>)
|
|
@ -1,48 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.task.formats.packwizv1
|
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
import link.infra.packwiz.installer.metadata.hash.Hash
|
|
||||||
import link.infra.packwiz.installer.metadata.hash.HashFormat
|
|
||||||
import link.infra.packwiz.installer.target.path.PackwizPath
|
|
||||||
import link.infra.packwiz.installer.task.CacheKey
|
|
||||||
import link.infra.packwiz.installer.task.Task
|
|
||||||
import link.infra.packwiz.installer.task.TaskCombinedResult
|
|
||||||
import link.infra.packwiz.installer.task.TaskContext
|
|
||||||
|
|
||||||
class PackwizV1PackTomlTask(ctx: TaskContext, val path: PackwizPath<*>): Task<PackwizV1PackFile>(ctx) {
|
|
||||||
// TODO: make hierarchically defined by caller? - then changing the pack format type doesn't leave junk in the cache
|
|
||||||
private var cache by ctx.cache[CacheKey<Hash<*>>("packwiz.v1.packtoml.hash", 1)]
|
|
||||||
|
|
||||||
private class PackFile {
|
|
||||||
var name: String? = null
|
|
||||||
var index: IndexFileLoc? = null
|
|
||||||
|
|
||||||
class IndexFileLoc {
|
|
||||||
var file: String? = null
|
|
||||||
@SerializedName("hash-format")
|
|
||||||
var hashFormat: HashFormat<*>? = null
|
|
||||||
var hash: String? = null
|
|
||||||
}
|
|
||||||
|
|
||||||
var versions: Map<String, String>? = null
|
|
||||||
}
|
|
||||||
|
|
||||||
private val internalResult by lazy {
|
|
||||||
// TODO: query, parse JSON
|
|
||||||
val packFile = PackFile()
|
|
||||||
//Toml().read(InputStreamReader(path.source(ctx.clients).inputStream(), "UTF-8")).to(PackFile::class.java)
|
|
||||||
|
|
||||||
val hashFormat = (packFile.index?.hashFormat ?: throw RuntimeException("Hash format required"))
|
|
||||||
val resolved = PackwizV1PackFile(
|
|
||||||
packFile.name ?: throw RuntimeException("Name required"), // TODO: better exception handling
|
|
||||||
path.resolve(packFile.index?.file ?: throw RuntimeException("File required")),
|
|
||||||
hashFormat.fromString(packFile.index?.hash ?: throw RuntimeException("Hash required"))
|
|
||||||
)
|
|
||||||
val hash = hashFormat.fromString("whatever was just read")
|
|
||||||
|
|
||||||
TaskCombinedResult(resolved, wasUpdated(::cache, hash))
|
|
||||||
}
|
|
||||||
|
|
||||||
override val value by internalResult::result
|
|
||||||
override val upToDate by internalResult::upToDate
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.ui
|
|
||||||
|
|
||||||
import link.infra.packwiz.installer.ui.data.ExceptionDetails
|
|
||||||
import link.infra.packwiz.installer.ui.data.IOptionDetails
|
|
||||||
import link.infra.packwiz.installer.ui.data.InstallProgress
|
|
||||||
|
|
||||||
interface IUserInterface {
|
|
||||||
fun show()
|
|
||||||
fun dispose()
|
|
||||||
|
|
||||||
fun showErrorAndExit(message: String): Nothing {
|
|
||||||
showErrorAndExit(message, null)
|
|
||||||
}
|
|
||||||
fun showErrorAndExit(message: String, e: Exception?): Nothing
|
|
||||||
|
|
||||||
var title: String
|
|
||||||
fun submitProgress(progress: InstallProgress)
|
|
||||||
// Return true if the installation was cancelled!
|
|
||||||
fun showOptions(options: List<IOptionDetails>): Boolean
|
|
||||||
|
|
||||||
fun showExceptions(exceptions: List<ExceptionDetails>, numTotal: Int, allowsIgnore: Boolean): ExceptionListResult
|
|
||||||
fun disableOptionsButton(hasOptions: Boolean) {}
|
|
||||||
|
|
||||||
fun showCancellationDialog(): CancellationResult = CancellationResult.QUIT
|
|
||||||
|
|
||||||
fun showUpdateConfirmationDialog(oldVersions: List<Pair<String, String?>>, newVersions: List<Pair<String, String?>>): UpdateConfirmationResult {
|
|
||||||
// Always update metadata when using the CLI
|
|
||||||
return UpdateConfirmationResult.UPDATE
|
|
||||||
}
|
|
||||||
|
|
||||||
fun awaitOptionalButton(showCancel: Boolean, timeout: Long)
|
|
||||||
|
|
||||||
enum class ExceptionListResult {
|
|
||||||
CONTINUE, CANCEL, IGNORE
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class CancellationResult {
|
|
||||||
QUIT, CONTINUE
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class UpdateConfirmationResult {
|
|
||||||
CANCELLED, CONTINUE, UPDATE
|
|
||||||
}
|
|
||||||
|
|
||||||
var optionsButtonPressed: Boolean
|
|
||||||
var cancelButtonPressed: Boolean
|
|
||||||
var cancelCallback: (() -> Unit)?
|
|
||||||
|
|
||||||
var firstInstall: Boolean
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <T> IUserInterface.wrap(message: String, inner: () -> T): T {
|
|
||||||
return try {
|
|
||||||
inner.invoke()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showErrorAndExit(message, e)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.ui.cli
|
|
||||||
|
|
||||||
import link.infra.packwiz.installer.ui.IUserInterface
|
|
||||||
import link.infra.packwiz.installer.ui.IUserInterface.ExceptionListResult
|
|
||||||
import link.infra.packwiz.installer.ui.data.ExceptionDetails
|
|
||||||
import link.infra.packwiz.installer.ui.data.IOptionDetails
|
|
||||||
import link.infra.packwiz.installer.ui.data.InstallProgress
|
|
||||||
import link.infra.packwiz.installer.util.Log
|
|
||||||
import kotlin.system.exitProcess
|
|
||||||
|
|
||||||
class CLIHandler : IUserInterface {
|
|
||||||
@Volatile
|
|
||||||
override var optionsButtonPressed = false
|
|
||||||
// TODO: treat ctrl+c as cancel?
|
|
||||||
@Volatile
|
|
||||||
override var cancelButtonPressed = false
|
|
||||||
@Volatile
|
|
||||||
override var cancelCallback: (() -> Unit)? = null
|
|
||||||
@Volatile
|
|
||||||
override var firstInstall = false
|
|
||||||
|
|
||||||
override var title: String = ""
|
|
||||||
|
|
||||||
override fun showErrorAndExit(message: String, e: Exception?): Nothing {
|
|
||||||
if (e != null) {
|
|
||||||
Log.fatal(message, e)
|
|
||||||
} else {
|
|
||||||
Log.fatal(message)
|
|
||||||
}
|
|
||||||
exitProcess(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun show() {}
|
|
||||||
override fun dispose() {}
|
|
||||||
override fun submitProgress(progress: InstallProgress) {
|
|
||||||
val sb = StringBuilder()
|
|
||||||
if (progress.hasProgress) {
|
|
||||||
sb.append('(')
|
|
||||||
sb.append(progress.progress)
|
|
||||||
sb.append('/')
|
|
||||||
sb.append(progress.progressTotal)
|
|
||||||
sb.append(") ")
|
|
||||||
}
|
|
||||||
sb.append(progress.message)
|
|
||||||
println(sb.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun showOptions(options: List<IOptionDetails>): Boolean {
|
|
||||||
for (opt in options) {
|
|
||||||
opt.optionValue = true
|
|
||||||
// TODO: implement option choice in the CLI?
|
|
||||||
Log.warn("Accepting option ${opt.name} as option choosing is not implemented in the CLI")
|
|
||||||
}
|
|
||||||
return false // Can't be cancelled!
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun showExceptions(exceptions: List<ExceptionDetails>, numTotal: Int, allowsIgnore: Boolean): ExceptionListResult {
|
|
||||||
println("Failed to download modpack, the following errors were encountered:")
|
|
||||||
for (ex in exceptions) {
|
|
||||||
print(ex.name + ": ")
|
|
||||||
ex.exception.printStackTrace()
|
|
||||||
}
|
|
||||||
return ExceptionListResult.CANCEL
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun awaitOptionalButton(showCancel: Boolean, timeout: Long) {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.ui.data
|
|
||||||
|
|
||||||
data class ExceptionDetails(
|
|
||||||
val name: String,
|
|
||||||
val exception: Exception,
|
|
||||||
val modUrl: String? = null
|
|
||||||
)
|
|
@ -1,7 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.ui.data
|
|
||||||
|
|
||||||
interface IOptionDetails {
|
|
||||||
val name: String
|
|
||||||
var optionValue: Boolean
|
|
||||||
val optionDescription: String
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.ui.data
|
|
||||||
|
|
||||||
data class InstallProgress(
|
|
||||||
val message: String,
|
|
||||||
val hasProgress: Boolean = false,
|
|
||||||
val progress: Int = 0,
|
|
||||||
val progressTotal: Int = 0
|
|
||||||
) {
|
|
||||||
constructor(message: String, progress: Int, progressTotal: Int) : this(message, true, progress, progressTotal)
|
|
||||||
|
|
||||||
constructor(message: String) : this(message, false)
|
|
||||||
}
|
|
@ -1,177 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.ui.gui
|
|
||||||
|
|
||||||
import link.infra.packwiz.installer.util.Log
|
|
||||||
import link.infra.packwiz.installer.ui.IUserInterface
|
|
||||||
import link.infra.packwiz.installer.ui.data.ExceptionDetails
|
|
||||||
import java.awt.BorderLayout
|
|
||||||
import java.awt.Desktop
|
|
||||||
import java.awt.event.WindowAdapter
|
|
||||||
import java.awt.event.WindowEvent
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.PrintWriter
|
|
||||||
import java.io.StringWriter
|
|
||||||
import java.net.URI
|
|
||||||
import java.net.URISyntaxException
|
|
||||||
import java.util.concurrent.CompletableFuture
|
|
||||||
import javax.swing.*
|
|
||||||
import javax.swing.border.EmptyBorder
|
|
||||||
|
|
||||||
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(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
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openUrl(url: String) {
|
|
||||||
try {
|
|
||||||
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
|
||||||
Desktop.getDesktop().browse(URI(url))
|
|
||||||
} else {
|
|
||||||
val process = Runtime.getRuntime().exec(arrayOf("xdg-open", url));
|
|
||||||
val exitValue = process.waitFor()
|
|
||||||
if (exitValue > 0) {
|
|
||||||
Log.warn("Failed to open $url: xdg-open exited with code $exitValue")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.warn("Failed to open $url", e)
|
|
||||||
} catch (e: URISyntaxException) {
|
|
||||||
Log.warn("Failed to open $url", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the dialog.
|
|
||||||
*/
|
|
||||||
init {
|
|
||||||
setBounds(100, 100, 540, 340)
|
|
||||||
setLocationRelativeTo(parentWindow)
|
|
||||||
|
|
||||||
contentPane.apply {
|
|
||||||
layout = BorderLayout()
|
|
||||||
|
|
||||||
// Error panel
|
|
||||||
add(JPanel().apply {
|
|
||||||
add(JLabel("One or more errors were encountered while installing the modpack!").apply {
|
|
||||||
icon = UIManager.getIcon("OptionPane.warningIcon")
|
|
||||||
})
|
|
||||||
}, BorderLayout.NORTH)
|
|
||||||
|
|
||||||
// Content panel
|
|
||||||
add(JPanel().apply {
|
|
||||||
border = EmptyBorder(5, 5, 5, 5)
|
|
||||||
layout = BorderLayout(0, 0)
|
|
||||||
|
|
||||||
add(JSplitPane().apply {
|
|
||||||
resizeWeight = 0.3
|
|
||||||
|
|
||||||
lblExceptionStacktrace = JTextArea("Select a file")
|
|
||||||
lblExceptionStacktrace.background = UIManager.getColor("List.background")
|
|
||||||
lblExceptionStacktrace.isOpaque = true
|
|
||||||
lblExceptionStacktrace.wrapStyleWord = true
|
|
||||||
lblExceptionStacktrace.lineWrap = true
|
|
||||||
lblExceptionStacktrace.isEditable = false
|
|
||||||
lblExceptionStacktrace.isFocusable = true
|
|
||||||
lblExceptionStacktrace.font = UIManager.getFont("Label.font")
|
|
||||||
lblExceptionStacktrace.border = EmptyBorder(5, 5, 5, 5)
|
|
||||||
|
|
||||||
rightComponent = JScrollPane(lblExceptionStacktrace)
|
|
||||||
|
|
||||||
leftComponent = JScrollPane(JList<String>().apply {
|
|
||||||
selectionMode = ListSelectionModel.SINGLE_SELECTION
|
|
||||||
border = EmptyBorder(5, 5, 5, 5)
|
|
||||||
val listModel = ExceptionListModel(eList)
|
|
||||||
model = listModel
|
|
||||||
addListSelectionListener {
|
|
||||||
val i = selectedIndex
|
|
||||||
if (i > -1) {
|
|
||||||
val sw = StringWriter()
|
|
||||||
listModel.getExceptionAt(i).printStackTrace(PrintWriter(sw))
|
|
||||||
lblExceptionStacktrace.text = sw.toString()
|
|
||||||
// Scroll to the top
|
|
||||||
lblExceptionStacktrace.caretPosition = 0
|
|
||||||
} else {
|
|
||||||
lblExceptionStacktrace.text = "Select a file"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}, BorderLayout.CENTER)
|
|
||||||
|
|
||||||
// Button pane
|
|
||||||
add(JPanel().apply {
|
|
||||||
layout = BorderLayout(0, 0)
|
|
||||||
|
|
||||||
// Right buttons
|
|
||||||
add(JPanel().apply {
|
|
||||||
add(JButton("Continue").apply {
|
|
||||||
toolTipText = "Attempt to continue installing, excluding the failed downloads"
|
|
||||||
addActionListener {
|
|
||||||
future.complete(IUserInterface.ExceptionListResult.CONTINUE)
|
|
||||||
this@ExceptionListWindow.dispose()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
add(JButton("Cancel launch").apply {
|
|
||||||
toolTipText = "Stop launching the game"
|
|
||||||
addActionListener {
|
|
||||||
future.complete(IUserInterface.ExceptionListResult.CANCEL)
|
|
||||||
this@ExceptionListWindow.dispose()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
add(JButton("Ignore update").apply {
|
|
||||||
toolTipText = "Start the game without attempting to update"
|
|
||||||
isEnabled = allowsIgnore
|
|
||||||
addActionListener {
|
|
||||||
future.complete(IUserInterface.ExceptionListResult.IGNORE)
|
|
||||||
this@ExceptionListWindow.dispose()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
val missingMods = eList.filter { it.modUrl != null }.map { it.modUrl!! }.toSet()
|
|
||||||
|
|
||||||
if (!missingMods.isEmpty()) {
|
|
||||||
add(JButton("Open missing mods").apply {
|
|
||||||
toolTipText = "Open missing mods in your browser"
|
|
||||||
addActionListener {
|
|
||||||
missingMods.forEach {
|
|
||||||
openUrl(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, BorderLayout.EAST)
|
|
||||||
|
|
||||||
// Errored label
|
|
||||||
add(JLabel(eList.size.toString() + "/" + numTotal + " errored").apply {
|
|
||||||
horizontalAlignment = SwingConstants.CENTER
|
|
||||||
}, BorderLayout.CENTER)
|
|
||||||
|
|
||||||
// Left buttons
|
|
||||||
add(JPanel().apply {
|
|
||||||
add(JButton("Report issue").apply {
|
|
||||||
addActionListener {
|
|
||||||
openUrl("https://github.com/packwiz/packwiz-installer/issues/new")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, BorderLayout.WEST)
|
|
||||||
}, BorderLayout.SOUTH)
|
|
||||||
}
|
|
||||||
|
|
||||||
addWindowListener(object : WindowAdapter() {
|
|
||||||
override fun windowClosing(e: WindowEvent) {
|
|
||||||
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(IUserInterface.ExceptionListResult.CANCEL)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,251 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.ui.gui
|
|
||||||
|
|
||||||
import link.infra.packwiz.installer.ui.IUserInterface
|
|
||||||
import link.infra.packwiz.installer.ui.IUserInterface.ExceptionListResult
|
|
||||||
import link.infra.packwiz.installer.ui.data.ExceptionDetails
|
|
||||||
import link.infra.packwiz.installer.ui.data.IOptionDetails
|
|
||||||
import link.infra.packwiz.installer.ui.data.InstallProgress
|
|
||||||
import link.infra.packwiz.installer.util.Log
|
|
||||||
import java.awt.EventQueue
|
|
||||||
import java.util.Timer
|
|
||||||
import java.util.concurrent.CompletableFuture
|
|
||||||
import java.util.concurrent.CountDownLatch
|
|
||||||
import javax.swing.JDialog
|
|
||||||
import javax.swing.JOptionPane
|
|
||||||
import javax.swing.UIManager
|
|
||||||
import kotlin.concurrent.timer
|
|
||||||
import kotlin.system.exitProcess
|
|
||||||
|
|
||||||
class GUIHandler : IUserInterface {
|
|
||||||
private lateinit var frmPackwizlauncher: InstallWindow
|
|
||||||
|
|
||||||
@Volatile
|
|
||||||
override var optionsButtonPressed = false
|
|
||||||
set(value) {
|
|
||||||
optionalSelectedLatch.countDown()
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
@Volatile
|
|
||||||
override var cancelButtonPressed = false
|
|
||||||
set(value) {
|
|
||||||
optionalSelectedLatch.countDown()
|
|
||||||
field = value
|
|
||||||
cancelCallback?.invoke()
|
|
||||||
}
|
|
||||||
@Volatile
|
|
||||||
override var cancelCallback: (() -> Unit)? = null
|
|
||||||
var okButtonPressed = false
|
|
||||||
set(value) {
|
|
||||||
optionalSelectedLatch.countDown()
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
@Volatile
|
|
||||||
override var firstInstall = false
|
|
||||||
|
|
||||||
override var title = "packwiz-installer"
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
EventQueue.invokeLater { frmPackwizlauncher.title = value }
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
EventQueue.invokeAndWait {
|
|
||||||
try {
|
|
||||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.warn("Failed to set look and feel", e)
|
|
||||||
}
|
|
||||||
frmPackwizlauncher = InstallWindow(this).apply {
|
|
||||||
title = this@GUIHandler.title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val visibleCountdownLatch = CountDownLatch(1)
|
|
||||||
private val optionalSelectedLatch = CountDownLatch(1)
|
|
||||||
|
|
||||||
override fun show() = EventQueue.invokeLater {
|
|
||||||
frmPackwizlauncher.isVisible = true
|
|
||||||
visibleCountdownLatch.countDown()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun dispose() = EventQueue.invokeAndWait {
|
|
||||||
frmPackwizlauncher.dispose()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun showErrorAndExit(message: String, e: Exception?): Nothing {
|
|
||||||
val buttons = arrayOf("Quit", if (firstInstall) "Continue without installing" else "Continue without updating")
|
|
||||||
if (e != null) {
|
|
||||||
Log.fatal(message, e)
|
|
||||||
EventQueue.invokeAndWait {
|
|
||||||
val result = JOptionPane.showOptionDialog(frmPackwizlauncher,
|
|
||||||
"$message: $e",
|
|
||||||
title,
|
|
||||||
JOptionPane.YES_NO_OPTION, JOptionPane.ERROR_MESSAGE, null, buttons, buttons[0])
|
|
||||||
if (result == 1) {
|
|
||||||
Log.info("User selected to continue without installing/updating, exiting with code 0...")
|
|
||||||
exitProcess(0)
|
|
||||||
} else {
|
|
||||||
Log.info("User selected to quit, exiting with code 1...")
|
|
||||||
exitProcess(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.fatal(message)
|
|
||||||
EventQueue.invokeAndWait {
|
|
||||||
val result = JOptionPane.showOptionDialog(frmPackwizlauncher,
|
|
||||||
message,
|
|
||||||
title,
|
|
||||||
JOptionPane.YES_NO_OPTION, JOptionPane.ERROR_MESSAGE, null, buttons, buttons[0])
|
|
||||||
if (result == 1) {
|
|
||||||
Log.info("User selected to continue without installing/updating, exiting with code 0...")
|
|
||||||
exitProcess(0)
|
|
||||||
} else {
|
|
||||||
Log.info("User selected to quit, exiting with code 1...")
|
|
||||||
exitProcess(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
exitProcess(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun submitProgress(progress: InstallProgress) {
|
|
||||||
val sb = StringBuilder()
|
|
||||||
if (progress.hasProgress) {
|
|
||||||
sb.append('(')
|
|
||||||
sb.append(progress.progress)
|
|
||||||
sb.append('/')
|
|
||||||
sb.append(progress.progressTotal)
|
|
||||||
sb.append(") ")
|
|
||||||
}
|
|
||||||
sb.append(progress.message)
|
|
||||||
Log.info(sb.toString())
|
|
||||||
EventQueue.invokeLater {
|
|
||||||
frmPackwizlauncher.displayProgress(progress)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun showOptions(options: List<IOptionDetails>): Boolean {
|
|
||||||
val future = CompletableFuture<Boolean>()
|
|
||||||
EventQueue.invokeAndWait {
|
|
||||||
if (options.isEmpty()) {
|
|
||||||
JOptionPane.showMessageDialog(null,
|
|
||||||
"This modpack has no optional mods!",
|
|
||||||
"Optional mods", JOptionPane.INFORMATION_MESSAGE)
|
|
||||||
future.complete(false)
|
|
||||||
} else {
|
|
||||||
OptionsSelectWindow(options, future, frmPackwizlauncher).apply {
|
|
||||||
defaultCloseOperation = JDialog.DISPOSE_ON_CLOSE
|
|
||||||
isVisible = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return future.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun showExceptions(exceptions: List<ExceptionDetails>, numTotal: Int, allowsIgnore: Boolean): ExceptionListResult {
|
|
||||||
val future = CompletableFuture<ExceptionListResult>()
|
|
||||||
EventQueue.invokeLater {
|
|
||||||
ExceptionListWindow(exceptions, future, numTotal, allowsIgnore, frmPackwizlauncher).apply {
|
|
||||||
defaultCloseOperation = JDialog.DISPOSE_ON_CLOSE
|
|
||||||
isVisible = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return future.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun disableOptionsButton(hasOptions: Boolean) = EventQueue.invokeLater {
|
|
||||||
frmPackwizlauncher.disableOptionsButton(hasOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun showCancellationDialog(): 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) IUserInterface.CancellationResult.QUIT else IUserInterface.CancellationResult.CONTINUE)
|
|
||||||
}
|
|
||||||
return future.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun showUpdateConfirmationDialog(oldVersions: List<Pair<String, String?>>, newVersions: List<Pair<String, String?>>): IUserInterface.UpdateConfirmationResult {
|
|
||||||
assert(newVersions.isNotEmpty())
|
|
||||||
val future = CompletableFuture<IUserInterface.UpdateConfirmationResult>()
|
|
||||||
EventQueue.invokeLater {
|
|
||||||
val oldVersIndex = oldVersions.map { it.first to it.second }.toMap()
|
|
||||||
val newVersIndex = newVersions.map { it.first to it.second }.toMap()
|
|
||||||
val message = StringBuilder()
|
|
||||||
message.append("<html>" +
|
|
||||||
"This modpack uses newer versions of the following:<br>" +
|
|
||||||
"<ul>")
|
|
||||||
|
|
||||||
for (oldVer in oldVersions) {
|
|
||||||
val correspondingNewVer = newVersIndex[oldVer.first]
|
|
||||||
message.append("<li>")
|
|
||||||
message.append(oldVer.first.replaceFirstChar { it.uppercase() })
|
|
||||||
message.append(": <font color=${if (oldVer.second != correspondingNewVer) "#ff0000" else "#000000"}>")
|
|
||||||
message.append(oldVer.second ?: "Not found")
|
|
||||||
message.append("</font></li>")
|
|
||||||
}
|
|
||||||
message.append("</ul>")
|
|
||||||
|
|
||||||
message.append("New versions:" +
|
|
||||||
"<ul>")
|
|
||||||
for (newVer in newVersions) {
|
|
||||||
val correspondingOldVer = oldVersIndex[newVer.first]
|
|
||||||
message.append("<li>")
|
|
||||||
message.append(newVer.first.replaceFirstChar { it.uppercase() })
|
|
||||||
message.append(": <font color=${if (newVer.second != correspondingOldVer) "#00ff00" else "#000000"}>")
|
|
||||||
message.append(newVer.second ?: "Not found")
|
|
||||||
message.append("</font></li>")
|
|
||||||
}
|
|
||||||
message.append("</ul><br>" +
|
|
||||||
"Would you like to update the versions, launch without updating, or cancel the launch?")
|
|
||||||
|
|
||||||
|
|
||||||
val options = arrayOf("Cancel", "Continue anyways", "Update")
|
|
||||||
val result = JOptionPane.showOptionDialog(frmPackwizlauncher, message,
|
|
||||||
"Updating MultiMC versions",
|
|
||||||
JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, options[2])
|
|
||||||
future.complete(
|
|
||||||
when (result) {
|
|
||||||
JOptionPane.CLOSED_OPTION, 0 -> IUserInterface.UpdateConfirmationResult.CANCELLED
|
|
||||||
1 -> IUserInterface.UpdateConfirmationResult.CONTINUE
|
|
||||||
2 -> IUserInterface.UpdateConfirmationResult.UPDATE
|
|
||||||
else -> IUserInterface.UpdateConfirmationResult.CANCELLED
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return future.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun awaitOptionalButton(showCancel: Boolean, timeout: Long) {
|
|
||||||
EventQueue.invokeAndWait {
|
|
||||||
frmPackwizlauncher.showOk(!showCancel)
|
|
||||||
}
|
|
||||||
visibleCountdownLatch.await()
|
|
||||||
|
|
||||||
var closeTimer: Timer? = null
|
|
||||||
if (timeout >= 0) {
|
|
||||||
var count = 0
|
|
||||||
closeTimer = timer("timeout", true, 0, 1000) {
|
|
||||||
if (count >= timeout) {
|
|
||||||
optionalSelectedLatch.countDown()
|
|
||||||
cancel()
|
|
||||||
} else {
|
|
||||||
frmPackwizlauncher.timeoutOk(timeout - count)
|
|
||||||
count += 1
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
optionalSelectedLatch.await()
|
|
||||||
closeTimer?.cancel()
|
|
||||||
EventQueue.invokeLater {
|
|
||||||
frmPackwizlauncher.hideOk()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,128 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.ui.gui
|
|
||||||
|
|
||||||
import link.infra.packwiz.installer.ui.data.InstallProgress
|
|
||||||
import java.awt.BorderLayout
|
|
||||||
import java.awt.Component
|
|
||||||
import java.awt.GridBagConstraints
|
|
||||||
import java.awt.GridBagLayout
|
|
||||||
import javax.swing.*
|
|
||||||
import javax.swing.border.EmptyBorder
|
|
||||||
|
|
||||||
class InstallWindow(private val handler: GUIHandler) : JFrame() {
|
|
||||||
private var lblProgresslabel: JLabel
|
|
||||||
private var progressBar: JProgressBar
|
|
||||||
private var btnOptions: JButton
|
|
||||||
private val btnCancel: JButton
|
|
||||||
private val btnOk: JButton
|
|
||||||
private val buttonsPanel: JPanel
|
|
||||||
|
|
||||||
init {
|
|
||||||
setBounds(100, 100, 493, 95)
|
|
||||||
// Works better with tiling window managers - there isn't any reason to change window size currently anyway
|
|
||||||
isResizable = false
|
|
||||||
defaultCloseOperation = EXIT_ON_CLOSE
|
|
||||||
setLocationRelativeTo(null)
|
|
||||||
|
|
||||||
// Progress bar and loading text
|
|
||||||
add(JPanel().apply {
|
|
||||||
border = EmptyBorder(10, 10, 10, 10)
|
|
||||||
layout = BorderLayout(0, 0)
|
|
||||||
|
|
||||||
progressBar = JProgressBar().apply {
|
|
||||||
isIndeterminate = true
|
|
||||||
}
|
|
||||||
add(progressBar, BorderLayout.CENTER)
|
|
||||||
|
|
||||||
lblProgresslabel = JLabel("Loading...")
|
|
||||||
add(lblProgresslabel, BorderLayout.SOUTH)
|
|
||||||
}, BorderLayout.CENTER)
|
|
||||||
|
|
||||||
// Buttons
|
|
||||||
buttonsPanel = JPanel().apply {
|
|
||||||
border = EmptyBorder(0, 5, 0, 5)
|
|
||||||
layout = GridBagLayout()
|
|
||||||
|
|
||||||
btnOptions = JButton("Optional mods...").apply {
|
|
||||||
alignmentX = Component.CENTER_ALIGNMENT
|
|
||||||
|
|
||||||
addActionListener {
|
|
||||||
text = "Loading..."
|
|
||||||
isEnabled = false
|
|
||||||
handler.optionsButtonPressed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
add(btnOptions, GridBagConstraints().apply {
|
|
||||||
gridx = 1
|
|
||||||
gridy = 0
|
|
||||||
})
|
|
||||||
|
|
||||||
btnCancel = JButton("Cancel").apply {
|
|
||||||
addActionListener {
|
|
||||||
isEnabled = false
|
|
||||||
handler.cancelButtonPressed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
add(btnCancel, GridBagConstraints().apply {
|
|
||||||
gridx = 1
|
|
||||||
gridy = 1
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
btnOk = JButton("Continue").apply {
|
|
||||||
addActionListener {
|
|
||||||
handler.okButtonPressed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
add(buttonsPanel, BorderLayout.EAST)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun displayProgress(progress: InstallProgress) {
|
|
||||||
if (progress.hasProgress) {
|
|
||||||
progressBar.isIndeterminate = false
|
|
||||||
progressBar.value = progress.progress
|
|
||||||
progressBar.maximum = progress.progressTotal
|
|
||||||
} else {
|
|
||||||
progressBar.isIndeterminate = true
|
|
||||||
progressBar.value = 0
|
|
||||||
}
|
|
||||||
lblProgresslabel.text = progress.message
|
|
||||||
}
|
|
||||||
|
|
||||||
fun disableOptionsButton(hasOptions: Boolean) {
|
|
||||||
btnOptions.apply {
|
|
||||||
text = if (hasOptions) { "Optional mods..." } else { "No optional mods" }
|
|
||||||
isEnabled = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showOk(hideCancel: Boolean) {
|
|
||||||
if (hideCancel) {
|
|
||||||
buttonsPanel.add(btnOk, GridBagConstraints().apply {
|
|
||||||
gridx = 1
|
|
||||||
gridy = 1
|
|
||||||
})
|
|
||||||
buttonsPanel.remove(btnCancel)
|
|
||||||
} else {
|
|
||||||
buttonsPanel.add(btnOk, GridBagConstraints().apply {
|
|
||||||
gridx = 0
|
|
||||||
gridy = 1
|
|
||||||
})
|
|
||||||
}
|
|
||||||
buttonsPanel.revalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hideOk() {
|
|
||||||
buttonsPanel.remove(btnOk)
|
|
||||||
if (!buttonsPanel.components.contains(btnCancel)) {
|
|
||||||
buttonsPanel.add(btnCancel, GridBagConstraints().apply {
|
|
||||||
gridx = 1
|
|
||||||
gridy = 1
|
|
||||||
})
|
|
||||||
}
|
|
||||||
buttonsPanel.revalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun timeoutOk(remaining: Long) {
|
|
||||||
btnOk.text = "Continue ($remaining)"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.ui.gui
|
|
||||||
|
|
||||||
import link.infra.packwiz.installer.ui.data.IOptionDetails
|
|
||||||
|
|
||||||
// Serves as a proxy for IOptionDetails, so that setOptionValue isn't called until OK is clicked
|
|
||||||
internal class OptionTempHandler(private val opt: IOptionDetails) : IOptionDetails {
|
|
||||||
override var optionValue = opt.optionValue
|
|
||||||
|
|
||||||
override val name get() = opt.name
|
|
||||||
override val optionDescription get() = opt.optionDescription
|
|
||||||
|
|
||||||
fun finalise() {
|
|
||||||
opt.optionValue = optionValue
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,167 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.ui.gui
|
|
||||||
|
|
||||||
import link.infra.packwiz.installer.ui.data.IOptionDetails
|
|
||||||
import java.awt.BorderLayout
|
|
||||||
import java.awt.FlowLayout
|
|
||||||
import java.awt.event.ActionEvent
|
|
||||||
import java.awt.event.ActionListener
|
|
||||||
import java.awt.event.WindowAdapter
|
|
||||||
import java.awt.event.WindowEvent
|
|
||||||
import java.util.*
|
|
||||||
import java.util.concurrent.CompletableFuture
|
|
||||||
import javax.swing.*
|
|
||||||
import javax.swing.border.EmptyBorder
|
|
||||||
import javax.swing.event.TableModelListener
|
|
||||||
import javax.swing.table.TableModel
|
|
||||||
|
|
||||||
class OptionsSelectWindow internal constructor(optList: List<IOptionDetails>, future: CompletableFuture<Boolean>, parentWindow: JFrame?) : JDialog(parentWindow, "Select optional mods...", true), ActionListener {
|
|
||||||
private val lblOptionDescription: JTextArea
|
|
||||||
private val tableModel: OptionTableModel
|
|
||||||
private val future: CompletableFuture<Boolean>
|
|
||||||
|
|
||||||
private class OptionTableModel(givenOpts: List<IOptionDetails>) : TableModel {
|
|
||||||
private val opts: List<OptionTempHandler>
|
|
||||||
|
|
||||||
init {
|
|
||||||
val mutOpts = ArrayList<OptionTempHandler>()
|
|
||||||
for (opt in givenOpts) {
|
|
||||||
mutOpts.add(OptionTempHandler(opt))
|
|
||||||
}
|
|
||||||
opts = mutOpts
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getRowCount() = opts.size
|
|
||||||
override fun getColumnCount() = 2
|
|
||||||
|
|
||||||
private val columnNames = arrayOf("Enabled", "Mod name")
|
|
||||||
private val columnTypes = arrayOf(Boolean::class.javaObjectType, String::class.java)
|
|
||||||
private val columnEditables = booleanArrayOf(true, false)
|
|
||||||
|
|
||||||
override fun getColumnName(columnIndex: Int) = columnNames[columnIndex]
|
|
||||||
override fun getColumnClass(columnIndex: Int) = columnTypes[columnIndex]
|
|
||||||
override fun isCellEditable(rowIndex: Int, columnIndex: Int) = columnEditables[columnIndex]
|
|
||||||
|
|
||||||
override fun getValueAt(rowIndex: Int, columnIndex: Int): Any {
|
|
||||||
val opt = opts[rowIndex]
|
|
||||||
return if (columnIndex == 0) opt.optionValue else opt.name
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setValueAt(aValue: Any, rowIndex: Int, columnIndex: Int) {
|
|
||||||
if (columnIndex == 0) {
|
|
||||||
val opt = opts[rowIndex]
|
|
||||||
opt.optionValue = aValue as Boolean
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Noop, the table model doesn't change!
|
|
||||||
override fun addTableModelListener(l: TableModelListener) {}
|
|
||||||
override fun removeTableModelListener(l: TableModelListener) {}
|
|
||||||
|
|
||||||
fun getDescription(rowIndex: Int) = opts[rowIndex].optionDescription
|
|
||||||
|
|
||||||
fun finalise() {
|
|
||||||
for (opt in opts) {
|
|
||||||
opt.finalise()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun actionPerformed(e: ActionEvent) {
|
|
||||||
if (e.actionCommand == "OK") {
|
|
||||||
tableModel.finalise()
|
|
||||||
future.complete(false)
|
|
||||||
dispose()
|
|
||||||
} else if (e.actionCommand == "Cancel") {
|
|
||||||
future.complete(true)
|
|
||||||
dispose()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the dialog.
|
|
||||||
*/
|
|
||||||
init {
|
|
||||||
tableModel = OptionTableModel(optList)
|
|
||||||
this.future = future
|
|
||||||
|
|
||||||
setBounds(100, 100, 450, 300)
|
|
||||||
setLocationRelativeTo(parentWindow)
|
|
||||||
|
|
||||||
contentPane.apply {
|
|
||||||
layout = BorderLayout()
|
|
||||||
add(JPanel().apply {
|
|
||||||
border = EmptyBorder(5, 5, 5, 5)
|
|
||||||
layout = BorderLayout(0, 0)
|
|
||||||
|
|
||||||
add(JSplitPane().apply {
|
|
||||||
resizeWeight = 0.5
|
|
||||||
|
|
||||||
lblOptionDescription = JTextArea("Select an option...").apply {
|
|
||||||
background = UIManager.getColor("List.background")
|
|
||||||
isOpaque = true
|
|
||||||
wrapStyleWord = true
|
|
||||||
lineWrap = true
|
|
||||||
isEditable = false
|
|
||||||
isFocusable = false
|
|
||||||
font = UIManager.getFont("Label.font")
|
|
||||||
border = EmptyBorder(10, 10, 10, 10)
|
|
||||||
}
|
|
||||||
|
|
||||||
leftComponent = JScrollPane(JTable().apply {
|
|
||||||
showVerticalLines = false
|
|
||||||
showHorizontalLines = false
|
|
||||||
setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
|
|
||||||
setShowGrid(false)
|
|
||||||
model = tableModel
|
|
||||||
columnModel.getColumn(0).resizable = false
|
|
||||||
columnModel.getColumn(0).preferredWidth = 15
|
|
||||||
columnModel.getColumn(0).maxWidth = 15
|
|
||||||
columnModel.getColumn(1).resizable = false
|
|
||||||
selectionModel.addListSelectionListener {
|
|
||||||
val i = selectedRow
|
|
||||||
if (i > -1) {
|
|
||||||
lblOptionDescription.text = tableModel.getDescription(i)
|
|
||||||
} else {
|
|
||||||
lblOptionDescription.text = "Select an option..."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tableHeader = null
|
|
||||||
}).apply {
|
|
||||||
viewport.background = UIManager.getColor("List.background")
|
|
||||||
horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
|
|
||||||
}
|
|
||||||
|
|
||||||
rightComponent = JScrollPane(lblOptionDescription)
|
|
||||||
})
|
|
||||||
|
|
||||||
add(JPanel().apply {
|
|
||||||
layout = FlowLayout(FlowLayout.RIGHT)
|
|
||||||
|
|
||||||
add(JButton("OK").apply {
|
|
||||||
actionCommand = "OK"
|
|
||||||
addActionListener(this@OptionsSelectWindow)
|
|
||||||
|
|
||||||
this@OptionsSelectWindow.rootPane.defaultButton = this
|
|
||||||
})
|
|
||||||
|
|
||||||
add(JButton("Cancel").apply {
|
|
||||||
actionCommand = "Cancel"
|
|
||||||
addActionListener(this@OptionsSelectWindow)
|
|
||||||
})
|
|
||||||
}, BorderLayout.SOUTH)
|
|
||||||
}, BorderLayout.CENTER)
|
|
||||||
}
|
|
||||||
|
|
||||||
addWindowListener(object : WindowAdapter() {
|
|
||||||
override fun windowClosing(e: WindowEvent) {
|
|
||||||
future.complete(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
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(true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.util
|
|
||||||
|
|
||||||
import cc.ekblad.toml.TomlMapper
|
|
||||||
import cc.ekblad.toml.configuration.TomlMapperConfigurator
|
|
||||||
import cc.ekblad.toml.model.TomlValue
|
|
||||||
|
|
||||||
inline fun <reified T: Any> TomlMapperConfigurator.delegateTransitive(mapper: TomlMapper) {
|
|
||||||
decoder { it: TomlValue -> mapper.decode<T>(it) }
|
|
||||||
encoder { it: T -> mapper.encode(it) }
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
package link.infra.packwiz.installer.util
|
|
||||||
|
|
||||||
object Log {
|
|
||||||
fun info(message: String) = println(message)
|
|
||||||
|
|
||||||
fun warn(message: String) = println("[Warning] $message")
|
|
||||||
fun warn(message: String, exception: Exception) = println("[Warning] $message: $exception")
|
|
||||||
|
|
||||||
fun fatal(message: String) {
|
|
||||||
println("[FATAL] $message")
|
|
||||||
}
|
|
||||||
fun fatal(message: String, exception: Exception) {
|
|
||||||
println("[FATAL] $message: ")
|
|
||||||
exception.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
-dontoptimize
|
|
||||||
-dontobfuscate
|
|
||||||
|
|
||||||
# Used by Okio and OkHttp
|
|
||||||
-dontwarn org.codehaus.mojo.animal_sniffer.*
|
|
||||||
-dontwarn okhttp3.internal.platform.**
|
|
||||||
-dontwarn org.conscrypt.**
|
|
||||||
-dontwarn org.bouncycastle.**
|
|
||||||
-dontwarn org.openjsse.**
|
|
||||||
|
|
||||||
-keep class !link.infra.packwiz.installer.deps.**,link.infra.packwiz.installer.** { *; }
|
|
||||||
|
|
||||||
-keep class com.google.gson.reflect.TypeToken { *; }
|
|
||||||
-keep class * extends com.google.gson.reflect.TypeToken
|
|
||||||
|
|
||||||
-keep @interface kotlin.Metadata { *; }
|
|
||||||
|
|
||||||
-renamesourcefileattribute SourceFile
|
|
||||||
-keepattributes *Annotation*,SourceFile,LineNumberTable,Signature
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user