├── .gitattributes ├── .github ├── auto-merge.yml ├── dependabot.yml └── workflows │ ├── auto-dependabot.yml │ └── gradle.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── build.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src └── main ├── kotlin └── org │ └── bundleproject │ └── bundle │ ├── Bundle.kt │ ├── api │ ├── data │ │ ├── ModData.kt │ │ └── Platform.kt │ ├── requests │ │ ├── BulkModRequest.kt │ │ └── ModRequest.kt │ └── responses │ │ ├── BulkModResponse.kt │ │ ├── ErrorResponse.kt │ │ └── ModResponse.kt │ ├── entities │ ├── Mod.kt │ └── RemoteMod.kt │ ├── gui │ ├── LoadingGui.kt │ └── UpdateOverviewGui.kt │ └── utils │ ├── Constants.kt │ ├── HttpClient.kt │ ├── ModPair.kt │ ├── TypeAdapters.kt │ └── utilities.kt └── resources ├── bundle.png └── logback.xml /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/auto-merge.yml: -------------------------------------------------------------------------------- 1 | - match: 2 | dependency-name: ktor-.* 3 | update-type: patch -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gradle" # See documentation for possible values 4 | directory: "/" # Location of package manifests 5 | schedule: 6 | interval: "daily" 7 | open-pull-requests-limit: 10 -------------------------------------------------------------------------------- /.github/workflows/auto-dependabot.yml: -------------------------------------------------------------------------------- 1 | name: Auto Merge Dependabot 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | auto-merge-dependabot: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: ahmadnassri/action-dependabot-auto-merge@v2.3.1 12 | with: 13 | github-token: ${{ secrets.FULL_ACCESS_TOKEN }} 14 | command: squash and merge 15 | config: .github/auto-merge.yml 16 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: Gradle CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | paths-ignore: 8 | - 'README.md' 9 | - 'LICENSE.md' 10 | - '.gitignore' 11 | - '.gitattributes' 12 | pull_request: 13 | branches: 14 | - '*' 15 | paths-ignore: 16 | - 'README.md' 17 | - 'LICENSE' 18 | - '.gitignore' 19 | - '.gitattributes' 20 | workflow_dispatch: 21 | jobs: 22 | build: 23 | 24 | runs-on: ubuntu-latest 25 | name: Build with gradle 26 | 27 | strategy: 28 | matrix: 29 | jdk: [8, 11, 16] 30 | 31 | steps: 32 | - uses: actions/checkout@v2 33 | - name: Set up JDK ${{ matrix.jdk }} 34 | uses: actions/setup-java@v2 35 | with: 36 | java-version: ${{ matrix.jdk }} 37 | distribution: 'temurin' 38 | - uses: actions/cache@v2 39 | with: 40 | path: | 41 | ~/.gradle/caches 42 | ~/.gradle/wrapper 43 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 44 | restore-keys: | 45 | ${{ runner.os }}-gradle- 46 | - name: Grant execute permission for gradlew 47 | run: chmod +x gradlew 48 | - name: Build with Gradle 49 | run: ./gradlew build --no-daemon 50 | - uses: actions/upload-artifact@v2 51 | with: 52 | path: build/libs/*.jar 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .gradle 3 | build 4 | run 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | All Rights Reserved 2 | 3 | Copyright (c) 2021 BundleProject 4 | 5 | Created by BundleProject 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 8 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 9 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 10 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 11 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 12 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 13 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bundle 2 | A program that hooks into minecraft with any mod loader that updates all of your mods with a cool gui. 3 | 4 | ### bundle.project.json 5 | `bundle.project.json` is recommended for mod developers to implement into their mods. 6 | It is not required for use of Bundle however it is advised. 7 | 8 | This file is similar to `fabric.mod.json` or `mcmod.info` and serves mostly 9 | the same purpose. The difference is, it can guarantee that all the version 10 | and other metadata matches the repository/api. 11 | 12 | Here is an example of a `bundle.project.json` for the mod EvergreenHUD 13 | Note that the version **MUST** follow [semver specification](https://semver.org). 14 | ```json 15 | { 16 | "id": "evergreenhud", 17 | "version": "2.0.0.94-pre.12", 18 | "minecraft_version": "1.8.9", 19 | "platform": "forge" 20 | } 21 | ``` 22 | ### Community 23 | Join our discord server https://discord.gg/gzJDUt3a8w and get support or talk about general topics and Bundle! 24 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.jetbrains.kotlin.jvm") version "1.6.0" 3 | id("com.github.johnrengelman.shadow") version "7.1.0" 4 | } 5 | 6 | group = "org.bundleproject" 7 | version = "0.1.2" 8 | 9 | repositories { 10 | mavenCentral() 11 | maven(url = "https://jitpack.io/") 12 | } 13 | 14 | dependencies { 15 | implementation(kotlin("stdlib-jdk8", "1.6.0")) 16 | implementation(kotlin("reflect", "1.6.0")) 17 | 18 | implementation("com.google.code.gson:gson:2.8.9") 19 | 20 | implementation("com.formdev:flatlaf:1.6.3") 21 | 22 | implementation("io.ktor:ktor-client-gson:1.6.5") 23 | implementation("io.ktor:ktor-client-core:1.6.5") 24 | implementation("io.ktor:ktor-client-apache:1.6.5") 25 | 26 | implementation("io.github.microutils:kotlin-logging-jvm:2.1.0") 27 | implementation("ch.qos.logback:logback-classic:1.2.7") 28 | 29 | implementation("org.bundleproject:libversion:0.0.2") 30 | } 31 | 32 | tasks { 33 | shadowJar { 34 | archiveClassifier.set("") 35 | duplicatesStrategy = DuplicatesStrategy.EXCLUDE 36 | 37 | relocate("com.google.code.gson", "org.bundleproject.lib.gson") 38 | } 39 | jar { 40 | dependsOn(shadowJar) 41 | } 42 | 43 | compileKotlin { 44 | kotlinOptions.jvmTarget = "1.8" 45 | } 46 | compileJava { 47 | options.encoding = "UTF-8" 48 | options.release.set(8) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BundleProject/Bundle/73ece617a3606ce8e0411846de39efb5fb79d64d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "Bundle" 2 | 3 | -------------------------------------------------------------------------------- /src/main/kotlin/org/bundleproject/bundle/Bundle.kt: -------------------------------------------------------------------------------- 1 | package org.bundleproject.bundle 2 | 3 | import com.formdev.flatlaf.FlatLightLaf 4 | import com.google.gson.JsonParser 5 | import kotlinx.coroutines.async 6 | import kotlinx.coroutines.awaitAll 7 | import kotlinx.coroutines.coroutineScope 8 | import org.bundleproject.bundle.entities.Mod 9 | import org.bundleproject.bundle.api.data.Platform 10 | import org.bundleproject.bundle.api.requests.BulkModRequest 11 | import org.bundleproject.bundle.gui.LoadingGui 12 | import org.bundleproject.bundle.utils.* 13 | import org.bundleproject.libversion.Version 14 | import java.io.File 15 | import java.io.InputStreamReader 16 | import java.util.jar.JarFile 17 | import javax.swing.* 18 | 19 | /** 20 | * The Bundle object holds the code to start the version checking 21 | * and update mods before the game is launched. Bundle works 22 | * by applying itself as a custom entrypoint 23 | * and does its actions and then begins to launch the game 24 | * once it has finished. 25 | * 26 | * @since 0.0.1 27 | */ 28 | class Bundle(gameDir: File, private val version: Version?, modFolderName: String) { 29 | private val modsDir = File(gameDir, modFolderName) 30 | 31 | suspend fun start() { 32 | try { 33 | logger.info("Starting Bundle...") 34 | println() 35 | 36 | try { UIManager.setLookAndFeel(FlatLightLaf()) } 37 | catch (e: Throwable) { logger.error(e) { "Couldn't set look and feel...." } } 38 | 39 | val outdated = getOutdatedMods() 40 | 41 | // return if outdated is empty 42 | if (outdated.isEmpty()) { 43 | logger.info("No outdated mods found.") 44 | return 45 | } 46 | 47 | // val lock = ReentrantLock() 48 | // val condition = lock.newCondition() 49 | // lock.withLock { 50 | // UpdateOverviewGui(this, outdated, condition).apply { 51 | // isVisible = true 52 | // } 53 | // condition.await() 54 | // } 55 | 56 | updateMods(outdated) 57 | 58 | } catch (e: Throwable) { 59 | logger.error(e) { "Error while updating mods! Bundle exited!" } 60 | } 61 | } 62 | 63 | /** 64 | * Walks through the mods and looks through each file 65 | * and attempts to check if it's a valid mod 66 | * 67 | * All the valid mods are collected into a list before 68 | * bulk-requesting the latest versions from the API. 69 | * They are matched into pairs and returned. 70 | * 71 | * @since 0.0.1 72 | */ 73 | private suspend fun getOutdatedMods(): MutableList { 74 | logger.info("Getting outdated mods...") 75 | 76 | val localMods = mutableListOf() 77 | for (mod in modsDir.walkTopDown()) { 78 | if (mod.isDirectory) continue 79 | 80 | val localMod = try { 81 | getModInfo(mod) 82 | } catch (e: Exception) { 83 | if (e is Version.VersionParseException) logger.error(e) { "Failed to parse version for ${mod.name}" } 84 | else logger.error(e) { "Failed to get mod info for ${mod.name}" } 85 | 86 | null 87 | } ?: continue 88 | 89 | if (localMod.minecraftVersion != (version ?: localMod.minecraftVersion)) continue 90 | 91 | localMods.add(localMod) 92 | } 93 | logger.info("Found: ${localMods.map { it.id }.toFormattedString()}") 94 | println() 95 | 96 | logger.info("Making bulk request to API.") 97 | val request = BulkModRequest(localMods.map { it.makeRequest() }) 98 | val response = request.request() 99 | 100 | logger.info("Comparing local to remote mod versions.") 101 | val outdated = mutableListOf() 102 | for (i in localMods.indices) { 103 | val local = localMods[i] 104 | val remote = local.applyData(response[i]) 105 | 106 | if (remote > local) 107 | outdated.add(ModPair(local, remote)) 108 | } 109 | logger.info("${outdated.size}/${localMods.size} mods need an update.") 110 | logger.info("Outdated: ${outdated.map { it.remote.id }.toFormattedString()}") 111 | println() 112 | 113 | return outdated 114 | } 115 | 116 | /** 117 | * Get metadata about a mod jar. 118 | * 119 | * @since 0.0.1 120 | */ 121 | private fun getModInfo(modFile: File): Mod? { 122 | logger.info("Getting mod info using method...") 123 | JarFile(modFile).use { jar -> 124 | jar.getJarEntry("bundle.project.json")?.let getJarEntry@{ modInfo -> 125 | logger.info("Bundle Info File") 126 | 127 | InputStreamReader(jar.getInputStream(modInfo)).use { 128 | val json = JsonParser.parseReader(it).asJsonObject 129 | 130 | if (json.has("update") && !json.get("update").asBoolean) return@getModInfo null 131 | 132 | return@getModInfo Mod( 133 | name = json.get("name")?.asString ?: return null, 134 | id = json.get("id")?.asString ?: return@getJarEntry, 135 | version = Version.of(json.get("version")?.asString ?: return@getJarEntry), 136 | minecraftVersion = Version.of(json.get("minecraft_version")?.asString ?: return@getJarEntry), 137 | fileName = modFile.name, 138 | platform = Platform.valueOf(json.get("platform")?.asString?.uppercase() ?: return@getJarEntry), 139 | ) 140 | } 141 | } 142 | 143 | jar.getJarEntry("fabric.mod.json")?.let { modInfo -> 144 | logger.info("Fabric Info File") 145 | 146 | InputStreamReader(jar.getInputStream(modInfo)).use { 147 | val json = JsonParser.parseReader(it).asJsonObject 148 | return@getModInfo Mod( 149 | name = json.get("name")?.asString ?: return null, 150 | id = json.get("id")?.asString ?: return null, 151 | version = Version.of(json.get("version")?.asString ?: return null), 152 | minecraftVersion = Version.of(json.get("depends").asJsonObject.get("minecraft")?.asString ?: return null), 153 | fileName = modFile.name, 154 | platform = Platform.Fabric, 155 | ) 156 | } 157 | } 158 | 159 | jar.getJarEntry("mcmod.info")?.let { modInfo -> 160 | logger.info("Forge Info File") 161 | 162 | InputStreamReader(jar.getInputStream(modInfo)).use { 163 | val json = JsonParser.parseReader(it).asJsonArray[0].asJsonObject 164 | return@getModInfo Mod( 165 | name = json.get("name")?.asString ?: return null, 166 | id = json.get("modid")?.asString ?: return null, 167 | version = Version.of(json.get("version")?.asString ?: return null), 168 | minecraftVersion = Version.of(json.get("mcversion")?.asString ?: return null), 169 | fileName = modFile.name, 170 | platform = Platform.Forge, 171 | ) 172 | } 173 | } 174 | } 175 | 176 | logger.error("None") 177 | return null 178 | } 179 | 180 | 181 | /** 182 | * Goes through a list of mods and asynchronously deletes 183 | * and replaces it with its updated counterpart 184 | * 185 | * @since 0.0.2 186 | */ 187 | suspend fun updateMods(mods: List) { 188 | logger.info("Updating Mods...") 189 | 190 | coroutineScope { 191 | val loading = LoadingGui(mods.size) 192 | loading.isVisible = true 193 | mods.map { (local, remote) -> 194 | async { 195 | val current = File(modsDir, local.fileName) 196 | val new = File(modsDir, remote.fileName) 197 | 198 | logger.info("Downloading: ${new.name}") 199 | if (http.downloadFile(new, remote.downloadEndpoint)) { 200 | logger.info("Deleting: ${current.name}") 201 | current.delete() 202 | } else { 203 | error("Failed to download! Skipping!") 204 | } 205 | 206 | loading.finish() 207 | } 208 | }.awaitAll() 209 | } 210 | } 211 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/bundleproject/bundle/api/data/ModData.kt: -------------------------------------------------------------------------------- 1 | package org.bundleproject.bundle.api.data 2 | 3 | import org.bundleproject.libversion.Version 4 | 5 | data class ModData( 6 | val url: String, 7 | val version: Version, 8 | val metadata: Metadata, 9 | ) { 10 | data class Metadata( 11 | val display: String, 12 | val creator: String, 13 | ) 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/bundleproject/bundle/api/data/Platform.kt: -------------------------------------------------------------------------------- 1 | package org.bundleproject.bundle.api.data 2 | 3 | enum class Platform(val id: String) { 4 | Forge("forge"), 5 | Fabric("fabric"); 6 | 7 | override fun toString(): String { 8 | return id 9 | } 10 | 11 | companion object { 12 | fun fromId(id: String): Platform? = 13 | values().find { it.id == id } 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/bundleproject/bundle/api/requests/BulkModRequest.kt: -------------------------------------------------------------------------------- 1 | package org.bundleproject.bundle.api.requests 2 | 3 | import com.google.gson.JsonArray 4 | import com.google.gson.JsonObject 5 | import io.ktor.client.request.* 6 | import org.bundleproject.bundle.api.data.ModData 7 | import org.bundleproject.bundle.api.responses.BulkModResponse 8 | import org.bundleproject.bundle.utils.* 9 | 10 | data class BulkModRequest( 11 | val mods: List 12 | ) { 13 | suspend fun request(): List { 14 | val json = JsonArray().apply { 15 | mods.forEach { add(gson.toJsonTree(it)) } 16 | } 17 | 18 | val base64 = gson.toJson(json).encodeBase64() 19 | return http.get("$API/$API_VERSION/mods/bulk/$base64").mods 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/bundleproject/bundle/api/requests/ModRequest.kt: -------------------------------------------------------------------------------- 1 | package org.bundleproject.bundle.api.requests 2 | 3 | import io.ktor.client.request.* 4 | import org.bundleproject.bundle.api.data.ModData 5 | import org.bundleproject.bundle.api.responses.ModResponse 6 | import org.bundleproject.bundle.api.data.Platform 7 | import org.bundleproject.bundle.utils.API 8 | import org.bundleproject.bundle.utils.API_VERSION 9 | import org.bundleproject.bundle.utils.http 10 | 11 | data class ModRequest( 12 | val id: String, 13 | val platform: Platform, 14 | val minecraftVersion: String, 15 | val version: String = "latest", 16 | ) { 17 | constructor( 18 | id: String, 19 | platform: String, 20 | minecraftVersion: String, 21 | version: String = "latest", 22 | ) : this(id, Platform.fromId(platform)!!, minecraftVersion, version) 23 | 24 | @Transient 25 | val endpoint = "$API/$API_VERSION/mods/$id/$platform/$minecraftVersion/$version" 26 | 27 | suspend fun request(): ModData { 28 | return http.get(endpoint).data 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/main/kotlin/org/bundleproject/bundle/api/responses/BulkModResponse.kt: -------------------------------------------------------------------------------- 1 | package org.bundleproject.bundle.api.responses 2 | 3 | import org.bundleproject.bundle.api.data.ModData 4 | 5 | data class BulkModResponse( 6 | val success: Boolean, 7 | val mods: List, 8 | ) 9 | -------------------------------------------------------------------------------- /src/main/kotlin/org/bundleproject/bundle/api/responses/ErrorResponse.kt: -------------------------------------------------------------------------------- 1 | package org.bundleproject.bundle.api.responses 2 | 3 | data class ErrorResponse( 4 | val success: Boolean, 5 | val error: String, 6 | ) 7 | -------------------------------------------------------------------------------- /src/main/kotlin/org/bundleproject/bundle/api/responses/ModResponse.kt: -------------------------------------------------------------------------------- 1 | package org.bundleproject.bundle.api.responses 2 | 3 | import org.bundleproject.bundle.api.data.ModData 4 | 5 | data class ModResponse( 6 | val success: Boolean, 7 | val data: ModData, 8 | ) 9 | -------------------------------------------------------------------------------- /src/main/kotlin/org/bundleproject/bundle/entities/Mod.kt: -------------------------------------------------------------------------------- 1 | package org.bundleproject.bundle.entities 2 | 3 | import org.bundleproject.bundle.api.data.ModData 4 | import org.bundleproject.bundle.api.data.Platform 5 | import org.bundleproject.bundle.api.requests.ModRequest 6 | import org.bundleproject.bundle.utils.getFileNameFromUrl 7 | import org.bundleproject.libversion.Version 8 | 9 | open class Mod( 10 | @Transient var enabled: Boolean = true, 11 | val name: String, 12 | val id: String, 13 | val version: Version, 14 | val minecraftVersion: Version, 15 | val fileName: String, 16 | val platform: Platform, 17 | ) { 18 | val downloadEndpoint = "${makeRequest().endpoint}/download" 19 | 20 | fun makeRequest(): ModRequest = 21 | ModRequest(id, platform, minecraftVersion.toString()) 22 | 23 | fun applyData(data: ModData): RemoteMod { 24 | return RemoteMod( 25 | enabled, 26 | name, 27 | id, 28 | data.version, 29 | minecraftVersion, 30 | getFileNameFromUrl(data.url), 31 | platform, 32 | data.url, 33 | ) 34 | } 35 | 36 | suspend fun latest(): RemoteMod { 37 | return applyData(makeRequest().request()) 38 | } 39 | 40 | operator fun compareTo(other: Mod): Int { 41 | return version.compareTo(other.version) 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/bundleproject/bundle/entities/RemoteMod.kt: -------------------------------------------------------------------------------- 1 | package org.bundleproject.bundle.entities 2 | 3 | import org.bundleproject.bundle.api.data.Platform 4 | import org.bundleproject.libversion.Version 5 | 6 | class RemoteMod( 7 | enabled: Boolean = true, 8 | name: String, 9 | id: String, 10 | version: Version, 11 | minecraftVersion: Version, 12 | fileName: String, 13 | platform: Platform, 14 | val downloadUrl: String, 15 | ) : Mod(enabled, name, id, version, minecraftVersion, fileName, platform) -------------------------------------------------------------------------------- /src/main/kotlin/org/bundleproject/bundle/gui/LoadingGui.kt: -------------------------------------------------------------------------------- 1 | package org.bundleproject.bundle.gui 2 | 3 | import org.bundleproject.bundle.utils.getResourceImage 4 | import java.awt.Dimension 5 | import java.awt.Rectangle 6 | import javax.swing.JFrame 7 | import javax.swing.JProgressBar 8 | import javax.swing.WindowConstants 9 | 10 | /** 11 | * Loading bar for updating mods 12 | * 13 | * @since 0.0.4 14 | */ 15 | class LoadingGui(private val updateCount: Int) : JFrame("Updating Mods") { 16 | private val progressBar: JProgressBar 17 | 18 | init { 19 | setSize(400, 20) 20 | iconImage = getResourceImage("/bundle.png") 21 | defaultCloseOperation = WindowConstants.DO_NOTHING_ON_CLOSE 22 | isResizable = false 23 | isUndecorated = true 24 | 25 | progressBar = JProgressBar(0, updateCount) 26 | progressBar.value = 0 27 | progressBar.bounds = Rectangle(0, 0, 400, 20) 28 | progressBar.preferredSize = Dimension(400, 20) 29 | add(progressBar) 30 | 31 | pack() 32 | } 33 | 34 | /** 35 | * Indicates to gui that another mod has finished downloading 36 | * and the progress bar should progress or complete. 37 | */ 38 | fun finish() { 39 | if (progressBar.value + 1 == updateCount) { 40 | dispose() 41 | } else { 42 | progressBar.value += 1 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/bundleproject/bundle/gui/UpdateOverviewGui.kt: -------------------------------------------------------------------------------- 1 | package org.bundleproject.bundle.gui 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import org.bundleproject.bundle.Bundle 5 | import org.bundleproject.bundle.entities.Mod 6 | import org.bundleproject.bundle.entities.RemoteMod 7 | import org.bundleproject.bundle.utils.ModPair 8 | import org.bundleproject.bundle.utils.getResourceImage 9 | import java.awt.GridBagConstraints 10 | import java.awt.GridBagLayout 11 | import java.net.URL 12 | import java.util.concurrent.locks.Condition 13 | import javax.swing.* 14 | 15 | /** 16 | * Allows the user to pick which mods they 17 | * would like to update and verify that 18 | * it is updating correctly. 19 | * 20 | * @since 0.0.2 21 | */ 22 | class UpdateOverviewGui(private val bundle: Bundle, mods: MutableList, condition: Condition? = null) : JFrame("Bundle") { 23 | init { 24 | iconImage = getResourceImage("/bundle.png") 25 | defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE 26 | 27 | val gbl = GridBagLayout() 28 | val gbc = GridBagConstraints() 29 | layout = gbl 30 | 31 | gbc.fill = GridBagConstraints.HORIZONTAL 32 | 33 | val rows = mutableListOf>() 34 | for ((local, remote) in mods) { 35 | rows.add(arrayOf( 36 | JCheckBox("", true).also { it.addActionListener { remote.enabled = false } }, 37 | remote.name, 38 | local.version.toString(), 39 | remote.version.toString(), 40 | URL(remote.downloadUrl).host, 41 | )) 42 | } 43 | val table = JTable(rows.toTypedArray(), arrayOf("", "Mod", "Current", "Remote", "Host")) 44 | gbc.gridx = 0 45 | gbc.gridy = 0 46 | gbc.gridwidth = 2 47 | gbc.gridheight = 4 48 | add(table, gbc) 49 | 50 | val skipButton = JButton("Skip") 51 | skipButton.addActionListener { 52 | mods.clear() 53 | dispose() 54 | condition?.signal() 55 | } 56 | gbc.gridx = 0 57 | gbc.gridy = 1 58 | gbc.gridwidth = 2 59 | gbc.gridheight = 1 60 | add(skipButton, gbc) 61 | 62 | val downloadButton = JButton("Update") 63 | downloadButton.addActionListener { 64 | runBlocking { bundle.updateMods(mods.filter { it.remote.enabled }) } 65 | dispose() 66 | condition?.signal() 67 | } 68 | gbc.gridx = 1 69 | gbc.gridy = 1 70 | gbc.gridwidth = 2 71 | gbc.gridheight = 1 72 | downloadButton.requestFocus() 73 | add(downloadButton, gbc) 74 | 75 | pack() 76 | } 77 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/bundleproject/bundle/utils/Constants.kt: -------------------------------------------------------------------------------- 1 | package org.bundleproject.bundle.utils 2 | 3 | const val API = "https://api.bundleproject.org" 4 | const val API_VERSION = "v1" -------------------------------------------------------------------------------- /src/main/kotlin/org/bundleproject/bundle/utils/HttpClient.kt: -------------------------------------------------------------------------------- 1 | package org.bundleproject.bundle.utils 2 | 3 | import io.ktor.client.* 4 | import io.ktor.client.engine.apache.* 5 | import io.ktor.client.features.json.* 6 | 7 | val http = HttpClient(Apache) { 8 | install(JsonFeature) { 9 | serializer = GsonSerializer { 10 | applyGson(this) 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/bundleproject/bundle/utils/ModPair.kt: -------------------------------------------------------------------------------- 1 | package org.bundleproject.bundle.utils 2 | 3 | import org.bundleproject.bundle.entities.Mod 4 | import org.bundleproject.bundle.entities.RemoteMod 5 | 6 | data class ModPair(val local: Mod, val remote: RemoteMod) 7 | -------------------------------------------------------------------------------- /src/main/kotlin/org/bundleproject/bundle/utils/TypeAdapters.kt: -------------------------------------------------------------------------------- 1 | package org.bundleproject.bundle.utils 2 | 3 | import com.google.gson.Gson 4 | import com.google.gson.GsonBuilder 5 | import com.google.gson.TypeAdapter 6 | import com.google.gson.stream.JsonReader 7 | import com.google.gson.stream.JsonWriter 8 | import org.bundleproject.bundle.api.data.Platform 9 | import org.bundleproject.libversion.Version 10 | 11 | val gson: Gson = GsonBuilder().apply { 12 | applyGson(this) 13 | }.create() 14 | 15 | fun applyGson(builder: GsonBuilder) { 16 | builder.apply { 17 | registerTypeAdapter(Platform::class.java, PlatformTypeAdapter) 18 | registerTypeAdapter(Version::class.java, VersionTypeAdapter) 19 | } 20 | } 21 | 22 | object PlatformTypeAdapter : TypeAdapter() { 23 | override fun write(out: JsonWriter?, value: Platform?) { 24 | out?.let { 25 | value?.let { out.value(it.id) } ?: out.nullValue() 26 | } 27 | } 28 | 29 | override fun read(`in`: JsonReader?): Platform { 30 | `in`?.let { 31 | return Platform.fromId(it.nextString()!!)!! 32 | } ?: error("The JsonReader given to the VersionTypeAdapter was null") 33 | } 34 | 35 | } 36 | 37 | object VersionTypeAdapter : TypeAdapter() { 38 | override fun write(out: JsonWriter?, value: Version?) { 39 | out?.let { 40 | value?.let { out.value(it.toString()) } ?: out.nullValue() 41 | } 42 | } 43 | 44 | override fun read(`in`: JsonReader?): Version { 45 | `in`?.let { 46 | return Version.of(it.nextString()) 47 | } ?: error("The JsonReader given to the VersionTypeAdapter was null") 48 | } 49 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/bundleproject/bundle/utils/utilities.kt: -------------------------------------------------------------------------------- 1 | package org.bundleproject.bundle.utils 2 | 3 | import io.ktor.client.* 4 | import io.ktor.client.call.* 5 | import io.ktor.client.request.* 6 | import io.ktor.client.statement.* 7 | import io.ktor.http.* 8 | import io.ktor.util.* 9 | import io.ktor.util.cio.* 10 | import io.ktor.utils.io.* 11 | import kotlinx.coroutines.CoroutineName 12 | import kotlinx.coroutines.CoroutineScope 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.launch 15 | import mu.KotlinLogging 16 | import org.bundleproject.bundle.Bundle 17 | import java.awt.image.BufferedImage 18 | import java.io.File 19 | import java.io.FileInputStream 20 | import java.io.FileOutputStream 21 | import java.net.URL 22 | import java.util.* 23 | import javax.imageio.ImageIO 24 | 25 | val logger = KotlinLogging.logger("Bundle Updater") 26 | 27 | /** 28 | * Downloads a file from a url using Ktor 29 | * 30 | * @return if the download was successful 31 | * @since 0.1.0 32 | */ 33 | suspend fun HttpClient.downloadFile(file: File, url: String): Boolean { 34 | val call = request { 35 | url(url) 36 | method = HttpMethod.Get 37 | } 38 | 39 | if (!call.status.isSuccess()) 40 | return false 41 | 42 | call.content.copyAndClose(file.writeChannel()) 43 | 44 | return true 45 | } 46 | 47 | fun getResourceImage(path: String): BufferedImage = 48 | ImageIO.read(Bundle::class.java.getResource(path)) 49 | 50 | fun getFileNameFromUrl(url: String): String = 51 | url.decodeURLPart(url.lastIndexOf('/') + 1) 52 | 53 | fun Iterable.toFormattedString(): String { 54 | return "[${this.joinToString(", ") { it.toString() }}]" 55 | } 56 | 57 | fun String.encodeBase64(): String { 58 | return Base64.getEncoder().encodeToString(this.toByteArray()) 59 | } 60 | -------------------------------------------------------------------------------- /src/main/resources/bundle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BundleProject/Bundle/73ece617a3606ce8e0411846de39efb5fb79d64d/src/main/resources/bundle.png -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %highlight(%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level (%logger{36}\) - %msg%n) 7 | 8 | 9 | 10 | logs/bundle-${byDay}.log 11 | true 12 | 13 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | --------------------------------------------------------------------------------