@@ -6,8 +6,10 @@ import java.awt.Toolkit
66import java.awt.datatransfer.StringSelection
77import java.io.*
88import java.net.URI
9+ import java.nio.file.*
910import java.util.concurrent.*
1011import javax.swing.JOptionPane
12+ import kotlin.io.path.extension
1113import kotlin.system.exitProcess
1214
1315object ModCheck {
@@ -27,6 +29,13 @@ object ModCheck {
2729
2830 @JvmStatic
2931 fun main (args : Array <String >) {
32+ // enable cli if arguments are given
33+ if (args.isNotEmpty()) {
34+ handleCliMode(args)
35+ return
36+ }
37+ // else run the gui
38+
3039 FlatDarkLaf .setup()
3140 threadExecutor.submit {
3241 try {
@@ -82,4 +91,293 @@ object ModCheck {
8291 }
8392 }
8493 }
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 <String >) {
116+ // Load mod list for CLI
117+ val mods = ModCheckUtils .json.decodeFromString<Meta >(
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 <directory> 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 <directory> or --instance <name> 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<Triple <Path , Meta .Mod , Meta .ModVersion >>()
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] <download|update>
359+
360+
361+ Options:
362+ --category <rsg|ssg> Specify the category (default: rsg)
363+ --version <version> Specify Minecraft version (default: 1.16.1)
364+ --accessibility Include accessibility mods (default: false)
365+ --instance <name> Specify your instance name (uses default PrismLauncher path)
366+ or
367+ --path <directory> 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+ }
85383}
0 commit comments