├── settings.gradle ├── .gitattributes ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── README.md ├── docs ├── changelogger.md └── modlist.md ├── src └── main │ └── java │ └── org │ └── moddingx │ └── modlistcreator │ ├── util │ ├── EnumConverters.java │ └── OptionUtil.java │ ├── output │ ├── OutputTarget.java │ ├── MarkdownTarget.java │ ├── HtmlTarget.java │ └── PlainTextTarget.java │ ├── modlist │ ├── ModListFormatter.java │ └── ModListCreator.java │ ├── Main.java │ ├── changelogger │ ├── Changelogger.java │ └── ChangelogFormatter.java │ └── platform │ ├── Modpack.java │ ├── CurseModpack.java │ └── ModrinthModpack.java ├── .github └── workflows │ └── publish-release.yml ├── gradlew.bat ├── gradlew └── LICENSE /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = "ModListCreator" 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ModdingX/ModListCreator/HEAD/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-8.8-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | .gradle/ 25 | .idea/ 26 | config/ 27 | build/ 28 | input/ 29 | /output/ 30 | *.mrpack 31 | /test/** -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ModListCreator 2 | 3 | [![Total downloads](https://img.shields.io/github/downloads/ModdingX/ModListCreator/total.svg)](https://www.github.com/ModdingX/ModListCreator/releases/) 4 | 5 | ## How to get 6 | 7 | 1. [Download here](https://github.com/ModdingX/ModListCreator/releases) 8 | 9 | OR 10 | 11 | 1. Clone this repository 12 | 2. Run `gradlew build` 13 | 3. Get file from path/build/libs/ 14 | 15 | ## Detailed information 16 | 17 | This tool works for both, Modrinth and CurseForge modpacks. 18 | - [Changelog](docs/changelogger.md) 19 | - [Modlist](docs/modlist.md) 20 | -------------------------------------------------------------------------------- /docs/changelogger.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | ## How to use 3 | 4 | 1. Put the ModListCreator file into a folder 5 | 2. Open terminal/cmd 6 | 3. Add `changelog` after `java -jar ModListCreator--fatjar.jar` 7 | 4. Set arguments listed below 8 | 5. Run it and wait for output file 9 | 10 | ## Arguments you could use 11 | 12 | | Argument | Description | 13 | |:----------:|--------------------------------------------------------------------------| 14 | | **old** | Defines the old modpack zip or json file | 15 | | **new** | Defines the new modpack zip or json file | 16 | | **output** | Defines the output file name | 17 | | no-header | Generates the file without pack name and version | 18 | | format | The output format to use (`plain_text`, `html`, or `markdown` (default)) | 19 | -------------------------------------------------------------------------------- /src/main/java/org/moddingx/modlistcreator/util/EnumConverters.java: -------------------------------------------------------------------------------- 1 | package org.moddingx.modlistcreator.util; 2 | 3 | import joptsimple.util.EnumConverter; 4 | 5 | import java.util.Arrays; 6 | import java.util.Locale; 7 | import java.util.stream.Collectors; 8 | 9 | public class EnumConverters { 10 | 11 | public static > EnumConverter enumArg(Class cls) { 12 | return new ConcreteEnumConverter<>(cls); 13 | } 14 | 15 | private static class ConcreteEnumConverter> extends EnumConverter { 16 | 17 | private ConcreteEnumConverter(Class clazz) { 18 | super(clazz); 19 | } 20 | 21 | @Override 22 | public String valuePattern() { 23 | return Arrays.stream(this.valueType().getEnumConstants()) 24 | .map(v -> v.name().toLowerCase(Locale.ROOT)) 25 | .collect(Collectors.joining("|")); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: 'Publish' 2 | on: 3 | create: 4 | branches: 5 | - 'main' 6 | 7 | jobs: 8 | publish: 9 | if: startsWith(github.repository, 'ModdingX/') && startsWith(github.ref, 'refs/tags/') 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: 'Checkout' 13 | uses: actions/checkout@v4 14 | - name: 'Setup Java' 15 | uses: actions/setup-java@v4 16 | with: 17 | distribution: 'temurin' 18 | java-version: '21' 19 | - name: 'Build' 20 | run: | 21 | ./gradlew \ 22 | -Pversion=${{ github.ref_name }} \ 23 | -PmoddingxUsername=${{ secrets.PUBLISH_USER }} \ 24 | -PmoddingxPassword=${{ secrets.PUBLISH_PASSWORD }} \ 25 | build publish 26 | - name: 'Upload Release' 27 | uses: ncipollo/release-action@v1.14.0 28 | with: 29 | generateReleaseNotes: true 30 | artifacts: build/libs/ModListCreator-${{ github.ref_name }}-fatjar.jar 31 | token: ${{ secrets.GITHUB_TOKEN }} 32 | -------------------------------------------------------------------------------- /src/main/java/org/moddingx/modlistcreator/output/OutputTarget.java: -------------------------------------------------------------------------------- 1 | package org.moddingx.modlistcreator.output; 2 | 3 | import java.net.URI; 4 | import java.util.function.Supplier; 5 | 6 | public interface OutputTarget { 7 | 8 | void addHeader(String content); 9 | void addSubHeader(String content); 10 | void addParagraph(String content); 11 | void beginList(boolean numbered); 12 | void addListElement(String content); 13 | void endList(); 14 | String formatLink(String text, URI url); 15 | 16 | String result(); 17 | 18 | enum Type { 19 | 20 | PLAIN_TEXT("txt", PlainTextTarget::new), 21 | HTML("html", HtmlTarget::new), 22 | MARKDOWN("md", MarkdownTarget::new); 23 | 24 | public final String extension; 25 | private final Supplier factory; 26 | 27 | Type(String extension, Supplier factory) { 28 | this.extension = extension; 29 | this.factory = factory; 30 | } 31 | 32 | public OutputTarget create() { 33 | return this.factory.get(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/org/moddingx/modlistcreator/modlist/ModListFormatter.java: -------------------------------------------------------------------------------- 1 | package org.moddingx.modlistcreator.modlist; 2 | 3 | import org.moddingx.modlistcreator.output.OutputTarget; 4 | import org.moddingx.modlistcreator.platform.Modpack; 5 | 6 | import java.util.Comparator; 7 | 8 | public class ModListFormatter { 9 | 10 | public static String format(Modpack pack, OutputTarget.Type outputType, boolean includeHeader, boolean detailed) { 11 | OutputTarget target = outputType.create(); 12 | if (includeHeader) { 13 | target.addHeader(pack.title() + " - " + pack.version()); 14 | } 15 | 16 | target.beginList(false); 17 | for (Modpack.File file : pack.files().stream().sorted(Comparator.comparing(Modpack.File::projectSlug)).toList()) { 18 | String projectPart = detailed ? target.formatLink(file.fileName(), file.fileWebsite()) : target.formatLink(file.projectName(), file.projectWebsite()); 19 | target.addListElement(projectPart + " (by " + target.formatLink(file.author(), file.authorWebsite()) + ")"); 20 | } 21 | target.endList(); 22 | 23 | return target.result(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/org/moddingx/modlistcreator/Main.java: -------------------------------------------------------------------------------- 1 | package org.moddingx.modlistcreator; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | import org.moddingx.modlistcreator.changelogger.Changelogger; 6 | import org.moddingx.modlistcreator.modlist.ModListCreator; 7 | 8 | import java.io.IOException; 9 | import java.util.Locale; 10 | 11 | public class Main { 12 | 13 | public static final Gson GSON; 14 | 15 | static { 16 | GsonBuilder builder = new GsonBuilder(); 17 | builder.disableHtmlEscaping(); 18 | GSON = builder.create(); 19 | } 20 | 21 | public static void main(String[] args) throws IOException { 22 | String cmd = args.length == 0 ? "" : args[0]; 23 | String[] newArgs = new String[Math.max(0, args.length - 1)]; 24 | if (newArgs.length > 0) { 25 | System.arraycopy(args, 1, newArgs, 0, newArgs.length); 26 | } 27 | switch (cmd.toLowerCase(Locale.ROOT)) { 28 | case "modlist" -> ModListCreator.run(newArgs); 29 | case "changelog" -> Changelogger.run(newArgs); 30 | default -> { 31 | System.err.println("ModListCreator - Choose sub-command\n"); 32 | System.err.println(" modlist: Create a modlist file from a CurseForge or Modrinth modpack."); 33 | System.err.println(" changelog: Create a changelog file from a CurseForge or Modrinth modpack."); 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/moddingx/modlistcreator/util/OptionUtil.java: -------------------------------------------------------------------------------- 1 | package org.moddingx.modlistcreator.util; 2 | 3 | import jakarta.annotation.Nullable; 4 | import joptsimple.OptionParser; 5 | import joptsimple.OptionSpec; 6 | import org.moddingx.modlistcreator.output.OutputTarget; 7 | import org.moddingx.modlistcreator.platform.Modpack; 8 | 9 | import java.io.IOException; 10 | import java.nio.file.Path; 11 | import java.util.function.BiFunction; 12 | 13 | public class OptionUtil { 14 | 15 | public static T missing(OptionParser options, OptionSpec spec) throws IOException { 16 | return missing(options, spec, "Missing required option"); 17 | } 18 | 19 | public static T missing(OptionParser options, OptionSpec spec, String msg) throws IOException { 20 | System.err.println(msg + ": " + spec + "\n"); 21 | options.printHelpOn(System.err); 22 | System.exit(1); 23 | throw new Error(); 24 | } 25 | 26 | public static BiFunction outputPathFunc(Path basePath, @Nullable String pattern) { 27 | Path normPath = basePath.toAbsolutePath().normalize(); 28 | if (pattern == null) return (pack, type) -> normPath; 29 | return (pack, type) -> normPath.resolve(pattern 30 | .replace("%n", pack.title().replace(' ', '_')) 31 | .replace("%v", pack.version().replace(' ', '_')) 32 | .replace("%%", "%") + "." + type.extension 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/org/moddingx/modlistcreator/output/MarkdownTarget.java: -------------------------------------------------------------------------------- 1 | package org.moddingx.modlistcreator.output; 2 | 3 | import java.net.URI; 4 | import java.util.Stack; 5 | 6 | public class MarkdownTarget implements OutputTarget { 7 | 8 | private final StringBuilder sb = new StringBuilder(); 9 | private final Stack lists = new Stack<>(); 10 | 11 | @Override 12 | public void addHeader(String content) { 13 | this.sb.append("## ").append(content).append("\n\n"); 14 | } 15 | 16 | @Override 17 | public void addSubHeader(String content) { 18 | this.sb.append("### ").append(content).append("\n\n"); 19 | } 20 | 21 | @Override 22 | public void addParagraph(String content) { 23 | this.sb.append(content).append("\n\n"); 24 | } 25 | 26 | @Override 27 | public void beginList(boolean numbered) { 28 | this.lists.push(numbered ? 1: 0); 29 | } 30 | 31 | @Override 32 | public void addListElement(String content) { 33 | this.sb.append(" ".repeat(this.lists.size())); 34 | if (this.lists.peek() == 0) { 35 | this.sb.append("* "); 36 | } else { 37 | int nextIdx = this.lists.pop(); 38 | this.sb.append(nextIdx).append(". "); 39 | this.lists.push(nextIdx + 1); 40 | } 41 | this.sb.append(content).append("\n"); 42 | } 43 | 44 | @Override 45 | public void endList() { 46 | this.lists.pop(); 47 | this.sb.append("\n"); 48 | } 49 | 50 | @Override 51 | public String formatLink(String text, URI url) { 52 | return "[" + text + "](" + url + ")"; 53 | } 54 | 55 | @Override 56 | public String result() { 57 | return this.sb.toString(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/org/moddingx/modlistcreator/output/HtmlTarget.java: -------------------------------------------------------------------------------- 1 | package org.moddingx.modlistcreator.output; 2 | 3 | import org.jsoup.nodes.Element; 4 | 5 | import java.net.URI; 6 | import java.util.Stack; 7 | 8 | public class HtmlTarget implements OutputTarget { 9 | 10 | private final Element tag = new Element("body"); 11 | private final Stack lists = new Stack<>(); 12 | 13 | @Override 14 | public void addHeader(String content) { 15 | this.tag.appendChild(new Element("h2").append(content)); 16 | } 17 | 18 | @Override 19 | public void addSubHeader(String content) { 20 | this.tag.appendChild(new Element("h3").append(content)); 21 | } 22 | 23 | @Override 24 | public void addParagraph(String content) { 25 | this.tag.appendChild(new Element("p").append(content)); 26 | } 27 | 28 | @Override 29 | public void beginList(boolean numbered) { 30 | this.lists.push(new Element(numbered ? "ol" : "ul")); 31 | } 32 | 33 | @Override 34 | public void addListElement(String content) { 35 | this.lists.peek().appendChild(new Element("li").append(content)); 36 | } 37 | 38 | @Override 39 | public void endList() { 40 | Element elem = this.lists.pop(); 41 | if (this.lists.isEmpty()) { 42 | this.tag.appendChild(elem); 43 | } else { 44 | this.lists.peek().appendChild(elem); 45 | } 46 | this.tag.appendChild(new Element("br")); 47 | } 48 | 49 | @Override 50 | public String formatLink(String text, URI url) { 51 | return new Element("a").attr("href", url.toString()).append(text).outerHtml(); 52 | } 53 | 54 | @Override 55 | public String result() { 56 | return this.tag.html(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/org/moddingx/modlistcreator/output/PlainTextTarget.java: -------------------------------------------------------------------------------- 1 | package org.moddingx.modlistcreator.output; 2 | 3 | import java.net.URI; 4 | import java.util.Stack; 5 | 6 | public class PlainTextTarget implements OutputTarget { 7 | 8 | private final StringBuilder sb = new StringBuilder(); 9 | private final Stack lists = new Stack<>(); 10 | 11 | @Override 12 | public void addHeader(String content) { 13 | this.sb.append(" ").append(content).append(" \n"); 14 | this.sb.append(" ").append("=".repeat(content.length() + 2)).append(" \n\n"); 15 | } 16 | 17 | @Override 18 | public void addSubHeader(String content) { 19 | this.sb.append(" ").append(content).append(" \n"); 20 | this.sb.append(" ").append("-".repeat(content.length() + 4)).append(" \n\n"); 21 | } 22 | 23 | @Override 24 | public void addParagraph(String content) { 25 | int len = 0; 26 | for (String word : content.split(" ")) { 27 | if (len > 0 && len + word.length() > 80) { 28 | len = 0; 29 | this.sb.append("\n"); 30 | } 31 | if (len > 0) { 32 | this.sb.append(" "); 33 | } 34 | len += word.length(); 35 | this.sb.append(word); 36 | } 37 | } 38 | 39 | @Override 40 | public void beginList(boolean numbered) { 41 | this.lists.push(numbered ? 1 : 0); 42 | } 43 | 44 | @Override 45 | public void addListElement(String content) { 46 | this.sb.append(" ".repeat(this.lists.size())); 47 | if (this.lists.peek() == 0) { 48 | this.sb.append("* "); 49 | } else { 50 | int nextIdx = this.lists.pop(); 51 | this.sb.append(nextIdx).append(". "); 52 | this.lists.push(nextIdx + 1); 53 | } 54 | this.sb.append(content).append("\n"); 55 | } 56 | 57 | @Override 58 | public void endList() { 59 | this.lists.pop(); 60 | this.sb.append("\n"); 61 | } 62 | 63 | @Override 64 | public String formatLink(String text, URI url) { 65 | return text; 66 | } 67 | 68 | @Override 69 | public String result() { 70 | return this.sb.toString(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/org/moddingx/modlistcreator/changelogger/Changelogger.java: -------------------------------------------------------------------------------- 1 | package org.moddingx.modlistcreator.changelogger; 2 | 3 | import joptsimple.OptionException; 4 | import joptsimple.OptionParser; 5 | import joptsimple.OptionSet; 6 | import joptsimple.OptionSpec; 7 | import joptsimple.util.PathConverter; 8 | import joptsimple.util.PathProperties; 9 | import org.moddingx.modlistcreator.output.OutputTarget; 10 | import org.moddingx.modlistcreator.platform.Modpack; 11 | import org.moddingx.modlistcreator.util.EnumConverters; 12 | import org.moddingx.modlistcreator.util.OptionUtil; 13 | 14 | import java.io.IOException; 15 | import java.nio.file.Files; 16 | import java.nio.file.Path; 17 | import java.nio.file.Paths; 18 | import java.nio.file.StandardOpenOption; 19 | import java.util.List; 20 | 21 | public class Changelogger { 22 | 23 | public static void run(String[] args) throws IOException { 24 | OptionParser options = new OptionParser(); 25 | OptionSpec specNoHeader = options.accepts("no-header", "Generates the file without pack name and version"); 26 | OptionSpec specOld = options.acceptsAll(List.of("o", "old"), "Defines the old modpack zip or json file").withRequiredArg().withValuesConvertedBy(new PathConverter(PathProperties.FILE_EXISTING)); 27 | OptionSpec specNew = options.acceptsAll(List.of("n", "new"), "Defines the new modpack zip or json file").withRequiredArg().withValuesConvertedBy(new PathConverter(PathProperties.FILE_EXISTING)); 28 | OptionSpec specOutput = options.accepts("output", "Defines the output file name").withOptionalArg().ofType(String.class); 29 | OptionSpec specFormat = options.accepts("format", "The output format to use").withRequiredArg().withValuesConvertedBy(EnumConverters.enumArg(OutputTarget.Type.class)).defaultsTo(OutputTarget.Type.MARKDOWN); 30 | 31 | OptionSet set; 32 | try { 33 | set = options.parse(args); 34 | if (!set.has(specOld)) OptionUtil.missing(options, specOld); 35 | if (!set.has(specNew)) OptionUtil.missing(options, specNew); 36 | if (!set.has(specOutput)) OptionUtil.missing(options, specOutput); 37 | } catch (OptionException e) { 38 | System.err.println(e.getMessage() + "\n"); 39 | options.printHelpOn(System.err); 40 | System.exit(1); 41 | throw new Error(); 42 | } 43 | 44 | Modpack from = Modpack.fromPath(set.valueOf(specOld)); 45 | Modpack to = Modpack.fromPath(set.valueOf(specNew)); 46 | Path output = Paths.get(set.valueOf(specOutput)); 47 | Files.writeString(output, ChangelogFormatter.format(from, to, set.valueOf(specFormat), set.has(specNoHeader)), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); 48 | System.exit(0); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /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 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /docs/modlist.md: -------------------------------------------------------------------------------- 1 | # Modlist 2 | ## How to use 3 | 4 | 1. Put the ModListCreator file into a folder 5 | 2. Open terminal/cmd 6 | 3. Add `modlist` after `java -jar ModListCreator--fatjar.jar` 7 | 4. Set arguments listed below 8 | 5. After all arguments, set the input files (`folder/*` for whole folder) 9 | 6. Run it and wait for output file(s) 10 | 11 | ## Arguments you could use 12 | 13 | | Argument | Description | 14 | |:------------:|---------------------------------------------------------------------------------------------------------------------------------| 15 | | no-header | Generates the file without pack name and version | 16 | | detailed | Shows exact version of each mod | 17 | | format | The output format to use (`plain_text`, `html`, or `markdown` (default)) | 18 | | **output** | Defines the output path for generated files. If --pattern is set, describes a directory for output files, else a concrete file. | 19 | | pattern | Defines the output file name pattern. %n is replaced with pack name, %v with pack version. | 20 | 21 | ## Examples 22 | ### Detailed 23 | 24 | To use this argument, use the following command: 25 | 26 | `$ java -jar ModListCreator--fatjar.jar --detailed` 27 | 28 | | Without argument | With argument | 29 | |---------------------------------|------------------------------------------| 30 | | AIOT Botania (by MelanX) | aiotbotania-1.16.2-1.3.2.jar (by MelanX) | 31 | | Automatic Tool Swap (by MelanX) | ToolSwap-1.16.2-1.2.0.jar (by MelanX) | 32 | | Botania (by Vazkii) | Botania-1.16.3-409.jar (by Vazkii) | 33 | 34 | ### No Header 35 | 36 | To use this argument, use the following command: 37 | 38 | `$ java -jar ModListCreator--fatjar.jar --no-header` 39 | 40 | | Without argument | With argument | 41 | |---------------------------------------------|---------------| 42 | | Garden of Glass (Questbook Edition) - 4.2.0 | _nothing_ | 43 | 44 | ### Pattern 45 | 46 | To use this argument, use the following command: 47 | 48 | `$ java -jar ModListCreator--fatjar.jar --pattern "This is %n in version %v` 49 | 50 | > This is CaveStone in version 0.4.0 51 | 52 | ### Input 53 | 54 | To use this argument, use the following command: 55 | 56 | `$ java -jar ModListCreator--fatjar.jar --pattern "Name" --output output modpacks/*` 57 | 58 | This will use the folder `modpacks` as input and tries to generate a modlist for each file in this folder. 59 | 60 | ### Output 61 | 62 | To use this argument, use the following command: 63 | 64 | `$ java -jar ModListCreator--fatjar.jar --output output.md` 65 | 66 | This will generate a file called `output.md`. If you set `--pattern` argument, it will generate a folder 67 | called `output.md`. 68 | 69 | ## Why use this instead of exported modlist? 70 | 71 | - This tool sorts the project names alphabetically 72 | - This tool links to the project and the author 73 | - The official `modlist.html` from CurseForge exports contains broken links to the projects -------------------------------------------------------------------------------- /src/main/java/org/moddingx/modlistcreator/platform/Modpack.java: -------------------------------------------------------------------------------- 1 | package org.moddingx.modlistcreator.platform; 2 | 3 | import com.google.gson.JsonElement; 4 | import org.moddingx.modlistcreator.Main; 5 | 6 | import java.io.IOException; 7 | import java.io.Reader; 8 | import java.net.URI; 9 | import java.nio.file.*; 10 | import java.util.List; 11 | import java.util.Locale; 12 | import java.util.Map; 13 | import java.util.Optional; 14 | 15 | public interface Modpack { 16 | 17 | String title(); 18 | Minecraft minecraft(); 19 | String version(); 20 | List files(); 21 | 22 | public static Modpack fromPath(Path path) throws IOException { 23 | try { 24 | return Modpack.loadZip(path); 25 | } catch (ProviderNotFoundException e) { 26 | // Not a zip file 27 | return Modpack.load(path); 28 | } 29 | } 30 | 31 | static Modpack loadZip(Path path) throws IOException { 32 | try (FileSystem fs = FileSystems.newFileSystem(URI.create("jar:" + path.toAbsolutePath().normalize().toUri()), Map.of())) { 33 | for (Type type : Type.values()) { 34 | Path manifest = fs.getPath("/").resolve(type.manifestPath).toAbsolutePath().normalize(); 35 | if (Files.isRegularFile(manifest)) { 36 | return load(manifest, type); 37 | } 38 | } 39 | throw new IOException("Failed to load modpack: Format unknown, no manifest file found in archive"); 40 | } 41 | } 42 | 43 | static Modpack load(Path path) throws IOException { 44 | JsonElement json; 45 | try (Reader reader = Files.newBufferedReader(path)) { 46 | json = Main.GSON.fromJson(reader, JsonElement.class); 47 | } 48 | for (Type type : Type.values()) { 49 | Optional pack = type.factory.apply(json); 50 | if (pack.isPresent()) { 51 | return pack.get(); 52 | } 53 | } 54 | throw new IOException("Failed to load modpack: Format unknown, manifest file has no known format"); 55 | } 56 | 57 | static Modpack load(Path path, Type type) throws IOException { 58 | JsonElement json; 59 | try (Reader reader = Files.newBufferedReader(path)) { 60 | json = Main.GSON.fromJson(reader, JsonElement.class); 61 | } 62 | Optional pack = type.factory.apply(json); 63 | if (pack.isEmpty()) { 64 | throw new IOException("Invalid " + type.name().toLowerCase(Locale.ROOT) + " modpack: Invalid manifest"); 65 | } else { 66 | return pack.get(); 67 | } 68 | } 69 | 70 | interface File { 71 | String projectSlug(); 72 | String projectName(); 73 | String fileName(); 74 | String fileId(); 75 | String author(); 76 | URI projectWebsite(); 77 | URI fileWebsite(); 78 | URI authorWebsite(); 79 | } 80 | 81 | record DefaultFile(String projectSlug, String projectName, String fileName, String fileId, String author, URI projectWebsite, URI fileWebsite, URI authorWebsite) implements File {} 82 | 83 | record Minecraft(String version, String loader, String loaderVersion) {} 84 | 85 | enum Type { 86 | CURSEFORGE("manifest.json", CurseModpack::load), 87 | MODRINTH("modrinth.index.json", ModrinthModpack::load); 88 | 89 | private final String manifestPath; 90 | private final IOFunction> factory; 91 | 92 | Type(String manifestPath, IOFunction> factory) { 93 | this.manifestPath = manifestPath; 94 | this.factory = factory; 95 | } 96 | } 97 | 98 | @FunctionalInterface 99 | interface IOFunction { 100 | R apply(T arg) throws IOException; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/org/moddingx/modlistcreator/changelogger/ChangelogFormatter.java: -------------------------------------------------------------------------------- 1 | package org.moddingx.modlistcreator.changelogger; 2 | 3 | import org.moddingx.modlistcreator.output.OutputTarget; 4 | import org.moddingx.modlistcreator.platform.Modpack; 5 | 6 | import java.util.Comparator; 7 | import java.util.List; 8 | import java.util.Locale; 9 | import java.util.Map; 10 | import java.util.stream.Collectors; 11 | 12 | public class ChangelogFormatter { 13 | 14 | public static String format(Modpack from, Modpack to, OutputTarget.Type outputType, boolean noHeader) { 15 | Map oldInfoBySlug = from.files().stream().collect(Collectors.toMap(Modpack.File::projectSlug, info -> info)); 16 | Map newInfoBySlug = to.files().stream().collect(Collectors.toMap(Modpack.File::projectSlug, info -> info)); 17 | 18 | boolean changedLoader = !from.minecraft().loaderVersion().equals(to.minecraft().loaderVersion()); 19 | List added = to.files().stream() 20 | .filter(file -> !oldInfoBySlug.containsKey(file.projectSlug())) 21 | .sorted(Comparator.comparing(o -> o.projectName().toLowerCase(Locale.ROOT))) 22 | .toList(); 23 | List removed = from.files().stream() 24 | .filter(file -> !newInfoBySlug.containsKey(file.projectSlug())) 25 | .sorted(Comparator.comparing(o -> o.projectName().toLowerCase(Locale.ROOT))) 26 | .toList(); 27 | record ChangedFile(Modpack.File oldFile, Modpack.File newFile) {} 28 | List changed = to.files().stream() 29 | .filter(file -> oldInfoBySlug.containsKey(file.projectSlug())) 30 | .filter(file -> !oldInfoBySlug.get(file.projectSlug()).fileId().equals(file.fileId())) 31 | .sorted(Comparator.comparing(o -> o.projectName().toLowerCase(Locale.ROOT))) 32 | .map(file -> new ChangedFile(oldInfoBySlug.get(file.projectSlug()), file)) 33 | .toList(); 34 | 35 | OutputTarget target = outputType.create(); 36 | 37 | if (!noHeader) { 38 | target.addHeader(from.title() + " - " + from.version() + " -> " + to.version()); 39 | } 40 | 41 | if (changedLoader) { 42 | target.addSubHeader(from.minecraft().loader() + " - " + from.minecraft().loaderVersion() + " -> " + to.minecraft().loaderVersion()); 43 | } 44 | 45 | if (added.isEmpty() && removed.isEmpty() && changed.isEmpty()) { 46 | return target.result(); 47 | } 48 | 49 | if (!added.isEmpty()) { 50 | target.addSubHeader("Added"); 51 | target.beginList(false); 52 | for (Modpack.File file : added) { 53 | target.addListElement(target.formatLink(file.projectName(), file.projectWebsite()) + " (by " + target.formatLink(file.author(), file.authorWebsite()) + ")"); 54 | } 55 | target.endList(); 56 | } 57 | 58 | if (!removed.isEmpty()) { 59 | target.addSubHeader("Removed"); 60 | target.beginList(false); 61 | for (Modpack.File file : removed) { 62 | target.addListElement(target.formatLink(file.projectName(), file.projectWebsite()) + " (by " + target.formatLink(file.author(), file.authorWebsite()) + ")"); 63 | } 64 | target.endList(); 65 | } 66 | 67 | if (!changed.isEmpty()) { 68 | target.addSubHeader("Changed"); 69 | target.beginList(false); 70 | for (ChangedFile changedFile : changed) { 71 | String oldFile = target.formatLink(changedFile.oldFile.fileName(), changedFile.oldFile.fileWebsite()); 72 | String newFile = target.formatLink(changedFile.newFile.fileName(), changedFile.newFile.fileWebsite()); 73 | target.addListElement(oldFile + " -> " + newFile); 74 | } 75 | target.endList(); 76 | } 77 | 78 | return target.result(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/org/moddingx/modlistcreator/modlist/ModListCreator.java: -------------------------------------------------------------------------------- 1 | package org.moddingx.modlistcreator.modlist; 2 | 3 | import joptsimple.OptionException; 4 | import joptsimple.OptionParser; 5 | import joptsimple.OptionSet; 6 | import joptsimple.OptionSpec; 7 | import joptsimple.util.PathConverter; 8 | import joptsimple.util.PathProperties; 9 | import org.moddingx.modlistcreator.output.OutputTarget; 10 | import org.moddingx.modlistcreator.platform.Modpack; 11 | import org.moddingx.modlistcreator.util.EnumConverters; 12 | import org.moddingx.modlistcreator.util.OptionUtil; 13 | 14 | import java.io.IOException; 15 | import java.nio.file.Files; 16 | import java.nio.file.Path; 17 | import java.nio.file.StandardOpenOption; 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | import java.util.concurrent.ExecutionException; 21 | import java.util.concurrent.Future; 22 | import java.util.concurrent.ScheduledExecutorService; 23 | import java.util.concurrent.ScheduledThreadPoolExecutor; 24 | import java.util.function.BiFunction; 25 | 26 | public class ModListCreator { 27 | 28 | public static void run(String[] args) throws IOException { 29 | OptionParser options = new OptionParser(); 30 | OptionSpec specNoHeader = options.accepts("no-header", "Generates the file without pack name and version"); 31 | OptionSpec specDetailed = options.accepts("detailed", "Shows exact version of each mod"); 32 | OptionSpec specFormat = options.accepts("format", "The output format to use").withRequiredArg().withValuesConvertedBy(EnumConverters.enumArg(OutputTarget.Type.class)).withValuesSeparatedBy(",").defaultsTo(OutputTarget.Type.MARKDOWN); 33 | OptionSpec specOutput = options.accepts("output", "Defines the output path for generated files. If --pattern is set, describes a directory for output files, else a concrete file.").withRequiredArg().withValuesConvertedBy(new PathConverter()); 34 | OptionSpec specPattern = options.accepts("pattern", "Defines the output file name pattern. %n is replaced with pack name, %v with pack version.").withRequiredArg().ofType(String.class); 35 | OptionSpec specInput = options.nonOptions("Input files. Can be either modpack zips or json files.").withValuesConvertedBy(new PathConverter(PathProperties.FILE_EXISTING, PathProperties.READABLE)); 36 | 37 | OptionSet set; 38 | try { 39 | set = options.parse(args); 40 | if (!set.has(specOutput)) OptionUtil.missing(options, specOutput); 41 | if (!set.has(specPattern) && set.valuesOf(specFormat).size() != 1) OptionUtil.missing(options, specPattern, "Name pattern needed for multiple output formats"); 42 | if (!set.has(specPattern) && set.valuesOf(specInput).size() != 1) OptionUtil.missing(options, specPattern, "Name pattern needed for multiple input files"); 43 | if (set.valuesOf(specInput).isEmpty()) OptionUtil.missing(options, specInput, "No inputs"); 44 | } catch (OptionException e) { 45 | System.err.println(e.getMessage() + "\n"); 46 | options.printHelpOn(System.err); 47 | System.exit(1); 48 | throw new Error(); 49 | } 50 | 51 | BiFunction outputPaths = OptionUtil.outputPathFunc(set.valueOf(specOutput), set.has(specPattern) ? set.valueOf(specPattern) : null); 52 | List outputTypes = set.valuesOf(specFormat).stream().distinct().toList(); 53 | boolean includeHeader = !set.has(specNoHeader); 54 | boolean detailed = set.has(specDetailed); 55 | List inputs = set.valuesOf(specInput); 56 | 57 | ScheduledExecutorService executor = new ScheduledThreadPoolExecutor(Math.min(inputs.size(), Runtime.getRuntime().availableProcessors() - 1)); 58 | List> joins = new ArrayList<>(); 59 | for (Path path : inputs) { 60 | joins.add(executor.submit(() -> { 61 | try { 62 | Modpack pack = Modpack.fromPath(path); 63 | for (OutputTarget.Type type : outputTypes) { 64 | Path outputPath = outputPaths.apply(pack, type); 65 | if (!Files.exists(outputPath.getParent())) { 66 | Files.createDirectories(outputPath.getParent()); 67 | } 68 | Files.writeString(outputPath, ModListFormatter.format(pack, type, includeHeader, detailed), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); 69 | } 70 | } catch (IOException e) { 71 | throw new RuntimeException("Failed for path: " + path, e); 72 | } 73 | })); 74 | } 75 | for (Future future : joins) { 76 | try { 77 | future.get(); 78 | } catch (InterruptedException e) { 79 | throw new RuntimeException(e); 80 | } catch (ExecutionException e) { 81 | e.getCause().printStackTrace(); 82 | } 83 | } 84 | System.exit(0); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/org/moddingx/modlistcreator/platform/CurseModpack.java: -------------------------------------------------------------------------------- 1 | package org.moddingx.modlistcreator.platform; 2 | 3 | import com.google.gson.JsonArray; 4 | import com.google.gson.JsonElement; 5 | import com.google.gson.JsonObject; 6 | import com.google.gson.JsonSyntaxException; 7 | import org.moddingx.cursewrapper.api.CurseWrapper; 8 | import org.moddingx.cursewrapper.api.response.FileInfo; 9 | import org.moddingx.cursewrapper.api.response.ProjectInfo; 10 | 11 | import java.io.IOException; 12 | import java.net.URI; 13 | import java.net.URLEncoder; 14 | import java.nio.charset.StandardCharsets; 15 | import java.util.*; 16 | import java.util.concurrent.TimeUnit; 17 | 18 | public record CurseModpack( 19 | String title, 20 | Modpack.Minecraft minecraft, 21 | String version, 22 | List files 23 | ) implements Modpack { 24 | 25 | public static final int MANIFEST_VERSION = 1; 26 | private static final CurseWrapper API = new CurseWrapper(URI.create("https://curse.moddingx.org/")); 27 | 28 | 29 | public static Optional load(JsonElement json) throws IOException { 30 | if (!json.isJsonObject() || !json.getAsJsonObject().has("manifestVersion") || json.getAsJsonObject().get("manifestVersion").getAsInt() != MANIFEST_VERSION) { 31 | return Optional.empty(); 32 | } 33 | JsonObject obj = json.getAsJsonObject(); 34 | String title = Objects.requireNonNull(obj.get("name"), "Missing property: name").getAsString(); 35 | String version = obj.has("version") ? obj.get("version").getAsString() : "unknown"; 36 | 37 | JsonObject minecraft = Objects.requireNonNull(obj.get("minecraft"), "Missing property: minecraft").getAsJsonObject(); 38 | String mcVersion = Objects.requireNonNull(minecraft.get("version"), "Missing property: minecraft.version").getAsString(); 39 | JsonArray loaderArray = Objects.requireNonNull(minecraft.get("modLoaders"), "Missing property: minecraft.modLoaders").getAsJsonArray(); 40 | if (loaderArray.size() != 1) throw new JsonSyntaxException("Modpack must define exactly one mod loader"); 41 | String loaderId = Objects.requireNonNull(loaderArray.get(0).getAsJsonObject().get("id"), "Missing property: minecraft.modLoaders[0].id").getAsString(); 42 | if (!loaderId.contains("-")) throw new JsonSyntaxException("Modpack loader id is invalid: " + loaderId); 43 | 44 | JsonArray filesArray = Objects.requireNonNull(obj.get("files"), "Missing property: files").getAsJsonArray(); 45 | Set projectIds = new HashSet<>(); 46 | Map fileIds = new HashMap<>(); 47 | for (int i = 0; i < filesArray.size(); i++) { 48 | JsonObject fileObj = filesArray.get(i).getAsJsonObject(); 49 | int projectId = Objects.requireNonNull(fileObj.get("projectID"), "Missing property: files[" + i + "].projectID").getAsInt(); 50 | int fileId = Objects.requireNonNull(fileObj.get("fileID"), "Missing property: files[" + i + "].fileID").getAsInt(); 51 | projectIds.add(projectId); 52 | fileIds.put(projectId, fileId); 53 | } 54 | Map resolvedProjects = API.getProjects(projectIds); 55 | if (projectIds.stream().anyMatch(id -> !resolvedProjects.containsKey(id))) { 56 | throw new IllegalStateException("Not all projects could be resolved."); 57 | } 58 | List files = fileIds.entrySet().stream() 59 | .map(entry -> new CurseFile(resolvedProjects.get(entry.getKey()), entry.getValue())) 60 | .toList(); 61 | 62 | return Optional.of(new CurseModpack(title, new Modpack.Minecraft( 63 | mcVersion, loaderId.substring(0, loaderId.indexOf('-')), loaderId.substring(loaderId.indexOf('-') + 1) 64 | ), version, List.copyOf(files))); 65 | } 66 | 67 | // Don't resolve file name and URL if not needed 68 | private static class CurseFile implements Modpack.File { 69 | 70 | private final ProjectInfo project; 71 | private final int fileId; 72 | private FileInfo file; 73 | private int tries = 0; 74 | 75 | private CurseFile(ProjectInfo project, int fileId) { 76 | this.project = project; 77 | this.fileId = fileId; 78 | this.file = null; 79 | } 80 | 81 | @Override 82 | public String projectSlug() { 83 | return this.project.slug(); 84 | } 85 | 86 | @Override 87 | public String projectName() { 88 | return this.project.name(); 89 | } 90 | 91 | @Override 92 | public String fileName() { 93 | if (this.file == null) { 94 | try { 95 | this.file = API.getFile(this.project.projectId(), this.fileId); 96 | System.out.println("Retrieved detailed information for \u001B[33m" + this.file.name() + "\u001B[0m"); 97 | } catch (IOException e) { 98 | if (this.tries < 5) { 99 | System.out.println("Failed to retrieve detailed information for project \u001B[33m" + this.project.name() + "\u001B[0m. Try again (" + ++this.tries + "/5)"); 100 | try { 101 | TimeUnit.SECONDS.sleep(3); 102 | } catch (InterruptedException ignored) {} 103 | return this.fileName(); 104 | } 105 | 106 | throw new RuntimeException(e); 107 | } 108 | } 109 | return this.file.name(); 110 | } 111 | 112 | @Override 113 | public String fileId() { 114 | return String.valueOf(this.fileId); 115 | } 116 | 117 | @Override 118 | public String author() { 119 | return this.project.owner(); 120 | } 121 | 122 | @Override 123 | public URI projectWebsite() { 124 | return this.project.website(); 125 | } 126 | 127 | @Override 128 | public URI fileWebsite() { 129 | return URI.create(this.projectWebsite() + "/").resolve("files/" + this.fileId); 130 | } 131 | 132 | @Override 133 | public URI authorWebsite() { 134 | return URI.create("https://www.curseforge.com/members/" + URLEncoder.encode(this.author(), StandardCharsets.UTF_8) + "/projects"); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original 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 POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /src/main/java/org/moddingx/modlistcreator/platform/ModrinthModpack.java: -------------------------------------------------------------------------------- 1 | package org.moddingx.modlistcreator.platform; 2 | 3 | import com.google.gson.*; 4 | import org.moddingx.modlistcreator.Main; 5 | 6 | import java.io.IOException; 7 | import java.net.URI; 8 | import java.net.URLEncoder; 9 | import java.net.http.HttpClient; 10 | import java.net.http.HttpRequest; 11 | import java.net.http.HttpResponse; 12 | import java.nio.charset.StandardCharsets; 13 | import java.util.*; 14 | 15 | public record ModrinthModpack( 16 | String title, 17 | Modpack.Minecraft minecraft, 18 | String version, 19 | List files 20 | ) implements Modpack { 21 | 22 | public static final int FORMAT_VERSION = 1; 23 | private static final HttpClient CLIENT = HttpClient.newHttpClient(); 24 | 25 | public static Optional load(JsonElement json) throws IOException { 26 | if (!json.isJsonObject() || !json.getAsJsonObject().has("formatVersion") || json.getAsJsonObject().get("formatVersion").getAsInt() != FORMAT_VERSION) { 27 | return Optional.empty(); 28 | } 29 | JsonObject obj = json.getAsJsonObject(); 30 | String title = Objects.requireNonNull(obj.get("name"), "Missing property: name").getAsString(); 31 | String version = Objects.requireNonNull(obj.get("versionId"), "Missing property: version").getAsString(); 32 | 33 | JsonObject deps = Objects.requireNonNull(obj.get("dependencies"), "Missing property: dependencies").getAsJsonObject(); 34 | String mcVersion = Objects.requireNonNull(deps.get("minecraft"), "Missing property: dependencies.minecraft").getAsString(); 35 | 36 | if (deps.size() != 2) throw new JsonSyntaxException("Modpack must specify exactly one loader dependency alog side its minecraft dependency"); 37 | String loader = null; 38 | String loaderVersion = null; 39 | for (String key : deps.keySet()) { 40 | if ("minecraft".equals(key)) continue; 41 | loader = key.endsWith("-loader") ? key.substring(0, key.length() - 7) : key; 42 | loaderVersion = deps.get(key).getAsString(); 43 | } 44 | 45 | JsonArray filesArray = Objects.requireNonNull(obj.get("files"), "Missing property: files").getAsJsonArray(); 46 | Set hashes = new HashSet<>(); 47 | for (int i = 0; i < filesArray.size(); i++) { 48 | JsonObject fileObj = filesArray.get(i).getAsJsonObject(); 49 | JsonObject hashesObj = Objects.requireNonNull(fileObj.get("hashes"), "Missing property: files[" + i + "].hashes").getAsJsonObject(); 50 | String sha512 = Objects.requireNonNull(hashesObj.get("sha512"), "Missing property: files[" + i + "].hashes.sha512").getAsString(); 51 | hashes.add(sha512); 52 | } 53 | 54 | JsonObject requestData = new JsonObject(); 55 | requestData.addProperty("algorithm", "sha512"); 56 | JsonArray hashesArray = new JsonArray(); 57 | hashes.forEach(hashesArray::add); 58 | requestData.add("hashes", hashesArray); 59 | 60 | List files = new ArrayList<>(); 61 | try { 62 | JsonObject filesResponse = makeRequest(HttpRequest.newBuilder() 63 | .uri(URI.create("https://api.modrinth.com/v2/version_files")) 64 | .POST(HttpRequest.BodyPublishers.ofString(Main.GSON.toJson(requestData), StandardCharsets.UTF_8)) 65 | .header("Content-Type", "application/json") 66 | ).getAsJsonObject(); 67 | 68 | record FileData(String projectId, String versionId, String fileName) {} 69 | List fileData = new ArrayList<>(); 70 | 71 | for (String hash : hashes) { 72 | if (!filesResponse.has(hash)) { 73 | throw new IllegalArgumentException("File not hosted on modrinth: sha512=" + hash); 74 | } 75 | 76 | JsonObject versionData = filesResponse.get(hash).getAsJsonObject(); 77 | String fileName = null; 78 | JsonArray versionFiles = versionData.get("files").getAsJsonArray(); 79 | if (versionFiles.size() == 1) { 80 | fileName = versionFiles.get(0).getAsJsonObject().get("filename").getAsString(); 81 | } else { 82 | for (JsonElement versionFile : versionFiles) { 83 | if (versionFile.getAsJsonObject().get("primary").getAsBoolean()) { 84 | fileName = versionFile.getAsJsonObject().get("filename").getAsString(); 85 | break; 86 | } 87 | } 88 | } 89 | 90 | if (fileName == null) { 91 | throw new IOException("Version has no primary file"); 92 | } 93 | fileData.add(new FileData(versionData.get("project_id").getAsString(), versionData.get("id").getAsString(), fileName)); 94 | } 95 | 96 | JsonArray allProjectIds = new JsonArray(); 97 | fileData.stream().map(FileData::projectId).distinct().forEach(allProjectIds::add); 98 | JsonArray projectsResponse = makeRequest(HttpRequest.newBuilder() 99 | .GET() 100 | .uri(URI.create("https://api.modrinth.com/v2/projects?ids=" + URLEncoder.encode(Main.GSON.toJson(allProjectIds), StandardCharsets.UTF_8))) 101 | ).getAsJsonArray(); 102 | 103 | record ProjectData(String slug, String name, URI website, String teamId, String organizationId) {} 104 | Map projectData = new HashMap<>(); 105 | for (JsonElement entry : projectsResponse) { 106 | JsonObject projectEntry = entry.getAsJsonObject(); 107 | projectData.put(projectEntry.get("id").getAsString(), new ProjectData( 108 | projectEntry.get("slug").getAsString(), 109 | projectEntry.get("title").getAsString(), 110 | URI.create("https://modrinth.com/" + URLEncoder.encode(projectEntry.get("project_type").getAsString(), StandardCharsets.UTF_8) + "/" + URLEncoder.encode(projectEntry.get("slug").getAsString(), StandardCharsets.UTF_8)), 111 | projectEntry.get("team").getAsString(), 112 | projectEntry.get("organization").isJsonNull() ? null : projectEntry.get("organization").getAsString() 113 | )); 114 | } 115 | 116 | JsonArray allOrganizationIds = new JsonArray(); 117 | projectData.values().stream().filter(pd -> pd.organizationId != null).map(ProjectData::organizationId).distinct().forEach(allOrganizationIds::add); 118 | JsonArray organizationsResponse = makeRequest(HttpRequest.newBuilder() 119 | .GET() 120 | .uri(URI.create("https://api.modrinth.com/v3/organizations?ids=" + URLEncoder.encode(Main.GSON.toJson(allOrganizationIds), StandardCharsets.UTF_8))) 121 | ).getAsJsonArray(); 122 | 123 | JsonArray allTeamIds = new JsonArray(); 124 | projectData.values().stream().map(ProjectData::teamId).distinct().forEach(allTeamIds::add); 125 | JsonArray teamsResponse = makeRequest(HttpRequest.newBuilder() 126 | .GET() 127 | .uri(URI.create("https://api.modrinth.com/v2/teams?ids=" + URLEncoder.encode(Main.GSON.toJson(allTeamIds), StandardCharsets.UTF_8))) 128 | ).getAsJsonArray(); 129 | 130 | record TeamData(String owner, URI teamURL) {} 131 | Map teamData = new HashMap<>(); 132 | for (JsonElement entryArr : teamsResponse) { 133 | for (JsonElement entry : entryArr.getAsJsonArray()) { 134 | JsonObject teamEntry = entry.getAsJsonObject(); 135 | if ("Owner".equals(teamEntry.get("role").getAsString())) { // todo v3 -> change to is_owner 136 | JsonObject user = teamEntry.get("user").getAsJsonObject(); 137 | String name = user.get("username").getAsString(); 138 | teamData.put(teamEntry.get("team_id").getAsString(), new TeamData( 139 | name, URI.create("https://modrinth.com/user/" + URLEncoder.encode(name, StandardCharsets.UTF_8)) 140 | )); 141 | } 142 | } 143 | } 144 | 145 | for (JsonElement entryArr : organizationsResponse) { 146 | JsonObject orgEntry = entryArr.getAsJsonObject(); 147 | teamData.put(orgEntry.get("id").getAsString(), new TeamData( 148 | orgEntry.get("name").getAsString(), URI.create("https://modrinth.com/organization/" + URLEncoder.encode(orgEntry.get("slug").getAsString(), StandardCharsets.UTF_8)) 149 | )); 150 | } 151 | 152 | for (FileData fd : fileData) { 153 | ProjectData pd = projectData.get(fd.projectId()); 154 | if (pd == null) throw new IOException("Project not resolved: " + fd.projectId()); 155 | TeamData td = teamData.get(pd.organizationId() == null ? pd.teamId() : pd.organizationId()); 156 | if (td == null) throw new IOException("Team not resolved: " + pd.teamId() + " (of project " + pd.slug() + ")"); 157 | files.add(new DefaultFile(pd.slug(), pd.name(), fd.fileName(), fd.versionId(), td.owner(), pd.website(), URI.create(pd.website() + "/").resolve("version/" + fd.versionId()), td.teamURL())); 158 | } 159 | } catch (JsonParseException e) { 160 | throw new IOException("Failed to query modrinth api", e); 161 | } 162 | 163 | return Optional.of(new ModrinthModpack(title, new Modpack.Minecraft(mcVersion, loader, loaderVersion), version, List.copyOf(files))); 164 | } 165 | 166 | private static JsonElement makeRequest(HttpRequest.Builder builder) throws IOException { 167 | HttpRequest request = builder 168 | .header("Accept", "application/json") 169 | .header("User-Agent", "ModdingX/ModListCreator") 170 | .build(); 171 | try { 172 | String response = CLIENT.send(request, resp -> { 173 | if ((resp.statusCode() / 100) == 2 && resp.statusCode() != 204) { 174 | return HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8); 175 | } else { 176 | return HttpResponse.BodySubscribers.replacing("\0" + resp.statusCode()); 177 | } 178 | }).body(); 179 | if (response.startsWith("\0")) { 180 | throw new IOException("HTTP " + response.substring(1)); 181 | } else { 182 | try { 183 | return Main.GSON.fromJson(response, JsonElement.class); 184 | } catch (JsonParseException e) { 185 | throw new IOException("Invalid jso nresponse from modrinth api: " + response, e); 186 | } 187 | } 188 | } catch (InterruptedException e) { 189 | Thread.currentThread().interrupt(); 190 | throw new IOException("Interrupted"); 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | --------------------------------------------------------------------------------