├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src └── main │ ├── resources │ └── end_crystal.png │ ├── kotlin │ └── com │ │ └── pistacium │ │ └── modcheck │ │ ├── FabricModJson.kt │ │ ├── util │ │ ├── Config.kt │ │ ├── ModCheckStatus.kt │ │ ├── SwingUtils.kt │ │ └── ModCheckUtils.kt │ │ ├── Meta.kt │ │ ├── ModCheck.kt │ │ └── ModCheckFrameFormExt.kt │ └── java │ └── com │ └── pistacium │ └── modcheck │ ├── ModCheckFrameForm.java │ └── ModCheckFrameForm.form ├── .gitignore ├── settings.gradle.kts ├── README.md ├── .github └── workflows │ └── build.yml ├── gradlew.bat └── gradlew /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tildejustin/modcheck/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/resources/end_crystal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tildejustin/modcheck/HEAD/src/main/resources/end_crystal.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project exclude paths 2 | /.gradle/ 3 | /build/ 4 | /build/classes/java/main/ 5 | /out/ 6 | modcheck.json 7 | /.idea/ 8 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0" 3 | } 4 | rootProject.name = "modcheck" 5 | -------------------------------------------------------------------------------- /src/main/kotlin/com/pistacium/modcheck/FabricModJson.kt: -------------------------------------------------------------------------------- 1 | package com.pistacium.modcheck 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | class FabricModJson(val version: String, var id: String, var name: String) 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Jan 16 16:07:38 EST 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /src/main/kotlin/com/pistacium/modcheck/util/Config.kt: -------------------------------------------------------------------------------- 1 | package com.pistacium.modcheck.util 2 | 3 | import kotlinx.serialization.Serializable 4 | import java.nio.file.* 5 | 6 | @Serializable 7 | data class Config(val filepath: String) { 8 | fun getDirectory(): Path = Paths.get(filepath) 9 | } 10 | -------------------------------------------------------------------------------- /src/main/kotlin/com/pistacium/modcheck/util/ModCheckStatus.kt: -------------------------------------------------------------------------------- 1 | package com.pistacium.modcheck.util 2 | 3 | enum class ModCheckStatus(val description: String) { 4 | IDLE(""), 5 | LOADING_AVAILABLE_VERSIONS("Loading available versions info"), 6 | LOADING_MOD_LIST("Loading mod list"), 7 | LOADING_MOD_RESOURCE("Loading mod's resource"), 8 | GETTING_INSTALLED_MODS("Getting installed mods info"), 9 | DOWNLOADING_MOD_FILE("Downloading file") 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ModCheck 2 | ![ModCheck](https://cdn.7tv.app/emote/60eefb20119bd109472f7f4b/4x) 3 | 4 | Minecraft SpeedRun Mods Auto Installer/Updater 5 | 6 | original idea by [pistacium](https://github.com/pistacium/ModCheck) 7 | 8 | ## CLI Interface 9 | ModCheck can be run on the command line. This is useful for tools, powerusers, and more. 10 | 11 | Try the `--help` command for information on all the command line options. 12 | 13 | ### Autoupdating 14 | Using `java -jar /path/to/modcheck-xyz.jar --path $INST_DIR update` as a Pre-launch command in Prism Launcher or MultiMC will automatically update your mods on every launch. 15 | 16 | ![image](https://user-images.githubusercontent.com/25276450/172102912-455735a5-558f-4330-84c6-fad5bf9aa92b.png) 17 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | # ignores tags 5 | branches: 6 | - "**" 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: checkout repository 15 | uses: actions/checkout@v4 16 | - name: validate gradle wrapper 17 | uses: gradle/wrapper-validation-action@v1 18 | - name: setup java 19 | uses: actions/setup-java@v4 20 | with: 21 | distribution: "temurin" 22 | java-version: 17 23 | - name: build 24 | run: | 25 | chmod +x ./gradlew 26 | ./gradlew build 27 | - name: upload artifacts 28 | uses: actions/upload-artifact@v4 29 | with: 30 | path: build/libs/ 31 | -------------------------------------------------------------------------------- /src/main/kotlin/com/pistacium/modcheck/util/SwingUtils.kt: -------------------------------------------------------------------------------- 1 | package com.pistacium.modcheck.util 2 | 3 | import java.awt.Container 4 | import javax.swing.JComponent 5 | 6 | /** 7 | * A collection of utility methods for Swing. 8 | * 9 | * @author Darryl Burke 10 | */ 11 | object SwingUtils { 12 | fun getDescendantsOfType(clazz: Class, container: Container): List { 13 | return getDescendantsOfType(clazz, container, true) 14 | } 15 | 16 | fun getDescendantsOfType(clazz: Class, container: Container, nested: Boolean): List { 17 | val tList: MutableList = ArrayList() 18 | for (component in container.components) { 19 | if (clazz.isAssignableFrom(component.javaClass)) { 20 | tList.add(clazz.cast(component)) 21 | } 22 | if (nested || !clazz.isAssignableFrom(component.javaClass)) { 23 | tList.addAll(getDescendantsOfType(clazz, component as Container, nested)) 24 | } 25 | } 26 | return tList 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/kotlin/com/pistacium/modcheck/Meta.kt: -------------------------------------------------------------------------------- 1 | package com.pistacium.modcheck 2 | 3 | import kotlinx.serialization.* 4 | 5 | @Serializable 6 | data class Meta(val schemaVersion: Int, val mods: List) { 7 | @Serializable 8 | @OptIn(ExperimentalSerializationApi::class) 9 | data class Mod( 10 | val modid: String, 11 | val name: String, 12 | val description: String, 13 | val sources: String, 14 | val versions: List, 15 | @EncodeDefault(EncodeDefault.Mode.NEVER) val traits: List = listOf(), 16 | @EncodeDefault(EncodeDefault.Mode.NEVER) val incompatibilities: List = listOf(), 17 | @EncodeDefault(EncodeDefault.Mode.NEVER) val recommended: Boolean = true, 18 | @EncodeDefault(EncodeDefault.Mode.NEVER) val obsolete: Boolean = false 19 | ) { 20 | fun getModVersion(minecraftVersion: String): ModVersion? = this.versions.firstOrNull { minecraftVersion in it.target_version } 21 | } 22 | 23 | @Serializable 24 | @OptIn(ExperimentalSerializationApi::class) 25 | data class ModVersion( 26 | val target_version: List, 27 | val version: String, 28 | val url: String, 29 | val hash: String, 30 | @EncodeDefault(EncodeDefault.Mode.NEVER) val recommended: Boolean = true, 31 | @EncodeDefault(EncodeDefault.Mode.NEVER) val obsolete: Boolean = false 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/main/kotlin/com/pistacium/modcheck/util/ModCheckUtils.kt: -------------------------------------------------------------------------------- 1 | package com.pistacium.modcheck.util 2 | 3 | import com.pistacium.modcheck.* 4 | import kotlinx.serialization.* 5 | import kotlinx.serialization.json.* 6 | import java.awt.Desktop 7 | import java.awt.event.ActionEvent 8 | import java.net.URI 9 | import java.nio.file.* 10 | import java.util.* 11 | import javax.swing.JOptionPane 12 | import kotlin.io.path.* 13 | 14 | object ModCheckUtils { 15 | private val config: Path = localizedConfigPath() 16 | 17 | private fun localizedConfigPath(): Path { 18 | val os = currentOS() 19 | val home = System.getProperty("user.home").replace("\\", "/") 20 | val basePath: Path = when (os) { 21 | "linux" -> Paths.get(home, ".config") 22 | "osx" -> Paths.get(".") 23 | "windows" -> Paths.get(home, "AppData/Local") 24 | else -> Paths.get(".") 25 | } 26 | val old = Paths.get("modcheck.json") 27 | val new = basePath.resolve("modcheck.json") 28 | // TODO: what is proper macos file loc? 29 | if (os != "osx" && Files.exists(old) && !Files.exists(new)) { 30 | Files.write(new, Files.readAllBytes(old)) 31 | Files.delete(old) 32 | } 33 | return new 34 | } 35 | 36 | fun readConfig(): Config? { 37 | if (!Files.exists(config)) { 38 | return null 39 | } 40 | return json.decodeFromString(config.readText()) 41 | } 42 | 43 | fun writeConfig(dir: Path) { 44 | val config = Config(dir.toString()) 45 | this.config.writeText(json.encodeToString(config)) 46 | } 47 | 48 | // TODO: enumify / use stdlib method for this info 49 | fun currentOS(): String { 50 | val osName = System.getProperty("os.name").lowercase(Locale.getDefault()) 51 | if (osName.contains("win")) return "windows" 52 | if (osName.contains("mac")) return "osx" 53 | if (osName.contains("nix") || osName.contains("nux") || osName.contains("aix")) { 54 | return "linux" 55 | } 56 | return "unknown" 57 | } 58 | 59 | fun latestVersion(): String? { 60 | val urlRequest = URI.create("https://api.github.com/repos/tildejustin/modcheck/releases/latest").toURL().readText() 61 | val jsonObject = json.parseToJsonElement(urlRequest).jsonObject 62 | val newVersion = jsonObject["tag_name"]?.jsonPrimitive?.content ?: return null 63 | return newVersion 64 | } 65 | 66 | @OptIn(ExperimentalSerializationApi::class) 67 | val json = Json { ignoreUnknownKeys = true; prettyPrint = true; prettyPrintIndent = " " } 68 | 69 | fun readFabricModJson(mod: Path): FabricModJson? { 70 | FileSystems.newFileSystem(mod, null as ClassLoader?).use { fs -> 71 | val jsonFilePath = fs.getPath("fabric.mod.json") 72 | val jsonData: ByteArray 73 | try { 74 | jsonData = Files.readAllBytes(jsonFilePath) 75 | } catch (e: NoSuchFileException) { 76 | return null 77 | } 78 | return json.decodeFromString(String(jsonData)) 79 | } 80 | } 81 | 82 | fun checkForUpdates(e: ActionEvent) { 83 | checkForUpdates(true) 84 | } 85 | 86 | fun checkForUpdates(verbose: Boolean) { 87 | val latestVersion = latestVersion() 88 | if (latestVersion != null && latestVersion > ModCheck.applicationVersion) { 89 | val result = JOptionPane.showOptionDialog( 90 | null, 91 | "Found new ModCheck update!

Current Version : " + ModCheck.applicationVersion + "
Updated Version : " + latestVersion + "", 92 | "Update Checker", 93 | JOptionPane.YES_NO_CANCEL_OPTION, 94 | JOptionPane.PLAIN_MESSAGE, 95 | null, 96 | arrayOf("Download", "Cancel"), 97 | "Download" 98 | ) 99 | if (result == 0) { 100 | Desktop.getDesktop().browse(URI.create("https://github.com/tildejustin/modcheck/releases/latest")) 101 | } 102 | } else if (verbose) { 103 | JOptionPane.showMessageDialog(ModCheck.frameInstance, "You are using the latest version!") 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /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 | MSYS* | 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 | -------------------------------------------------------------------------------- /src/main/kotlin/com/pistacium/modcheck/ModCheck.kt: -------------------------------------------------------------------------------- 1 | package com.pistacium.modcheck 2 | 3 | import com.formdev.flatlaf.FlatDarkLaf 4 | import com.pistacium.modcheck.util.* 5 | import java.awt.Toolkit 6 | import java.awt.datatransfer.StringSelection 7 | import java.io.* 8 | import java.net.URI 9 | import java.nio.file.* 10 | import java.util.concurrent.* 11 | import javax.swing.JOptionPane 12 | import kotlin.io.path.extension 13 | import kotlin.system.exitProcess 14 | 15 | object ModCheck { 16 | fun setStatus(status: ModCheckStatus) { 17 | frameInstance.progressBar?.string = status.description 18 | } 19 | 20 | val threadExecutor: ExecutorService = Executors.newSingleThreadExecutor() 21 | 22 | lateinit var frameInstance: ModCheckFrameFormExt 23 | 24 | lateinit var availableVersions: MutableList 25 | 26 | val availableMods: ArrayList = ArrayList() 27 | 28 | val applicationVersion: String = ModCheck.javaClass.`package`.implementationVersion ?: "dev" 29 | 30 | @JvmStatic 31 | fun main(args: Array) { 32 | // enable cli if arguments are given 33 | if (args.isNotEmpty()) { 34 | handleCliMode(args) 35 | return 36 | } 37 | // else run the gui 38 | 39 | FlatDarkLaf.setup() 40 | threadExecutor.submit { 41 | try { 42 | frameInstance = ModCheckFrameFormExt() 43 | 44 | // Get available versions 45 | setStatus(ModCheckStatus.LOADING_AVAILABLE_VERSIONS) 46 | availableVersions = ModCheckUtils.json.decodeFromString( 47 | URI.create("https://raw.githubusercontent.com/tildejustin/mcsr-meta/${if (applicationVersion == "dev") "staging" else "schema-6"}/important_versions.json").toURL() 48 | .readText() 49 | ) 50 | 51 | // Get mod list 52 | setStatus(ModCheckStatus.LOADING_MOD_LIST) 53 | val mods = ModCheckUtils.json.decodeFromString( 54 | URI.create("https://raw.githubusercontent.com/tildejustin/mcsr-meta/${if (applicationVersion == "dev") "staging" else "schema-6"}/mods.json").toURL().readText() 55 | ).mods 56 | // val mods = Json.decodeFromString(Path.of("/home/justin/IdeaProjects/mcsr-meta/mods.json").readText()).mods 57 | frameInstance.progressBar?.value = 60 58 | 59 | setStatus(ModCheckStatus.LOADING_MOD_RESOURCE) 60 | val maxCount = mods.count() 61 | for ((count, mod) in mods.withIndex()) { 62 | frameInstance.progressBar?.string = "Loading information of " + mod.name 63 | availableMods.add(mod) 64 | frameInstance.progressBar?.value = (60 + ((((count + 1) * 1) / maxCount) * 40)) 65 | } 66 | frameInstance.progressBar?.value = 100 67 | setStatus(ModCheckStatus.IDLE) 68 | frameInstance.updateVersionList() 69 | ModCheckUtils.checkForUpdates(false) 70 | } catch (e: Throwable) { 71 | val sw = StringWriter() 72 | val pw = PrintWriter(sw) 73 | e.printStackTrace(pw) 74 | val result = JOptionPane.showOptionDialog( 75 | null, 76 | sw.toString(), 77 | "Error exception!", 78 | JOptionPane.YES_NO_CANCEL_OPTION, 79 | JOptionPane.PLAIN_MESSAGE, 80 | null, 81 | arrayOf("Copy to clipboard a logs", "Cancel"), 82 | "Copy to clipboard a logs" 83 | ) 84 | if (result == 0) { 85 | val selection = StringSelection(sw.toString()) 86 | val clipboard = Toolkit.getDefaultToolkit().systemClipboard 87 | clipboard.setContents(selection, selection) 88 | } 89 | 90 | exitProcess(0) 91 | } 92 | } 93 | } 94 | 95 | private fun isValidPath(path: String): Boolean { 96 | val basePath = Paths.get(path) 97 | val mcPath = basePath.resolve("minecraft") 98 | val dotMcPath = basePath.resolve(".minecraft") 99 | 100 | // Check if the path exists and is a directory 101 | if (!Files.exists(basePath) || !Files.isDirectory(basePath)) { 102 | return false 103 | } 104 | 105 | // Check if it's a valid Minecraft instance structure 106 | return when { 107 | Files.isDirectory(mcPath) && Files.isDirectory(mcPath.resolve("mods")) -> true 108 | Files.isDirectory(dotMcPath) && Files.isDirectory(dotMcPath.resolve("mods")) -> true 109 | Files.isDirectory(basePath.resolve("mods")) -> true 110 | basePath.endsWith("mods") && Files.isDirectory(basePath) -> true 111 | else -> false 112 | } 113 | } 114 | 115 | private fun handleCliMode(args: Array) { 116 | // Load mod list for CLI 117 | val mods = ModCheckUtils.json.decodeFromString( 118 | URI.create("https://raw.githubusercontent.com/tildejustin/mcsr-meta/${if (applicationVersion == "dev") "staging" else "schema-6"}/mods.json").toURL().readText() 119 | ).mods 120 | availableMods.addAll(mods) 121 | 122 | // Defaults 123 | var category = "rsg" 124 | var os = ModCheckUtils.currentOS() 125 | var accessibility = false 126 | var version = "1.16.1" 127 | var path: String? = null 128 | var function: String? = null 129 | 130 | 131 | // Parsing args 132 | var i = 0 133 | while (i < args.size) { 134 | when (args[i].lowercase()) { 135 | "help", "-h", "--help" -> { 136 | printHelp() 137 | return 138 | } 139 | "version", "-v" -> { 140 | println("ModCheck version: $applicationVersion") 141 | return 142 | } 143 | "--category" -> { 144 | if (i + 1 < args.size) { 145 | val value = args[i + 1].lowercase() 146 | if (value == "rsg" || value == "ssg") { 147 | category = value 148 | } else { 149 | println("Invalid category: $value") 150 | exitProcess(1) 151 | } 152 | i++ 153 | } 154 | } 155 | "--accessibility" -> { 156 | accessibility = true 157 | } 158 | "--version" -> { 159 | if (i + 1 < args.size) { 160 | version = args[i + 1] 161 | i++ 162 | } 163 | } 164 | "--path" -> { 165 | if (i + 1 < args.size) { 166 | var pathTemporary = args[i + 1] 167 | var pathIndex = i + 1 168 | 169 | // Concatenate arguments until path is valid 170 | while (!isValidPath(pathTemporary) && pathIndex + 1 < args.size) { 171 | // Stop loop if next argument is a flag 172 | if (args[pathIndex + 1].startsWith("-")) { 173 | break 174 | } 175 | pathIndex++ 176 | pathTemporary += " " + args[pathIndex] 177 | } 178 | 179 | path = pathTemporary 180 | i = pathIndex 181 | } 182 | } 183 | "--instance" -> { 184 | if (i + 1 < args.size) { 185 | var instanceTemporary = args[i + 1] 186 | var instanceIndex = i + 1 187 | val userHome = System.getProperty("user.home") 188 | 189 | // Get initial resolved path from instance name 190 | var pathTemporary = when { 191 | os == "windows" -> { 192 | println("Error: --instance is not supported on Windows. Please use --path instead.") 193 | exitProcess(1) 194 | } 195 | os == "linux" -> "$userHome/.local/share/PrismLauncher/instances/$instanceTemporary" 196 | os == "osx" -> "$userHome/Library/Application Support/PrismLauncher/instances/$instanceTemporary" 197 | else -> { 198 | println("Unknown OS for --instance path resolution: $os") 199 | exitProcess(1) 200 | } 201 | } 202 | 203 | // Concatenate arguments until path is valid (same loop as --path) 204 | while (!isValidPath(pathTemporary) && instanceIndex + 1 < args.size) { 205 | // Stop loop if next argument is a flag or command 206 | if (args[instanceIndex + 1].startsWith("-") || 207 | args[instanceIndex + 1].lowercase() in listOf("download", "update")) { 208 | break 209 | } 210 | instanceIndex++ 211 | instanceTemporary += " " + args[instanceIndex] 212 | 213 | // Update resolved path with new instance name 214 | pathTemporary = when { 215 | os == "linux" -> "$userHome/.local/share/PrismLauncher/instances/$instanceTemporary" 216 | os == "osx" -> "$userHome/Library/Application Support/PrismLauncher/instances/$instanceTemporary" 217 | else -> pathTemporary // shouldn't reach here 218 | } 219 | } 220 | 221 | path = pathTemporary 222 | i = instanceIndex 223 | } 224 | } 225 | "download", "update" -> { 226 | function = args[i].lowercase() 227 | } 228 | else -> { 229 | // edge cases? 230 | } 231 | } 232 | i++ 233 | } 234 | 235 | if (function == null) { 236 | printHelp() 237 | exitProcess(1) 238 | } 239 | 240 | if (path == null) { 241 | println("Error: Either --path or --instance is required.") 242 | printHelp() 243 | exitProcess(1) 244 | } 245 | 246 | // Print out the values 247 | val basePath = Paths.get(path) 248 | val mcPath = basePath.resolve("minecraft") 249 | val dotMcPath = basePath.resolve(".minecraft") 250 | val modsDir = when { 251 | Files.isDirectory(mcPath) && Files.isDirectory(mcPath.resolve("mods")) -> mcPath.resolve("mods") 252 | Files.isDirectory(dotMcPath) && Files.isDirectory(dotMcPath.resolve("mods")) -> dotMcPath.resolve("mods") 253 | Files.isDirectory(basePath.resolve("mods")) -> basePath.resolve("mods") 254 | Files.isDirectory(basePath) && basePath.endsWith("mods") -> basePath 255 | else -> null 256 | } 257 | 258 | if (modsDir == null || !Files.exists(modsDir)) { 259 | println("No mods directory found at: $modsDir") 260 | return 261 | } 262 | 263 | println("Options:") 264 | println(" Category: ${if (category == "rsg") "Random Seed Glitchless" else "Set Seed Glitchless"}") 265 | println(" OS: ${os.replaceFirstChar { it.uppercase() }}") 266 | println(" Accessibility: $accessibility") 267 | println(" Version: $version") 268 | println(" Mod Folder: $modsDir") 269 | 270 | if (function == "download") { 271 | // 1. Select mods 272 | // assumes that there are no conflicting recommended mods, which is a choice in meta design I will try to stick to 273 | val selectedMods = availableMods.filter { mod -> 274 | val modVersion = mod.getModVersion(version) 275 | if (modVersion == null) return@filter false 276 | // prioritize sodium-mac 277 | if ( 278 | mod.modid == "sodium" && 279 | os == "osx" && 280 | availableMods.find { it.modid == "sodiummac" } 281 | ?.versions?.any { version in it.target_version } == true 282 | ) return@filter false 283 | if (mod.obsolete || modVersion.obsolete) return@filter false 284 | if (!mod.recommended) return@filter false 285 | for (trait in mod.traits) { 286 | if (trait == "ssg-only" && category != "ssg") return@filter false 287 | if (trait == "rsg-only" && category != "rsg") return@filter false 288 | if (trait == "accessibility" && !accessibility) return@filter false 289 | if (trait == "mac-only" && os != "osx") return@filter false 290 | } 291 | true 292 | } 293 | if (selectedMods.isEmpty()) { 294 | println("Warning: No mods matched the selection criteria. Nothing to download.") 295 | return 296 | } 297 | for (mod in selectedMods) { 298 | println("Selected ${mod.name}") 299 | } 300 | // 2. Download selected mods 301 | var count = 0 302 | for (mod in selectedMods) { 303 | val modVersion = mod.getModVersion(version)!! // null results are thrown out in initial filter 304 | val url = modVersion.url 305 | val filename = url.substringAfterLast("/") 306 | try { 307 | println("Downloading ${mod.name}") 308 | val bytes = URI.create(url).toURL().readBytes() 309 | Files.write(modsDir.resolve(filename), bytes) 310 | } catch (e: Exception) { 311 | println("Failed to download ${mod.name}: ${e.message}") 312 | } 313 | count++ 314 | } 315 | println("Downloading mods complete") 316 | } else if (function == "update") { 317 | // 1. Find installed mods 318 | val modFiles = Files.list(modsDir) 319 | val toUpdate = mutableListOf>() 320 | for (file in modFiles) { 321 | if (file.extension != "jar") continue 322 | val fmj = try { ModCheckUtils.readFabricModJson(file) } catch (_: Exception) { null } 323 | if (fmj == null) continue 324 | val mod = availableMods.find { it.modid == fmj.id || it.name == fmj.name } 325 | if (mod != null) { 326 | val modVersion = mod.getModVersion(version) 327 | if (modVersion != null && modVersion.version != fmj.version) { 328 | toUpdate.add(Triple(file, mod, modVersion)) 329 | } 330 | } 331 | } 332 | if (toUpdate.isEmpty()) { 333 | println("All known mods are up to date.") 334 | return 335 | } 336 | var count = 0 337 | for ((oldFile, mod, modVersion) in toUpdate) { 338 | val url = modVersion.url 339 | val filename = url.substringAfterLast("/") 340 | try { 341 | println("Updating ${mod.name} from ${oldFile.fileName} to $filename") 342 | val bytes = URI.create(url).toURL().readBytes() 343 | Files.write(modsDir.resolve(filename), bytes) 344 | Files.deleteIfExists(oldFile) 345 | } catch (e: Exception) { 346 | println("Failed to update ${mod.name}: ${e.message}") 347 | } 348 | count++ 349 | } 350 | println("Updated $count mod(s). Done.") 351 | } 352 | } 353 | 354 | private fun printHelp() { 355 | println(""" 356 | ModCheck CLI 357 | 358 | Usage: java -jar modcheck.jar [options] 359 | 360 | 361 | Options: 362 | --category Specify the category (default: rsg) 363 | --version Specify Minecraft version (default: 1.16.1) 364 | --accessibility Include accessibility mods (default: false) 365 | --instance Specify your instance name (uses default PrismLauncher path) 366 | or 367 | --path Specify a different path to your instance 368 | 369 | help, -h, --help Show this help message 370 | version, -v Show modcheck version information 371 | 372 | Commands: 373 | download Download mods 374 | update Update existing mods 375 | 376 | Examples: 377 | java -jar modcheck.jar --category random --version 1.16.1 --instance instance1 download 378 | Downloads speedrunning mods for 1.16.1 RSG into instance1 379 | 380 | Run without arguments to start the GUI. 381 | """.trimIndent()) 382 | } 383 | } 384 | -------------------------------------------------------------------------------- /src/main/java/com/pistacium/modcheck/ModCheckFrameForm.java: -------------------------------------------------------------------------------- 1 | package com.pistacium.modcheck; 2 | 3 | import com.intellij.uiDesigner.core.*; 4 | 5 | import javax.swing.*; 6 | import javax.swing.plaf.FontUIResource; 7 | import javax.swing.text.StyleContext; 8 | import java.awt.*; 9 | import java.util.Locale; 10 | 11 | public class ModCheckFrameForm extends JFrame { 12 | public JProgressBar progressBar; 13 | public JButton downloadButton; 14 | public JCheckBox deleteAllJarCheckbox; 15 | public JButton selectInstancePathsButton; 16 | public JComboBox mcVersionCombo; 17 | public JRadioButton randomSeedRadioButton; 18 | public JRadioButton setSeedRadioButton; 19 | public JRadioButton windowsRadioButton; 20 | public JRadioButton macRadioButton; 21 | public JRadioButton linuxRadioButton; 22 | public JCheckBox accessibilityCheckBox; 23 | public JCheckBox obsoleteCheckBox; 24 | public JScrollPane modListScroll; 25 | public JPanel mainPanel; 26 | public JLabel selectedDirLabel; 27 | public JButton deselectAllButton; 28 | public JButton selectAllRecommendsButton; 29 | public JPanel modListPanel; 30 | public JScrollBar scrollBar; 31 | public JButton updateExistingModsButton; 32 | 33 | { 34 | // GUI initializer generated by IntelliJ IDEA GUI Designer 35 | // >>> IMPORTANT!! <<< 36 | // DO NOT EDIT OR ADD ANY CODE HERE! 37 | $$$setupUI$$$(); 38 | } 39 | 40 | /** 41 | * Method generated by IntelliJ IDEA GUI Designer 42 | * >>> IMPORTANT!! <<< 43 | * DO NOT edit this method OR call it in your code! 44 | * 45 | * @noinspection ALL 46 | */ 47 | private void $$$setupUI$$$() { 48 | mainPanel = new JPanel(); 49 | mainPanel.setLayout(new GridLayoutManager(3, 1, new Insets(20, 20, 20, 20), -1, -1)); 50 | final JPanel panel1 = new JPanel(); 51 | panel1.setLayout(new GridLayoutManager(1, 2, new Insets(0, 0, 0, 0), -1, -1)); 52 | mainPanel.add(panel1, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_SOUTH, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 53 | progressBar = new JProgressBar(); 54 | progressBar.setBorderPainted(true); 55 | progressBar.setEnabled(true); 56 | Font progressBarFont = this.$$$getFont$$$(null, -1, 14, progressBar.getFont()); 57 | if (progressBarFont != null) progressBar.setFont(progressBarFont); 58 | progressBar.setStringPainted(true); 59 | panel1.add(progressBar, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 60 | final JPanel panel2 = new JPanel(); 61 | panel2.setLayout(new GridLayoutManager(2, 2, new Insets(0, 0, 0, 0), -1, -1)); 62 | panel1.add(panel2, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); 63 | deleteAllJarCheckbox = new JCheckBox(); 64 | deleteAllJarCheckbox.setText("Delete all .jar files"); 65 | panel2.add(deleteAllJarCheckbox, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 66 | downloadButton = new JButton(); 67 | Font downloadButtonFont = this.$$$getFont$$$(null, -1, 14, downloadButton.getFont()); 68 | if (downloadButtonFont != null) downloadButton.setFont(downloadButtonFont); 69 | downloadButton.setText("Download"); 70 | panel2.add(downloadButton, new GridConstraints(1, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 71 | updateExistingModsButton = new JButton(); 72 | Font updateExistingModsButtonFont = this.$$$getFont$$$(null, -1, 14, updateExistingModsButton.getFont()); 73 | if (updateExistingModsButtonFont != null) updateExistingModsButton.setFont(updateExistingModsButtonFont); 74 | updateExistingModsButton.setText("Update existing mods"); 75 | panel2.add(updateExistingModsButton, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 76 | final JPanel panel3 = new JPanel(); 77 | panel3.setLayout(new GridLayoutManager(3, 1, new Insets(0, 0, 0, 0), -1, -1)); 78 | mainPanel.add(panel3, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_NORTH, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, true)); 79 | selectInstancePathsButton = new JButton(); 80 | Font selectInstancePathsButtonFont = this.$$$getFont$$$(null, -1, 14, selectInstancePathsButton.getFont()); 81 | if (selectInstancePathsButtonFont != null) selectInstancePathsButton.setFont(selectInstancePathsButtonFont); 82 | selectInstancePathsButton.setText("Select Instance Paths"); 83 | selectInstancePathsButton.setToolTipText("You may select multiple instances pressing the Shift or Ctrl key"); 84 | panel3.add(selectInstancePathsButton, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 85 | selectedDirLabel = new JLabel(); 86 | Font selectedDirLabelFont = this.$$$getFont$$$(null, -1, 14, selectedDirLabel.getFont()); 87 | if (selectedDirLabelFont != null) selectedDirLabel.setFont(selectedDirLabelFont); 88 | selectedDirLabel.setText("Selected Instances: none"); 89 | panel3.add(selectedDirLabel, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 90 | final JPanel panel4 = new JPanel(); 91 | panel4.setLayout(new GridLayoutManager(1, 2, new Insets(0, 0, 0, 0), -1, -1)); 92 | panel3.add(panel4, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_VERTICAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); 93 | final JLabel label1 = new JLabel(); 94 | Font label1Font = this.$$$getFont$$$(null, -1, 14, label1.getFont()); 95 | if (label1Font != null) label1.setFont(label1Font); 96 | label1.setText("Minecraft Version:"); 97 | panel4.add(label1, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 98 | mcVersionCombo = new JComboBox(); 99 | Font mcVersionComboFont = this.$$$getFont$$$(null, -1, 14, mcVersionCombo.getFont()); 100 | if (mcVersionComboFont != null) mcVersionCombo.setFont(mcVersionComboFont); 101 | panel4.add(mcVersionCombo, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 102 | final JPanel panel5 = new JPanel(); 103 | panel5.setLayout(new GridLayoutManager(1, 2, new Insets(20, 0, 10, 0), -1, -1)); 104 | mainPanel.add(panel5, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, true)); 105 | final JPanel panel6 = new JPanel(); 106 | panel6.setLayout(new GridLayoutManager(3, 1, new Insets(0, 0, 0, 0), -1, -1)); 107 | panel5.add(panel6, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_NORTHWEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); 108 | final JPanel panel7 = new JPanel(); 109 | panel7.setLayout(new GridLayoutManager(3, 1, new Insets(0, 0, 0, 0), -1, -1)); 110 | panel6.add(panel7, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); 111 | randomSeedRadioButton = new JRadioButton(); 112 | randomSeedRadioButton.setSelected(true); 113 | randomSeedRadioButton.setText("Random Seed"); 114 | panel7.add(randomSeedRadioButton, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 115 | setSeedRadioButton = new JRadioButton(); 116 | setSeedRadioButton.setText("Set Seed"); 117 | panel7.add(setSeedRadioButton, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 118 | final JLabel label2 = new JLabel(); 119 | Font label2Font = this.$$$getFont$$$(null, -1, 14, label2.getFont()); 120 | if (label2Font != null) label2.setFont(label2Font); 121 | label2.setText("Run Type"); 122 | panel7.add(label2, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 123 | final JPanel panel8 = new JPanel(); 124 | panel8.setLayout(new GridLayoutManager(4, 1, new Insets(20, 0, 20, 0), -1, -1)); 125 | panel6.add(panel8, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); 126 | final JLabel label3 = new JLabel(); 127 | Font label3Font = this.$$$getFont$$$(null, -1, 14, label3.getFont()); 128 | if (label3Font != null) label3.setFont(label3Font); 129 | label3.setText("OS"); 130 | panel8.add(label3, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 131 | windowsRadioButton = new JRadioButton(); 132 | windowsRadioButton.setSelected(true); 133 | windowsRadioButton.setText("Windows"); 134 | panel8.add(windowsRadioButton, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 135 | macRadioButton = new JRadioButton(); 136 | macRadioButton.setText("Mac"); 137 | panel8.add(macRadioButton, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 138 | linuxRadioButton = new JRadioButton(); 139 | linuxRadioButton.setText("Linux"); 140 | panel8.add(linuxRadioButton, new GridConstraints(3, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 141 | final JPanel panel9 = new JPanel(); 142 | panel9.setLayout(new GridLayoutManager(3, 1, new Insets(0, 0, 0, 0), -1, -1)); 143 | panel6.add(panel9, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); 144 | final JLabel label4 = new JLabel(); 145 | Font label4Font = this.$$$getFont$$$(null, -1, 14, label4.getFont()); 146 | if (label4Font != null) label4.setFont(label4Font); 147 | label4.setText("Optional"); 148 | panel9.add(label4, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 149 | accessibilityCheckBox = new JCheckBox(); 150 | accessibilityCheckBox.setText("Accessibility"); 151 | panel9.add(accessibilityCheckBox, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 152 | obsoleteCheckBox = new JCheckBox(); 153 | obsoleteCheckBox.setText("Obsolete"); 154 | panel9.add(obsoleteCheckBox, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 155 | final JPanel panel10 = new JPanel(); 156 | panel10.setLayout(new GridLayoutManager(2, 1, new Insets(0, 0, 0, 0), -1, -1)); 157 | panel5.add(panel10, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); 158 | final JPanel panel11 = new JPanel(); 159 | panel11.setLayout(new GridLayoutManager(1, 2, new Insets(0, 0, 0, 0), -1, -1)); 160 | panel10.add(panel11, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_NORTH, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, true)); 161 | deselectAllButton = new JButton(); 162 | Font deselectAllButtonFont = this.$$$getFont$$$(null, -1, 14, deselectAllButton.getFont()); 163 | if (deselectAllButtonFont != null) deselectAllButton.setFont(deselectAllButtonFont); 164 | deselectAllButton.setText("Deselect All"); 165 | panel11.add(deselectAllButton, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 166 | selectAllRecommendsButton = new JButton(); 167 | Font selectAllRecommendsButtonFont = this.$$$getFont$$$(null, -1, 14, selectAllRecommendsButton.getFont()); 168 | if (selectAllRecommendsButtonFont != null) selectAllRecommendsButton.setFont(selectAllRecommendsButtonFont); 169 | selectAllRecommendsButton.setText("Select All Recommends"); 170 | panel11.add(selectAllRecommendsButton, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 171 | modListScroll = new JScrollPane(); 172 | panel10.add(modListScroll, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); 173 | modListPanel = new JPanel(); 174 | modListPanel.setLayout(new BorderLayout(0, 0)); 175 | modListPanel.setEnabled(false); 176 | modListScroll.setViewportView(modListPanel); 177 | scrollBar = new JScrollBar(); 178 | scrollBar.setEnabled(true); 179 | scrollBar.setUnitIncrement(24); 180 | modListPanel.add(scrollBar, BorderLayout.WEST); 181 | modListScroll.setVerticalScrollBar(scrollBar); 182 | ButtonGroup buttonGroup; 183 | buttonGroup = new ButtonGroup(); 184 | buttonGroup.add(randomSeedRadioButton); 185 | buttonGroup.add(randomSeedRadioButton); 186 | buttonGroup.add(setSeedRadioButton); 187 | buttonGroup = new ButtonGroup(); 188 | buttonGroup.add(windowsRadioButton); 189 | buttonGroup.add(windowsRadioButton); 190 | buttonGroup.add(macRadioButton); 191 | buttonGroup.add(linuxRadioButton); 192 | } 193 | 194 | /** 195 | * @noinspection ALL 196 | */ 197 | private Font $$$getFont$$$(String fontName, int style, int size, Font currentFont) { 198 | if (currentFont == null) return null; 199 | String resultName; 200 | if (fontName == null) { 201 | resultName = currentFont.getName(); 202 | } else { 203 | Font testFont = new Font(fontName, Font.PLAIN, 10); 204 | if (testFont.canDisplay('a') && testFont.canDisplay('1')) { 205 | resultName = fontName; 206 | } else { 207 | resultName = currentFont.getName(); 208 | } 209 | } 210 | Font font = new Font(resultName, style >= 0 ? style : currentFont.getStyle(), size >= 0 ? size : currentFont.getSize()); 211 | boolean isMac = System.getProperty("os.name", "").toLowerCase(Locale.ENGLISH).startsWith("mac"); 212 | Font fontWithFallback = isMac ? new Font(font.getFamily(), font.getStyle(), font.getSize()) : new StyleContext().getFont(font.getFamily(), font.getStyle(), font.getSize()); 213 | return fontWithFallback instanceof FontUIResource ? fontWithFallback : new FontUIResource(fontWithFallback); 214 | } 215 | 216 | /** 217 | * @noinspection ALL 218 | */ 219 | public JComponent $$$getRootComponent$$$() { 220 | return mainPanel; 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/main/java/com/pistacium/modcheck/ModCheckFrameForm.form: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | -------------------------------------------------------------------------------- /src/main/kotlin/com/pistacium/modcheck/ModCheckFrameFormExt.kt: -------------------------------------------------------------------------------- 1 | package com.pistacium.modcheck 2 | 3 | import com.pistacium.modcheck.util.* 4 | import java.awt.* 5 | import java.io.File 6 | import java.net.URI 7 | import java.nio.file.* 8 | import java.util.* 9 | import javax.swing.* 10 | import javax.swing.border.EmptyBorder 11 | import javax.swing.plaf.FontUIResource 12 | import javax.swing.plaf.basic.BasicComboBoxEditor 13 | import kotlin.io.path.* 14 | 15 | class ModCheckFrameFormExt : ModCheckFrameForm() { 16 | private val modCheckBoxes = HashMap() 17 | private var currentOS: String = ModCheckUtils.currentOS() 18 | private var selectDirs: Array? = null 19 | 20 | init { 21 | contentPane = mainPanel 22 | title = "ModCheck v" + ModCheck.applicationVersion + " by RedLime" 23 | setSize(1100, 700) 24 | isVisible = true 25 | setLocationRelativeTo(null) 26 | defaultCloseOperation = EXIT_ON_CLOSE 27 | 28 | font = FontUIResource("SansSerif", Font.BOLD, 13) 29 | UIManager.getLookAndFeel().defaults.forEach { 30 | if (it.value is Font) { 31 | UIManager.put(it.key, font) 32 | } 33 | } 34 | 35 | val resource = javaClass.classLoader.getResource("end_crystal.png") 36 | if (resource != null) iconImage = ImageIcon(resource).image 37 | 38 | initMenuBar() 39 | 40 | selectInstancePathsButton.addActionListener { 41 | val instanceDir = ModCheckUtils.readConfig()?.getDirectory() 42 | val pathSelector = JFileChooser(instanceDir?.toFile()) 43 | pathSelector.isMultiSelectionEnabled = true 44 | pathSelector.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY 45 | pathSelector.dialogType = JFileChooser.CUSTOM_DIALOG 46 | pathSelector.dialogTitle = "Select Instance Paths" 47 | val jComboBox = SwingUtils.getDescendantsOfType(JComboBox::class.java, pathSelector).first() 48 | jComboBox.isEditable = true 49 | jComboBox.editor = object : BasicComboBoxEditor.UIResource() { 50 | override fun getItem(): Any { 51 | return try { 52 | File(super.getItem() as String) 53 | } catch (e: Exception) { 54 | super.getItem() 55 | } 56 | } 57 | } 58 | 59 | val showDialog = pathSelector.showDialog(this, "Select") 60 | val instanceDirectories = pathSelector.selectedFiles 61 | if (instanceDirectories != null && showDialog == JFileChooser.APPROVE_OPTION) { 62 | selectDirs = instanceDirectories 63 | var parentDir = "" 64 | val stringBuilder = StringBuilder() 65 | for (selectDir in instanceDirectories.slice(IntRange(0, instanceDirectories.size.coerceAtMost(4) - 1))) { 66 | stringBuilder.append(if (parentDir.isEmpty()) selectDir.path else selectDir.path.replace(parentDir, "")).append(", ") 67 | parentDir = selectDir.parent 68 | } 69 | if (instanceDirectories.size > 4) { 70 | stringBuilder.append("and " + (instanceDirectories.size - 4) + " more...") 71 | } 72 | selectedDirLabel.text = 73 | "Selected Instances: " + stringBuilder.removeSuffix(", ") + "" 74 | } 75 | if (instanceDirectories.isNotEmpty()) { 76 | ModCheckUtils.writeConfig(instanceDirectories[0].parentFile.toPath()) 77 | } 78 | } 79 | 80 | progressBar.string = "Idle..." 81 | downloadButton.addActionListener { 82 | if (selectDirs == null || selectDirs!!.isEmpty()) { 83 | return@addActionListener 84 | } 85 | 86 | if (mcVersionCombo.selectedItem == null) { 87 | JOptionPane.showMessageDialog(this, "Error: selected item is null") 88 | downloadButton.isEnabled = true 89 | return@addActionListener 90 | } 91 | 92 | downloadButton.isEnabled = false 93 | val modsFileStack = Stack() 94 | 95 | var overrideDownloadChecks = false 96 | 97 | for (instanceDir in selectDirs!!) { 98 | var instancePath = instanceDir.toPath() 99 | 100 | // if mods is selected, we don't have to do anything 101 | if (instancePath.fileName.toString() == "mods" && Files.isDirectory(instancePath) && instancePath.parent.fileName.toString() in listOf(".minecraft", "minecraft")) { 102 | modsFileStack.push(instancePath) 103 | continue 104 | } 105 | 106 | if (Files.isDirectory(instancePath.resolve(".minecraft"))) { 107 | instancePath = instancePath.resolve(".minecraft") 108 | } else if (Files.isDirectory(instancePath.resolve("minecraft"))) { 109 | instancePath = instancePath.resolve("minecraft") 110 | } else if (Files.exists(instancePath.resolve("mmc-pack.json")) || Files.exists(instancePath.resolve("instance.cfg"))) { 111 | // when adding a new instance in multimc / prism, 112 | // right away only mmc-pack.json & instance.cfg get created 113 | instancePath = Files.createDirectory(instancePath.resolve(".minecraft")) 114 | println("Created '.minecraft' folder for multimc: $instancePath") 115 | } 116 | 117 | if (instancePath.fileName.toString() !in listOf(".minecraft", "minecraft") && !overrideDownloadChecks) { 118 | if (JOptionPane.showConfirmDialog( 119 | this, 120 | "You have selected a directory but not a Minecraft instance directory.\nAre you sure you want to download in this directory?", 121 | "Wrong instance directory", 122 | JOptionPane.OK_CANCEL_OPTION 123 | ) == JOptionPane.CANCEL_OPTION 124 | ) { 125 | downloadButton.isEnabled = true 126 | return@addActionListener 127 | } else { 128 | overrideDownloadChecks = true 129 | } 130 | } 131 | 132 | val modsPath = instancePath.resolve("mods") 133 | if (!Files.exists(modsPath)) { 134 | println("Created 'mods' folder: $modsPath") 135 | Files.createDirectory(modsPath) 136 | } 137 | modsFileStack.push(modsPath) 138 | } 139 | 140 | val targetMods = ArrayList() 141 | var maxCount = 0 142 | for ((key, value) in modCheckBoxes) { 143 | if (value.isSelected && value.isEnabled) { 144 | println("Selected " + key.name) 145 | targetMods.add(key) 146 | maxCount++ 147 | } 148 | } 149 | val minecraftVersion = mcVersionCombo.selectedItem as String 150 | 151 | for (instanceDir in modsFileStack) { 152 | val modFiles = Files.list(instanceDir) ?: return@addActionListener 153 | for (file in modFiles) { 154 | if (file.name.endsWith(".jar")) { 155 | if (deleteAllJarCheckbox.isSelected) { 156 | Files.deleteIfExists(file) 157 | } else { 158 | val modName = file.name.split("-").first().split("+").first() 159 | for (targetMod in targetMods) { 160 | val targetModFileName = targetMod.getModVersion(minecraftVersion)?.url?.substringAfterLast("/") 161 | if (targetModFileName?.startsWith(modName) == true) { 162 | Files.deleteIfExists(file) 163 | } 164 | } 165 | } 166 | } 167 | if (file.name.lowercase().contains("serversiderng")) { 168 | askDeleteSSRNG(file) 169 | continue 170 | } 171 | } 172 | } 173 | 174 | progressBar.value = 0 175 | ModCheck.setStatus(ModCheckStatus.DOWNLOADING_MOD_FILE) 176 | 177 | val finalMaxCount = maxCount 178 | ModCheck.threadExecutor.submit { 179 | val failedMods = ArrayList() 180 | for ((count, targetMod) in targetMods.withIndex()) { 181 | progressBar.string = "Downloading " + targetMod.name 182 | println("Downloading " + targetMod.name) 183 | val downloadFiles = Stack() 184 | downloadFiles.addAll(modsFileStack) 185 | if (!downloadFile(minecraftVersion, targetMod, downloadFiles)) { 186 | println("Failed to download " + targetMod.name) 187 | failedMods.add(targetMod) 188 | } 189 | progressBar.value = (((count + 1) / (finalMaxCount * 1f)) * 100).toInt() 190 | } 191 | progressBar.value = 100 192 | ModCheck.setStatus(ModCheckStatus.IDLE) 193 | 194 | println("Downloading mods complete") 195 | 196 | if (failedMods.isNotEmpty()) { 197 | val failedModString = StringBuilder() 198 | for (failedMod in failedMods) { 199 | failedModString.append(failedMod.name).append(", ") 200 | } 201 | JOptionPane.showMessageDialog( 202 | this, 203 | "Failed to download " + failedModString.substring(0, failedModString.length - 2) + ".", 204 | "Please try again", 205 | JOptionPane.ERROR_MESSAGE 206 | ) 207 | } else { 208 | JOptionPane.showMessageDialog(this, "All selected mods have been downloaded!") 209 | } 210 | downloadButton.isEnabled = true 211 | } 212 | } 213 | downloadButton.isEnabled = false 214 | 215 | mcVersionCombo.addActionListener { updateModList() } 216 | 217 | selectAllRecommendsButton.addActionListener { 218 | for ((key, value) in modCheckBoxes) { 219 | if (!key.recommended || !key.getModVersion(mcVersionCombo.selectedItem as String)!!.recommended 220 | || key.incompatibilities.stream().anyMatch { incompatible: String -> 221 | modCheckBoxes.entries.stream().anyMatch { entry2: Map.Entry -> entry2.key.name == incompatible && entry2.value.isSelected } 222 | } 223 | ) continue 224 | 225 | if (value.isEnabled) { 226 | value.isSelected = true 227 | } 228 | } 229 | // JOptionPane.showMessageDialog( 230 | // this, 231 | // "Some mods that have warnings (like noPeaceful)
or incompatible with other mods (like Starlight and Phosphor) aren't automatically selected.
You have to select them yourself.", 232 | // "WARNING!", 233 | // JOptionPane.WARNING_MESSAGE 234 | // ) 235 | } 236 | 237 | deselectAllButton.addActionListener { 238 | for (cb in modCheckBoxes.values) { 239 | cb.isSelected = false 240 | cb.isEnabled = true 241 | } 242 | } 243 | 244 | // TODO: enumification 245 | windowsRadioButton.addActionListener { 246 | currentOS = "windows" 247 | updateModList() 248 | } 249 | if (currentOS == "windows") windowsRadioButton.isSelected = true 250 | macRadioButton.addActionListener { 251 | currentOS = "osx" 252 | updateModList() 253 | } 254 | if (currentOS == "osx") macRadioButton.isSelected = true 255 | linuxRadioButton.addActionListener { 256 | currentOS = "linux" 257 | updateModList() 258 | } 259 | if (currentOS == "linux") linuxRadioButton.isSelected = true 260 | 261 | randomSeedRadioButton.addActionListener { updateModList() } 262 | setSeedRadioButton.addActionListener { updateModList() } 263 | accessibilityCheckBox.addActionListener { 264 | if (accessibilityCheckBox.isSelected) { 265 | val message = "These mods have consequences to legality and possibility of rejection if used improperly, please make yourself familiar with the rules about each one if you choose to use them." 266 | val result = JOptionPane.showConfirmDialog(this, message, "THIS OPTION IS NOT FOR ALL!", JOptionPane.OK_CANCEL_OPTION) 267 | if (result == 0) { 268 | updateModList() 269 | } else { 270 | accessibilityCheckBox.isSelected = false 271 | } 272 | } else { 273 | updateModList() 274 | } 275 | } 276 | obsoleteCheckBox.addActionListener { updateModList() } 277 | 278 | updateExistingModsButton.addActionListener { 279 | if (selectDirs == null || selectDirs!!.isEmpty()) { 280 | return@addActionListener 281 | } 282 | progressBar.value = 0 283 | ModCheck.setStatus(ModCheckStatus.GETTING_INSTALLED_MODS) 284 | val resolvedModFolders = ArrayList() 285 | for (dir in selectDirs!!) { 286 | var modFolder = dir.toPath() 287 | // support for selecting either the outer mmc dir or .minecraft 288 | if (modFolder.isDirectory() && modFolder.fileName.toString() == "mods" && modFolder.parent.name in arrayOf("minecraft", ".minecraft")) { 289 | resolvedModFolders.add(modFolder) 290 | continue 291 | } else if (modFolder.resolve(".minecraft").isDirectory()) { 292 | modFolder = modFolder.resolve(".minecraft") 293 | } else if (modFolder.resolve("minecraft").isDirectory()) { 294 | modFolder = modFolder.resolve("minecraft") 295 | } 296 | modFolder = modFolder.resolve("mods") 297 | // no mod folder, no service 298 | if (modFolder.isDirectory()) { 299 | resolvedModFolders.add(modFolder) 300 | } 301 | } 302 | ModCheck.setStatus(ModCheckStatus.DOWNLOADING_MOD_FILE) 303 | for (modFolder in resolvedModFolders) { 304 | // store the file to be replaced, so we don't have to guess the name via string splicing 305 | val replacedJars = ArrayList>() 306 | for (modJar in modFolder.listDirectoryEntries()) { 307 | // retain .disabled mods 308 | if (modJar.extension != "jar") continue 309 | val fmj = ModCheckUtils.readFabricModJson(modJar) ?: continue 310 | 311 | if (fmj.id == "serversiderng") { 312 | askDeleteSSRNG(modJar) 313 | continue 314 | } 315 | // if mod id is in available mods, find the newest version for the selected version 316 | // we can't get the minecraft version from the instance itself easily while still supporting vanilla launcher 317 | val modVersion = ModCheck.availableMods.find { it.modid == fmj.id || it.name == fmj.name }?.getModVersion(mcVersionCombo.selectedItem as String) ?: continue 318 | if (modVersion.version != fmj.version) { 319 | replacedJars.add(Pair(modJar, modVersion)) 320 | } 321 | } 322 | var count = 0 323 | for (newMod in replacedJars) { 324 | // requires that the new mod and old mod have different filenames 325 | if (downloadFile(newMod.second, modFolder)) { 326 | Files.deleteIfExists(newMod.first) 327 | } 328 | count++ 329 | progressBar.value = count / replacedJars.size 330 | } 331 | } 332 | progressBar.value = 100 333 | ModCheck.setStatus(ModCheckStatus.IDLE) 334 | JOptionPane.showMessageDialog(this, "Known mods have been updated!") 335 | } 336 | } 337 | 338 | private fun downloadFile(minecraftVersion: String, targetMod: Meta.Mod, instances: Stack): Boolean { 339 | val url = targetMod.getModVersion(minecraftVersion)?.url ?: return false 340 | val filename = url.substringAfterLast("/") 341 | val bytes = URI.create(url).toURL().readBytes() 342 | instances.forEach { 343 | it.resolve(filename).writeBytes(bytes) 344 | } 345 | return true 346 | } 347 | 348 | // TODO: bytearr caching 349 | private fun downloadFile(modVersion: Meta.ModVersion, instance: Path): Boolean { 350 | val url = modVersion.url 351 | val filename = url.substringAfterLast("/") 352 | val bytes = URI.create(url).toURL().readBytes() 353 | instance.resolve(filename).writeBytes(bytes) 354 | return true 355 | } 356 | 357 | 358 | private fun initMenuBar() { 359 | val menuBar = JMenuBar() 360 | 361 | val source = JMenu("Info") 362 | 363 | val githubSource = JMenuItem("GitHub...") 364 | githubSource.addActionListener { 365 | try { 366 | Desktop.getDesktop().browse(URI("https://github.com/tildejustin/modcheck")) 367 | } catch (ignored: Exception) { 368 | } 369 | } 370 | source.add(githubSource) 371 | 372 | val donateSource = JMenuItem("Support") 373 | donateSource.addActionListener { 374 | try { 375 | Desktop.getDesktop().browse(URI("https://ko-fi.com/redlimerl")) 376 | } catch (ignored: Exception) { 377 | } 378 | } 379 | source.add(donateSource) 380 | 381 | val checkChangeLogSource = JMenuItem("Changelog") 382 | checkChangeLogSource.addActionListener { 383 | try { 384 | Desktop.getDesktop().browse(URI("https://github.com/tildejustin/modcheck/releases/tag/" + ModCheck.applicationVersion)) 385 | } catch (ignored: Exception) { 386 | } 387 | } 388 | source.add(checkChangeLogSource) 389 | 390 | val updateCheckSource = JMenuItem("Check for updates") 391 | updateCheckSource.addActionListener(ModCheckUtils::checkForUpdates) 392 | source.add(updateCheckSource) 393 | 394 | menuBar.add(source) 395 | 396 | this.jMenuBar = menuBar 397 | } 398 | 399 | fun updateVersionList() { 400 | mcVersionCombo.removeAllItems() 401 | for (availableVersion in ModCheck.availableVersions) { 402 | mcVersionCombo.addItem(availableVersion) 403 | } 404 | mcVersionCombo.selectedItem = ModCheck.availableVersions.first() 405 | updateModList() 406 | } 407 | 408 | 409 | private fun updateModList() { 410 | modListPanel.removeAll() 411 | modListPanel.layout = BoxLayout(modListPanel, BoxLayout.Y_AXIS) 412 | modCheckBoxes.clear() 413 | 414 | if (mcVersionCombo.selectedItem == null) return 415 | 416 | val mcVersion: String = mcVersionCombo.selectedItem as String 417 | 418 | outer@ for (mod in ModCheck.availableMods) { 419 | val modVersion = mod.getModVersion(mcVersion) 420 | if (modVersion != null) { 421 | // prioritize sodium-mac 422 | if (mod.modid == "sodium" && currentOS == "osx" && ModCheck.availableMods.find { it.modid == "sodiummac" }?.versions?.find { it.target_version.contains(mcVersion) } != null) continue@outer 423 | if ((mod.obsolete || modVersion.obsolete) && !obsoleteCheckBox.isSelected) continue@outer 424 | for (condition in mod.traits) { 425 | if (condition == "ssg-only" && !setSeedRadioButton.isSelected) continue@outer 426 | if (condition == "rsg-only" && !randomSeedRadioButton.isSelected) continue@outer 427 | if (condition == "accessibility" && !accessibilityCheckBox.isSelected) continue@outer 428 | if (condition == "mac-only" && currentOS != "osx") continue@outer 429 | } 430 | 431 | val modPanel = JPanel() 432 | modPanel.layout = BoxLayout(modPanel, BoxLayout.Y_AXIS) 433 | 434 | val versionName: String = modVersion.version 435 | val checkBox = JCheckBox(mod.name + " (" + versionName + ")") 436 | checkBox.addChangeListener { 437 | modCheckBoxes.entries.stream() 438 | .filter { 439 | it.key.incompatibilities.contains(mod.modid) || mod.incompatibilities.contains(it.key.modid) 440 | } 441 | .forEach { entry -> 442 | entry.value.setEnabled( 443 | modCheckBoxes.entries.stream() 444 | .noneMatch { 445 | it.key.incompatibilities 446 | .contains(it.key.modid) || it.key.incompatibilities.contains(entry.key.modid) && it.value.isSelected 447 | } 448 | ) 449 | } 450 | } 451 | 452 | val line: Int = mod.description.split("\n").size 453 | val description = 454 | JLabel("" + mod.description.replace("\n", "
").replace("", "") + "") 455 | description.maximumSize = Dimension(800, 70 * line) 456 | description.border = EmptyBorder(0, 15, 0, 0) 457 | val f = description.font 458 | description.font = f.deriveFont(f.style and Font.BOLD.inv()) 459 | 460 | modPanel.add(checkBox) 461 | modPanel.add(description) 462 | modPanel.maximumSize = Dimension(950, 60 * line) 463 | modPanel.border = EmptyBorder(0, 10, 10, 0) 464 | 465 | modListPanel.add(modPanel) 466 | modCheckBoxes[mod] = checkBox 467 | } 468 | } 469 | modListPanel.updateUI() 470 | modListScroll.updateUI() 471 | downloadButton.isEnabled = true 472 | } 473 | 474 | private fun askDeleteSSRNG(file: Path) { 475 | val shouldDelete = JOptionPane.showConfirmDialog( 476 | null, 477 | "ServerSideRNG has been detected in your mod folder. As the mod is now illegal, would you like it to be deleted?", 478 | UIManager.getString("OptionPane.titleText"), 479 | JOptionPane.YES_NO_OPTION 480 | ) == JOptionPane.YES_OPTION 481 | if (shouldDelete) { 482 | Files.deleteIfExists(file) 483 | } 484 | } 485 | } 486 | --------------------------------------------------------------------------------