[
  {
    "path": ".gitattributes",
    "content": "#\r\n# https://help.github.com/articles/dealing-with-line-endings/\r\n#\r\n# These are explicitly windows files and should use crlf\r\n*.bat           text eol=crlf\r\n\r\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: 'BUG :'\nlabels: bug\nassignees: FlowArg\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Desktop (please complete the following information):**\n - OS: [e.g. Windows]\n - Version [e.g. 10]\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: 'Feature Request :'\nlabels: feature request\nassignees: FlowArg\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/workflows/docs.yml",
    "content": "name: CI Documentation\n\non:\n  push:\n    branches: [ master ]\n\njobs:\n  docs:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      packages: write\n\n    steps:\n      - uses: actions/checkout@v5\n      - name: Set up JDK 21\n        uses: actions/setup-java@v4\n        with:\n          java-version: '21'\n          distribution: 'zulu'\n\n      - name: Build documentation\n        run: gradle javadoc\n      - name: Publish Github Pages\n        uses: peaceiris/actions-gh-pages@v4\n        with:\n          personal_token: ${{ secrets.FLOW_TOKEN }}\n          publish_dir: ./build/docs/javadoc\n"
  },
  {
    "path": ".github/workflows/gradle-ci.yml",
    "content": "name: Gradle CI\n\non:\n  push:\n    branches: [ master ]\n\njobs:\n  testjava:\n    strategy:\n      matrix:\n        jdk: [17, 21]\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      packages: write\n\n    steps:\n      - uses: actions/checkout@v5\n      - name: Set up JDK ${{ matrix.jdk }}\n        uses: actions/setup-java@v4\n        with:\n          java-version: ${{ matrix.jdk }}\n          distribution: 'zulu'\n\n      - name: Build and test project with Java ${{ matrix.jdk }}\n        run: gradle build javadoc\n"
  },
  {
    "path": ".github/workflows/gradle-publish.yml",
    "content": "name: Gradle Package\n\non:\n  release:\n    types: [published]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      packages: write\n\n    steps:\n    - uses: actions/checkout@v5\n    - name: Set up JDK 21\n      uses: actions/setup-java@v4\n      with:\n        java-version: '21'\n        distribution: 'zulu'\n\n    - name: Publish FlowUpdater to MavenCentral\n      run: gradle publish\n      env:\n        OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }}\n        OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}\n        SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}\n        SONATYPE_TOKEN: ${{ secrets.SONATYPE_TOKEN }}\n        GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}\n        GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}\n        NEW_CENTRAL_ID: ${{ secrets.NEW_CENTRAL_ID }}\n        NEW_CENTRAL_TOKEN: ${{ secrets.NEW_CENTRAL_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": ".gradle\nbuild\nupdater\nrun\nsrc/test/java/fr/flowarg/flowupdatertest\n.idea\nout\nforge-installer.jar.log\nforge-installer-patched.jar.log\ninstaller.log\ntesting_directory\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\r\n                       Version 3, 29 June 2007\r\n\r\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\r\n Everyone is permitted to copy and distribute verbatim copies\r\n of this license document, but changing it is not allowed.\r\n\r\n                            Preamble\r\n\r\n  The GNU General Public License is a free, copyleft license for\r\nsoftware and other kinds of works.\r\n\r\n  The licenses for most software and other practical works are designed\r\nto take away your freedom to share and change the works.  By contrast,\r\nthe GNU General Public License is intended to guarantee your freedom to\r\nshare and change all versions of a program--to make sure it remains free\r\nsoftware for all its users.  We, the Free Software Foundation, use the\r\nGNU General Public License for most of our software; it applies also to\r\nany other work released this way by its authors.  You can apply it to\r\nyour programs, too.\r\n\r\n  When we speak of free software, we are referring to freedom, not\r\nprice.  Our General Public Licenses are designed to make sure that you\r\nhave the freedom to distribute copies of free software (and charge for\r\nthem if you wish), that you receive source code or can get it if you\r\nwant it, that you can change the software or use pieces of it in new\r\nfree programs, and that you know you can do these things.\r\n\r\n  To protect your rights, we need to prevent others from denying you\r\nthese rights or asking you to surrender the rights.  Therefore, you have\r\ncertain responsibilities if you distribute copies of the software, or if\r\nyou modify it: responsibilities to respect the freedom of others.\r\n\r\n  For example, if you distribute copies of such a program, whether\r\ngratis or for a fee, you must pass on to the recipients the same\r\nfreedoms that you received.  You must make sure that they, too, receive\r\nor can get the source code.  And you must show them these terms so they\r\nknow their rights.\r\n\r\n  Developers that use the GNU GPL protect your rights with two steps:\r\n(1) assert copyright on the software, and (2) offer you this License\r\ngiving you legal permission to copy, distribute and/or modify it.\r\n\r\n  For the developers' and authors' protection, the GPL clearly explains\r\nthat there is no warranty for this free software.  For both users' and\r\nauthors' sake, the GPL requires that modified versions be marked as\r\nchanged, so that their problems will not be attributed erroneously to\r\nauthors of previous versions.\r\n\r\n  Some devices are designed to deny users access to install or run\r\nmodified versions of the software inside them, although the manufacturer\r\ncan do so.  This is fundamentally incompatible with the aim of\r\nprotecting users' freedom to change the software.  The systematic\r\npattern of such abuse occurs in the area of products for individuals to\r\nuse, which is precisely where it is most unacceptable.  Therefore, we\r\nhave designed this version of the GPL to prohibit the practice for those\r\nproducts.  If such problems arise substantially in other domains, we\r\nstand ready to extend this provision to those domains in future versions\r\nof the GPL, as needed to protect the freedom of users.\r\n\r\n  Finally, every program is threatened constantly by software patents.\r\nStates should not allow patents to restrict development and use of\r\nsoftware on general-purpose computers, but in those that do, we wish to\r\navoid the special danger that patents applied to a free program could\r\nmake it effectively proprietary.  To prevent this, the GPL assures that\r\npatents cannot be used to render the program non-free.\r\n\r\n  The precise terms and conditions for copying, distribution and\r\nmodification follow.\r\n\r\n                       TERMS AND CONDITIONS\r\n\r\n  0. Definitions.\r\n\r\n  \"This License\" refers to version 3 of the GNU General Public License.\r\n\r\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\r\nworks, such as semiconductor masks.\r\n\r\n  \"The Program\" refers to any copyrightable work licensed under this\r\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\r\n\"recipients\" may be individuals or organizations.\r\n\r\n  To \"modify\" a work means to copy from or adapt all or part of the work\r\nin a fashion requiring copyright permission, other than the making of an\r\nexact copy.  The resulting work is called a \"modified version\" of the\r\nearlier work or a work \"based on\" the earlier work.\r\n\r\n  A \"covered work\" means either the unmodified Program or a work based\r\non the Program.\r\n\r\n  To \"propagate\" a work means to do anything with it that, without\r\npermission, would make you directly or secondarily liable for\r\ninfringement under applicable copyright law, except executing it on a\r\ncomputer or modifying a private copy.  Propagation includes copying,\r\ndistribution (with or without modification), making available to the\r\npublic, and in some countries other activities as well.\r\n\r\n  To \"convey\" a work means any kind of propagation that enables other\r\nparties to make or receive copies.  Mere interaction with a user through\r\na computer network, with no transfer of a copy, is not conveying.\r\n\r\n  An interactive user interface displays \"Appropriate Legal Notices\"\r\nto the extent that it includes a convenient and prominently visible\r\nfeature that (1) displays an appropriate copyright notice, and (2)\r\ntells the user that there is no warranty for the work (except to the\r\nextent that warranties are provided), that licensees may convey the\r\nwork under this License, and how to view a copy of this License.  If\r\nthe interface presents a list of user commands or options, such as a\r\nmenu, a prominent item in the list meets this criterion.\r\n\r\n  1. Source Code.\r\n\r\n  The \"source code\" for a work means the preferred form of the work\r\nfor making modifications to it.  \"Object code\" means any non-source\r\nform of a work.\r\n\r\n  A \"Standard Interface\" means an interface that either is an official\r\nstandard defined by a recognized standards body, or, in the case of\r\ninterfaces specified for a particular programming language, one that\r\nis widely used among developers working in that language.\r\n\r\n  The \"System Libraries\" of an executable work include anything, other\r\nthan the work as a whole, that (a) is included in the normal form of\r\npackaging a Major Component, but which is not part of that Major\r\nComponent, and (b) serves only to enable use of the work with that\r\nMajor Component, or to implement a Standard Interface for which an\r\nimplementation is available to the public in source code form.  A\r\n\"Major Component\", in this context, means a major essential component\r\n(kernel, window system, and so on) of the specific operating system\r\n(if any) on which the executable work runs, or a compiler used to\r\nproduce the work, or an object code interpreter used to run it.\r\n\r\n  The \"Corresponding Source\" for a work in object code form means all\r\nthe source code needed to generate, install, and (for an executable\r\nwork) run the object code and to modify the work, including scripts to\r\ncontrol those activities.  However, it does not include the work's\r\nSystem Libraries, or general-purpose tools or generally available free\r\nprograms which are used unmodified in performing those activities but\r\nwhich are not part of the work.  For example, Corresponding Source\r\nincludes interface definition files associated with source files for\r\nthe work, and the source code for shared libraries and dynamically\r\nlinked subprograms that the work is specifically designed to require,\r\nsuch as by intimate data communication or control flow between those\r\nsubprograms and other parts of the work.\r\n\r\n  The Corresponding Source need not include anything that users\r\ncan regenerate automatically from other parts of the Corresponding\r\nSource.\r\n\r\n  The Corresponding Source for a work in source code form is that\r\nsame work.\r\n\r\n  2. Basic Permissions.\r\n\r\n  All rights granted under this License are granted for the term of\r\ncopyright on the Program, and are irrevocable provided the stated\r\nconditions are met.  This License explicitly affirms your unlimited\r\npermission to run the unmodified Program.  The output from running a\r\ncovered work is covered by this License only if the output, given its\r\ncontent, constitutes a covered work.  This License acknowledges your\r\nrights of fair use or other equivalent, as provided by copyright law.\r\n\r\n  You may make, run and propagate covered works that you do not\r\nconvey, without conditions so long as your license otherwise remains\r\nin force.  You may convey covered works to others for the sole purpose\r\nof having them make modifications exclusively for you, or provide you\r\nwith facilities for running those works, provided that you comply with\r\nthe terms of this License in conveying all material for which you do\r\nnot control copyright.  Those thus making or running the covered works\r\nfor you must do so exclusively on your behalf, under your direction\r\nand control, on terms that prohibit them from making any copies of\r\nyour copyrighted material outside their relationship with you.\r\n\r\n  Conveying under any other circumstances is permitted solely under\r\nthe conditions stated below.  Sublicensing is not allowed; section 10\r\nmakes it unnecessary.\r\n\r\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\r\n\r\n  No covered work shall be deemed part of an effective technological\r\nmeasure under any applicable law fulfilling obligations under article\r\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\r\nsimilar laws prohibiting or restricting circumvention of such\r\nmeasures.\r\n\r\n  When you convey a covered work, you waive any legal power to forbid\r\ncircumvention of technological measures to the extent such circumvention\r\nis effected by exercising rights under this License with respect to\r\nthe covered work, and you disclaim any intention to limit operation or\r\nmodification of the work as a means of enforcing, against the work's\r\nusers, your or third parties' legal rights to forbid circumvention of\r\ntechnological measures.\r\n\r\n  4. Conveying Verbatim Copies.\r\n\r\n  You may convey verbatim copies of the Program's source code as you\r\nreceive it, in any medium, provided that you conspicuously and\r\nappropriately publish on each copy an appropriate copyright notice;\r\nkeep intact all notices stating that this License and any\r\nnon-permissive terms added in accord with section 7 apply to the code;\r\nkeep intact all notices of the absence of any warranty; and give all\r\nrecipients a copy of this License along with the Program.\r\n\r\n  You may charge any price or no price for each copy that you convey,\r\nand you may offer support or warranty protection for a fee.\r\n\r\n  5. Conveying Modified Source Versions.\r\n\r\n  You may convey a work based on the Program, or the modifications to\r\nproduce it from the Program, in the form of source code under the\r\nterms of section 4, provided that you also meet all of these conditions:\r\n\r\n    a) The work must carry prominent notices stating that you modified\r\n    it, and giving a relevant date.\r\n\r\n    b) The work must carry prominent notices stating that it is\r\n    released under this License and any conditions added under section\r\n    7.  This requirement modifies the requirement in section 4 to\r\n    \"keep intact all notices\".\r\n\r\n    c) You must license the entire work, as a whole, under this\r\n    License to anyone who comes into possession of a copy.  This\r\n    License will therefore apply, along with any applicable section 7\r\n    additional terms, to the whole of the work, and all its parts,\r\n    regardless of how they are packaged.  This License gives no\r\n    permission to license the work in any other way, but it does not\r\n    invalidate such permission if you have separately received it.\r\n\r\n    d) If the work has interactive user interfaces, each must display\r\n    Appropriate Legal Notices; however, if the Program has interactive\r\n    interfaces that do not display Appropriate Legal Notices, your\r\n    work need not make them do so.\r\n\r\n  A compilation of a covered work with other separate and independent\r\nworks, which are not by their nature extensions of the covered work,\r\nand which are not combined with it such as to form a larger program,\r\nin or on a volume of a storage or distribution medium, is called an\r\n\"aggregate\" if the compilation and its resulting copyright are not\r\nused to limit the access or legal rights of the compilation's users\r\nbeyond what the individual works permit.  Inclusion of a covered work\r\nin an aggregate does not cause this License to apply to the other\r\nparts of the aggregate.\r\n\r\n  6. Conveying Non-Source Forms.\r\n\r\n  You may convey a covered work in object code form under the terms\r\nof sections 4 and 5, provided that you also convey the\r\nmachine-readable Corresponding Source under the terms of this License,\r\nin one of these ways:\r\n\r\n    a) Convey the object code in, or embodied in, a physical product\r\n    (including a physical distribution medium), accompanied by the\r\n    Corresponding Source fixed on a durable physical medium\r\n    customarily used for software interchange.\r\n\r\n    b) Convey the object code in, or embodied in, a physical product\r\n    (including a physical distribution medium), accompanied by a\r\n    written offer, valid for at least three years and valid for as\r\n    long as you offer spare parts or customer support for that product\r\n    model, to give anyone who possesses the object code either (1) a\r\n    copy of the Corresponding Source for all the software in the\r\n    product that is covered by this License, on a durable physical\r\n    medium customarily used for software interchange, for a price no\r\n    more than your reasonable cost of physically performing this\r\n    conveying of source, or (2) access to copy the\r\n    Corresponding Source from a network server at no charge.\r\n\r\n    c) Convey individual copies of the object code with a copy of the\r\n    written offer to provide the Corresponding Source.  This\r\n    alternative is allowed only occasionally and noncommercially, and\r\n    only if you received the object code with such an offer, in accord\r\n    with subsection 6b.\r\n\r\n    d) Convey the object code by offering access from a designated\r\n    place (gratis or for a charge), and offer equivalent access to the\r\n    Corresponding Source in the same way through the same place at no\r\n    further charge.  You need not require recipients to copy the\r\n    Corresponding Source along with the object code.  If the place to\r\n    copy the object code is a network server, the Corresponding Source\r\n    may be on a different server (operated by you or a third party)\r\n    that supports equivalent copying facilities, provided you maintain\r\n    clear directions next to the object code saying where to find the\r\n    Corresponding Source.  Regardless of what server hosts the\r\n    Corresponding Source, you remain obligated to ensure that it is\r\n    available for as long as needed to satisfy these requirements.\r\n\r\n    e) Convey the object code using peer-to-peer transmission, provided\r\n    you inform other peers where the object code and Corresponding\r\n    Source of the work are being offered to the general public at no\r\n    charge under subsection 6d.\r\n\r\n  A separable portion of the object code, whose source code is excluded\r\nfrom the Corresponding Source as a System Library, need not be\r\nincluded in conveying the object code work.\r\n\r\n  A \"User Product\" is either (1) a \"consumer product\", which means any\r\ntangible personal property which is normally used for personal, family,\r\nor household purposes, or (2) anything designed or sold for incorporation\r\ninto a dwelling.  In determining whether a product is a consumer product,\r\ndoubtful cases shall be resolved in favor of coverage.  For a particular\r\nproduct received by a particular user, \"normally used\" refers to a\r\ntypical or common use of that class of product, regardless of the status\r\nof the particular user or of the way in which the particular user\r\nactually uses, or expects or is expected to use, the product.  A product\r\nis a consumer product regardless of whether the product has substantial\r\ncommercial, industrial or non-consumer uses, unless such uses represent\r\nthe only significant mode of use of the product.\r\n\r\n  \"Installation Information\" for a User Product means any methods,\r\nprocedures, authorization keys, or other information required to install\r\nand execute modified versions of a covered work in that User Product from\r\na modified version of its Corresponding Source.  The information must\r\nsuffice to ensure that the continued functioning of the modified object\r\ncode is in no case prevented or interfered with solely because\r\nmodification has been made.\r\n\r\n  If you convey an object code work under this section in, or with, or\r\nspecifically for use in, a User Product, and the conveying occurs as\r\npart of a transaction in which the right of possession and use of the\r\nUser Product is transferred to the recipient in perpetuity or for a\r\nfixed term (regardless of how the transaction is characterized), the\r\nCorresponding Source conveyed under this section must be accompanied\r\nby the Installation Information.  But this requirement does not apply\r\nif neither you nor any third party retains the ability to install\r\nmodified object code on the User Product (for example, the work has\r\nbeen installed in ROM).\r\n\r\n  The requirement to provide Installation Information does not include a\r\nrequirement to continue to provide support service, warranty, or updates\r\nfor a work that has been modified or installed by the recipient, or for\r\nthe User Product in which it has been modified or installed.  Access to a\r\nnetwork may be denied when the modification itself materially and\r\nadversely affects the operation of the network or violates the rules and\r\nprotocols for communication across the network.\r\n\r\n  Corresponding Source conveyed, and Installation Information provided,\r\nin accord with this section must be in a format that is publicly\r\ndocumented (and with an implementation available to the public in\r\nsource code form), and must require no special password or key for\r\nunpacking, reading or copying.\r\n\r\n  7. Additional Terms.\r\n\r\n  \"Additional permissions\" are terms that supplement the terms of this\r\nLicense by making exceptions from one or more of its conditions.\r\nAdditional permissions that are applicable to the entire Program shall\r\nbe treated as though they were included in this License, to the extent\r\nthat they are valid under applicable law.  If additional permissions\r\napply only to part of the Program, that part may be used separately\r\nunder those permissions, but the entire Program remains governed by\r\nthis License without regard to the additional permissions.\r\n\r\n  When you convey a copy of a covered work, you may at your option\r\nremove any additional permissions from that copy, or from any part of\r\nit.  (Additional permissions may be written to require their own\r\nremoval in certain cases when you modify the work.)  You may place\r\nadditional permissions on material, added by you to a covered work,\r\nfor which you have or can give appropriate copyright permission.\r\n\r\n  Notwithstanding any other provision of this License, for material you\r\nadd to a covered work, you may (if authorized by the copyright holders of\r\nthat material) supplement the terms of this License with terms:\r\n\r\n    a) Disclaiming warranty or limiting liability differently from the\r\n    terms of sections 15 and 16 of this License; or\r\n\r\n    b) Requiring preservation of specified reasonable legal notices or\r\n    author attributions in that material or in the Appropriate Legal\r\n    Notices displayed by works containing it; or\r\n\r\n    c) Prohibiting misrepresentation of the origin of that material, or\r\n    requiring that modified versions of such material be marked in\r\n    reasonable ways as different from the original version; or\r\n\r\n    d) Limiting the use for publicity purposes of names of licensors or\r\n    authors of the material; or\r\n\r\n    e) Declining to grant rights under trademark law for use of some\r\n    trade names, trademarks, or service marks; or\r\n\r\n    f) Requiring indemnification of licensors and authors of that\r\n    material by anyone who conveys the material (or modified versions of\r\n    it) with contractual assumptions of liability to the recipient, for\r\n    any liability that these contractual assumptions directly impose on\r\n    those licensors and authors.\r\n\r\n  All other non-permissive additional terms are considered \"further\r\nrestrictions\" within the meaning of section 10.  If the Program as you\r\nreceived it, or any part of it, contains a notice stating that it is\r\ngoverned by this License along with a term that is a further\r\nrestriction, you may remove that term.  If a license document contains\r\na further restriction but permits relicensing or conveying under this\r\nLicense, you may add to a covered work material governed by the terms\r\nof that license document, provided that the further restriction does\r\nnot survive such relicensing or conveying.\r\n\r\n  If you add terms to a covered work in accord with this section, you\r\nmust place, in the relevant source files, a statement of the\r\nadditional terms that apply to those files, or a notice indicating\r\nwhere to find the applicable terms.\r\n\r\n  Additional terms, permissive or non-permissive, may be stated in the\r\nform of a separately written license, or stated as exceptions;\r\nthe above requirements apply either way.\r\n\r\n  8. Termination.\r\n\r\n  You may not propagate or modify a covered work except as expressly\r\nprovided under this License.  Any attempt otherwise to propagate or\r\nmodify it is void, and will automatically terminate your rights under\r\nthis License (including any patent licenses granted under the third\r\nparagraph of section 11).\r\n\r\n  However, if you cease all violation of this License, then your\r\nlicense from a particular copyright holder is reinstated (a)\r\nprovisionally, unless and until the copyright holder explicitly and\r\nfinally terminates your license, and (b) permanently, if the copyright\r\nholder fails to notify you of the violation by some reasonable means\r\nprior to 60 days after the cessation.\r\n\r\n  Moreover, your license from a particular copyright holder is\r\nreinstated permanently if the copyright holder notifies you of the\r\nviolation by some reasonable means, this is the first time you have\r\nreceived notice of violation of this License (for any work) from that\r\ncopyright holder, and you cure the violation prior to 30 days after\r\nyour receipt of the notice.\r\n\r\n  Termination of your rights under this section does not terminate the\r\nlicenses of parties who have received copies or rights from you under\r\nthis License.  If your rights have been terminated and not permanently\r\nreinstated, you do not qualify to receive new licenses for the same\r\nmaterial under section 10.\r\n\r\n  9. Acceptance Not Required for Having Copies.\r\n\r\n  You are not required to accept this License in order to receive or\r\nrun a copy of the Program.  Ancillary propagation of a covered work\r\noccurring solely as a consequence of using peer-to-peer transmission\r\nto receive a copy likewise does not require acceptance.  However,\r\nnothing other than this License grants you permission to propagate or\r\nmodify any covered work.  These actions infringe copyright if you do\r\nnot accept this License.  Therefore, by modifying or propagating a\r\ncovered work, you indicate your acceptance of this License to do so.\r\n\r\n  10. Automatic Licensing of Downstream Recipients.\r\n\r\n  Each time you convey a covered work, the recipient automatically\r\nreceives a license from the original licensors, to run, modify and\r\npropagate that work, subject to this License.  You are not responsible\r\nfor enforcing compliance by third parties with this License.\r\n\r\n  An \"entity transaction\" is a transaction transferring control of an\r\norganization, or substantially all assets of one, or subdividing an\r\norganization, or merging organizations.  If propagation of a covered\r\nwork results from an entity transaction, each party to that\r\ntransaction who receives a copy of the work also receives whatever\r\nlicenses to the work the party's predecessor in interest had or could\r\ngive under the previous paragraph, plus a right to possession of the\r\nCorresponding Source of the work from the predecessor in interest, if\r\nthe predecessor has it or can get it with reasonable efforts.\r\n\r\n  You may not impose any further restrictions on the exercise of the\r\nrights granted or affirmed under this License.  For example, you may\r\nnot impose a license fee, royalty, or other charge for exercise of\r\nrights granted under this License, and you may not initiate litigation\r\n(including a cross-claim or counterclaim in a lawsuit) alleging that\r\nany patent claim is infringed by making, using, selling, offering for\r\nsale, or importing the Program or any portion of it.\r\n\r\n  11. Patents.\r\n\r\n  A \"contributor\" is a copyright holder who authorizes use under this\r\nLicense of the Program or a work on which the Program is based.  The\r\nwork thus licensed is called the contributor's \"contributor version\".\r\n\r\n  A contributor's \"essential patent claims\" are all patent claims\r\nowned or controlled by the contributor, whether already acquired or\r\nhereafter acquired, that would be infringed by some manner, permitted\r\nby this License, of making, using, or selling its contributor version,\r\nbut do not include claims that would be infringed only as a\r\nconsequence of further modification of the contributor version.  For\r\npurposes of this definition, \"control\" includes the right to grant\r\npatent sublicenses in a manner consistent with the requirements of\r\nthis License.\r\n\r\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\r\npatent license under the contributor's essential patent claims, to\r\nmake, use, sell, offer for sale, import and otherwise run, modify and\r\npropagate the contents of its contributor version.\r\n\r\n  In the following three paragraphs, a \"patent license\" is any express\r\nagreement or commitment, however denominated, not to enforce a patent\r\n(such as an express permission to practice a patent or covenant not to\r\nsue for patent infringement).  To \"grant\" such a patent license to a\r\nparty means to make such an agreement or commitment not to enforce a\r\npatent against the party.\r\n\r\n  If you convey a covered work, knowingly relying on a patent license,\r\nand the Corresponding Source of the work is not available for anyone\r\nto copy, free of charge and under the terms of this License, through a\r\npublicly available network server or other readily accessible means,\r\nthen you must either (1) cause the Corresponding Source to be so\r\navailable, or (2) arrange to deprive yourself of the benefit of the\r\npatent license for this particular work, or (3) arrange, in a manner\r\nconsistent with the requirements of this License, to extend the patent\r\nlicense to downstream recipients.  \"Knowingly relying\" means you have\r\nactual knowledge that, but for the patent license, your conveying the\r\ncovered work in a country, or your recipient's use of the covered work\r\nin a country, would infringe one or more identifiable patents in that\r\ncountry that you have reason to believe are valid.\r\n\r\n  If, pursuant to or in connection with a single transaction or\r\narrangement, you convey, or propagate by procuring conveyance of, a\r\ncovered work, and grant a patent license to some of the parties\r\nreceiving the covered work authorizing them to use, propagate, modify\r\nor convey a specific copy of the covered work, then the patent license\r\nyou grant is automatically extended to all recipients of the covered\r\nwork and works based on it.\r\n\r\n  A patent license is \"discriminatory\" if it does not include within\r\nthe scope of its coverage, prohibits the exercise of, or is\r\nconditioned on the non-exercise of one or more of the rights that are\r\nspecifically granted under this License.  You may not convey a covered\r\nwork if you are a party to an arrangement with a third party that is\r\nin the business of distributing software, under which you make payment\r\nto the third party based on the extent of your activity of conveying\r\nthe work, and under which the third party grants, to any of the\r\nparties who would receive the covered work from you, a discriminatory\r\npatent license (a) in connection with copies of the covered work\r\nconveyed by you (or copies made from those copies), or (b) primarily\r\nfor and in connection with specific products or compilations that\r\ncontain the covered work, unless you entered into that arrangement,\r\nor that patent license was granted, prior to 28 March 2007.\r\n\r\n  Nothing in this License shall be construed as excluding or limiting\r\nany implied license or other defenses to infringement that may\r\notherwise be available to you under applicable patent law.\r\n\r\n  12. No Surrender of Others' Freedom.\r\n\r\n  If conditions are imposed on you (whether by court order, agreement or\r\notherwise) that contradict the conditions of this License, they do not\r\nexcuse you from the conditions of this License.  If you cannot convey a\r\ncovered work so as to satisfy simultaneously your obligations under this\r\nLicense and any other pertinent obligations, then as a consequence you may\r\nnot convey it at all.  For example, if you agree to terms that obligate you\r\nto collect a royalty for further conveying from those to whom you convey\r\nthe Program, the only way you could satisfy both those terms and this\r\nLicense would be to refrain entirely from conveying the Program.\r\n\r\n  13. Use with the GNU Affero General Public License.\r\n\r\n  Notwithstanding any other provision of this License, you have\r\npermission to link or combine any covered work with a work licensed\r\nunder version 3 of the GNU Affero General Public License into a single\r\ncombined work, and to convey the resulting work.  The terms of this\r\nLicense will continue to apply to the part which is the covered work,\r\nbut the special requirements of the GNU Affero General Public License,\r\nsection 13, concerning interaction through a network will apply to the\r\ncombination as such.\r\n\r\n  14. Revised Versions of this License.\r\n\r\n  The Free Software Foundation may publish revised and/or new versions of\r\nthe GNU General Public License from time to time.  Such new versions will\r\nbe similar in spirit to the present version, but may differ in detail to\r\naddress new problems or concerns.\r\n\r\n  Each version is given a distinguishing version number.  If the\r\nProgram specifies that a certain numbered version of the GNU General\r\nPublic License \"or any later version\" applies to it, you have the\r\noption of following the terms and conditions either of that numbered\r\nversion or of any later version published by the Free Software\r\nFoundation.  If the Program does not specify a version number of the\r\nGNU General Public License, you may choose any version ever published\r\nby the Free Software Foundation.\r\n\r\n  If the Program specifies that a proxy can decide which future\r\nversions of the GNU General Public License can be used, that proxy's\r\npublic statement of acceptance of a version permanently authorizes you\r\nto choose that version for the Program.\r\n\r\n  Later license versions may give you additional or different\r\npermissions.  However, no additional obligations are imposed on any\r\nauthor or copyright holder as a result of your choosing to follow a\r\nlater version.\r\n\r\n  15. Disclaimer of Warranty.\r\n\r\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\r\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\r\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\r\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\r\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\r\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\r\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\r\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\r\n\r\n  16. Limitation of Liability.\r\n\r\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\r\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\r\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\r\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\r\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\r\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\r\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\r\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\r\nSUCH DAMAGES.\r\n\r\n  17. Interpretation of Sections 15 and 16.\r\n\r\n  If the disclaimer of warranty and limitation of liability provided\r\nabove cannot be given local legal effect according to their terms,\r\nreviewing courts shall apply local law that most closely approximates\r\nan absolute waiver of all civil liability in connection with the\r\nProgram, unless a warranty or assumption of liability accompanies a\r\ncopy of the Program in return for a fee.\r\n\r\n                     END OF TERMS AND CONDITIONS\r\n\r\n            How to Apply These Terms to Your New Programs\r\n\r\n  If you develop a new program, and you want it to be of the greatest\r\npossible use to the public, the best way to achieve this is to make it\r\nfree software which everyone can redistribute and change under these terms.\r\n\r\n  To do so, attach the following notices to the program.  It is safest\r\nto attach them to the start of each source file to most effectively\r\nstate the exclusion of warranty; and each file should have at least\r\nthe \"copyright\" line and a pointer to where the full notice is found.\r\n\r\n    FlowUpdater - The free and opensource solution to update Minecraft.\r\n    Copyright (C) 2020  Flow Arg (FlowArg)\r\n\r\n    This program is free software: you can redistribute it and/or modify\r\n    it under the terms of the GNU General Public License as published by\r\n    the Free Software Foundation, either version 3 of the License, or\r\n    (at your option) any later version.\r\n\r\n    This program is distributed in the hope that it will be useful,\r\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r\n    GNU General Public License for more details.\r\n\r\n    You should have received a copy of the GNU General Public License\r\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\r\n\r\nAlso add information on how to contact you by electronic and paper mail.\r\n\r\n  If the program does terminal interaction, make it output a short\r\nnotice like this when it starts in an interactive mode:\r\n\r\n    FlowUpdater  Copyright (C) 2020  Flow Arg (FlowArg)\r\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\r\n    This is free software, and you are welcome to redistribute it\r\n    under certain conditions; type `show c' for details.\r\n\r\nThe hypothetical commands `show w' and `show c' should show the appropriate\r\nparts of the General Public License.  Of course, your program's commands\r\nmight be different; for a GUI interface, you would use an \"about box\".\r\n\r\n  You should also get your employer (if you work as a programmer) or school,\r\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\r\nFor more information on this, and how to apply and follow the GNU GPL, see\r\n<https://www.gnu.org/licenses/>.\r\n\r\n  The GNU General Public License does not permit incorporating your program\r\ninto proprietary programs.  If your program is a subroutine library, you\r\nmay consider it more useful to permit linking proprietary applications with\r\nthe library.  If this is what you want to do, use the GNU Lesser General\r\nPublic License instead of this License.  But first, please read\r\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\r\n"
  },
  {
    "path": "README.MD",
    "content": "[version]: https://img.shields.io/maven-central/v/fr.flowarg/flowupdater.svg?label=Download\r\n[download]: https://search.maven.org/search?q=g:%22fr.flowarg%22%20AND%20a:%22flowupdater%22\r\n\r\n[discord-shield]: https://discordapp.com/api/guilds/730758985376071750/widget.png\r\n[discord-invite]: https://discord.gg/dN6HWHp\r\n\r\n[ ![version][] ][download]\r\n[ ![discord-shield][] ][discord-invite]\r\n\r\n# FlowUpdater\r\nWelcome on FlowUpdater's repository. FlowUpdater is a free and open source solution to update Minecraft in Java.\r\nIt was mainly designed for launcher's purposes but can be used for other usages as well. FlowUpdater focuses on customization and reliability.\r\nThe best documentation is the JavaDoc included in FlowUpdater's source code. The rest of the documentation (for instance this readme or the wiki tab on GitHub) has a chance of not being updated.\r\n\r\n## Legal and fork notices :warning:\r\nThe CurseForge integration works with an API Key which is mine at the moment. **You CAN'T use this key for other purposes outside FlowUpdater.**\r\nIf you wish to fork this project, **you HAVE TO use your own API Key**.\r\n\r\n## Alternatives\r\nIf you are a developer or know a developer who has made a similar library in another programming language,\r\nfeel free to ask to appear in this list:\r\n- [Rust Launcher Lib](https://github.com/knightmar/rust_launcher_lib) (Rust)\r\n\r\n## Usage\r\n\r\n### Vanilla\r\n\r\nFirst, create a new VanillaVersion, and build the version:\r\n```java\r\nVanillaVersion version = new VanillaVersion.VanillaVersionBuilder().withName(\"1.20.4\").build();\r\n```\r\n`VanillaVersion` accepts some arguments to add more libraries, assets or to reach snapshots or custom version of the game.\r\nAll accepted arguments are available in the `VanillaVersionBuilder` class.\r\n\r\nAdd the version to a new `FlowUpdater` instance and build it:\r\n```java\r\nFlowUpdater updater = new FlowUpdater.FlowUpdaterBuilder()\r\n        .withVanillaVersion(version)\r\n        .build();\r\n```\r\n\r\nIn the same way, `FlowUpdater` accepts many arguments that you can use as you want.\r\nThe more important ones to know about are: the logger, the progress callback, the vanilla version, possibly a mod loader version. The full list is available in the `FlowUpdaterBuilder` class.\r\n\r\n\r\nFinally, call the update function:\r\n```java\r\nupdater.update(Paths.get(\"your/path/\"));\r\n```\r\nThis `update` method will start the whole checks-and-download pipeline and will return when all the work is done.\r\nYou usually need to put this method in a new `Thread` / `ExecutorService` because apart from the assets part, all actions are run on the same thread.\r\n\r\n\r\n### Forge\r\n\r\n(You need to setup a vanilla version like above!)\r\n\r\nNext, make a List of Mod objects (except if you don't need some).\r\n```java\r\nList<Mod> mods = new ArrayList<>();\r\nmods.add(new Mod(\"OneMod.jar\", \"sha1ofmod\", 85120, \"https://link.com/of/mod.jar\"));\r\nmods.add(new Mod(\"AnotherMod.jar\", \"sha1ofanothermod\", 86120, \"https://link.com/of/another/mod.jar\"));\r\n```\r\nYou can also get a list of mods by providing a json link: `List<Mod> mods = Mod.getModsFromJson(\"https://url.com/launcher/mods.json\");`. A template is available in Mod class.\r\n\r\nYou can get mods from CurseForge too:\r\n```java\r\nList<CurseFileInfo> modInfos = new ArrayList<>();\r\n// project ID and file ID\r\nmodInfos.add(new CurseFileInfo(238222, 2988823));\r\n```\r\nYou can also get a list of curse mods by providing a json link: `List<CurseFileInfo> mods = CurseFileInfo.getFilesFromJson(\"https://url.com/launcher/cursemods.json\");`.\r\n\r\nOn the same pattern, you can get mods from Modrinth.\r\n\r\nThen, build a forge version. For example, I will build a NewForgeVersion.\r\n```java\r\nForgeVersion forgeVersion = new ForgeVersionBuilder()\r\n            .withForgeVersion(\"1.20.6-50.1.12\") // mandatory\r\n            .withCurseMods(modInfos) // optional\r\n            .withOptiFine(new OptiFineInfo(\"preview_OptiFine_1.20.6_HD_U_I9_pre1\")) // installing OptiFine (optional)\r\n            .withFileDeleter(new ModFileDeleter(\"jei.jar\")) // (optional, but recommended) delete bad mods, don't remove the file jei.jar if it's present in mods directory. You can also provide A `Pattern` with a regex rule.\r\n            .build();\r\n```\r\n\r\nFinally, set the Forge version object to your `FlowUpdaterBuilder`:\r\n```java\r\n.withModLoaderVersion(forgeVersion);\r\n```\r\n\r\n### NeoForge\r\nWorks almost the same way as Forge, but you need to use `NeoForgeVersion` instead of `ForgeVersion` and `NeoForgeVersionBuilder` instead of `ForgeVersionBuilder`.\r\nBe careful when passing the neoforge version, it must be in the forge format for 1.20.1 (1.20.1-47.1.5 for example) ; but you should only pass the neoforge version for versions >= 1.21 (21.8.31 for example).\r\n\r\n\r\n### Fabric\r\n\r\n(You need to setup a vanilla updater!)\r\n\r\nNext, make a List of Mod objects like for a ForgeVersion if you need some.\r\n\r\nThen, build a Fabric version.\r\n```java\r\nFabricVersion fabricVersion = new FabricVersionBuilder()\r\n            .withFabricVersion(\"0.10.8\") // optional, if you don't set one, it will take the latest fabric loader version available.\r\n            .withCurseMods(modInfos) // optional\r\n            .withMods(mods) // optional\r\n            .withFileDeleter(new ModFileDeleter(\"sodium.jar\")) // (optional but recommended) delete bad mods ; but it won't remove the file sodium.jar if it's present in the mods' dir.\r\n            .build();\r\n```\r\n\r\nFinally, set the Fabric version to your `FlowUpdaterBuilder`:\r\n```java\r\n.withModLoaderVersion(fabricVersion);\r\n```\r\n\r\n### MCP\r\n\r\n(You need to setup a vanilla updater!)\r\n\r\nThere are two ways to setup an MCP version. You can either (1) provide an MCP object (for a simple client for example) or (2) a JSON link to a custom json version which can contains custom assets, custom libraries etc...\r\n\r\n(1) set to vanilla version builder a MCP version:\r\n```java\r\n.withMCP(new MCP(\"clientURL\", \"clientSha1\", 25008229));\r\n```\r\nIf you set an empty/null string in url and sha1 and 0 in size, the updater will use the default minecraft jar.\r\nExample for a client-only mcp downloading:\r\n```java\r\n.withMCP(new MCP(\"https://mighya.eu/resources/Client.jar\", \"f2c219e485831af2bae9464eebbe4765128c6ad6\", 23005862));\r\n```\r\nYou can get an MCP object instance by providing a json link too: `.withMCP(\"https://url.com/launcher/mcp.json\");`.\r\n\r\n(2)\r\nStill in the vanilla version builder, set a json link to a custom MCP version:\r\n```java\r\n.withCustomVersionJson(new URL(\"https://url.com/launcher/mcp.json\"));\r\n```\r\n\r\nYou can also provide some more additional libraries or assets with all methods in the `VanillaVersionBuilder` class\r\n(`withAnotherLibraries`, `withAnotherAssets`, `withCustomAssetIndex`).\r\n\r\n## External Files\r\n\r\nWith FlowUpdater, you can download other files in your update dir! This system is designed mainly for configs, resource packs.\r\nYou can also configure a keep-policy for these files (should the updater download the file again if it is modified?).\r\nIn your FlowUpdaterBuilder, define an array list of ExternalFile (by `ExternalFile#getExternalFilesFromJson` for more convenience).\r\n\r\n### About json files...\r\n\r\n**Deprecated**: All json files can be generated by the [FlowUpdaterJsonCreator](https://github.com/FlowArg/FlowUpdaterJsonCreator)!\r\n\r\nThere are new tools made by the community that can help you generate some JSON files:\r\n- [FlowJsonCreator by Paulem79](https://github.com/Paulem79/FlowJsonCreator) (Java)\r\n- [FUJC by Zuygui](https://github.com/zuygui/flowupdater-json-creator) (Rust)\r\n\r\n## Post executions\r\n\r\nWith FlowUpdater, you can execute some actions after update, like patch a file, kill a process, launch a process, review a config etc...\r\nIn your FlowUpdaterBuilder, you have to set a list of Runnable.\r\nIt's not always relevant to use this feature, but it can be useful in some specific cases.\r\n"
  },
  {
    "path": "build.gradle",
    "content": "plugins {\r\n    id 'java-library'\r\n    id 'idea'\r\n    id 'maven-publish'\r\n    id 'signing'\r\n}\r\n\r\ngroup = 'fr.flowarg'\r\nversion = '1.9.4'\r\n\r\njava {\r\n    toolchain {\r\n        languageVersion = JavaLanguageVersion.of(17)\r\n    }\r\n    withJavadocJar()\r\n    withSourcesJar()\r\n}\r\n\r\ntasks.withType(JavaCompile).configureEach {\r\n    options.encoding = 'UTF-8'\r\n}\r\n\r\ntasks.named(\"compileJava\").configure {\r\n    options.release.set(8)\r\n}\r\n\r\nrepositories {\r\n    mavenCentral()\r\n    mavenLocal()\r\n}\r\n\r\ndependencies {\r\n    api libs.gson\r\n    api libs.flowmultitools\r\n    api libs.annotations\r\n\r\n    // Only for internal tests\r\n    testImplementation libs.oll\r\n    testImplementation libs.junit.jupiter\r\n    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'\r\n}\r\n\r\ntasks.withType(Jar).configureEach {\r\n    archiveBaseName.set(\"flowupdater\")\r\n}\r\n\r\npublishing {\r\n    publications {\r\n        mavenJava(MavenPublication) {\r\n            from components.java\r\n\r\n            pom {\r\n                groupId = project.group\r\n                version = project.version\r\n                artifactId = 'flowupdater'\r\n                name = project.name\r\n                description = 'The free and open source solution to update Minecraft.'\r\n                url = 'https://github.com/FlowArg/FlowUpdater'\r\n\r\n                scm {\r\n                    connection = 'scm:git:git://github.com/FlowArg/FlowUpdater.git'\r\n                    developerConnection = 'scm:git:ssh://github.com:FlowArg/FlowUpdater.git'\r\n                    url = 'https://github.com/FlowArg/FlowUpdater/tree/master'\r\n                }\r\n\r\n                licenses {\r\n                    license {\r\n                        name = 'GNU General Public License v3.0'\r\n                        url = 'https://www.gnu.org/licenses/gpl-3.0.txt'\r\n                    }\r\n                }\r\n\r\n                developers {\r\n                    developer {\r\n                        id = 'flowarg'\r\n                        name = 'Flow Arg'\r\n                        email = 'flow@flowarg.fr'\r\n                    }\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    repositories {\r\n        maven {        \r\n            credentials {\r\n                username = System.getenv(\"NEW_CENTRAL_ID\")\r\n                password = System.getenv(\"NEW_CENTRAL_TOKEN\")\r\n            }\r\n            url = \"https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/\"\r\n        }\r\n    }\r\n}\r\n\r\nsigning {\r\n    def signingKey = System.getenv(\"GPG_PRIVATE_KEY\")\r\n    def signingPassword = System.getenv(\"GPG_PASSPHRASE\")\r\n    useInMemoryPgpKeys(signingKey, signingPassword)\r\n    sign publishing.publications.mavenJava\r\n}\r\n\r\ntest {\r\n    useJUnitPlatform()\r\n}\r\n"
  },
  {
    "path": "flowupdater-schema.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n    \"description\": \"\",\n    \"type\": \"object\",\n    \"properties\": {\n        \"curseFiles\": {\n            \"type\": \"array\",\n            \"uniqueItems\": true,\n            \"minItems\": 1,\n            \"items\": {\n                \"required\": [\n                    \"projectID\",\n                    \"fileID\"\n                ],\n                \"properties\": {\n                    \"projectID\": {\n                        \"type\": \"number\",\n                        \"description\": \"Project ID of the mod on CurseForge\"\n                    },\n                    \"fileID\": {\n                        \"type\": \"number\",\n                        \"description\": \"File ID of the mod on CurseForge\"\n                    },\n                    \"required\": {\n                        \"type\": \"boolean\",\n                        \"description\": \"If true, the mod is required to be present in the mod folder.\"\n                    }\n                }\n            }\n        },\n        \"modrinthMods\": {\n            \"type\": \"array\",\n            \"uniqueItems\": true,\n            \"minItems\": 1,\n            \"items\": {\n                \"required\": [\n                    \"versionId\",\n                    \"projectReference\",\n                    \"versionNumber\"\n                ],\n                \"oneOf\": [\n                    {\n                        \"required\": [\n                            \"versionId\"\n                        ],\n                        \"properties\": {\n                            \"versionId\": {\n                                \"type\": \"number\",\n                                \"description\": \"Mod version file ID\"\n                            }\n                        }\n                    },\n                    {\n                        \"required\": [\n                            \"projectReference\",\n                            \"versionNumber\"\n                        ],\n                        \"properties\": {\n                            \"projectReference\": {\n                                \"type\": \"number\",\n                                \"description\": \"Projet ID of the mod on Modrinth\"\n                            },\n                            \"versionNumber\": {\n                                \"type\": \"number\",\n                                \"description\": \"Version ID of the mod on Modrinth\"\n                            }\n                        }\n                    }\n                ]\n            }\n        },\n        \"mods\": {\n            \"type\": \"array\",\n            \"uniqueItems\": true,\n            \"minItems\": 1,\n            \"items\": {\n                \"required\": [\n                    \"name\",\n                    \"downloadURL\",\n                    \"sha1\",\n                    \"size\"\n                ],\n                \"properties\": {\n                    \"name\": {\n                        \"type\": \"string\",\n                        \"minLength\": 1,\n                        \"description\": \"Name of mod file\"\n                    },\n                    \"downloadURL\": {\n                        \"type\": \"string\",\n                        \"minLength\": 1,\n                        \"description\": \"Mod download URL\"\n                    },\n                    \"sha1\": {\n                        \"type\": \"string\",\n                        \"minLength\": 1,\n                        \"description\": \"Sha1 of mod file\"\n                    },\n                    \"size\": {\n                        \"type\": \"number\",\n                        \"description\": \"Size of mod file (in bytes)\"\n                    }\n                }\n            }\n        },\n        \"extfiles\": {\n            \"type\": \"array\",\n            \"uniqueItems\": true,\n            \"minItems\": 1,\n            \"items\": {\n                \"required\": [\n                    \"path\",\n                    \"downloadURL\",\n                    \"sha1\",\n                    \"size\"\n                ],\n                \"properties\": {\n                    \"path\": {\n                        \"type\": \"string\",\n                        \"minLength\": 1,\n                        \"description\": \"Path of external file\"\n                    },\n                    \"downloadURL\": {\n                        \"type\": \"string\",\n                        \"minLength\": 1,\n                        \"description\": \"external file URL\"\n                    },\n                    \"sha1\": {\n                        \"type\": \"string\",\n                        \"minLength\": 1,\n                        \"description\": \"Sha1 of external file\"\n                    },\n                    \"size\": {\n                        \"type\": \"number\",\n                        \"description\": \"Size of external file (in bytes)\"\n                    },\n                    \"update\": {\n                        \"type\": \"boolean\",\n                        \"description\": \"If false, the file will not be checked again if the file is valid.\"\n                    }\n                }\n            }\n        },\n        \"clientURL\": {\n            \"type\": \"string\",\n            \"minLength\": 1,\n            \"description\": \"URL of client.jar\"\n        },\n        \"clientSha1\": {\n            \"type\": \"string\",\n            \"minLength\": 1,\n            \"description\": \"SHA1 of client.jar\"\n        },\n        \"clientSize\": {\n            \"type\": \"number\",\n            \"description\": \"Size of client.jar (in bytes)\"\n        }\n    }\n}"
  },
  {
    "path": "gradle/libs.versions.toml",
    "content": "[versions]\ngson = \"2.13.2\"\nflowmultitools = \"1.4.5\"\n\nannotations = \"26.1.0\"\n\noll = \"3.2.11\"\njunit = \"6.0.3\"\n\n[libraries]\ngson = { module = \"com.google.code.gson:gson\", version.ref = \"gson\" }\nflowmultitools = { module = \"fr.flowarg:flowmultitools\", version.ref = \"flowmultitools\" }\nannotations = { module = \"org.jetbrains:annotations\", version.ref = \"annotations\" }\noll = { module = \"fr.flowarg:openlauncherlib\", version.ref = \"oll\" }\njunit-jupiter = { module = \"org.junit.jupiter:junit-jupiter\", version.ref = \"junit\" }\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-9.4.0-all.zip\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradle.properties",
    "content": "org.gradle.configuration-cache=true\n"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env sh\n\n#\n# Copyright 2015 the original author or authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn () {\n    echo \"$*\"\n}\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" -a \"$nonstop\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif [ \"$cygwin\" = \"true\" -o \"$msys\" = \"true\" ] ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=`expr $i + 1`\n    done\n    case $i in\n        0) set -- ;;\n        1) set -- \"$args0\" ;;\n        2) set -- \"$args0\" \"$args1\" ;;\n        3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Escape application args\nsave () {\n    for i do printf %s\\\\n \"$i\" | sed \"s/'/'\\\\\\\\''/g;1s/^/'/;\\$s/\\$/' \\\\\\\\/\" ; done\n    echo \" \"\n}\nAPP_ARGS=`save \"$@\"`\n\n# Collect all arguments for the java command, following the shell quoting and substitution rules\neval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \"\\\"-Dorg.gradle.appname=$APP_BASE_NAME\\\"\" -classpath \"\\\"$CLASSPATH\\\"\" org.gradle.wrapper.GradleWrapperMain \"$APP_ARGS\"\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n\r\n@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif \"%ERRORLEVEL%\" == \"0\" goto execute\r\n\r\necho.\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto execute\r\n\r\necho.\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %*\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\r\nexit /b 1\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "settings.gradle",
    "content": "rootProject.name = 'FlowUpdater'"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/FlowUpdater.java",
    "content": "package fr.flowarg.flowupdater;\r\n\r\nimport fr.flowarg.flowio.FileUtils;\r\nimport fr.flowarg.flowlogger.ILogger;\r\nimport fr.flowarg.flowlogger.Logger;\r\nimport fr.flowarg.flowupdater.download.*;\r\nimport fr.flowarg.flowupdater.download.json.ExternalFile;\r\nimport fr.flowarg.flowupdater.download.json.Mod;\r\nimport fr.flowarg.flowupdater.integrations.IntegrationManager;\r\nimport fr.flowarg.flowupdater.integrations.curseforgeintegration.ICurseForgeCompatible;\r\nimport fr.flowarg.flowupdater.integrations.modrinthintegration.IModrinthCompatible;\r\nimport fr.flowarg.flowupdater.integrations.optifineintegration.IOptiFineCompatible;\r\nimport fr.flowarg.flowupdater.utils.IOUtils;\r\nimport fr.flowarg.flowupdater.utils.UpdaterOptions;\r\nimport fr.flowarg.flowupdater.utils.VersionChecker;\r\nimport fr.flowarg.flowupdater.utils.builderapi.BuilderArgument;\r\nimport fr.flowarg.flowupdater.utils.builderapi.BuilderException;\r\nimport fr.flowarg.flowupdater.utils.builderapi.IBuilder;\r\nimport fr.flowarg.flowupdater.versions.IModLoaderVersion;\r\nimport fr.flowarg.flowupdater.versions.VanillaVersion;\r\nimport org.jetbrains.annotations.NotNull;\r\n\r\nimport java.net.URL;\r\nimport java.nio.file.Files;\r\nimport java.nio.file.Path;\r\nimport java.util.ArrayList;\r\nimport java.util.Arrays;\r\nimport java.util.Collection;\r\nimport java.util.List;\r\nimport java.util.concurrent.ExecutorService;\r\nimport java.util.concurrent.Executors;\r\nimport java.util.function.Consumer;\r\n\r\n/**\r\n * Represent the base class of the updater.<br>\r\n * You can define some parameters about your version (Forge, Vanilla, MCP, Fabric...).\r\n * @author FlowArg\r\n */\r\npublic class FlowUpdater\r\n{\r\n    /** FlowUpdater's version string constant */\r\n    public static final String FU_VERSION = \"1.9.4\";\r\n\r\n    /** Vanilla version's object to update/install */\r\n    private final VanillaVersion vanillaVersion;\r\n\r\n    /** Logger object */\r\n    private final ILogger logger;\r\n\r\n    /** Mod loader version to install, can be null if you want a vanilla or MCP version */\r\n    private final IModLoaderVersion modLoaderVersion;\r\n\r\n    /** Progress callback to notify installation progress */\r\n    private final IProgressCallback callback;\r\n\r\n    /** Information about download status */\r\n    private final DownloadList downloadList;\r\n\r\n    /** Represent some settings for FlowUpdater */\r\n    private final UpdaterOptions updaterOptions;\r\n\r\n    /** Represent a list of ExternalFile. External files are downloaded before post-executions.*/\r\n    private final List<ExternalFile> externalFiles;\r\n\r\n    /** Represent a list of Runnable. Post-Executions are called after update. */\r\n    private final List<Runnable> postExecutions;\r\n\r\n    /** The integration manager object */\r\n    private final IntegrationManager integrationManager;\r\n\r\n    /** Default callback */\r\n    public static final IProgressCallback NULL_CALLBACK = new IProgressCallback()\r\n    {\r\n        @Override\r\n        public void init(@NotNull ILogger logger)\r\n        {\r\n            logger.info(\"Default callback will be used.\");\r\n        }\r\n    };\r\n\r\n    /** Default logger, null for path argument = no file logger */\r\n    public static final ILogger DEFAULT_LOGGER = new Logger(\"[FlowUpdater]\", null);\r\n\r\n    /**\r\n     * Basic constructor for {@link FlowUpdater}, use {@link FlowUpdaterBuilder} to instantiate a new {@link FlowUpdater}.\r\n     * @param vanillaVersion {@link VanillaVersion} to update.\r\n     * @param logger {@link ILogger} used for log information.\r\n     * @param updaterOptions {@link UpdaterOptions} for this updater\r\n     * @param callback {@link IProgressCallback} used for update progression. If it's null, it will be\r\n     * automatically assigned to {@link FlowUpdater#NULL_CALLBACK}.\r\n     * @param externalFiles {@link List<ExternalFile>} are downloaded before postExecutions.\r\n     * @param postExecutions {@link List<Runnable>} are called after update.\r\n     * @param modLoaderVersion {@link IModLoaderVersion} to install can be null.\r\n     */\r\n    private FlowUpdater(VanillaVersion vanillaVersion, ILogger logger,\r\n            UpdaterOptions updaterOptions, IProgressCallback callback,\r\n            List<ExternalFile> externalFiles, List<Runnable> postExecutions,\r\n            IModLoaderVersion modLoaderVersion)\r\n    {\r\n        this.logger = logger;\r\n        this.vanillaVersion = vanillaVersion;\r\n        this.externalFiles = externalFiles;\r\n        this.postExecutions = postExecutions;\r\n        this.modLoaderVersion = modLoaderVersion;\r\n        this.updaterOptions = updaterOptions;\r\n        this.callback = callback;\r\n        this.downloadList = new DownloadList();\r\n        this.integrationManager = new IntegrationManager(this);\r\n        this.logger.info(String.format(\r\n                \"------------------------- FlowUpdater for Minecraft %s v%s -------------------------\",\r\n                this.vanillaVersion.getName(), FU_VERSION));\r\n\r\n        if(this.updaterOptions.isVersionChecker())\r\n            VersionChecker.run(this.logger);\r\n\r\n        this.callback.init(this.logger);\r\n    }\r\n\r\n    /**\r\n     * This method updates the Minecraft Installation in the given directory.\r\n     * If the {@link #vanillaVersion} is {@link VanillaVersion#NULL_VERSION}, the updater will\r\n     * only run external files and post-executions.\r\n     * @param dir Directory where is the Minecraft installation.\r\n     * @throws Exception if an I/O problem occurred.\r\n     * @throws fr.flowarg.flowupdater.utils.FlowUpdaterException if an important error occurred during the update.\r\n     */\r\n    public void update(Path dir) throws Exception\r\n    {\r\n        this.checkExtFiles(dir);\r\n        this.updateMinecraft(dir);\r\n        this.updateExtFiles(dir);\r\n        this.runPostExecutions();\r\n        this.endUpdate();\r\n    }\r\n\r\n    private void checkExtFiles(Path dir) throws Exception\r\n    {\r\n        this.updaterOptions.getExternalFileDeleter().delete(this.externalFiles, this.downloadList, dir);\r\n    }\r\n\r\n    private void updateMinecraft(@NotNull Path dir) throws Exception\r\n    {\r\n        this.loadVanillaStuff();\r\n\r\n        if(this.modLoaderVersion != null)\r\n            this.loadModLoader(dir);\r\n\r\n        this.startVanillaDownload(dir);\r\n\r\n        if(this.modLoaderVersion != null)\r\n            this.installModLoader(dir);\r\n    }\r\n\r\n    private void loadVanillaStuff() throws Exception\r\n    {\r\n        if(this.vanillaVersion == VanillaVersion.NULL_VERSION)\r\n        {\r\n            this.downloadList.init();\r\n            return;\r\n        }\r\n\r\n        this.logger.info(String.format(\"Reading data about %s Minecraft version...\", this.vanillaVersion.getName()));\r\n        new VanillaReader(this).read();\r\n    }\r\n\r\n    private void loadModLoader(@NotNull Path dir) throws Exception\r\n    {\r\n        final Path modsDirPath = dir.resolve(\"mods\");\r\n\r\n        this.checkMods(this.modLoaderVersion, modsDirPath);\r\n\r\n        if(this.modLoaderVersion instanceof ICurseForgeCompatible)\r\n            this.integrationManager.loadCurseForgeIntegration(modsDirPath, (ICurseForgeCompatible)this.modLoaderVersion);\r\n\r\n        if(this.modLoaderVersion instanceof IModrinthCompatible)\r\n            this.integrationManager.loadModrinthIntegration(modsDirPath, (IModrinthCompatible)this.modLoaderVersion);\r\n\r\n        if(this.modLoaderVersion instanceof IOptiFineCompatible)\r\n            this.integrationManager.loadOptiFineIntegration(modsDirPath, (IOptiFineCompatible)this.modLoaderVersion);\r\n    }\r\n\r\n    private void checkMods(@NotNull IModLoaderVersion modLoader, Path modsDir) throws Exception\r\n    {\r\n        for(Mod mod : modLoader.getMods())\r\n        {\r\n            final Path filePath = modsDir.resolve(mod.getName());\r\n\r\n            if(Files.notExists(filePath) ||\r\n                    Files.size(filePath) != mod.getSize() ||\r\n                    (!mod.getSha1().isEmpty() && !FileUtils.getSHA1(filePath).equalsIgnoreCase(mod.getSha1())))\r\n                this.downloadList.getMods().add(mod);\r\n        }\r\n    }\r\n\r\n    private void startVanillaDownload(Path dir) throws Exception\r\n    {\r\n        if (Files.notExists(dir))\r\n            Files.createDirectories(dir);\r\n\r\n        new VanillaDownloader(dir, this).download();\r\n    }\r\n\r\n    private void installModLoader(Path dir) throws Exception\r\n    {\r\n        if(this.modLoaderVersion == null)\r\n            return;\r\n\r\n        this.modLoaderVersion.attachFlowUpdater(this);\r\n        if(!this.modLoaderVersion.isModLoaderAlreadyInstalled(dir))\r\n        {\r\n            this.modLoaderVersion.install(dir);\r\n            this.logger.info(this.modLoaderVersion.name() + \", version: \" + this.modLoaderVersion.getModLoaderVersion() + \" has been successfully installed!\");\r\n        }\r\n        else this.logger.info(this.modLoaderVersion.name() + \" is already installed! Skipping installation...\");\r\n        this.modLoaderVersion.installMods(dir.resolve(\"mods\"));\r\n    }\r\n\r\n    private void updateExtFiles(Path dir)\r\n    {\r\n        if(this.downloadList.getExtFiles().isEmpty()) return;\r\n\r\n        this.callback.step(Step.EXTERNAL_FILES);\r\n        this.logger.info(\"Downloading external file(s)...\");\r\n\r\n        final Consumer<ExternalFile> extFileDownloadConsumer = extFile -> {\r\n            try\r\n            {\r\n                final Path filePath = dir.resolve(extFile.getPath());\r\n                IOUtils.download(this.logger, new URL(extFile.getDownloadURL()), filePath);\r\n                this.callback.onFileDownloaded(filePath);\r\n            }\r\n            catch (Exception e)\r\n            {\r\n                this.logger.printStackTrace(e);\r\n            }\r\n            this.downloadList.incrementDownloaded(extFile.getSize());\r\n            this.callback.update(this.downloadList.getDownloadInfo());\r\n        };\r\n\r\n        if(this.updaterOptions.shouldDisableExtFilesAsyncDownload())\r\n            this.downloadList.getExtFiles().forEach(extFileDownloadConsumer);\r\n        else IOUtils.executeAsyncForEach(this.downloadList.getExtFiles(), Executors.newWorkStealingPool(), extFileDownloadConsumer);\r\n    }\r\n\r\n    private void runPostExecutions()\r\n    {\r\n        if(this.postExecutions.isEmpty()) return;\r\n\r\n        this.callback.step(Step.POST_EXECUTIONS);\r\n        this.logger.info(\"Running post executions...\");\r\n        this.postExecutions.forEach(Runnable::run);\r\n    }\r\n\r\n    private void endUpdate()\r\n    {\r\n        this.callback.step(Step.END);\r\n        this.callback.update(this.downloadList.getDownloadInfo());\r\n        this.downloadList.clear();\r\n    }\r\n\r\n    /**\r\n     * Builder of {@link FlowUpdater}.\r\n     * @author Flow Arg (FlowArg)\r\n     */\r\n    public static class FlowUpdaterBuilder implements IBuilder<FlowUpdater>\r\n    {\r\n        private final BuilderArgument<VanillaVersion> versionArgument = new BuilderArgument<>(\"VanillaVersion\", () -> VanillaVersion.NULL_VERSION).optional();\r\n        private final BuilderArgument<ILogger> loggerArgument = new BuilderArgument<>(\"Logger\", () -> DEFAULT_LOGGER).optional();\r\n        private final BuilderArgument<UpdaterOptions> updaterOptionsArgument = new BuilderArgument<>(\"UpdaterOptions\", () -> UpdaterOptions.DEFAULT).optional();\r\n        private final BuilderArgument<IProgressCallback> progressCallbackArgument = new BuilderArgument<>(\"Callback\", () -> NULL_CALLBACK).optional();\r\n        private final BuilderArgument<List<ExternalFile>> externalFilesArgument = new BuilderArgument<List<ExternalFile>>(\"External Files\", ArrayList::new).optional();\r\n        private final BuilderArgument<List<Runnable>> postExecutionsArgument = new BuilderArgument<List<Runnable>>(\"Post Executions\", ArrayList::new).optional();\r\n        private final BuilderArgument<IModLoaderVersion> modLoaderVersionArgument = new BuilderArgument<IModLoaderVersion>(\"ModLoader\").optional().require(this.versionArgument);\r\n\r\n        /**\r\n         * Append a {@link VanillaVersion} object in the final FlowUpdater instance.\r\n         * @param version the {@link VanillaVersion} to append and install.\r\n         * @return the builder.\r\n         */\r\n        public FlowUpdaterBuilder withVanillaVersion(VanillaVersion version)\r\n        {\r\n            this.versionArgument.set(version);\r\n            return this;\r\n        }\r\n\r\n        /**\r\n         * Append a {@link ILogger} object in the final FlowUpdater instance.\r\n         * @param logger the {@link ILogger} to append and use.\r\n         * @return the builder.\r\n         */\r\n        public FlowUpdaterBuilder withLogger(ILogger logger)\r\n        {\r\n            this.loggerArgument.set(logger);\r\n            return this;\r\n        }\r\n\r\n        /**\r\n         * Append a {@link UpdaterOptions} object in the final FlowUpdater instance.\r\n         * @param updaterOptions the {@link UpdaterOptions} to append and propagate.\r\n         * @return the builder.\r\n         */\r\n        public FlowUpdaterBuilder withUpdaterOptions(UpdaterOptions updaterOptions)\r\n        {\r\n            this.updaterOptionsArgument.set(updaterOptions);\r\n            return this;\r\n        }\r\n\r\n        /**\r\n         * Append a {@link IProgressCallback} object in the final FlowUpdater instance.\r\n         * @param callback the {@link IProgressCallback} to append and use.\r\n         * @return the builder.\r\n         */\r\n        public FlowUpdaterBuilder withProgressCallback(IProgressCallback callback)\r\n        {\r\n            this.progressCallbackArgument.set(callback);\r\n            return this;\r\n        }\r\n\r\n        /**\r\n         * Append a {@link List} object in the final FlowUpdater instance.\r\n         * @param externalFiles the {@link List} to append and update.\r\n         * @return the builder.\r\n         */\r\n        public FlowUpdaterBuilder withExternalFiles(Collection<ExternalFile> externalFiles)\r\n        {\r\n            this.externalFilesArgument.get().addAll(externalFiles);\r\n            return this;\r\n        }\r\n\r\n        /**\r\n         * Append an array object in the final FlowUpdater instance.\r\n         * @param externalFiles the array to append and update.\r\n         * @return the builder.\r\n         */\r\n        public FlowUpdaterBuilder withExternalFiles(ExternalFile... externalFiles)\r\n        {\r\n            return withExternalFiles(Arrays.asList(externalFiles));\r\n        }\r\n\r\n        /**\r\n         * Append external files in the final FlowUpdater instance.\r\n         * @param externalFilesJsonUrl the URL of the json of external files append and update.\r\n         * @return the builder.\r\n         */\r\n        public FlowUpdaterBuilder withExternalFiles(URL externalFilesJsonUrl)\r\n        {\r\n            return withExternalFiles(ExternalFile.getExternalFilesFromJson(externalFilesJsonUrl));\r\n        }\r\n\r\n        /**\r\n         * Append external files in the final FlowUpdater instance.\r\n         * @param externalFilesJsonUrl the URL of the json of external files append and update.\r\n         * @return the builder.\r\n         */\r\n        public FlowUpdaterBuilder withExternalFiles(String externalFilesJsonUrl)\r\n        {\r\n            return withExternalFiles(ExternalFile.getExternalFilesFromJson(externalFilesJsonUrl));\r\n        }\r\n\r\n        /**\r\n         * Append a {@link List} object in the final FlowUpdater instance.\r\n         * @param postExecutions the {@link List} to append and run after the update.\r\n         * @return the builder.\r\n         */\r\n        public FlowUpdaterBuilder withPostExecutions(Collection<Runnable> postExecutions)\r\n        {\r\n            this.postExecutionsArgument.get().addAll(postExecutions);\r\n            return this;\r\n        }\r\n\r\n        /**\r\n         * Append an array object in the final FlowUpdater instance.\r\n         * @param postExecutions the array to append and run after the update.\r\n         * @return the builder.\r\n         */\r\n        public FlowUpdaterBuilder withPostExecutions(Runnable... postExecutions)\r\n        {\r\n            return withPostExecutions(Arrays.asList(postExecutions));\r\n        }\r\n\r\n        /**\r\n         * Necessary if you want to install a mod loader like Forge or Fabric, for instance.\r\n         * Append a {@link IModLoaderVersion} object in the final FlowUpdater instance.\r\n         * @param modLoaderVersion the {@link IModLoaderVersion} to append and install.\r\n         * @return the builder.\r\n         */\r\n        public FlowUpdaterBuilder withModLoaderVersion(IModLoaderVersion modLoaderVersion)\r\n        {\r\n            this.modLoaderVersionArgument.set(modLoaderVersion);\r\n            return this;\r\n        }\r\n\r\n        /**\r\n         * Build a new {@link FlowUpdater} instance with provided arguments.\r\n         * @return the new {@link FlowUpdater} instance.\r\n         * @throws BuilderException if an error occurred on FlowUpdater instance building.\r\n         */\r\n        @Override\r\n        public FlowUpdater build() throws BuilderException\r\n        {\r\n            return new FlowUpdater(\r\n                    this.versionArgument.get(),\r\n                    this.loggerArgument.get(),\r\n                    this.updaterOptionsArgument.get(),\r\n                    this.progressCallbackArgument.get(),\r\n                    this.externalFilesArgument.get(),\r\n                    this.postExecutionsArgument.get(),\r\n                    this.modLoaderVersionArgument.get()\r\n            );\r\n        }\r\n    }\r\n\r\n    // Some getters\r\n\r\n    /**\r\n     * Get the {@link VanillaVersion} attached to this {@link FlowUpdater} instance.\r\n     * @return a vanilla version.\r\n     */\r\n    public VanillaVersion getVanillaVersion()\r\n    {\r\n        return this.vanillaVersion;\r\n    }\r\n\r\n    /**\r\n     * Get th {@link IModLoaderVersion} attached to this {@link FlowUpdater} instance.\r\n     * @return a mod loader version.\r\n     */\r\n    public IModLoaderVersion getModLoaderVersion()\r\n    {\r\n        return this.modLoaderVersion;\r\n    }\r\n\r\n    /**\r\n     * Get the current logger.\r\n     * @return a logger.\r\n     */\r\n    public ILogger getLogger()\r\n    {\r\n        return this.logger;\r\n    }\r\n\r\n    /**\r\n     * Get the current callback.\r\n     * @return a callback.\r\n     */\r\n    public IProgressCallback getCallback()\r\n    {\r\n        return this.callback;\r\n    }\r\n\r\n    /**\r\n     * Get the list of external files.\r\n     * @return external files.\r\n     */\r\n    public List<ExternalFile> getExternalFiles()\r\n    {\r\n        return this.externalFiles;\r\n    }\r\n\r\n    /**\r\n     * Get the list of post-executions.\r\n     * @return all post-executions\r\n     */\r\n    public List<Runnable> getPostExecutions()\r\n    {\r\n        return this.postExecutions;\r\n    }\r\n\r\n    /**\r\n     * Get the download list which contains all download information.\r\n     * @return a {@link DownloadList} instance.\r\n     */\r\n    public DownloadList getDownloadList()\r\n    {\r\n        return this.downloadList;\r\n    }\r\n\r\n    /**\r\n     * Get the FlowUpdater's options.\r\n     * @return some useful settings.\r\n     */\r\n    public UpdaterOptions getUpdaterOptions()\r\n    {\r\n        return this.updaterOptions;\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/download/DownloadList.java",
    "content": "package fr.flowarg.flowupdater.download;\n\nimport fr.flowarg.flowupdater.download.json.AssetDownloadable;\nimport fr.flowarg.flowupdater.download.json.Downloadable;\nimport fr.flowarg.flowupdater.download.json.ExternalFile;\nimport fr.flowarg.flowupdater.download.json.Mod;\nimport fr.flowarg.flowupdater.integrations.optifineintegration.OptiFine;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicLong;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReentrantLock;\n\n/**\n * Represent information about download status. Used with {@link IProgressCallback} progress system.\n *\n * @author FlowArg\n */\npublic class DownloadList\n{\n    private final List<Downloadable> downloadableFiles = new ArrayList<>();\n    private final List<AssetDownloadable> downloadableAssets = new ArrayList<>();\n    private final List<ExternalFile> extFiles = new ArrayList<>();\n    private final List<Mod> mods = new ArrayList<>();\n    private final Lock updateInfoLock = new ReentrantLock();\n    private OptiFine optiFine = null;\n    private DownloadInfo downloadInfo;\n    private boolean init = false;\n\n    /**\n     * This method initializes fields.\n     */\n    public void init()\n    {\n        if (this.init) return;\n\n        this.downloadInfo = new DownloadInfo();\n        this.downloadableFiles.forEach(\n                downloadable -> this.downloadInfo.totalToDownloadBytes.set(\n                        this.downloadInfo.totalToDownloadBytes.get() + downloadable.getSize()));\n        this.downloadableAssets.forEach(\n                downloadable -> this.downloadInfo.totalToDownloadBytes.set(\n                        this.downloadInfo.totalToDownloadBytes.get() + downloadable.getSize()));\n        this.extFiles.forEach(\n                externalFile -> this.downloadInfo.totalToDownloadBytes.set(\n                        this.downloadInfo.totalToDownloadBytes.get() + externalFile.getSize()));\n        this.mods.forEach(\n                mod -> this.downloadInfo.totalToDownloadBytes.set(this.downloadInfo.totalToDownloadBytes.get() + mod.getSize()));\n\n        this.downloadInfo.totalToDownloadFiles.set(\n                this.downloadInfo.totalToDownloadFiles.get() +\n                        this.downloadableFiles.size() +\n                        this.downloadableAssets.size() +\n                        this.extFiles.size() +\n                        this.mods.size());\n\n        if (this.optiFine != null)\n        {\n            this.downloadInfo.totalToDownloadBytes.set(this.downloadInfo.totalToDownloadBytes.get() + this.optiFine.getSize());\n            this.downloadInfo.totalToDownloadFiles.incrementAndGet();\n        }\n        this.init = true;\n    }\n\n    /**\n     * This method increments the number of bytes downloaded by the number of bytes passed in parameter.\n     * @param bytes number of bytes to add to downloaded bytes.\n     */\n    public void incrementDownloaded(long bytes)\n    {\n        this.updateInfoLock.lock();\n        this.downloadInfo.downloadedFiles.incrementAndGet();\n        this.downloadInfo.downloadedBytes.set(this.downloadInfo.downloadedBytes.get() + bytes);\n        this.updateInfoLock.unlock();\n    }\n\n    /**\n     * Get the new API to get information about the progress of the download.\n     * @return the instance of {@link DownloadInfo}.\n     */\n    public DownloadInfo getDownloadInfo()\n    {\n        return this.downloadInfo;\n    }\n\n    /**\n     * Get the queue that contains all assets to download.\n     * @return the queue that contains all assets to download.\n     */\n    public List<AssetDownloadable> getDownloadableAssets()\n    {\n        return this.downloadableAssets;\n    }\n\n    /**\n     * Get the list that contains all downloadable files.\n     * @return the list that contains all downloadable files.\n     */\n    public List<Downloadable> getDownloadableFiles()\n    {\n        return this.downloadableFiles;\n    }\n\n    /**\n     * Get the list that contains all external files.\n     * @return the list that contains all external files.\n     */\n    public List<ExternalFile> getExtFiles()\n    {\n        return this.extFiles;\n    }\n\n    /**\n     * Get the list that contains all mods.\n     * @return the list that contains all mods.\n     */\n    public List<Mod> getMods()\n    {\n        return this.mods;\n    }\n\n    /**\n     * Get the OptiFine object.\n     * @return the OptiFine object.\n     */\n    public OptiFine getOptiFine()\n    {\n        return this.optiFine;\n    }\n\n    /**\n     * Define the OptiFine object.\n     * @param optiFine the OptiFine object to define.\n     */\n    public void setOptiFine(OptiFine optiFine)\n    {\n        this.optiFine = optiFine;\n    }\n\n    /**\n     * Clear and reset this download list object.\n     */\n    public void clear()\n    {\n        this.downloadableFiles.clear();\n        this.extFiles.clear();\n        this.downloadableAssets.clear();\n        this.mods.clear();\n        this.optiFine = null;\n        this.downloadInfo.reset();\n        this.init = false;\n    }\n\n    public static class DownloadInfo\n    {\n        private final AtomicLong totalToDownloadBytes = new AtomicLong(0);\n        private final AtomicLong downloadedBytes = new AtomicLong(0);\n        private final AtomicInteger totalToDownloadFiles = new AtomicInteger(0);\n        private final AtomicInteger downloadedFiles = new AtomicInteger(0);\n\n        /**\n         * Reset this download info object.\n         */\n        public void reset()\n        {\n            this.totalToDownloadBytes.set(0);\n            this.downloadedBytes.set(0);\n            this.totalToDownloadFiles.set(0);\n            this.downloadedFiles.set(0);\n        }\n\n        /**\n         * Get the total of bytes to download.\n         * @return bytes to download.\n         */\n        public long getTotalToDownloadBytes()\n        {\n            return this.totalToDownloadBytes.get();\n        }\n\n        /**\n         * Get the downloaded bytes.\n         * @return the downloaded bytes.\n         */\n        public long getDownloadedBytes()\n        {\n            return this.downloadedBytes.get();\n        }\n\n        /**\n         * Get the number of files to download.\n         * @return number of files to download.\n         */\n        public int getTotalToDownloadFiles()\n        {\n            return this.totalToDownloadFiles.get();\n        }\n\n        /**\n         * Get the number of downloaded files.\n         * @return the number of downloaded files.\n         */\n        public int getDownloadedFiles()\n        {\n            return this.downloadedFiles.get();\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/download/IProgressCallback.java",
    "content": "package fr.flowarg.flowupdater.download;\n\nimport fr.flowarg.flowlogger.ILogger;\nimport fr.flowarg.flowupdater.FlowUpdater;\n\nimport java.nio.file.Path;\n\n/**\n * This interface provides useful methods to implement to access to download and update status.\n */\npublic interface IProgressCallback\n{\n    /**\n     * This method is called at {@link FlowUpdater} initialization.\n     * @param logger {@link ILogger} of FlowUpdater instance.\n     */\n    default void init(ILogger logger) {}\n\n    /**\n     * This method is called when a step started.\n     * @param step Actual {@link Step}.\n     */\n    default void step(Step step) {}\n\n    /**\n     * This method is called when a new file is downloaded.\n     * @param info The {@link DownloadList.DownloadInfo} instance that contains all wanted information.\n     */\n    default void update(DownloadList.DownloadInfo info) {}\n\n    /**\n     * This method is called before {@link #update(DownloadList.DownloadInfo)} when a file is downloaded.\n     * @param path the file downloaded.\n     */\n    default void onFileDownloaded(Path path) {}\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/download/Step.java",
    "content": "package fr.flowarg.flowupdater.download;\n\nimport fr.flowarg.flowupdater.FlowUpdater;\n\n/**\n * Represent each step of a Minecraft Installation\n * @author flow\n */\npublic enum Step\n{\n    /** Integration loading */\n    INTEGRATION,\n    /** ModPack preparation */\n    MOD_PACK,\n    /** JSON reading */\n    READ,\n    /** Download libraries */\n    DL_LIBS,\n    /** Download assets */\n    DL_ASSETS,\n    /** Extract natives */\n    EXTRACT_NATIVES,\n    /** Install a mod loader version. Skipped if {@link FlowUpdater#getModLoaderVersion()} is null. */\n    MOD_LOADER,\n    /** Download mods. Skipped if {@link FlowUpdater#getModLoaderVersion()} is null. */\n    MODS,\n    /** Download other files. */\n    EXTERNAL_FILES,\n    /** Runs a list of runnable at the end of update. */\n    POST_EXECUTIONS,\n    /** All tasks are finished */\n    END\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/download/VanillaDownloader.java",
    "content": "package fr.flowarg.flowupdater.download;\r\n\r\nimport fr.flowarg.flowio.FileUtils;\r\nimport fr.flowarg.flowlogger.ILogger;\r\nimport fr.flowarg.flowstringer.StringUtils;\r\nimport fr.flowarg.flowupdater.FlowUpdater;\r\nimport fr.flowarg.flowupdater.download.json.Downloadable;\r\nimport fr.flowarg.flowupdater.utils.IOUtils;\r\nimport fr.flowarg.flowzipper.ZipUtils;\r\nimport org.jetbrains.annotations.NotNull;\r\n\r\nimport java.net.URL;\r\nimport java.nio.file.Files;\r\nimport java.nio.file.Path;\r\nimport java.util.Enumeration;\r\nimport java.util.List;\r\nimport java.util.concurrent.Executors;\r\nimport java.util.jar.JarFile;\r\nimport java.util.stream.Collectors;\r\nimport java.util.stream.Stream;\r\nimport java.util.zip.ZipEntry;\r\n\r\n/**\r\n * This class handles the downloading of vanilla files (client, assets, natives...).\r\n */\r\npublic class VanillaDownloader\r\n{\r\n    private final Path dir;\r\n    private final ILogger logger;\r\n    private final IProgressCallback callback;\r\n    private final DownloadList downloadList;\r\n    private final Path natives;\r\n    private final Path assets;\r\n    private final String vanillaJsonURL;\r\n\r\n    /**\r\n     * Construct a new VanillaDownloader object.\r\n     * @param dir the installation directory.\r\n     * @param flowUpdater the flow updater object.\r\n     * @throws Exception if an I/O error occurred.\r\n     */\r\n    public VanillaDownloader(Path dir, @NotNull FlowUpdater flowUpdater) throws Exception\r\n    {\r\n        this.dir = dir;\r\n        this.logger = flowUpdater.getLogger();\r\n        this.callback = flowUpdater.getCallback();\r\n        this.downloadList = flowUpdater.getDownloadList();\r\n\r\n        this.natives = this.dir.resolve(\"natives\");\r\n        this.assets = this.dir.resolve(\"assets\");\r\n        this.vanillaJsonURL = flowUpdater.getVanillaVersion().getJsonURL();\r\n\r\n        Files.createDirectories(this.dir.resolve(\"libraries\"));\r\n        Files.createDirectories(this.assets);\r\n        Files.createDirectories(this.natives);\r\n\r\n        this.downloadList.init();\r\n    }\r\n\r\n    /**\r\n     * This method downloads calls other methods to download and verify all files.\r\n     * @throws Exception if an I/O error occurred.\r\n     */\r\n    public void download() throws Exception\r\n    {\r\n        this.downloadLibraries();\r\n        this.downloadAssets();\r\n        this.extractNatives();\r\n\r\n        this.logger.info(\"All vanilla files were successfully downloaded!\");\r\n    }\r\n\r\n    private void downloadLibraries() throws Exception\r\n    {\r\n        this.logger.info(\"Checking library files...\");\r\n        this.callback.step(Step.DL_LIBS);\r\n\r\n        if(this.vanillaJsonURL != null)\r\n            this.downloadVanillaJson();\r\n\r\n        for (Downloadable downloadable : this.downloadList.getDownloadableFiles())\r\n        {\r\n            final Path filePath = this.dir.resolve(downloadable.getName());\r\n\r\n            if(Files.notExists(filePath) ||\r\n                    !FileUtils.getSHA1(filePath).equalsIgnoreCase(downloadable.getSha1()) ||\r\n                    Files.size(filePath) != downloadable.getSize())\r\n            {\r\n                IOUtils.download(this.logger, new URL(downloadable.getUrl()), filePath);\r\n                this.callback.onFileDownloaded(filePath);\r\n            }\r\n\r\n            this.downloadList.incrementDownloaded(downloadable.getSize());\r\n            this.callback.update(this.downloadList.getDownloadInfo());\r\n        }\r\n    }\r\n\r\n    private void downloadVanillaJson() throws Exception\r\n    {\r\n        final Path vanillaJsonTarget = this.dir.resolve(this.vanillaJsonURL.substring(this.vanillaJsonURL.lastIndexOf('/') + 1));\r\n        final String vanillaJsonResourceName = this.vanillaJsonURL.substring(this.vanillaJsonURL.lastIndexOf('/'));\r\n        final String vanillaJsonPathUrl = StringUtils.empty(StringUtils.empty(this.vanillaJsonURL, \"https://launchermeta.mojang.com/v1/packages/\"), \"https://piston-meta.mojang.com/v1/packages/\");\r\n\r\n        if(Files.notExists(vanillaJsonTarget) || !FileUtils.getSHA1(vanillaJsonTarget)\r\n                .equals(StringUtils.empty(vanillaJsonPathUrl, vanillaJsonResourceName)))\r\n            IOUtils.download(this.logger, new URL(this.vanillaJsonURL), vanillaJsonTarget);\r\n    }\r\n\r\n    private void extractNatives() throws Exception\r\n    {\r\n        boolean flag = false;\r\n        final List<Path> existingNatives = FileUtils.list(this.natives);\r\n        if (!existingNatives.isEmpty())\r\n        {\r\n            for (Path minecraftNative : FileUtils.list(this.natives)\r\n                    .stream()\r\n                    .filter(path -> path.getFileName().toString().endsWith(\".jar\"))\r\n                    .collect(Collectors.toList()))\r\n            {\r\n                final JarFile jarFile = new JarFile(minecraftNative.toFile());\r\n                final Enumeration<? extends ZipEntry> entries = jarFile.entries();\r\n                while (entries.hasMoreElements())\r\n                {\r\n                    final ZipEntry entry = entries.nextElement();\r\n                    if (entry.isDirectory() ||\r\n                            entry.getName().endsWith(\".git\") ||\r\n                            entry.getName().endsWith(\".sha1\") ||\r\n                            entry.getName().contains(\"META-INF\")) continue;\r\n\r\n                    final Path flPath = this.natives.resolve(entry.getName());\r\n\r\n                    if(Files.exists(flPath) && entry.getCrc() == FileUtils.getCRC32(flPath)) continue;\r\n\r\n                    flag = true;\r\n                    break;\r\n                }\r\n                jarFile.close();\r\n                if (flag) break;\r\n            }\r\n        }\r\n\r\n        if (flag)\r\n        {\r\n            this.logger.info(\"Extracting natives...\");\r\n            this.callback.step(Step.EXTRACT_NATIVES);\r\n\r\n            final Stream<Path> natives = FileUtils.list(this.natives).stream();\r\n            natives.filter(file -> !Files.isDirectory(file) && file.getFileName().toString().endsWith(\".jar\"))\r\n                    .forEach(file -> {\r\n                        try\r\n                        {\r\n                            ZipUtils.unzipJar(this.natives, file, \"ignoreMetaInf\");\r\n                        } catch (Exception e)\r\n                        {\r\n                            this.logger.printStackTrace(e);\r\n                        }\r\n                    });\r\n            natives.close();\r\n        }\r\n\r\n        try(Stream<Path> natives = Files.list(this.natives))\r\n        {\r\n            natives.forEach(path -> {\r\n                try\r\n                {\r\n                    if (path.getFileName().toString().endsWith(\".git\") || path.getFileName().toString().endsWith(\".sha1\"))\r\n                        Files.delete(path);\r\n                    else if(Files.isDirectory(path))\r\n                        FileUtils.deleteDirectory(path);\r\n                } catch (Exception e)\r\n                {\r\n                    this.logger.printStackTrace(e);\r\n                }\r\n            });\r\n        }\r\n    }\r\n\r\n    private void downloadAssets()\r\n    {\r\n        this.logger.info(\"Checking assets...\");\r\n        this.callback.step(Step.DL_ASSETS);\r\n\r\n        IOUtils.executeAsyncForEach(this.downloadList.getDownloadableAssets(), Executors.newWorkStealingPool(), assetDownloadable -> {\r\n            try\r\n            {\r\n                final Path downloadPath = this.assets.resolve(assetDownloadable.getFile());\r\n\r\n                if (Files.notExists(downloadPath) || Files.size(downloadPath) != assetDownloadable.getSize())\r\n                {\r\n                    final Path localAssetPath = IOUtils.getMinecraftFolder().resolve(\"assets\").resolve(assetDownloadable.getFile());\r\n                    if (Files.exists(localAssetPath) && Files.size(localAssetPath) == assetDownloadable.getSize())\r\n                        IOUtils.copy(this.logger, localAssetPath, downloadPath);\r\n                    else\r\n                    {\r\n                        IOUtils.download(this.logger, new URL(assetDownloadable.getUrl()), downloadPath);\r\n                        this.callback.onFileDownloaded(downloadPath);\r\n                    }\r\n                }\r\n\r\n                this.downloadList.incrementDownloaded(assetDownloadable.getSize());\r\n                this.callback.update(this.downloadList.getDownloadInfo());\r\n            }\r\n            catch (Exception e)\r\n            {\r\n                this.logger.printStackTrace(e);\r\n            }\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/download/VanillaReader.java",
    "content": "package fr.flowarg.flowupdater.download;\r\n\r\nimport com.google.gson.GsonBuilder;\r\nimport com.google.gson.JsonElement;\r\nimport com.google.gson.JsonObject;\r\nimport fr.flowarg.flowcompat.Platform;\r\nimport fr.flowarg.flowupdater.FlowUpdater;\r\nimport fr.flowarg.flowupdater.download.json.AssetDownloadable;\r\nimport fr.flowarg.flowupdater.download.json.AssetIndex;\r\nimport fr.flowarg.flowupdater.download.json.Downloadable;\r\nimport fr.flowarg.flowupdater.utils.IOUtils;\r\nimport fr.flowarg.flowupdater.versions.VanillaVersion;\r\nimport org.jetbrains.annotations.NotNull;\r\n\r\nimport java.net.URL;\r\nimport java.util.HashSet;\r\nimport java.util.Set;\r\nimport java.util.concurrent.atomic.AtomicBoolean;\r\n\r\n/**\r\n * This class handles all parsing stuff about vanilla files.\r\n */\r\npublic class VanillaReader\r\n{\r\n    private final VanillaVersion version;\r\n    private final IProgressCallback callback;\r\n    private final DownloadList downloadList;\r\n\r\n    /**\r\n     * Construct a new VanillaReader.\r\n     * @param flowUpdater the flow updater object.\r\n     */\r\n    public VanillaReader(@NotNull FlowUpdater flowUpdater)\r\n    {\r\n        this.version = flowUpdater.getVanillaVersion();\r\n        this.callback = flowUpdater.getCallback();\r\n        this.downloadList = flowUpdater.getDownloadList();\r\n    }\r\n\r\n    /**\r\n     * This method calls other methods to parse each part of the given Minecraft Version.\r\n     * @throws Exception if an I/O error occurred.\r\n     */\r\n    public void read() throws Exception\r\n    {\r\n        this.callback.step(Step.READ);\r\n        this.parseLibraries();\r\n        this.parseAssetIndex();\r\n        this.parseClient();\r\n        this.parseNatives();\r\n        this.parseAssets();\r\n    }\r\n\r\n    private void parseLibraries()\r\n    {\r\n        this.version.getMinecraftLibrariesJson().forEach(jsonElement -> {\r\n            final JsonObject element = jsonElement.getAsJsonObject();\r\n\r\n            if (element == null || !this.checkRules(element))\r\n                return;\r\n\r\n            final JsonObject downloads = element.getAsJsonObject(\"downloads\");\r\n\r\n            if(downloads == null)\r\n                return;\r\n\r\n            block: {\r\n                final String name = element.getAsJsonPrimitive(\"name\").getAsString();\r\n\r\n                if(!name.contains(\"lwjgl\") || !name.contains(\"natives\") || !name.contains(\"macos\"))\r\n                    break block;\r\n\r\n                boolean platformCheck = (Platform.isOnMac() &&\r\n                        Platform.getArch().equals(\"64\") &&\r\n                        System.getProperty(\"os.arch\").equals(\"aarch64\"));\r\n\r\n                if(platformCheck != name.contains(\"arm64\"))\r\n                    return;\r\n            }\r\n\r\n            final JsonObject artifact = downloads.getAsJsonObject(\"artifact\");\r\n\r\n            if (artifact == null)\r\n                return;\r\n\r\n            final String url = artifact.getAsJsonPrimitive(\"url\").getAsString();\r\n            final int size = artifact.getAsJsonPrimitive(\"size\").getAsInt();\r\n            final String path = \"libraries/\" + artifact.getAsJsonPrimitive(\"path\").getAsString();\r\n            final String sha1 = artifact.getAsJsonPrimitive(\"sha1\").getAsString();\r\n            final Downloadable downloadable = new Downloadable(url, size, sha1, path);\r\n\r\n            if(!this.downloadList.getDownloadableFiles().contains(downloadable))\r\n                this.downloadList.getDownloadableFiles().add(downloadable);\r\n        });\r\n        this.downloadList.getDownloadableFiles().addAll(this.version.getAnotherLibraries());\r\n    }\r\n\r\n    private void parseAssetIndex()\r\n    {\r\n        if(this.version.getCustomAssetIndex() != null)\r\n            return;\r\n\r\n        final JsonObject assetIndex = this.version.getMinecraftAssetIndex();\r\n        final String url = assetIndex.getAsJsonPrimitive(\"url\").getAsString();\r\n        final int size = assetIndex.getAsJsonPrimitive(\"size\").getAsInt();\r\n        final String name = \"assets/indexes/\" + url.substring(url.lastIndexOf('/') + 1);\r\n        final String sha1 = assetIndex.getAsJsonPrimitive(\"sha1\").getAsString();\r\n\r\n        this.downloadList.getDownloadableFiles().add(new Downloadable(url, size, sha1, name));\r\n    }\r\n\r\n    private void parseClient()\r\n    {\r\n        final JsonObject client = this.version.getMinecraftClient();\r\n        final String clientURL = client.getAsJsonPrimitive(\"url\").getAsString();\r\n        final int clientSize = client.getAsJsonPrimitive(\"size\").getAsInt();\r\n        final String clientName = clientURL.substring(clientURL.lastIndexOf('/') + 1);\r\n        final String clientSha1 = client.getAsJsonPrimitive(\"sha1\").getAsString();\r\n\r\n        this.downloadList.getDownloadableFiles().add(new Downloadable(clientURL, clientSize, clientSha1, clientName));\r\n    }\r\n\r\n    private void parseNatives()\r\n    {\r\n        this.version.getMinecraftLibrariesJson().forEach(jsonElement -> {\r\n            final JsonObject obj = jsonElement.getAsJsonObject()\r\n                    .getAsJsonObject(\"downloads\")\r\n                    .getAsJsonObject(\"classifiers\");\r\n\r\n            if (obj == null)\r\n                return;\r\n\r\n            final JsonObject macObj = obj.getAsJsonObject(\"natives-macos\");\r\n            final JsonObject osxObj = obj.getAsJsonObject(\"natives-osx\");\r\n            JsonObject windowsObj = obj.getAsJsonObject(String.format(\"natives-windows-%s\", Platform.getArch()));\r\n            if (windowsObj == null) windowsObj = obj.getAsJsonObject(\"natives-windows\");\r\n            final JsonObject linuxObj = obj.getAsJsonObject(\"natives-linux\");\r\n\r\n            if (macObj != null && Platform.isOnMac())\r\n                this.getNativeForOS(\"mac\", macObj);\r\n            else if (osxObj != null && Platform.isOnMac())\r\n                this.getNativeForOS(\"mac\", osxObj);\r\n            else if (windowsObj != null && Platform.isOnWindows())\r\n                this.getNativeForOS(\"win\", windowsObj);\r\n            else if (linuxObj != null && Platform.isOnLinux())\r\n                this.getNativeForOS(\"linux\", linuxObj);\r\n        });\r\n    }\r\n    \r\n    private void getNativeForOS(@NotNull String os, @NotNull JsonObject obj)\r\n    {\r\n        final String url = obj.getAsJsonPrimitive(\"url\").getAsString();\r\n        final int size = obj.getAsJsonPrimitive(\"size\").getAsInt();\r\n        final String path = obj.getAsJsonPrimitive(\"path\").getAsString();\r\n        final String name = \"natives/\" + path.substring(path.lastIndexOf('/') + 1);\r\n        final String sha1 = obj.getAsJsonPrimitive(\"sha1\").getAsString();\r\n\r\n        if(!os.equals(\"mac\"))\r\n        {\r\n            if (name.contains(\"-3.2.1-\") && name.contains(\"lwjgl\"))\r\n                return;\r\n            if (name.contains(\"-2.9.2-\") && name.contains(\"lwjgl\"))\r\n                return;\r\n        }\r\n        else if(name.contains(\"-3.2.2-\") && name.contains(\"lwjgl\"))\r\n            return;\r\n\r\n        this.downloadList.getDownloadableFiles().add(new Downloadable(url, size, sha1, name));\r\n    }\r\n\r\n    private void parseAssets() throws Exception\r\n    {\r\n        final Set<AssetDownloadable> toDownload = new HashSet<>(this.version.getAnotherAssets());\r\n        final AssetIndex assetIndex;\r\n\r\n        if(this.version.getCustomAssetIndex() == null)\r\n            assetIndex = new GsonBuilder()\r\n                    .disableHtmlEscaping()\r\n                    .create()\r\n                    .fromJson(IOUtils.getContent(new URL(this.version.getMinecraftAssetIndex().get(\"url\").getAsString())), AssetIndex.class);\r\n        else assetIndex = this.version.getCustomAssetIndex();\r\n\r\n        assetIndex.getUniqueObjects()\r\n                .values()\r\n                .forEach(assetDownloadable ->\r\n                                 toDownload.add(new AssetDownloadable(assetDownloadable.getHash(), assetDownloadable.getSize())));\r\n        this.downloadList.getDownloadableAssets().addAll(toDownload);\r\n    }\r\n\r\n    private boolean checkRules(@NotNull JsonObject obj)\r\n    {\r\n        final JsonElement rulesElement = obj.get(\"rules\");\r\n\r\n        if (rulesElement == null)\r\n            return true;\r\n\r\n        final AtomicBoolean canDownload = new AtomicBoolean(true);\r\n\r\n        rulesElement.getAsJsonArray().forEach(jsonElement -> {\r\n            final JsonObject object = jsonElement.getAsJsonObject();\r\n            final String actionValue = object.getAsJsonPrimitive(\"action\").getAsString();\r\n            final JsonObject osObject = object.getAsJsonObject(\"os\");\r\n\r\n            if (actionValue.equals(\"allow\"))\r\n            {\r\n                if (osObject == null) return;\r\n\r\n                final String os = osObject.getAsJsonPrimitive(\"name\").getAsString();\r\n                canDownload.set(this.check(os));\r\n            }\r\n            else if (actionValue.equals(\"disallow\"))\r\n            {\r\n                final String os = osObject.getAsJsonPrimitive(\"name\").getAsString();\r\n                canDownload.set(!this.check(os));\r\n            }\r\n        });\r\n\r\n        return canDownload.get();\r\n    }\r\n    \r\n    private boolean check(@NotNull String os)\r\n    {\r\n        return (os.equalsIgnoreCase(\"osx\") && Platform.isOnMac()) ||\r\n                (os.equalsIgnoreCase(\"macos\") && Platform.isOnMac()) ||\r\n                (os.equalsIgnoreCase(\"windows\") && Platform.isOnWindows()) ||\r\n                (os.equalsIgnoreCase(\"linux\") && Platform.isOnLinux());\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/download/json/AssetDownloadable.java",
    "content": "package fr.flowarg.flowupdater.download.json;\r\n\r\n/**\r\n * This class represents an asset.\r\n */\r\npublic class AssetDownloadable\r\n{\r\n    private final String hash;\r\n    private final long size;\r\n    private final String url;\r\n    private final String file;\r\n\r\n    /**\r\n     * Construct a new asset object.\r\n     * @param hash the sha1 of the asset.\r\n     * @param size the size of the asset.\r\n     */\r\n    public AssetDownloadable(String hash, long size)\r\n    {\r\n        this.hash = hash;\r\n        this.size = size;\r\n        final String assetsPath = \"/\" + this.hash.substring(0, 2) + \"/\" + this.hash;\r\n        this.url = \"https://resources.download.minecraft.net\" + assetsPath;\r\n        this.file = \"objects\" + assetsPath;\r\n    }\r\n\r\n    /**\r\n     * Get the hash of the asset.\r\n     * @return the sha1 of the asset.\r\n     */\r\n    public String getHash()\r\n    {\r\n        return this.hash;\r\n    }\r\n\r\n    /**\r\n     * Get the length of the asset.\r\n     * @return the size of the asset.\r\n     */\r\n    public long getSize()\r\n    {\r\n        return this.size;\r\n    }\r\n\r\n    /**\r\n     * Get the remote url of the asset.\r\n     * @return the url of the asset.\r\n     */\r\n    public String getUrl()\r\n    {\r\n        return this.url;\r\n    }\r\n\r\n    /**\r\n     * Get the file path of the asset.\r\n     * @return the relative local path of this asset.\r\n     */\r\n    public String getFile()\r\n    {\r\n        return this.file;\r\n    }\r\n\r\n    @Override\r\n    public boolean equals(Object o)\r\n    {\r\n        if (this == o) return true;\r\n        if (o == null || getClass() != o.getClass()) return false;\r\n\r\n        final AssetDownloadable that = (AssetDownloadable)o;\r\n        return this.file.equals(that.file) && this.size == that.size && this.hash.equals(that.hash) && this.url.equals(that.url);\r\n    }\r\n\r\n    @Override\r\n    public int hashCode()\r\n    {\r\n        int result = this.hash.hashCode();\r\n        result = 31 * result + Long.hashCode(this.size);\r\n        result = 31 * result + this.url.hashCode();\r\n        return result;\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/download/json/AssetIndex.java",
    "content": "package fr.flowarg.flowupdater.download.json;\r\n\r\nimport java.util.Collections;\r\nimport java.util.LinkedHashMap;\r\nimport java.util.Map;\r\n\r\n/**\r\n * This class represents an asset index of a Minecraft version.\r\n */\r\npublic class AssetIndex\r\n{\r\n    private final Map<String, AssetDownloadable> objects = new LinkedHashMap<>();\r\n\r\n    /**\r\n     * Internal getter.\r\n     * @return asset objects\r\n     */\r\n    private Map<String, AssetDownloadable> getObjects()\r\n    {\r\n        return this.objects;\r\n    }\r\n\r\n    /**\r\n     * Get an immutable collection of asset objects.\r\n     * @return asset objects.\r\n     */\r\n    public Map<String, AssetDownloadable> getUniqueObjects()\r\n    {\r\n        return Collections.unmodifiableMap(this.getObjects());\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/download/json/CurseFileInfo.java",
    "content": "package fr.flowarg.flowupdater.download.json;\n\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonObject;\nimport fr.flowarg.flowupdater.utils.FlowUpdaterException;\nimport fr.flowarg.flowupdater.utils.IOUtils;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.net.URL;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\n\n/**\n * This class represents a file in the CurseForge API.\n */\npublic class CurseFileInfo\n{\n    private final int projectID;\n    private final int fileID;\n\n    /**\n     * Construct a new CurseFileInfo object.\n     * @param projectID the ID of the project.\n     * @param fileID the ID of the file.\n     */\n    public CurseFileInfo(int projectID, int fileID)\n    {\n        this.projectID = projectID;\n        this.fileID = fileID;\n    }\n\n    /**\n     * Retrieve a collection of {@link CurseFileInfo} by parsing a remote JSON file.\n     * @param jsonUrl the url of the remote JSON file.\n     * @return a collection of {@link CurseFileInfo}.\n     */\n    public static @NotNull List<CurseFileInfo> getFilesFromJson(URL jsonUrl)\n    {\n        final List<CurseFileInfo> result = new ArrayList<>();\n        final JsonObject object = IOUtils.readJson(jsonUrl).getAsJsonObject();\n        final JsonArray mods = object.getAsJsonArray(\"curseFiles\");\n        mods.forEach(curseModElement -> {\n            final JsonObject obj = curseModElement.getAsJsonObject();\n            final int projectID = obj.get(\"projectID\").getAsInt();\n            final int fileID = obj.get(\"fileID\").getAsInt();\n            result.add(new CurseFileInfo(projectID, fileID));\n        });\n        return result;\n    }\n\n    /**\n     * Retrieve a collection of {@link CurseFileInfo} by parsing a remote JSON file.\n     * @param jsonUrl the url of the remote JSON file.\n     * @return a collection of {@link CurseFileInfo}.\n     */\n    public static @NotNull List<CurseFileInfo> getFilesFromJson(String jsonUrl)\n    {\n        try\n        {\n            return getFilesFromJson(new URL(jsonUrl));\n        }\n        catch (Exception e)\n        {\n            throw new FlowUpdaterException(e);\n        }\n    }\n\n    /**\n     * Get the project ID.\n     * @return the project ID.\n     */\n    public int getProjectID()\n    {\n        return this.projectID;\n    }\n\n    /**\n     * Get the file ID.\n     * @return the file ID.\n     */\n    public int getFileID()\n    {\n        return this.fileID;\n    }\n\n    @Override\n    public boolean equals(Object o)\n    {\n        if (this == o) return true;\n        if (o == null || this.getClass() != o.getClass()) return false;\n        final CurseFileInfo that = (CurseFileInfo)o;\n        return this.projectID == that.projectID && this.fileID == that.fileID;\n    }\n\n    @Override\n    public int hashCode()\n    {\n        return Objects.hash(this.projectID, this.fileID);\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/download/json/CurseModPackInfo.java",
    "content": "package fr.flowarg.flowupdater.download.json;\n\n/**\n * This class represents a mod pack file in the CurseForge API.\n */\npublic class CurseModPackInfo extends CurseFileInfo\n{\n    private final boolean installExtFiles;\n    private final String[] excluded;\n\n    private String url = \"\";\n\n    /**\n     * Construct a new CurseModPackInfo object.\n     * @param projectID the ID of the project.\n     * @param fileID the ID of the file.\n     * @param installExtFiles should install external files like config and resource packs.\n     * @param excluded mods to exclude.\n     */\n    public CurseModPackInfo(int projectID, int fileID, boolean installExtFiles, String... excluded)\n    {\n        super(projectID, fileID);\n        this.installExtFiles = installExtFiles;\n        this.excluded = excluded;\n    }\n\n    /**\n     * Construct a new CurseModPackInfo object.\n     * @param url the url of the custom mod pack endpoint.\n     * @param installExtFiles should install external files like config and resource packs.\n     * @param excluded mods to exclude.\n     */\n    public CurseModPackInfo(String url, boolean installExtFiles, String... excluded)\n    {\n        super(0, 0);\n        this.url = url;\n        this.installExtFiles = installExtFiles;\n        this.excluded = excluded;\n    }\n\n    /**\n     * Get the {@link #installExtFiles} option.\n     * @return the {@link #installExtFiles} option.\n     */\n    public boolean isInstallExtFiles()\n    {\n        return this.installExtFiles;\n    }\n\n    /**\n     * Get the excluded mods.\n     * @return the excluded mods.\n     */\n    public String[] getExcluded()\n    {\n        return this.excluded;\n    }\n\n    /**\n     * Get the url of the mod pack endpoint.\n     * Should be of the form:\n     * {\n     *     \"data\": {\n     *         \"fileName\": \"modpack.zip\",\n     *         \"downloadUrl\": \"https://site.com/modpack.zip\",\n     *         \"fileLength\": 123456789,\n     *         \"hashes\": [\n     *             {\n     *                 \"value\": \"a02b0499589bc6982fced96dcc85c3b3e33af119\",\n     *                 \"algo\": 1\n     *             }\n     *         ]\n     *     }\n     * }\n     * @return the url of the mod pack endpoint if it's not from CurseForge's servers.\n     */\n    public String getUrl()\n    {\n        return this.url;\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/download/json/Downloadable.java",
    "content": "package fr.flowarg.flowupdater.download.json;\r\n\r\nimport java.util.Objects;\r\n\r\n/**\r\n * This class represents a classic downloadable file such as a library, the client/server or natives.\r\n */\r\npublic class Downloadable\r\n{\r\n    private final String url;\r\n    private final long size;\r\n    private final String sha1;\r\n    private final String name;\r\n\r\n    /**\r\n     * Construct a new Downloadable object.\r\n     * @param url the url where to download the file.\r\n     * @param size the size of the file.\r\n     * @param sha1 the sha1 of the file.\r\n     * @param name the name (path) of the file.\r\n     */\r\n    public Downloadable(String url, long size, String sha1, String name)\r\n    {\r\n        this.url = url;\r\n        this.size = size;\r\n        this.sha1 = sha1;\r\n        this.name = name;\r\n    }\r\n\r\n    /**\r\n     * Get the url of the file.\r\n     * @return the url of the file.\r\n     */\r\n    public String getUrl()\r\n    {\r\n        return this.url;\r\n    }\r\n\r\n    /**\r\n     * Get the size of the file.\r\n     * @return the size of the file.\r\n     */\r\n    public long getSize()\r\n    {\r\n        return this.size;\r\n    }\r\n\r\n    /**\r\n     * Get the sha1 of the file.\r\n     * @return the sha1 of the file.\r\n     */\r\n    public String getSha1()\r\n    {\r\n        return this.sha1;\r\n    }\r\n\r\n    /**\r\n     * Get the relative path of the file.\r\n     * @return the relative path of the file.\r\n     */\r\n    public String getName()\r\n    {\r\n        return this.name;\r\n    }\r\n\r\n    @Override\r\n    public boolean equals(Object o)\r\n    {\r\n        if (this == o) return true;\r\n        if (o == null || getClass() != o.getClass()) return false;\r\n        final Downloadable that = (Downloadable)o;\r\n        return this.size == that.size &&\r\n                Objects.equals(this.url, that.url) &&\r\n                Objects.equals(this.sha1, that.sha1) &&\r\n                Objects.equals(this.name, that.name);\r\n    }\r\n}\r\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/download/json/ExternalFile.java",
    "content": "package fr.flowarg.flowupdater.download.json;\n\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonObject;\nimport fr.flowarg.flowupdater.utils.FlowUpdaterException;\nimport fr.flowarg.flowupdater.utils.IOUtils;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.net.URL;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * This class represents an external file object.\n */\npublic class ExternalFile\n{\n    private final String path;\n    private final String downloadURL;\n    private final String sha1;\n    private final long size;\n    private final boolean update;\n    \n    /**\n     * Construct a new ExternalFile object.\n     * @param path Path of external file.\n     * @param sha1 Sha1 of external file.\n     * @param size Size of external file.\n     * @param downloadURL external file URL.\n     */\n    public ExternalFile(String path, String downloadURL, String sha1, long size)\n    {\n        this.path = path;\n        this.downloadURL = downloadURL;\n        this.sha1 = sha1;\n        this.size = size;\n        this.update = true;\n    }\n\n    /**\n     * Construct a new ExternalFile object.\n     * @param path Path of external file.\n     * @param sha1 Sha1 of external file.\n     * @param size Size of external file.\n     * @param downloadURL external file URL.\n     * @param update false: not checking if the file is valid. true: checking if the file is valid.\n     */\n    public ExternalFile(String path, String downloadURL, String sha1, long size, boolean update)\n    {\n        this.path = path;\n        this.downloadURL = downloadURL;\n        this.sha1 = sha1;\n        this.size = size;\n        this.update = update;\n    }\n    \n    /**\n     * Provide a List of external file from a JSON file.\n     * Template of a JSON file :\n     * <pre>\n     * {\n     *   \"extfiles\": [\n     *     {\n     *       \"path\": \"other/path/AnExternalFile.binpatch\",\n     *       \"downloadURL\": \"https://url.com/launcher/extern/AnExtFile.binpatch\",\n     *       \"sha1\": \"40f784892989du0fc6f45c895d4l6c5db9378f48\",\n     *       \"size\": 25652\n     *     },\n     *     {\n     *       \"path\": \"config/config.json\",\n     *       \"downloadURL\": \"https://url.com/launcher/ext/modconfig.json\",\n     *       \"sha1\": \"eef74b3fbab6400cb14b02439cf092cca3c2125c\",\n     *       \"size\": 19683,\n     *       \"update\": false\n     *     }\n     *   ]\n     * }\n     * </pre>\n     * @param jsonUrl the JSON file URL.\n     * @return an external file list.\n     */\n    public static @NotNull List<ExternalFile> getExternalFilesFromJson(URL jsonUrl)\n    {\n        final List<ExternalFile> result = new ArrayList<>();\n        final JsonArray extfiles = IOUtils.readJson(jsonUrl).getAsJsonObject().getAsJsonArray(\"extfiles\");\n        extfiles.forEach(extFileElement -> {\n            final JsonObject obj = extFileElement.getAsJsonObject();\n            final String path = obj.get(\"path\").getAsString();\n            final String sha1 = obj.get(\"sha1\").getAsString();\n            final String downloadURL = obj.get(\"downloadURL\").getAsString();\n            final long size = obj.get(\"size\").getAsLong();\n            if(obj.get(\"update\") != null)\n                result.add(new ExternalFile(path, downloadURL, sha1, size, obj.get(\"update\").getAsBoolean()));\n            else result.add(new ExternalFile(path, downloadURL, sha1, size));\n        });\n        return result;\n    }\n\n    /**\n     * Provide a List of external file from a JSON file.\n     * @param jsonUrl the JSON file URL.\n     * @return an external file list.\n     */\n    public static @NotNull List<ExternalFile> getExternalFilesFromJson(String jsonUrl)\n    {\n        try\n        {\n            return getExternalFilesFromJson(new URL(jsonUrl));\n        } catch (Exception e)\n        {\n            throw new FlowUpdaterException(e);\n        }\n    }\n\n    /**\n     * Get the path of the external file.\n     * @return the path of the external file.\n     */\n    public String getPath()\n    {\n        return this.path;\n    }\n\n    /**\n     * Get the url of the external file.\n     * @return the url of the external file.\n     */\n    public String getDownloadURL()\n    {\n        return this.downloadURL;\n    }\n\n    /**\n     * Get the sha1 of the external file.\n     * @return the sha1 of the external file.\n     */\n    public String getSha1()\n    {\n        return this.sha1;\n    }\n\n    /**\n     * Get the size of the external file.\n     * @return the size of the external file.\n     */\n    public long getSize()\n    {\n        return this.size;\n    }\n\n    /**\n     * Should {@link fr.flowarg.flowupdater.utils.ExternalFileDeleter} check the file?\n     * @return if the external file deleter should check and delete the file.\n     */\n    public boolean isUpdate()\n    {\n        return this.update;\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/download/json/MCP.java",
    "content": "package fr.flowarg.flowupdater.download.json;\n\nimport com.google.gson.JsonObject;\nimport fr.flowarg.flowupdater.utils.FlowUpdaterException;\nimport fr.flowarg.flowupdater.utils.IOUtils;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.net.MalformedURLException;\nimport java.net.URL;\n\n/**\n * This class represents an MCP object.\n */\npublic class MCP\n{\n    private final String clientURL;\n    private final String clientSha1;\n    private final long clientSize;\n\n    /**\n     * Construct a new MCP object.\n     * @param clientURL URL of client.jar\n     * @param clientSha1 SHA1 of client.jar\n     * @param clientSize Size (bytes) of client.jar\n     */\n    public MCP(String clientURL, String clientSha1, long clientSize)\n    {\n        this.clientURL = clientURL;\n        this.clientSha1 = clientSha1;\n        this.clientSize = clientSize;\n    }\n    \n    /**\n     * Provide an MCP instance from a JSON file.\n     * Template of a JSON file :\n     * <pre>\n     * {\n     *   \"clientURL\": \"https://url.com/launcher/client.jar\",\n     *   \"clientSha1\": \"9b0a9d70320811e7af2e8741653f029151a6719a\",\n     *   \"clientSize\": 1234\n     * }\n     * </pre>\n     * @param jsonUrl the JSON file URL.\n     * @return the MCP instance.\n     */\n    public static @NotNull MCP getMCPFromJson(URL jsonUrl)\n    {\n        final JsonObject object = IOUtils.readJson(jsonUrl).getAsJsonObject();\n        return new MCP(object.get(\"clientURL\").getAsString(), object.get(\"clientSha1\").getAsString(), object.get(\"clientSize\").getAsLong());\n    }\n\n    /**\n     * Provide an MCP instance from a JSON file.\n     * @param jsonUrl the JSON file URL.\n     * @return the MCP instance.\n     */\n    public static @NotNull MCP getMCPFromJson(String jsonUrl)\n    {\n        try\n        {\n            return getMCPFromJson(new URL(jsonUrl));\n        } catch (Exception e)\n        {\n            throw new FlowUpdaterException(e);\n        }\n    }\n\n    /**\n     * Return the client url.\n     * @return the client url.\n     */\n    public String getClientURL()\n    {\n        return this.clientURL;\n    }\n\n    /**\n     * Return the client sha1.\n     * @return the client sha1.\n     */\n    public String getClientSha1()\n    {\n        return this.clientSha1;\n    }\n\n    /**\n     * Return the client size.\n     * @return the client size.\n     */\n    public long getClientSize()\n    {\n        return this.clientSize;\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/download/json/Mod.java",
    "content": "package fr.flowarg.flowupdater.download.json;\n\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport fr.flowarg.flowupdater.utils.FlowUpdaterException;\nimport fr.flowarg.flowupdater.utils.IOUtils;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.net.URL;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * This class represents a Mod object.\n */\npublic class Mod\n{\n    private final String name;\n    private final String sha1;\n    private final long size;\n    private final String downloadURL;\n    \n    /**\n     * Construct a new Mod object.\n     * @param name Name of mod file.\n     * @param downloadURL Mod download URL.\n     * @param sha1 Sha1 of mod file.\n     * @param size Size of mod file.\n     */\n    public Mod(String name, String downloadURL, String sha1, long size)\n    {\n        this.name = name;\n        this.downloadURL = downloadURL;\n        this.sha1 = sha1;\n        this.size = size;\n    }\n    \n    /**\n     * Provide a List of Mods from a JSON file.\n     * Template of a JSON file :\n     * <pre>\n     * {\n     *   \"mods\": [\n     *     {\n     *       \"name\": \"KeyStroke\",\n     *       \"downloadURL\": \"https://url.com/launcher/mods/KeyStroke.jar\",\n     *       \"sha1\": \"70e564892989d8bbc6f45c895df56c5db9378f48\",\n     *       \"size\": 1234\n     *     },\n     *     {\n     *       \"name\": \"JourneyMap\",\n     *       \"downloadURL\": \"https://url.com/launcher/mods/JourneyMap.jar\",\n     *       \"sha1\": \"eef74b3fbab6400cb14b02439cf092cca3c2125c\",\n     *       \"size\": 1234\n     *     }\n     *   ]\n     * }\n     * </pre>\n     * @param jsonUrl the JSON file URL.\n     * @return a Mod list.\n    */\n    public static @NotNull List<Mod> getModsFromJson(URL jsonUrl)\n    {\n        final List<Mod> result = new ArrayList<>();\n        final JsonObject object = IOUtils.readJson(jsonUrl).getAsJsonObject();\n        final JsonArray mods = object.getAsJsonArray(\"mods\");\n        mods.forEach(modElement -> result.add(fromJson(modElement)));\n        return result;\n    }\n\n    public static Mod fromJson(JsonElement modElement)\n    {\n        final JsonObject obj = modElement.getAsJsonObject();\n\n        return new Mod(\n                obj.get(\"name\").getAsString(),\n                obj.get(\"downloadURL\").getAsString(),\n                obj.get(\"sha1\").getAsString(),\n                obj.get(\"size\").getAsLong()\n        );\n    }\n\n    /**\n     * Provide a List of Mods from a JSON file.\n     * Template of a JSON file :\n     * @param jsonUrl the JSON file URL.\n     * @return a Mod list.\n     */\n    public static @NotNull List<Mod> getModsFromJson(String jsonUrl)\n    {\n        try\n        {\n            return getModsFromJson(new URL(jsonUrl));\n        }\n        catch (Exception e)\n        {\n            throw new FlowUpdaterException(e);\n        }\n    }\n\n    /**\n     * Get the mod name.\n     * @return the mod name.\n     */\n    public String getName()\n    {\n        return this.name;\n    }\n\n    /**\n     * Get the sha1 of the mod.\n     * @return the sha1 of the mod.\n     */\n    public String getSha1()\n    {\n        return this.sha1;\n    }\n\n    /**\n     * Get the mod size.\n     * @return the mod size.\n     */\n    public long getSize()\n    {\n        return this.size;\n    }\n\n    /**\n     * Get the mod url.\n     * @return the mod url.\n     */\n    public String getDownloadURL()\n    {\n        return this.downloadURL;\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/download/json/ModrinthModPackInfo.java",
    "content": "package fr.flowarg.flowupdater.download.json;\n\npublic class ModrinthModPackInfo extends ModrinthVersionInfo\n{\n    private final boolean installExtFiles;\n    private final String[] excluded;\n\n    public ModrinthModPackInfo(String projectReference, String versionNumber, boolean installExtFiles, String... excluded)\n    {\n        super(projectReference, versionNumber);\n        this.installExtFiles = installExtFiles;\n        this.excluded = excluded;\n    }\n\n    public ModrinthModPackInfo(String versionId, boolean installExtFiles, String... excluded)\n    {\n        super(versionId);\n        this.installExtFiles = installExtFiles;\n        this.excluded = excluded;\n    }\n\n    /**\n     * Get the {@link #installExtFiles} option.\n     * @return the {@link #installExtFiles} option.\n     */\n    public boolean isInstallExtFiles()\n    {\n        return this.installExtFiles;\n    }\n\n    /**\n     * Get the excluded mods.\n     * @return the excluded mods.\n     */\n    public String[] getExcluded()\n    {\n        return this.excluded;\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/download/json/ModrinthVersionInfo.java",
    "content": "package fr.flowarg.flowupdater.download.json;\n\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport fr.flowarg.flowupdater.utils.FlowUpdaterException;\nimport fr.flowarg.flowupdater.utils.IOUtils;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.net.MalformedURLException;\nimport java.net.URL;\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class ModrinthVersionInfo\n{\n    private String projectReference = \"\";\n    private String versionNumber = \"\";\n    private String versionId = \"\";\n\n    /**\n     * Construct a new ModrinthVersionInfo object.\n     * @param projectReference the project reference can be slug or id.\n     * @param versionNumber the version number (and NOT the version name unless they are the same).\n     */\n    public ModrinthVersionInfo(String projectReference, String versionNumber)\n    {\n        this.projectReference = projectReference.trim();\n        this.versionNumber = versionNumber.trim();\n    }\n\n    /**\n     * Construct a new ModrinthVersionInfo object.\n     * This constructor doesn't need a project reference because\n     * we can access the version without any project information.\n     * @param versionId the version id.\n     */\n    public ModrinthVersionInfo(String versionId)\n    {\n        this.versionId = versionId.trim();\n    }\n\n    public static @NotNull List<ModrinthVersionInfo> getModrinthVersionsFromJson(URL jsonUrl)\n    {\n        final List<ModrinthVersionInfo> result = new ArrayList<>();\n        final JsonObject object = IOUtils.readJson(jsonUrl).getAsJsonObject();\n        final JsonArray mods = object.getAsJsonArray(\"modrinthMods\");\n        mods.forEach(modElement -> {\n            final JsonObject obj = modElement.getAsJsonObject();\n            final JsonElement versionIdElement = obj.get(\"versionId\");\n\n            if(versionIdElement instanceof JsonNull)\n                result.add(new ModrinthVersionInfo(obj.get(\"projectReference\").getAsString(), obj.get(\"versionNumber\").getAsString()));\n            else result.add(new ModrinthVersionInfo(versionIdElement.getAsString()));\n        });\n        return result;\n    }\n\n    public static @NotNull List<ModrinthVersionInfo> getModrinthVersionsFromJson(String jsonUrl)\n    {\n        try\n        {\n            return getModrinthVersionsFromJson(new URL(jsonUrl));\n        } catch (Exception e)\n        {\n            throw new FlowUpdaterException(e);\n        }\n    }\n\n    public String getProjectReference()\n    {\n        return this.projectReference;\n    }\n\n    public String getVersionNumber()\n    {\n        return this.versionNumber;\n    }\n\n    public String getVersionId()\n    {\n        return this.versionId;\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/download/json/OptiFineInfo.java",
    "content": "package fr.flowarg.flowupdater.download.json;\n\n/**\n * This class represents an OptiFineInfo object.\n */\npublic class OptiFineInfo\n{\n    private final String version;\n    private final boolean preview;\n\n    /**\n     * Construct a new OptiFineInfo object.\n     * @param version the OptiFine's version.\n     * @param preview if the version is a preview.\n     */\n    public OptiFineInfo(String version, boolean preview)\n    {\n        this.version = version;\n        this.preview = preview;\n    }\n\n    /**\n     * Construct a new OptiFineInfo object, use {@link OptiFineInfo#OptiFineInfo(String, boolean)} .\n     * @param version the OptiFine's version.\n     */\n    public OptiFineInfo(String version)\n    {\n        this(version, version.startsWith(\"preview_\"));\n    }\n\n    /**\n     * Get the OptiFine's version.\n     * @return the OptiFine's version.\n     */\n    public String getVersion()\n    {\n        return this.version;\n    }\n\n    /**\n     * Is the version a preview?\n     * @return if the version is a preview or not.\n     */\n    public boolean isPreview()\n    {\n        return this.preview;\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/download/json/package-info.java",
    "content": "/**\n * This package contains some objects that can be/are parsed as a JSON.\n */\npackage fr.flowarg.flowupdater.download.json;"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/download/package-info.java",
    "content": "/**\n * This package contains some things about download stuff.\n */\npackage fr.flowarg.flowupdater.download;"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/integrations/Integration.java",
    "content": "package fr.flowarg.flowupdater.integrations;\n\nimport fr.flowarg.flowlogger.ILogger;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\n/**\n * The new Integration system replaces an old plugin system\n * which had some problems such as unavailability to communicate directly with FlowUpdater.\n * This new system is easier to use: no more annoying updater's options, no more extra-dependencies.\n * Polymorphism and inheritance can now be used to avoid code duplication.\n */\npublic abstract class Integration\n{\n    protected final ILogger logger;\n    protected final Path folder;\n\n    /**\n     * Default constructor of a basic Integration.\n     * @param logger the logger used.\n     * @param folder the folder where the plugin can work.\n     * @throws Exception if an error occurred.\n     */\n    public Integration(ILogger logger, Path folder) throws Exception\n    {\n        this.logger = logger;\n        this.folder = folder;\n        Files.createDirectories(this.folder);\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/integrations/IntegrationManager.java",
    "content": "package fr.flowarg.flowupdater.integrations;\n\nimport fr.flowarg.flowio.FileUtils;\nimport fr.flowarg.flowlogger.ILogger;\nimport fr.flowarg.flowupdater.FlowUpdater;\nimport fr.flowarg.flowupdater.download.DownloadList;\nimport fr.flowarg.flowupdater.download.IProgressCallback;\nimport fr.flowarg.flowupdater.download.Step;\nimport fr.flowarg.flowupdater.download.json.*;\nimport fr.flowarg.flowupdater.integrations.curseforgeintegration.CurseForgeIntegration;\nimport fr.flowarg.flowupdater.integrations.curseforgeintegration.CurseModPack;\nimport fr.flowarg.flowupdater.integrations.curseforgeintegration.ICurseForgeCompatible;\nimport fr.flowarg.flowupdater.integrations.modrinthintegration.IModrinthCompatible;\nimport fr.flowarg.flowupdater.integrations.modrinthintegration.ModrinthIntegration;\nimport fr.flowarg.flowupdater.integrations.modrinthintegration.ModrinthModPack;\nimport fr.flowarg.flowupdater.integrations.optifineintegration.IOptiFineCompatible;\nimport fr.flowarg.flowupdater.integrations.optifineintegration.OptiFine;\nimport fr.flowarg.flowupdater.integrations.optifineintegration.OptiFineIntegration;\nimport fr.flowarg.flowupdater.utils.FlowUpdaterException;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * The integration manager loads integration's stuff at the startup of FlowUpdater.\n */\npublic class IntegrationManager\n{\n    private final IProgressCallback progressCallback;\n    private final ILogger logger;\n    private final DownloadList downloadList;\n\n    /**\n     * Construct a new Integration Manager.\n     * @param updater a {@link FlowUpdater} instance.\n     */\n    public IntegrationManager(@NotNull FlowUpdater updater)\n    {\n        this.progressCallback = updater.getCallback();\n        this.logger = updater.getLogger();\n        this.downloadList = updater.getDownloadList();\n    }\n\n    /**\n     * This method loads the CurseForge integration and fetches some data.\n     * @param dir the installation directory.\n     * @param curseForgeCompatible a version that accepts CurseForge's feature stuff.\n     */\n    public void loadCurseForgeIntegration(Path dir, ICurseForgeCompatible curseForgeCompatible)\n    {\n        this.progressCallback.step(Step.INTEGRATION);\n        try\n        {\n            final CurseModPackInfo modPackInfo = curseForgeCompatible.getCurseModPackInfo();\n            final List<Mod> allCurseMods = new ArrayList<>();\n\n            if(curseForgeCompatible.getCurseMods().isEmpty() && modPackInfo == null)\n            {\n                curseForgeCompatible.setAllCurseMods(allCurseMods);\n                return;\n            }\n\n            final CurseForgeIntegration curseForgeIntegration = new CurseForgeIntegration(this.logger, dir.getParent().resolve(\".cfp\"));\n\n            for (CurseFileInfo info : curseForgeCompatible.getCurseMods())\n            {\n                try {\n                    final Mod mod = curseForgeIntegration.fetchMod(info);\n\n                    if(mod == null)\n                        break;\n\n                    this.checkMod(mod, allCurseMods, dir);\n                }\n                catch (Exception e)\n                {\n                    this.logger.printStackTrace(e);\n                }\n            }\n\n            if (modPackInfo == null)\n            {\n                curseForgeCompatible.setAllCurseMods(allCurseMods);\n                return;\n            }\n\n            this.progressCallback.step(Step.MOD_PACK);\n            final CurseModPack modPack = curseForgeIntegration.getCurseModPack(modPackInfo);\n            this.logger.info(String.format(\"Loading mod pack: %s (%s) by %s.\", modPack.getName(), modPack.getVersion(), modPack.getAuthor()));\n            modPack.getMods().forEach(mod -> {\n                allCurseMods.add(mod);\n                try\n                {\n                    final Path filePath = dir.resolve(mod.getName());\n                    boolean flag = false;\n                    for (String exclude : modPackInfo.getExcluded())\n                    {\n                        if (!mod.getName().equalsIgnoreCase(exclude)) continue;\n\n                        flag = !mod.isRequired();\n                        break;\n                    }\n\n                    if(flag) return;\n\n                    if(Files.exists(filePath)\n                            && Files.size(filePath) == mod.getSize()\n                            && (mod.getSha1().isEmpty() || FileUtils.getSHA1(filePath).equalsIgnoreCase(mod.getSha1())))\n                        return;\n\n                    Files.deleteIfExists(filePath);\n                    this.downloadList.getMods().add(mod);\n                } catch (Exception e)\n                {\n                    this.logger.printStackTrace(e);\n                }\n            });\n\n            curseForgeCompatible.setAllCurseMods(allCurseMods);\n        }\n        catch (Exception e)\n        {\n            throw new FlowUpdaterException(e);\n        }\n    }\n\n    public void loadModrinthIntegration(Path dir, IModrinthCompatible modrinthCompatible)\n    {\n        try\n        {\n            final ModrinthModPackInfo modPackInfo = modrinthCompatible.getModrinthModPackInfo();\n            final List<Mod> allModrinthMods = new ArrayList<>();\n\n            if(modrinthCompatible.getModrinthMods().isEmpty() && modPackInfo == null)\n            {\n                modrinthCompatible.setAllModrinthMods(allModrinthMods);\n                return;\n            }\n\n            final ModrinthIntegration modrinthIntegration = new ModrinthIntegration(this.logger, dir.getParent().resolve(\".modrinth\"));\n\n            for (ModrinthVersionInfo info : modrinthCompatible.getModrinthMods())\n            {\n                final Mod mod = modrinthIntegration.fetchMod(info);\n                this.checkMod(mod, allModrinthMods, dir);\n            }\n\n            if (modPackInfo == null)\n            {\n                modrinthCompatible.setAllModrinthMods(allModrinthMods);\n                return;\n            }\n\n            this.progressCallback.step(Step.MOD_PACK);\n            final ModrinthModPack modPack = modrinthIntegration.getCurseModPack(modPackInfo);\n            this.logger.info(String.format(\"Loading mod pack: %s (%s).\", modPack.getName(), modPack.getVersion()));\n            modrinthCompatible.setModrinthModPack(modPack);\n\n            for (Mod mod : modPack.getMods())\n                this.checkMod(mod, allModrinthMods, dir);\n\n            modrinthCompatible.setAllModrinthMods(allModrinthMods);\n        }\n        catch (Exception e)\n        {\n            throw new FlowUpdaterException(e);\n        }\n    }\n\n    /**\n     * This method loads the OptiFine integration and fetches OptiFine data.\n     * @param dir the installation directory.\n     * @param optiFineCompatible the current Forge version.\n     */\n    public void loadOptiFineIntegration(Path dir, @NotNull IOptiFineCompatible optiFineCompatible)\n    {\n        final OptiFineInfo info = optiFineCompatible.getOptiFineInfo();\n\n        if(info == null)\n            return;\n\n        try\n        {\n            final OptiFineIntegration optifineIntegration = new OptiFineIntegration(this.logger, dir.getParent().resolve(\".op\"));\n            final OptiFine optifine = optifineIntegration.getOptiFine(info.getVersion(), info.isPreview());\n            this.downloadList.setOptiFine(optifine);\n        } catch (Exception e)\n        {\n            throw new FlowUpdaterException(e);\n        }\n    }\n\n    private void checkMod(Mod mod, @NotNull List<Mod> allMods, @NotNull Path dir) throws Exception\n    {\n        allMods.add(mod);\n\n        final Path filePath = dir.resolve(mod.getName());\n\n        if(Files.exists(filePath)\n                && Files.size(filePath) == mod.getSize()\n                && (mod.getSha1().isEmpty() || FileUtils.getSHA1(filePath).equalsIgnoreCase(mod.getSha1())))\n            return;\n\n        Files.deleteIfExists(filePath);\n        this.downloadList.getMods().add(mod);\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/integrations/curseforgeintegration/CurseForgeIntegration.java",
    "content": "package fr.flowarg.flowupdater.integrations.curseforgeintegration;\n\nimport com.google.gson.*;\nimport fr.flowarg.flowio.FileUtils;\nimport fr.flowarg.flowlogger.ILogger;\nimport fr.flowarg.flowstringer.StringUtils;\nimport fr.flowarg.flowupdater.download.json.CurseFileInfo;\nimport fr.flowarg.flowupdater.download.json.CurseModPackInfo;\nimport fr.flowarg.flowupdater.download.json.Mod;\nimport fr.flowarg.flowupdater.integrations.Integration;\nimport fr.flowarg.flowupdater.utils.IOUtils;\nimport org.jetbrains.annotations.Contract;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.net.HttpURLConnection;\nimport java.net.URL;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\nimport java.util.*;\nimport java.util.concurrent.ConcurrentLinkedQueue;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.atomic.AtomicReference;\nimport java.util.zip.ZipEntry;\nimport java.util.zip.ZipFile;\n\n/**\n * This integration supports all CurseForge stuff that FlowUpdater needs such as retrieve mods and mod packs from CurseForge.\n */\npublic class CurseForgeIntegration extends Integration\n{\n    private static final String CF_API_URL = \"https://api.curseforge.com\";\n    private static final String CF_API_KEY = \"JDJhJDEwJHBFZjhacXFwWE4zbVdtLm5aZ2pBMC5kdm9ibnhlV3hQZWZma2Q5ZEhCRWFid2VaUWh2cUtpJDJhJ\";\n    private static final String MOD_FILE_ENDPOINT = \"/v1/mods/{modId}/files/{fileId}\";\n\n    private boolean manifestChanged = false;\n\n    /**\n     * Default constructor of a basic Integration.\n     *\n     * @param logger the logger used.\n     * @param folder the folder where the plugin can work.\n     * @throws Exception if an error occurred.\n     */\n    public CurseForgeIntegration(ILogger logger, Path folder) throws Exception\n    {\n        super(logger, folder);\n    }\n\n    public Mod fetchMod(CurseFileInfo curseFileInfo) throws CurseForgeException\n    {\n        try\n        {\n            return this.parseModFile(this.fetchModLink(curseFileInfo));\n        } catch (Exception e)\n        {\n            throw new CurseForgeException(String.format(\"Failed to fetch mod project id: %d file id: %d\", curseFileInfo.getProjectID(), curseFileInfo.getFileID()), e);\n        }\n    }\n\n    public static class CurseForgeException extends Exception\n    {\n        public CurseForgeException(String message, Throwable cause)\n        {\n            super(message, cause);\n        }\n    }\n\n    public String fetchModLink(@NotNull CurseFileInfo curseFileInfo)\n    {\n        final String url = CF_API_URL + MOD_FILE_ENDPOINT\n                .replace(\"{modId}\", String.valueOf(curseFileInfo.getProjectID()))\n                .replace(\"{fileId}\", String.valueOf(curseFileInfo.getFileID()));\n\n        return this.makeRequest(url);\n    }\n\n    /**\n     * Make a request to the CurseForge API.\n     * Oh my god, fuck Java 8 HTTP API, it's so fucking bad. Hope we drop Java 8 as soon as possible.\n     *\n     * @param url the url to request.\n     * @return the response of the request.\n     */\n    private @NotNull String makeRequest(String url)\n    {\n        HttpURLConnection connection = null;\n        try\n        {\n            connection = (HttpURLConnection)new URL(url).openConnection();\n            connection.setRequestMethod(\"GET\");\n            connection.setInstanceFollowRedirects(true);\n            connection.setUseCaches(false);\n            connection.setRequestProperty(\"Accept\", \"application/json\");\n            connection.setRequestProperty(\"x-api-key\", this.getCurseForgeAPIKey());\n\n            return IOUtils.getContent(connection.getInputStream());\n        }\n        catch (Exception e)\n        {\n            return \"\";\n        }\n        finally\n        {\n            if(connection != null)\n                connection.disconnect();\n        }\n    }\n\n    /**\n     * Parse the CurseForge API to retrieve the mod file.\n     */\n    private @NotNull Mod parseModFile(String jsonResponse)\n    {\n        final JsonObject data = JsonParser.parseString(jsonResponse).getAsJsonObject().getAsJsonObject(\"data\");\n        final String fileName = data.get(\"fileName\").getAsString();\n        final JsonElement downloadURLElement = data.get(\"downloadUrl\");\n        String downloadURL;\n\n        if(downloadURLElement instanceof JsonNull)\n        {\n            logger.warn(String.format(\"Mod file %s not available. The download can fail because of this! %s\", data.get(\"displayName\").getAsString(), jsonResponse));\n            final String id = Integer.toString(data.get(\"id\").getAsInt());\n            downloadURL = String.format(\"https://edge.forgecdn.net/files/%s/%s/%s\", id.substring(0, 4), id.substring(4), fileName);\n        }\n        else downloadURL = downloadURLElement.getAsString();\n        final long fileLength = data.get(\"fileLength\").getAsLong();\n\n        final AtomicReference<String> sha1 = new AtomicReference<>(\"\");\n\n        data.getAsJsonArray(\"hashes\").forEach(hashObject -> {\n            final String hash = hashObject.getAsJsonObject().get(\"value\").getAsString();\n            if(hash.length() == 40)\n                sha1.set(hash);\n        });\n\n        return new Mod(fileName, downloadURL, sha1.get(), fileLength);\n    }\n\n    /**\n     * Get a CurseForge's mod pack object with a project identifier and a file identifier.\n     * @param info CurseForge's mod pack info.\n     * @return the curse's mod pack corresponding to given parameters.\n     * @throws Exception if an error occurred.\n     */\n    public CurseModPack getCurseModPack(CurseModPackInfo info) throws Exception\n    {\n        final Path modPackFile = this.checkForUpdate(info);\n        this.extractModPack(modPackFile, info.isInstallExtFiles());\n        return this.parseMods();\n    }\n\n    private @NotNull Path checkForUpdate(@NotNull CurseModPackInfo info) throws Exception\n    {\n        final String responseData = info.getUrl().isEmpty() ? this.fetchModLink(info) : this.makeRequest(info.getUrl());\n        final Mod modPackFile = this.parseModFile(responseData);\n\n        final Path outPath = this.folder.resolve(modPackFile.getName());\n        if(Files.notExists(outPath) || (!modPackFile.getSha1().isEmpty() && !FileUtils.getSHA1(outPath).equalsIgnoreCase(modPackFile.getSha1())) || Files.size(outPath) != modPackFile.getSize())\n            IOUtils.download(this.logger, new URL(modPackFile.getDownloadURL()), outPath);\n\n        return outPath;\n    }\n\n    private void extractModPack(@NotNull Path out, boolean installExtFiles) throws Exception\n    {\n        this.logger.info(\"Extracting mod pack...\");\n        final ZipFile zipFile = new ZipFile(out.toFile(), ZipFile.OPEN_READ, StandardCharsets.UTF_8);\n        final Path dirPath = this.folder.getParent();\n        final Enumeration<? extends ZipEntry> entries = zipFile.entries();\n        while (entries.hasMoreElements())\n        {\n            final ZipEntry entry = entries.nextElement();\n            final Path flPath = dirPath.resolve(StringUtils.empty(entry.getName(), \"overrides/\"));\n            final String entryName = entry.getName();\n\n            if(entryName.equalsIgnoreCase(\"manifest.json\"))\n            {\n                if(Files.notExists(flPath) || entry.getCrc() != FileUtils.getCRC32(flPath))\n                {\n                    this.manifestChanged = true;\n                    this.transferAndClose(flPath, zipFile, entry);\n                }\n                if(!installExtFiles)\n                    break;\n                continue;\n            }\n\n            if(!installExtFiles)\n                continue;\n\n            if(entryName.equals(\"modlist.html\"))\n                continue;\n\n            if(Files.exists(flPath))\n            {\n                if(!Files.isDirectory(flPath))\n                {\n                    if(entry.getCrc() == FileUtils.getCRC32(flPath))\n                        continue;\n                }\n            }\n            else if (flPath.getFileName().toString().endsWith(flPath.getFileSystem().getSeparator()))\n                Files.createDirectories(flPath);\n\n            if (entry.isDirectory()) continue;\n\n            this.transferAndClose(flPath, zipFile, entry);\n        }\n        zipFile.close();\n    }\n\n    private @NotNull CurseModPack parseMods() throws Exception\n    {\n        this.logger.info(\"Fetching mods...\");\n\n        final Path dirPath = this.folder.getParent();\n        final String manifestJson = StringUtils.toString(Files.readAllLines(dirPath.resolve(\"manifest.json\")));\n        final JsonObject manifestObj = JsonParser.parseString(manifestJson).getAsJsonObject();\n        final String modPackName = manifestObj.get(\"name\").getAsString();\n        final String modPackVersion = manifestObj.get(\"version\").getAsString();\n        final String modPackAuthor = manifestObj.get(\"author\").getAsString();\n        final List<CurseModPack.CurseModPackMod> mods = this.processCacheFile(dirPath, this.populateManifest(manifestObj));\n\n        return new CurseModPack(modPackName, modPackVersion, modPackAuthor, mods);\n    }\n\n    private @NotNull List<ProjectMod> populateManifest(@NotNull JsonObject manifestObj)\n    {\n        final List<ProjectMod> manifestFiles = new ArrayList<>();\n\n        manifestObj.getAsJsonArray(\"files\")\n                .forEach(jsonElement -> manifestFiles.add(ProjectMod.fromJson(jsonElement.getAsJsonObject())));\n\n        return manifestFiles;\n    }\n\n    private @NotNull List<CurseModPack.CurseModPackMod> processCacheFile(@NotNull Path dirPath, List<ProjectMod> manifestFiles) throws Exception\n    {\n        final Path cachePath = dirPath.resolve(\"manifest.cache.json\");\n\n        if(Files.notExists(cachePath))\n        {\n            Files.createFile(cachePath);\n            Files.write(cachePath, Collections.singletonList(\"[]\"), StandardCharsets.UTF_8);\n        }\n\n        String json = StringUtils.toString(Files.readAllLines(cachePath, StandardCharsets.UTF_8));\n\n        if(this.manifestChanged || json.contains(\"\\\"md5\\\"\") || json.contains(\"\\\"length\\\"\"))\n        {\n            Files.delete(cachePath);\n            Files.createFile(cachePath);\n            Files.write(cachePath, Collections.singletonList(\"[]\"), StandardCharsets.UTF_8);\n            json = StringUtils.toString(Files.readAllLines(cachePath, StandardCharsets.UTF_8));\n        }\n\n        return this.deserializeWriteCache(json, manifestFiles, cachePath);\n    }\n\n    @Contract(\"_, _, _ -> new\")\n    private @NotNull List<CurseModPack.CurseModPackMod> deserializeWriteCache(String json,\n            List<ProjectMod> manifestFiles, Path cachePath) throws Exception\n    {\n        final JsonArray cacheArray = JsonParser.parseString(json).getAsJsonArray();\n        final Queue<CurseModPack.CurseModPackMod> mods = new ConcurrentLinkedQueue<>();\n\n        cacheArray.forEach(jsonElement -> {\n            final JsonObject object = jsonElement.getAsJsonObject();\n            final Mod mod = Mod.fromJson(jsonElement);\n            final ProjectMod projectMod = ProjectMod.fromJson(object);\n\n            mods.add(new CurseModPack.CurseModPackMod(mod, projectMod.isRequired()));\n            manifestFiles.remove(projectMod);\n        });\n\n        IOUtils.executeAsyncForEach(manifestFiles, Executors.newWorkStealingPool(), projectMod -> this.fetchAndSerializeProjectMod(projectMod, cacheArray, mods));\n        Files.write(cachePath, Collections.singletonList(cacheArray.toString()), StandardCharsets.UTF_8);\n\n        return new ArrayList<>(mods);\n    }\n\n    private void fetchAndSerializeProjectMod(@NotNull ProjectMod projectMod, JsonArray cacheArray,\n            Queue<CurseModPack.CurseModPackMod> mods)\n    {\n        final boolean required = projectMod.isRequired();\n\n        try\n        {\n            final Mod retrievedMod = this.fetchMod(projectMod);\n\n            if(retrievedMod == null)\n                return;\n\n            final CurseModPack.CurseModPackMod mod = new CurseModPack.CurseModPackMod(retrievedMod, required);\n            final JsonObject inCache = new JsonObject();\n\n            inCache.addProperty(\"name\", mod.getName());\n            inCache.addProperty(\"downloadURL\", mod.getDownloadURL());\n            inCache.addProperty(\"sha1\", mod.getSha1());\n            inCache.addProperty(\"size\", mod.getSize());\n            inCache.addProperty(\"required\", required);\n            inCache.addProperty(\"projectID\", projectMod.getProjectID());\n            inCache.addProperty(\"fileID\", projectMod.getFileID());\n\n            cacheArray.add(inCache);\n            mods.add(mod);\n        }\n        catch (Exception e)\n        {\n            this.logger.printStackTrace(e);\n        }\n    }\n\n    private void transferAndClose(@NotNull Path flPath, ZipFile zipFile, ZipEntry entry) throws Exception\n    {\n        if(Files.notExists(flPath.getParent()))\n            Files.createDirectories(flPath.getParent());\n\n        Files.copy(zipFile.getInputStream(entry), flPath, StandardCopyOption.REPLACE_EXISTING);\n    }\n\n    private static class ProjectMod extends CurseFileInfo\n    {\n        private final boolean required;\n\n        public ProjectMod(int projectID, int fileID, boolean required)\n        {\n            super(projectID, fileID);\n            this.required = required;\n        }\n\n        private static @NotNull ProjectMod fromJson(@NotNull JsonObject object)\n        {\n            return new ProjectMod(object.get(\"projectID\").getAsInt(),\n                                  object.get(\"fileID\").getAsInt(),\n                                  object.get(\"required\").getAsBoolean());\n        }\n\n        public boolean isRequired()\n        {\n            return this.required;\n        }\n    }\n\n    /**\n     * Get the CurseForge API Key.\n     */\n    private static String cacheKey = \"\";\n\n    private String getCurseForgeAPIKey()\n    {\n        return cacheKey.isEmpty() ? cacheKey = StringUtils.toString(Base64.getDecoder().decode(CF_API_KEY.substring(0, CF_API_KEY.length() - 5))) : cacheKey;\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/integrations/curseforgeintegration/CurseModPack.java",
    "content": "package fr.flowarg.flowupdater.integrations.curseforgeintegration;\n\nimport fr.flowarg.flowupdater.download.json.Mod;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.util.List;\n\n/**\n * Basic object that represents a CurseForge's mod pack.\n */\npublic class CurseModPack\n{\n    private final String name;\n    private final String version;\n    private final String author;\n    private final List<CurseModPackMod> mods;\n\n    CurseModPack(String name, String version, String author, List<CurseModPackMod> mods)\n    {\n        this.name = name;\n        this.version = version;\n        this.author = author;\n        this.mods = mods;\n    }\n\n    /**\n     * Get the mod pack's name.\n     * @return the mod pack's name.\n     */\n    public String getName()\n    {\n        return this.name;\n    }\n\n\n    /**\n     * Get the mod pack's version.\n     * @return the mod pack's version.\n     */\n    public String getVersion()\n    {\n        return this.version;\n    }\n\n    /**\n     * Get the mod pack's author.\n     * @return the mod pack's author.\n     */\n    public String getAuthor()\n    {\n        return this.author;\n    }\n\n    /**\n     * Get the mods in the mod pack.\n     * @return the mods in the mod pack.\n     */\n    public List<CurseModPackMod> getMods()\n    {\n        return this.mods;\n    }\n\n    /**\n     * A Curse Forge's mod from a mod pack.\n     */\n    public static class CurseModPackMod extends Mod\n    {\n        private final boolean required;\n\n        CurseModPackMod(String name, String downloadURL, String sha1, long size, boolean required)\n        {\n            super(name, downloadURL, sha1, size);\n            this.required = required;\n        }\n\n        CurseModPackMod(@NotNull Mod base, boolean required)\n        {\n            this(base.getName(), base.getDownloadURL(), base.getSha1(), base.getSize(), required);\n        }\n\n        /**\n         * Is the mod required.\n         * @return if the mod is required.\n         */\n        public boolean isRequired()\n        {\n            return this.required;\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/integrations/curseforgeintegration/ICurseForgeCompatible.java",
    "content": "package fr.flowarg.flowupdater.integrations.curseforgeintegration;\n\nimport fr.flowarg.flowupdater.download.json.CurseFileInfo;\nimport fr.flowarg.flowupdater.download.json.CurseModPackInfo;\nimport fr.flowarg.flowupdater.download.json.Mod;\n\nimport java.util.List;\n\n/**\n * This class represents an object that using CurseForge features.\n */\npublic interface ICurseForgeCompatible\n{\n    /**\n     * Get all curse mods to update.\n     * @return all curse mods.\n     */\n    List<CurseFileInfo> getCurseMods();\n\n    /**\n     * Get information about the mod pack to update.\n     * @return mod pack's information.\n     */\n    CurseModPackInfo getCurseModPackInfo();\n\n    /**\n     * Define all curse mods to update.\n     * @param curseMods curse mods to define.\n     */\n    void setAllCurseMods(List<Mod> curseMods);\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/integrations/curseforgeintegration/package-info.java",
    "content": "/**\n * CurseForge Integration package.\n */\npackage fr.flowarg.flowupdater.integrations.curseforgeintegration;"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/integrations/modrinthintegration/IModrinthCompatible.java",
    "content": "package fr.flowarg.flowupdater.integrations.modrinthintegration;\n\nimport fr.flowarg.flowupdater.download.json.Mod;\nimport fr.flowarg.flowupdater.download.json.ModrinthModPackInfo;\nimport fr.flowarg.flowupdater.download.json.ModrinthVersionInfo;\n\nimport java.util.List;\n\npublic interface IModrinthCompatible\n{\n    /**\n     * Get all modrinth mods to update.\n     * @return all modrinth mods.\n     */\n    List<ModrinthVersionInfo> getModrinthMods();\n\n    /**\n     * Get information about the mod pack to update.\n     * @return mod pack's information.\n     */\n    ModrinthModPackInfo getModrinthModPackInfo();\n\n    /**\n     * Get the modrinth mod pack.\n     * @return the modrinth mod pack.\n     */\n    ModrinthModPack getModrinthModPack();\n\n    /**\n     * Define the modrinth mod pack.\n     * @param modrinthModPack the modrinth mod pack.\n     */\n    void setModrinthModPack(ModrinthModPack modrinthModPack);\n\n    /**\n     * Define all modrinth mods to update.\n     * @param modrinthMods modrinth mods to define.\n     */\n    void setAllModrinthMods(List<Mod> modrinthMods);\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/integrations/modrinthintegration/ModrinthIntegration.java",
    "content": "package fr.flowarg.flowupdater.integrations.modrinthintegration;\n\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport fr.flowarg.flowio.FileUtils;\nimport fr.flowarg.flowlogger.ILogger;\nimport fr.flowarg.flowstringer.StringUtils;\nimport fr.flowarg.flowupdater.download.json.Mod;\nimport fr.flowarg.flowupdater.download.json.ModrinthModPackInfo;\nimport fr.flowarg.flowupdater.download.json.ModrinthVersionInfo;\nimport fr.flowarg.flowupdater.integrations.Integration;\nimport fr.flowarg.flowupdater.utils.FlowUpdaterException;\nimport fr.flowarg.flowupdater.utils.IOUtils;\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\n\nimport java.net.URL;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\nimport java.util.ArrayList;\nimport java.util.Enumeration;\nimport java.util.List;\nimport java.util.zip.ZipEntry;\nimport java.util.zip.ZipFile;\n\npublic class ModrinthIntegration extends Integration\n{\n    private static final String MODRINTH_URL = \"https://api.modrinth.com/v2/\";\n    private static final String MODRINTH_VERSION_ENDPOINT = \"version/{versionId}\";\n    private static final String MODRINTH_PROJECT_VERSION_ENDPOINT = \"project/{projectId}/version\";\n\n    private final List<Mod> builtInMods = new ArrayList<>();\n\n    /**\n     * Default constructor of a basic Integration.\n     *\n     * @param logger the logger used.\n     * @param folder the folder where the plugin can work.\n     * @throws Exception if an error occurred.\n     */\n    public ModrinthIntegration(ILogger logger, Path folder) throws Exception\n    {\n        super(logger, folder);\n    }\n\n    public Mod fetchMod(@NotNull ModrinthVersionInfo versionInfo) throws Exception\n    {\n        if(!versionInfo.getVersionId().isEmpty())\n        {\n            final URL url = new URL(MODRINTH_URL + MODRINTH_VERSION_ENDPOINT\n                    .replace(\"{versionId}\", versionInfo.getVersionId()));\n\n            return this.parseModFile(JsonParser.parseString(IOUtils.getContent(url)).getAsJsonObject());\n        }\n\n        final URL url = new URL(MODRINTH_URL + MODRINTH_PROJECT_VERSION_ENDPOINT.replace(\"{projectId}\", versionInfo.getProjectReference()));\n        final JsonArray versions = JsonParser.parseString(IOUtils.getContent(url)).getAsJsonArray();\n        JsonObject version = null;\n        for (JsonElement jsonElement : versions)\n        {\n            if(!jsonElement.getAsJsonObject().get(\"version_number\").getAsString().equals(versionInfo.getVersionNumber()))\n                continue;\n\n            version = jsonElement.getAsJsonObject();\n            break;\n        }\n\n        if(version == null)\n            throw new FlowUpdaterException(\n                    \"No version found for \" + versionInfo.getVersionNumber() +\n                            \" in project \" + versionInfo.getProjectReference());\n\n        return this.parseModFile(version);\n    }\n\n    public Mod parseModFile(@NotNull JsonObject version)\n    {\n        final JsonObject fileJson = version.getAsJsonArray(\"files\").get(0).getAsJsonObject();\n        final String fileName = fileJson.get(\"filename\").getAsString();\n        final String downloadURL = fileJson.get(\"url\").getAsString();\n        final String sha1 = fileJson.getAsJsonObject(\"hashes\").get(\"sha1\").getAsString();\n        final long size = fileJson.get(\"size\").getAsLong();\n\n        return new Mod(fileName, downloadURL, sha1, size);\n    }\n\n    /**\n     * Get a CurseForge's mod pack object with a project identifier and a file identifier.\n     * @param info CurseForge's mod pack info.\n     * @return the curse's mod pack corresponding to given parameters.\n     * @throws Exception if an error occurred.\n     */\n    public ModrinthModPack getCurseModPack(ModrinthModPackInfo info) throws Exception\n    {\n        final Path modPackFile = this.checkForUpdate(info);\n        if(modPackFile == null)\n            throw new FlowUpdaterException(\"Can't find the mod pack file with the provided Modrinth mod pack info.\");\n        this.extractModPack(modPackFile, info.isInstallExtFiles());\n        return this.parseMods();\n    }\n\n    private @Nullable Path checkForUpdate(@NotNull ModrinthModPackInfo info) throws Exception\n    {\n        final Mod modPackFile = this.fetchMod(info);\n\n        if(modPackFile == null)\n        {\n            this.logger.err(\"This mod pack isn't available anymore on Modrinth (for 3rd parties maybe). \");\n            return null;\n        }\n\n        final Path outPath = this.folder.resolve(modPackFile.getName());\n\n        if(Files.notExists(outPath) || !FileUtils.getSHA1(outPath).equalsIgnoreCase(modPackFile.getSha1()))\n            IOUtils.download(this.logger, new URL(modPackFile.getDownloadURL()), outPath);\n\n        return outPath;\n    }\n\n    private void extractModPack(@NotNull Path out, boolean installExtFiles) throws Exception\n    {\n        this.logger.info(\"Extracting mod pack...\");\n        final ZipFile zipFile = new ZipFile(out.toFile(), ZipFile.OPEN_READ, StandardCharsets.UTF_8);\n        final Path dirPath = this.folder.getParent();\n        final Enumeration<? extends ZipEntry> entries = zipFile.entries();\n        while (entries.hasMoreElements())\n        {\n            final ZipEntry entry = entries.nextElement();\n            final String entryName = entry.getName();\n            final Path flPath = dirPath.resolve(StringUtils.empty(StringUtils.empty(entryName, \"client-overrides/\"), \"overrides/\"));\n\n            if(entryName.equalsIgnoreCase(\"modrinth.index.json\"))\n            {\n                if(Files.notExists(flPath) || entry.getCrc() != FileUtils.getCRC32(flPath))\n                    this.transferAndClose(flPath, zipFile, entry);\n                continue;\n            }\n\n            final String withoutOverrides = StringUtils.empty(StringUtils.empty(entryName, \"overrides/\"), \"client-overrides/\");\n\n            if(withoutOverrides.startsWith(\"mods/\") || withoutOverrides.startsWith(\"mods\\\\\"))\n            {\n                final String modName = withoutOverrides.substring(withoutOverrides.lastIndexOf('/') + 1);\n                final Mod mod = new Mod(modName, \"\", \"\", entry.getSize());\n                this.builtInMods.add(mod);\n            }\n\n            if(!installExtFiles || Files.exists(flPath)) continue;\n\n            if (flPath.getFileName().toString().endsWith(flPath.getFileSystem().getSeparator()))\n                Files.createDirectories(flPath);\n\n            if (entry.isDirectory()) continue;\n\n            this.transferAndClose(flPath, zipFile, entry);\n        }\n        zipFile.close();\n    }\n\n    private @NotNull ModrinthModPack parseMods() throws Exception\n    {\n        this.logger.info(\"Fetching mods...\");\n\n        final Path dirPath = this.folder.getParent();\n        final String manifestJson = StringUtils.toString(Files.readAllLines(dirPath.resolve(\"modrinth.index.json\")));\n        final JsonObject manifestObj = JsonParser.parseString(manifestJson).getAsJsonObject();\n        final String modPackName = manifestObj.get(\"name\").getAsString();\n        final String modPackVersion = manifestObj.get(\"versionId\").getAsString();\n        final List<Mod> mods = this.parseManifest(manifestObj);\n\n        return new ModrinthModPack(modPackName, modPackVersion, mods, this.builtInMods);\n    }\n\n    private @NotNull List<Mod> parseManifest(@NotNull JsonObject manifestObject)\n    {\n        final List<Mod> mods = new ArrayList<>();\n\n        final JsonArray files = manifestObject.getAsJsonArray(\"files\");\n\n        files.forEach(jsonElement -> {\n            final JsonObject file = jsonElement.getAsJsonObject();\n\n            if(file.getAsJsonObject(\"env\").get(\"client\").getAsString().equals(\"unsupported\"))\n                return;\n\n            final String name = StringUtils.empty(StringUtils.empty(file.get(\"path\").getAsString(), \"mods/\"), \"mods\\\\\");\n            final String downloadURL = file.getAsJsonArray(\"downloads\").get(0).getAsString();\n            final String sha1 = file.getAsJsonObject(\"hashes\").get(\"sha1\").getAsString();\n            final long size = file.get(\"fileSize\").getAsLong();\n\n            mods.add(new Mod(name, downloadURL, sha1, size));\n        });\n\n        return mods;\n    }\n\n    private void transferAndClose(@NotNull Path flPath, ZipFile zipFile, ZipEntry entry) throws Exception\n    {\n        if(Files.notExists(flPath.getParent()))\n            Files.createDirectories(flPath.getParent());\n\n        Files.copy(zipFile.getInputStream(entry), flPath, StandardCopyOption.REPLACE_EXISTING);\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/integrations/modrinthintegration/ModrinthModPack.java",
    "content": "package fr.flowarg.flowupdater.integrations.modrinthintegration;\n\nimport fr.flowarg.flowupdater.download.json.Mod;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class ModrinthModPack\n{\n    private final String name;\n    private final String version;\n    private final List<Mod> mods;\n    private final List<Mod> builtInMods;\n\n    ModrinthModPack(String name, String version, List<Mod> mods)\n    {\n        this(name, version, mods, new ArrayList<>());\n    }\n\n    ModrinthModPack(String name, String version, List<Mod> mods, List<Mod> builtInMods)\n    {\n        this.name = name;\n        this.version = version;\n        this.mods = mods;\n        this.builtInMods = builtInMods;\n    }\n\n    /**\n     * Get the mod pack's name.\n     * @return the mod pack's name.\n     */\n    public String getName()\n    {\n        return this.name;\n    }\n\n\n    /**\n     * Get the mod pack's version.\n     * @return the mod pack's version.\n     */\n    public String getVersion()\n    {\n        return this.version;\n    }\n\n    /**\n     * Get the mods in the mod pack.\n     * @return the mods in the mod pack.\n     */\n    public List<Mod> getMods()\n    {\n        return this.mods;\n    }\n\n    /**\n     * Get the built-in mods in the mod pack.\n     * Built-in mods are mods directly put in the mods folder in the .mrpack file.\n     * They are not downloaded from a remote server.\n     * This is not a very good way to add mods because it disables some mod verification on these mods.\n     * We recommend mod pack creators to use the built-in mods feature only for mods that are not available remotely.\n     * @return the built-in mods in the mod pack.\n     */\n    public List<Mod> getBuiltInMods()\n    {\n        return this.builtInMods;\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/integrations/modrinthintegration/package-info.java",
    "content": "/**\n * Modrinth Integration package.\n */\npackage fr.flowarg.flowupdater.integrations.modrinthintegration;"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/integrations/optifineintegration/IOptiFineCompatible.java",
    "content": "package fr.flowarg.flowupdater.integrations.optifineintegration;\n\nimport fr.flowarg.flowupdater.download.json.OptiFineInfo;\n\npublic interface IOptiFineCompatible\n{\n    /**\n     * Get information about OptiFine to update.\n     * @return OptiFine's information.\n     */\n    OptiFineInfo getOptiFineInfo();\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/integrations/optifineintegration/OptiFine.java",
    "content": "package fr.flowarg.flowupdater.integrations.optifineintegration;\n\n/**\n * This class represents a basic OptiFine object.\n */\npublic class OptiFine\n{\n    private final String name;\n    private final long size;\n\n    OptiFine(String name, long size)\n    {\n        this.name = name;\n        this.size = size;\n    }\n\n    /**\n     * Get the OptiFine filename.\n     * @return the OptiFine filename.\n     */\n    public String getName() {\n        return this.name;\n    }\n\n    /**\n     * Get the OptiFine file size.\n     * @return the OptiFine file size.\n     */\n    public long getSize() {\n        return this.size;\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/integrations/optifineintegration/OptiFineIntegration.java",
    "content": "package fr.flowarg.flowupdater.integrations.optifineintegration;\n\nimport fr.flowarg.flowlogger.ILogger;\nimport fr.flowarg.flowupdater.integrations.Integration;\nimport fr.flowarg.flowupdater.utils.FlowUpdaterException;\nimport fr.flowarg.flowupdater.utils.IOUtils;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.net.URL;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Arrays;\nimport java.util.Optional;\n\n/**\n * This integration supports the download of OptiFine in any version from the official site\n * (<a href=\"https://optifine.net\">OptiFine</a>).\n */\npublic class OptiFineIntegration extends Integration\n{\n    public OptiFineIntegration(ILogger logger, Path folder) throws Exception\n    {\n        super(logger, folder);\n    }\n\n    /**\n     * Get an OptiFine object from the official website.\n     * @param optiFineVersion the version of OptiFine\n     * @param preview if the OptiFine version is a preview.\n     * @return the object that defines the plugin\n     */\n    public OptiFine getOptiFine(String optiFineVersion, boolean preview)\n    {\n        try\n        {\n            final String fixedVersion = preview ? (optiFineVersion.startsWith(\"preview_OptiFine_\") ?\n                            optiFineVersion : optiFineVersion.startsWith(\"OptiFine_\") ?\n                    \"preview_\" + optiFineVersion : \"preview_OptiFine_\" + optiFineVersion) :\n                    optiFineVersion.startsWith(\"OptiFine_\") ? optiFineVersion : \"OptiFine_\" + optiFineVersion;\n            final String name = fixedVersion + \".jar\";\n            final String newUrl = this.getNewURL(name, preview, fixedVersion);\n\n            return new OptiFine(name, this.checkForUpdatesAndGetSize(name, newUrl));\n        }\n        catch (FlowUpdaterException e)\n        {\n            throw e;\n        }\n        catch (Exception e)\n        {\n            throw new FlowUpdaterException(e);\n        }\n    }\n\n    private @NotNull String getNewURL(String name, boolean preview, String optiFineVersion)\n    {\n        return \"https://optifine.net/downloadx?f=\" +\n                name +\n                \"&x=\" +\n                (preview ? this.getJsonPreview(optiFineVersion) : this.getJson(optiFineVersion));\n    }\n\n    private long checkForUpdatesAndGetSize(String name, String newUrl) throws Exception\n    {\n        final Path outputPath = this.folder.resolve(name);\n        if(Files.notExists(outputPath))\n            IOUtils.download(this.logger, new URL(newUrl), outputPath);\n        return Files.size(outputPath);\n    }\n\n    private @NotNull String getJson(String optiFineVersion)\n    {\n        try\n        {\n            final String[] respLine = IOUtils.getContent(new URL(\"https://optifine.net/adloadx?f=OptiFine_\" + optiFineVersion))\n                    .split(\"\\n\");\n            final Optional<String> result = Arrays.stream(respLine).filter(s -> s.contains(\"downloadx?f=OptiFine\")).findFirst();\n            if(result.isPresent())\n                return result.get()\n                        .replace(\"' onclick='onDownload()'>OptiFine \" + optiFineVersion.replace(\"_\", \" \") +\n                                         \"</a>\", \"\")\n                        .replace(\"<a href='downloadx?f=OptiFine_\" + optiFineVersion + \"&x=\", \"\")\n                        .replace(\" \", \"\");\n            else throw new FlowUpdaterException(\"No line found in: \" + Arrays.toString(respLine));\n        }\n        catch (Exception e)\n        {\n            throw new FlowUpdaterException(e);\n        }\n    }\n\n    private @NotNull String getJsonPreview(String optiFineVersion)\n    {\n        try\n        {\n            final String[] respLine = IOUtils.getContent(new URL(\"https://optifine.net/adloadx?f=\" + optiFineVersion))\n                    .split(\"\\n\");\n            final Optional<String> result = Arrays.stream(respLine).filter(s -> s.contains(\"downloadx?f=preview\")).findFirst();\n            if(result.isPresent())\n                return result.get()\n                        .replace(\"' onclick='onDownload()'>\" + optiFineVersion.replace(\"_\", \" \") +\n                                         \"</a>\", \"\")\n                        .replace(\"<a href='downloadx?f=\" + optiFineVersion + \"&x=\", \"\")\n                        .replace(\" \", \"\");\n            else throw new FlowUpdaterException(\"No line found in: \" + Arrays.toString(respLine));\n        }\n        catch (Exception e)\n        {\n            throw new FlowUpdaterException(e);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/integrations/optifineintegration/package-info.java",
    "content": "/**\n * OptiFine Integration package.\n */\npackage fr.flowarg.flowupdater.integrations.optifineintegration;"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/integrations/package-info.java",
    "content": "/**\n * Integration API package.\n */\npackage fr.flowarg.flowupdater.integrations;"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/package-info.java",
    "content": "/**\n * Main package of FlowUpdater. The only class contained in this package is {@link fr.flowarg.flowupdater.FlowUpdater}.\n */\npackage fr.flowarg.flowupdater;"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/utils/ExternalFileDeleter.java",
    "content": "package fr.flowarg.flowupdater.utils;\n\nimport fr.flowarg.flowio.FileUtils;\nimport fr.flowarg.flowupdater.download.DownloadList;\nimport fr.flowarg.flowupdater.download.json.ExternalFile;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.List;\n\n/**\n * A file deleter designed to check external files.\n */\npublic class ExternalFileDeleter implements IFileDeleter\n{\n    /**\n     * Delete all bad files in the provided directory.\n     * @param externalFiles the list of external files.\n     * @param downloadList the download list.\n     * @param dir the base dir.\n     * @throws Exception thrown if an error occurred\n     */\n    public void delete(@NotNull List<ExternalFile> externalFiles, DownloadList downloadList, Path dir) throws Exception\n    {\n        if(externalFiles.isEmpty()) return;\n\n        for(ExternalFile extFile : externalFiles)\n        {\n            final Path filePath = dir.resolve(extFile.getPath());\n\n            if (Files.exists(filePath))\n            {\n                if(!extFile.isUpdate()) continue;\n\n                if (FileUtils.getSHA1(filePath).equalsIgnoreCase(extFile.getSha1())) continue;\n\n                Files.delete(filePath);\n            }\n            downloadList.getExtFiles().add(extFile);\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/utils/FlowUpdaterException.java",
    "content": "package fr.flowarg.flowupdater.utils;\n\n/**\n * A simple runtime exception class that represents a fatal FlowUpdater error.\n */\npublic class FlowUpdaterException extends RuntimeException\n{\n    /**\n     * Initialize the exception.\n     */\n    public FlowUpdaterException()\n    {\n        super();\n    }\n\n    /**\n     * Initialize the exception with an error message.\n     * @param message error message.\n     */\n    public FlowUpdaterException(String message)\n    {\n        super(message);\n    }\n\n    /**\n     * Initialize the exception with an error message and a cause.\n     * @param message error message.\n     * @param cause cause.\n     */\n    public FlowUpdaterException(String message, Throwable cause)\n    {\n        super(message, cause);\n    }\n\n    /**\n     * Initialize the exception with a cause.\n     * @param cause cause.\n     */\n    public FlowUpdaterException(Throwable cause)\n    {\n        super(cause);\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/utils/IFileDeleter.java",
    "content": "package fr.flowarg.flowupdater.utils;\n\n/**\n * Just a marker class that is extended by all file deleter classes.\n */\npublic interface IFileDeleter {}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/utils/IOUtils.java",
    "content": "package fr.flowarg.flowupdater.utils;\n\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonParser;\nimport fr.flowarg.flowcompat.Platform;\nimport fr.flowarg.flowlogger.ILogger;\nimport fr.flowarg.flowupdater.FlowUpdater;\nimport org.jetbrains.annotations.NotNull;\nimport org.jetbrains.annotations.Nullable;\nimport org.w3c.dom.Document;\nimport org.w3c.dom.Element;\nimport org.w3c.dom.Node;\nimport org.w3c.dom.NodeList;\n\nimport javax.net.ssl.SSLHandshakeException;\nimport javax.xml.parsers.DocumentBuilder;\nimport javax.xml.parsers.DocumentBuilderFactory;\nimport java.io.*;\nimport java.net.HttpURLConnection;\nimport java.net.URL;\nimport java.nio.channels.Channels;\nimport java.nio.channels.ReadableByteChannel;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.StandardCopyOption;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.TimeUnit;\nimport java.util.function.Consumer;\n\n/**\n * A basic I/O utility class.\n */\npublic class IOUtils\n{\n    private static Path cachedMinecraftPath = null;\n    private static final Map<String, Integer> DOWNLOAD_RETRIES_CAUSED_BY_SSL_HANDSHAKE_EXCEPTION = new HashMap<>();\n\n    /**\n     * Download a remote file to a destination file.\n     * @param logger a valid logger instance.\n     * @param in the input url.\n     * @param out the output file.\n     */\n    public static void download(@NotNull ILogger logger, @NotNull URL in, @NotNull Path out)\n    {\n        final String url = in.toExternalForm();\n        try\n        {\n            logger.info(String.format(\"Downloading %s from %s...\", out.getFileName().toString(), url));\n            Files.createDirectories(out.toAbsolutePath().getParent());\n            Files.copy(catchForbidden(in), out, StandardCopyOption.REPLACE_EXISTING);\n        }\n        catch (SSLHandshakeException e)\n        {\n            if(DOWNLOAD_RETRIES_CAUSED_BY_SSL_HANDSHAKE_EXCEPTION.getOrDefault(url, 0) > 3)\n            {\n                logger.err(\"Too many retries caused by SSLHandshakeException when downloading file from: \" + url);\n                logger.printStackTrace(e);\n                DOWNLOAD_RETRIES_CAUSED_BY_SSL_HANDSHAKE_EXCEPTION.remove(url);\n                return;\n            }\n            download(logger, in, out);\n            DOWNLOAD_RETRIES_CAUSED_BY_SSL_HANDSHAKE_EXCEPTION.put(\n                    url,\n                    DOWNLOAD_RETRIES_CAUSED_BY_SSL_HANDSHAKE_EXCEPTION.getOrDefault(url, 0) + 1\n            );\n        }\n        catch (Exception e)\n        {\n            logger.printStackTrace(e);\n        }\n    }\n\n    /**\n     * Copy a local file to a destination file.\n     * @param logger a valid logger instance.\n     * @param in the input file.\n     * @param out the output file.\n     */\n    public static void copy(@NotNull ILogger logger, @NotNull Path in, @NotNull Path out)\n    {\n        try\n        {\n            logger.info(String.format(\"Copying %s to %s...\", in, out));\n            Files.createDirectories(out.getParent());\n            Files.copy(in, out, StandardCopyOption.REPLACE_EXISTING);\n        }\n        catch (Exception e)\n        {\n            logger.printStackTrace(e);\n        }\n    }\n\n    /**\n     * Get the content from a remote url.\n     * @param url the destination url\n     * @return the content.\n     */\n    public static @NotNull String getContent(URL url)\n    {\n        try\n        {\n            return getContent(catchForbidden(url));\n        } catch (Exception e)\n        {\n            FlowUpdater.DEFAULT_LOGGER.printStackTrace(e);\n            return \"\";\n        }\n    }\n\n    /**\n     * Get the content from a remote stream.\n     * @param remote the remote stream\n     * @return the content.\n     */\n    public static @NotNull String getContent(InputStream remote)\n    {\n        final StringBuilder sb = new StringBuilder();\n\n        try(InputStream stream = new BufferedInputStream(remote))\n        {\n            final ReadableByteChannel rbc = Channels.newChannel(stream);\n            final Reader enclosedReader = Channels.newReader(rbc, StandardCharsets.UTF_8.newDecoder(), -1);\n            final BufferedReader reader = new BufferedReader(enclosedReader);\n\n            int character;\n            while ((character = reader.read()) != -1) sb.append((char)character);\n\n            reader.close();\n            enclosedReader.close();\n            rbc.close();\n\n        } catch (Exception e)\n        {\n            FlowUpdater.DEFAULT_LOGGER.printStackTrace(e);\n        }\n        return sb.toString();\n    }\n\n    /**\n     * Reading an url in a json element\n     * @param jsonURL json input\n     * @return a json element\n     */\n    public static JsonElement readJson(URL jsonURL)\n    {\n        try\n        {\n            return readJson(catchForbidden(jsonURL));\n        } catch (Exception e)\n        {\n            FlowUpdater.DEFAULT_LOGGER.printStackTrace(e);\n        }\n        return JsonNull.INSTANCE;\n    }\n\n    /**\n     * Reading an inputStream in a json element\n     * @param inputStream json input\n     * @return a json element\n     */\n    public static JsonElement readJson(InputStream inputStream)\n    {\n        JsonElement element = JsonNull.INSTANCE;\n        try(InputStream stream = new BufferedInputStream(inputStream))\n        {\n            final ReadableByteChannel rbc = Channels.newChannel(stream);\n            final Reader enclosedReader = Channels.newReader(rbc, StandardCharsets.UTF_8.newDecoder(), -1);\n            final BufferedReader reader = new BufferedReader(enclosedReader);\n            final StringBuilder sb = new StringBuilder();\n\n            int character;\n            while ((character = reader.read()) != -1) sb.append((char)character);\n\n            element = JsonParser.parseString(sb.toString());\n\n            reader.close();\n            enclosedReader.close();\n            rbc.close();\n        } catch (Exception e)\n        {\n            FlowUpdater.DEFAULT_LOGGER.printStackTrace(e);\n        }\n\n        return element.getAsJsonObject();\n    }\n\n    /**\n     * A trick to avoid some forbidden response.\n     * @param url the destination url.\n     * @return the opened connection.\n     * @throws Exception if an I/O error occurred.\n     */\n    public static InputStream catchForbidden(@NotNull URL url) throws Exception\n    {\n        final HttpURLConnection connection = (HttpURLConnection)url.openConnection();\n        connection.addRequestProperty(\"User-Agent\", \"Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.124 Safari/537.36\");\n        connection.setInstanceFollowRedirects(true);\n        return connection.getInputStream();\n    }\n\n    /**\n     * Execute asynchronously a task for a collection of items.\n     * @param iterable the collection of items.\n     * @param service the executor service.\n     * @param runnable the task to execute.\n     * @param <T> the type of the items.\n     */\n    public static <T> void executeAsyncForEach(@NotNull Iterable<T> iterable, @NotNull ExecutorService service, Consumer<T> runnable)\n    {\n        try\n        {\n            iterable.forEach(t -> service.submit(() -> runnable.accept(t)));\n            service.shutdown();\n            service.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);\n        } catch (InterruptedException e)\n        {\n            throw new FlowUpdaterException(e);\n        }\n    }\n\n    public static @Nullable String getLatestArtifactVersion(String mavenMetadataUrl)\n    {\n        try\n        {\n            final DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();\n            final DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();\n            final Document doc = dBuilder.parse(new URL(mavenMetadataUrl).openStream());\n\n            return getLatestArtifactVersion(doc);\n        }\n        catch (Exception e)\n        {\n            FlowUpdater.DEFAULT_LOGGER.printStackTrace(e);\n            return null;\n        }\n    }\n\n    public static String getLatestArtifactVersion(@NotNull Document doc)\n    {\n        doc.getDocumentElement().normalize();\n\n        final Element root = doc.getDocumentElement();\n        final NodeList nList = root.getElementsByTagName(\"versioning\");\n        String version = \"\";\n\n        for (int temp = 0; temp < nList.getLength(); temp++)\n        {\n            final Node node = nList.item(temp);\n            if (node.getNodeType() != Node.ELEMENT_NODE)\n                continue;\n            version = ((Element) node).getElementsByTagName(\"release\").item(0).getTextContent();\n        }\n        return version;\n    }\n\n    /**\n     * Retrieve the local Minecraft folder path.\n     * @return the Minecraft folder path.\n     */\n    public static Path getMinecraftFolder()\n    {\n        return cachedMinecraftPath == null ?\n            cachedMinecraftPath = Paths.get(\n                    Platform.isOnWindows() ? System.getenv(\"APPDATA\")\n                    : (Platform.isOnMac() ? System.getProperty(\"user.home\") + \"/Library/Application Support/\"\n                            : System.getProperty(\"user.home\")), \".minecraft\")\n                : cachedMinecraftPath;\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/utils/ModFileDeleter.java",
    "content": "package fr.flowarg.flowupdater.utils;\n\nimport fr.flowarg.flowio.FileUtils;\nimport fr.flowarg.flowlogger.ILogger;\nimport fr.flowarg.flowupdater.download.json.Mod;\nimport fr.flowarg.flowupdater.integrations.modrinthintegration.ModrinthModPack;\nimport fr.flowarg.flowupdater.integrations.optifineintegration.OptiFine;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.*;\nimport java.util.regex.Pattern;\nimport java.util.stream.Collectors;\n\n/**\n * A file deleter designed to check mods.\n */\npublic class ModFileDeleter implements IFileDeleter\n{\n    private final boolean useFileDeleter;\n    private final String[] modsToIgnore;\n    private final Pattern modsToIgnorePattern;\n\n    public ModFileDeleter(boolean useFileDeleter, String... modsToIgnore)\n    {\n        this.useFileDeleter = useFileDeleter;\n        this.modsToIgnore = modsToIgnore;\n        this.modsToIgnorePattern = null;\n    }\n\n    public ModFileDeleter(String... modsToIgnore)\n    {\n        this(true, modsToIgnore);\n    }\n\n    public ModFileDeleter(boolean useFileDeleter, Pattern modsToIgnorePattern)\n    {\n        this.useFileDeleter = useFileDeleter;\n        this.modsToIgnore = null;\n        this.modsToIgnorePattern = modsToIgnorePattern;\n    }\n\n    public ModFileDeleter(Pattern modsToIgnorePattern)\n    {\n        this(true, modsToIgnorePattern);\n    }\n\n    /**\n     * Delete all bad files in the provided directory.\n     * @param logger the logger.\n     * @param modsDir the mod's folder.\n     * @param mods the mods list.\n     * @param optiFine the OptiFine object. (SPECIFIC USE CASE)\n     * @param modrinthModPack the modrinth mod pack. (SPECIFIC USE CASE)\n     * @throws Exception thrown if an error occurred\n     */\n    public void delete(ILogger logger, Path modsDir, List<Mod> mods, OptiFine optiFine, ModrinthModPack modrinthModPack) throws Exception\n    {\n        if(!this.isUseFileDeleter())\n            return;\n\n        final Set<Path> badFiles = new HashSet<>();\n        final List<Path> verifiedFiles = new ArrayList<>();\n\n        if(this.modsToIgnore != null)\n            Arrays.stream(this.modsToIgnore).forEach(fileName -> verifiedFiles.add(modsDir.resolve(fileName)));\n        else if(this.modsToIgnorePattern != null)\n        {\n            FileUtils.list(modsDir).stream().filter(path -> !Files.isDirectory(path)).forEach(path -> {\n                if(this.modsToIgnorePattern.matcher(path.getFileName().toString()).matches())\n                    verifiedFiles.add(path);\n            });\n        }\n\n        if(modrinthModPack != null)\n            modrinthModPack.getBuiltInMods().forEach(mod -> verifiedFiles.add(modsDir.resolve(mod.getName())));\n\n        for(Path fileInDir : FileUtils.list(modsDir).stream().filter(path -> !Files.isDirectory(path)).collect(Collectors.toList()))\n        {\n            if(verifiedFiles.contains(fileInDir))\n                continue;\n\n            if(mods.isEmpty() && optiFine == null)\n            {\n                if (!verifiedFiles.contains(fileInDir))\n                    badFiles.add(fileInDir);\n            }\n            else\n            {\n                if (optiFine != null)\n                {\n                    if (optiFine.getName().equalsIgnoreCase(fileInDir.getFileName().toString()))\n                    {\n                        if (Files.size(fileInDir) == optiFine.getSize())\n                        {\n                            badFiles.remove(fileInDir);\n                            verifiedFiles.add(fileInDir);\n                        }\n                        else badFiles.add(fileInDir);\n                    }\n                    else\n                    {\n                        if (!verifiedFiles.contains(fileInDir)) badFiles.add(fileInDir);\n                    }\n                }\n\n                for (Mod mod : mods)\n                {\n                    if (mod.getName().equalsIgnoreCase(fileInDir.getFileName().toString()))\n                    {\n                        if (Files.size(fileInDir) == mod.getSize() && (mod.getSha1().isEmpty() || FileUtils.getSHA1(fileInDir).equalsIgnoreCase(mod.getSha1())))\n                        {\n                            badFiles.remove(fileInDir);\n                            verifiedFiles.add(fileInDir);\n                        }\n                        else badFiles.add(fileInDir);\n                    }\n                    else\n                    {\n                        if (!verifiedFiles.contains(fileInDir))\n                            badFiles.add(fileInDir);\n                    }\n                }\n            }\n        }\n\n        badFiles.forEach(path -> {\n            try\n            {\n                Files.deleteIfExists(path);\n            } catch (Exception e)\n            {\n                logger.printStackTrace(e);\n            }\n        });\n        badFiles.clear();\n    }\n\n    public boolean isUseFileDeleter()\n    {\n        return this.useFileDeleter;\n    }\n\n    public String[] getModsToIgnore()\n    {\n        return this.modsToIgnore;\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/utils/UpdaterOptions.java",
    "content": "package fr.flowarg.flowupdater.utils;\n\nimport fr.flowarg.flowupdater.utils.builderapi.BuilderArgument;\nimport fr.flowarg.flowupdater.utils.builderapi.BuilderException;\nimport fr.flowarg.flowupdater.utils.builderapi.IBuilder;\n\nimport java.nio.file.Paths;\n\n/**\n * Represent some settings for FlowUpdater\n *\n * @author FlowArg\n */\npublic class UpdaterOptions\n{\n    public static final UpdaterOptions DEFAULT = new UpdaterOptions(new ExternalFileDeleter(), true, System.getProperty(\"java.home\") != null ? Paths.get(System.getProperty(\"java.home\"))\n            .resolve(\"bin\")\n            .resolve(\"java\")\n            .toAbsolutePath()\n            .toString() : \"java\"\n            , false);\n\n    private final ExternalFileDeleter externalFileDeleter;\n    private final boolean versionChecker;\n    private final String javaPath;\n    private final boolean disableExtFilesAsyncDownload;\n\n    private UpdaterOptions(ExternalFileDeleter externalFileDeleter, boolean versionChecker, String javaPath, boolean disableExtFilesAsyncDownload)\n    {\n        this.externalFileDeleter = externalFileDeleter;\n        this.versionChecker = versionChecker;\n        this.javaPath = javaPath;\n        this.disableExtFilesAsyncDownload = disableExtFilesAsyncDownload;\n    }\n\n    /**\n     * The external file deleter is used to check if some external files need to be downloaded.\n     * Default: {@link fr.flowarg.flowupdater.utils.ExternalFileDeleter}\n     * @return externalFileDeleter value.\n     */\n    public ExternalFileDeleter getExternalFileDeleter()\n    {\n        return this.externalFileDeleter;\n    }\n\n    /**\n     * Should check the version of FlowUpdater.\n     * @return true or false.\n     */\n    public boolean isVersionChecker()\n    {\n        return this.versionChecker;\n    }\n\n    /**\n     * The path to the java executable to use with Forge and Fabric installers.\n     * By default, it's taken from System.getProperty(\"java.home\").\n     * @return the path to the java executable.\n     */\n    public String getJavaPath()\n    {\n        return this.javaPath;\n    }\n\n    /**\n     * If set to true, external files will be downloaded 1 by 1 (as it always been). Asynchronous downloading of\n     * external files has been introduced in 1.9.4 in order to fasten the downloading step when a project\n     * needs many external files.\n     * @return true if asynchronous downloading of external files is disabled. False otherwise.\n     */\n    public boolean shouldDisableExtFilesAsyncDownload()\n    {\n        return this.disableExtFilesAsyncDownload;\n    }\n\n    /**\n     * Builder of {@link UpdaterOptions}\n     */\n    public static class UpdaterOptionsBuilder implements IBuilder<UpdaterOptions>\n    {\n        private final BuilderArgument<ExternalFileDeleter> externalFileDeleterArgument = new BuilderArgument<>(\"External FileDeleter\", ExternalFileDeleter::new).optional();\n        private final BuilderArgument<Boolean> versionChecker = new BuilderArgument<>(\"VersionChecker\", () -> true).optional();\n        private final BuilderArgument<String> javaPath = new BuilderArgument<>(\"JavaPath\", () ->\n                System.getProperty(\"java.home\") != null ? Paths.get(System.getProperty(\"java.home\"))\n                        .resolve(\"bin\")\n                        .resolve(\"java\")\n                        .toAbsolutePath()\n                        .toString() : \"java\")\n                .optional();\n        private final BuilderArgument<Boolean> disableExtFilesAsyncDownload = new BuilderArgument<>(\"DisableExtFilesAsyncDownload\", () -> false).optional();\n\n        /**\n         * Append an {@link ExternalFileDeleter} object.\n         * @param externalFileDeleter the file deleter to define.\n         * @return the builder.\n         */\n        public UpdaterOptionsBuilder withExternalFileDeleter(ExternalFileDeleter externalFileDeleter)\n        {\n            this.externalFileDeleterArgument.set(externalFileDeleter);\n            return this;\n        }\n\n        /**\n         * Enable or disable the version checker.\n         * @param versionChecker the value to define.\n         * @return the builder.\n         */\n        public UpdaterOptionsBuilder withVersionChecker(boolean versionChecker)\n        {\n            this.versionChecker.set(versionChecker);\n            return this;\n        }\n\n        /**\n         * Set the path to the java executable to use with Forge and Fabric installers.\n         * (Directly the java executable, not the java home)\n         * If you wish to set up the java home, you should use the {@link System#setProperty(String, String)} method\n         * with the \"java.home\" key.\n         * By default, it's taken from {@code System.getProperty(\"java.home\")}.\n         * @param javaPath the path to the java executable.\n         * @return the builder.\n         */\n        public UpdaterOptionsBuilder withJavaPath(String javaPath)\n        {\n            this.javaPath.set(javaPath);\n            return this;\n        }\n\n        /**\n         * Disable asynchronous downloading of external files. See {@link UpdaterOptions#shouldDisableExtFilesAsyncDownload()} for more information.\n         * @param disableExtFilesAsyncDownload true to disable asynchronous downloading of external files. False otherwise.\n         * @return the builder.\n         */\n        public UpdaterOptionsBuilder withDisableExtFilesAsyncDownload(boolean disableExtFilesAsyncDownload)\n        {\n            this.disableExtFilesAsyncDownload.set(disableExtFilesAsyncDownload);\n            return this;\n        }\n\n        /**\n         * Build an {@link UpdaterOptions} object.\n         */\n        @Override\n        public UpdaterOptions build() throws BuilderException\n        {\n            return new UpdaterOptions(\n                    this.externalFileDeleterArgument.get(),\n                    this.versionChecker.get(),\n                    this.javaPath.get(),\n                    this.disableExtFilesAsyncDownload.get()\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/utils/Version.java",
    "content": "package fr.flowarg.flowupdater.utils;\n\nimport org.jetbrains.annotations.Contract;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\n\npublic class Version implements Comparable<Version>\n{\n    private final List<Integer> version;\n\n    public Version(List<Integer> version)\n    {\n        this.version = version;\n    }\n\n    @Contract(\"_ -> new\")\n    public static @NotNull Version gen(@NotNull String version)\n    {\n        if(version.isEmpty())\n            throw new IllegalArgumentException(\"Version cannot be empty.\");\n        final String[] parts = version.split(\"\\\\.\");\n        final List<Integer> versionList = new ArrayList<>();\n        for (String part : parts)\n            versionList.add(Integer.parseInt(part));\n        return new Version(versionList);\n    }\n\n    @Override\n    public int compareTo(@NotNull Version o)\n    {\n        final int thisSize = this.version.size();\n        final int oSize = o.version.size();\n\n        for (int i = 0; i < Math.min(thisSize, oSize); i++)\n            if (!Objects.equals(this.version.get(i), o.version.get(i)))\n                return Integer.compare(this.version.get(i), o.version.get(i));\n\n        return Integer.compare(thisSize, oSize);\n    }\n\n    @Override\n    public String toString()\n    {\n        final StringBuilder builder = new StringBuilder();\n        for (int i = 0; i < this.version.size(); i++)\n        {\n            builder.append(this.version.get(i));\n            if (i < this.version.size() - 1)\n                builder.append(\".\");\n        }\n        return builder.toString();\n    }\n\n    public boolean isNewerThan(@NotNull Version o)\n    {\n        return this.compareTo(o) > 0;\n    }\n\n    public boolean isNewerOrEqualTo(@NotNull Version o)\n    {\n        return this.compareTo(o) >= 0;\n    }\n\n    public boolean isOlderThan(@NotNull Version o)\n    {\n        return this.compareTo(o) < 0;\n    }\n\n    public boolean isOlderOrEqualTo(@NotNull Version o)\n    {\n        return this.compareTo(o) <= 0;\n    }\n\n    public boolean isEqualTo(@NotNull Version o)\n    {\n        return this.compareTo(o) == 0;\n    }\n\n    public boolean isBetweenOrEqual(@NotNull Version min, @NotNull Version max)\n    {\n        return this.isNewerOrEqualTo(min) && this.isOlderOrEqualTo(max);\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/utils/VersionChecker.java",
    "content": "package fr.flowarg.flowupdater.utils;\n\nimport fr.flowarg.flowlogger.ILogger;\nimport fr.flowarg.flowupdater.FlowUpdater;\n\npublic class VersionChecker\n{\n    public static void run(ILogger logger)\n    {\n        new Thread(() -> {\n            final String version = IOUtils.getLatestArtifactVersion(\"https://repo1.maven.org/maven2/fr/flowarg/flowupdater/maven-metadata.xml\");\n\n            if (version == null)\n            {\n                logger.err(\"Couldn't get the latest version of FlowUpdater.\");\n                logger.err(\"Maybe the maven repository is down? Or your internet connection sucks?\");\n                return;\n            }\n\n            final int compare = Version.gen(FlowUpdater.FU_VERSION).compareTo(Version.gen(version));\n\n            if(compare > 0)\n            {\n                logger.info(\"You're running on an unpublished version of FlowUpdater. Are you in a dev environment?\");\n                return;\n            }\n\n            if(compare < 0)\n                logger.warn(String.format(\"Detected a new version of FlowUpdater (%s). You should update!\", version));\n        }).start();\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/utils/builderapi/BuilderArgument.java",
    "content": "package fr.flowarg.flowupdater.utils.builderapi;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport java.util.function.Supplier;\n\n/**\n * Builder API; Represent an argument for a Builder implementation.\n * @version 1.6\n * @author flow\n * \n * @param <T> Object Argument\n */\npublic class BuilderArgument<T>\n{\n    private final String objectName;\n    private T badObject = null;\n    private T object = null;\n    private boolean isRequired;\n\n    /**\n     * Construct a new BuilderArgument.\n     * @param objectName The name of the object.\n     * @param initialValue The initial value's wrapper.\n     */\n    public BuilderArgument(String objectName, @NotNull Supplier<T> initialValue)\n    {\n        this.objectName = objectName;\n        this.object = initialValue.get();\n    }\n\n    /**\n     * Construct a new basic BuilderArgument.\n     * @param objectName The name of the object.\n     */\n    public BuilderArgument(String objectName)\n    {\n        this.objectName = objectName;\n    }\n\n    /**\n     * Construct a new BuilderArgument.\n     * @param objectName The name of the object.\n     * @param initialValue The initial value's wrapper.\n     * @param badObject The initial bad value's wrapper.\n     */\n    public BuilderArgument(String objectName, @NotNull Supplier<T> initialValue, @NotNull Supplier<T> badObject)\n    {\n        this.objectName = objectName;\n        this.object = initialValue.get();\n        this.badObject = badObject.get();\n    }\n\n    /**\n     * Construct a new BuilderArgument.\n     * @param badObject The initial bad value's wrapper.\n     * @param objectName The name of the object.\n     */\n    public BuilderArgument(@NotNull Supplier<T> badObject, String objectName)\n    {\n        this.objectName = objectName;\n        this.badObject = badObject.get();\n    }\n\n    /**\n     * Check and get the wrapped object.\n     * @return the wrapper object.\n     * @throws BuilderException it the builder configuration is invalid.\n     */\n    public T get() throws BuilderException\n    {\n        if(this.object == this.badObject && this.badObject != null)\n            throw new BuilderException(\"Argument\" + this.objectName + \" is a bad object!\");\n\n        if(this.isRequired)\n        {\n            if(this.object == null)\n                throw new BuilderException(\"Argument\" + this.objectName + \" is null!\");\n            else return this.object;\n        }\n        else return this.object;\n    }\n\n    /**\n     * Define the new wrapped object.\n     * @param object the new wrapper object to define.\n     */\n    public void set(T object)\n    {\n        this.object = object;\n    }\n\n    /**\n     * Indicate that provided arguments are required if this argument is built.\n     * @param required required arguments.\n     * @return this.\n     */\n    public BuilderArgument<T> require(BuilderArgument<?> @NotNull ... required)\n    {\n        for (BuilderArgument<?> arg : required)\n            arg.isRequired = true;\n        return this;\n    }\n\n    /**\n     * Indicate that argument is required.\n     * @return this.\n     */\n    public BuilderArgument<T> required()\n    {\n        this.isRequired = true;\n        return this;\n    }\n\n    /**\n     * Indicate that argument is optional.\n     * @return this.\n     */\n    public BuilderArgument<T> optional()\n    {\n        this.isRequired = false;\n        return this;\n    }\n\n    /**\n     * Get the name of the current object's name.\n     * @return the object's name.\n     */\n    public String getObjectName()\n    {\n        return this.objectName;\n    }\n\n    /**\n     * Get the bad object.\n     * @return the bad object.\n     */\n    public T badObject()\n    {\n        return this.badObject;\n    }\n\n    @Override\n    public String toString()\n    {\n        return \"BuilderArgument{\" + \"objectName='\" + this.objectName + '\\'' + \", isRequired=\" + this.isRequired + '}';\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/utils/builderapi/BuilderException.java",
    "content": "package fr.flowarg.flowupdater.utils.builderapi;\n\n/**\n * Builder API; This exception is thrown when an error occurred with Builder API.\n * @version 1.6\n * @author flow\n */\npublic class BuilderException extends RuntimeException\n{\n    private static final long serialVersionUID = 1L;\n\n    public BuilderException()\n    {\n        super();\n    }\n\n    public BuilderException(String reason)\n    {\n        super(reason);\n    }\n\n    public BuilderException(String reason, Throwable cause)\n    {\n        super(reason, cause);\n    }\n\n    public BuilderException(Throwable cause)\n    {\n        super(cause);\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/utils/builderapi/IBuilder.java",
    "content": "package fr.flowarg.flowupdater.utils.builderapi;\n\n/**\n * Builder API ; Builder interface.\n * @version 1.6\n * @author flow\n *\n * @param <T> Object returned.\n */\n@FunctionalInterface\npublic interface IBuilder<T> \n{\n    /**\n     * Build a {@link T} object.\n     * @return a {@link T} object.\n     * @throws BuilderException if an error occurred when building an object.\n     */\n    T build() throws BuilderException;\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/utils/builderapi/package-info.java",
    "content": "/**\n * Builder API package.\n */\npackage fr.flowarg.flowupdater.utils.builderapi;"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/utils/package-info.java",
    "content": "/**\n * Utility package.\n */\npackage fr.flowarg.flowupdater.utils;"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/versions/AbstractModLoaderVersion.java",
    "content": "package fr.flowarg.flowupdater.versions;\n\nimport fr.flowarg.flowlogger.ILogger;\nimport fr.flowarg.flowupdater.FlowUpdater;\nimport fr.flowarg.flowupdater.download.DownloadList;\nimport fr.flowarg.flowupdater.download.IProgressCallback;\nimport fr.flowarg.flowupdater.download.Step;\nimport fr.flowarg.flowupdater.download.json.*;\nimport fr.flowarg.flowupdater.integrations.curseforgeintegration.ICurseForgeCompatible;\nimport fr.flowarg.flowupdater.integrations.modrinthintegration.IModrinthCompatible;\nimport fr.flowarg.flowupdater.integrations.modrinthintegration.ModrinthModPack;\nimport fr.flowarg.flowupdater.integrations.optifineintegration.IOptiFineCompatible;\nimport fr.flowarg.flowupdater.integrations.optifineintegration.OptiFine;\nimport fr.flowarg.flowupdater.utils.IOUtils;\nimport fr.flowarg.flowupdater.utils.ModFileDeleter;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.List;\n\npublic abstract class AbstractModLoaderVersion implements IModLoaderVersion, ICurseForgeCompatible, IModrinthCompatible, IOptiFineCompatible\n{\n    protected final List<Mod> mods;\n    protected final List<CurseFileInfo> curseMods;\n    protected final List<ModrinthVersionInfo> modrinthMods;\n    protected final ModFileDeleter fileDeleter;\n    protected final CurseModPackInfo curseModPackInfo;\n    protected final ModrinthModPackInfo modrinthModPackInfo;\n    protected final OptiFineInfo optiFineInfo;\n\n    protected String modLoaderVersion;\n    protected ILogger logger;\n    protected VanillaVersion vanilla;\n    protected DownloadList downloadList;\n    protected IProgressCallback callback;\n    protected String javaPath;\n    protected ModrinthModPack modrinthModPack;\n\n    public AbstractModLoaderVersion(String modLoaderVersion, List<Mod> mods, List<CurseFileInfo> curseMods,\n            List<ModrinthVersionInfo> modrinthMods, ModFileDeleter fileDeleter, CurseModPackInfo curseModPackInfo,\n            ModrinthModPackInfo modrinthModPackInfo, OptiFineInfo optiFineInfo)\n    {\n        this.modLoaderVersion = modLoaderVersion;\n        this.mods = mods;\n        this.curseMods = curseMods;\n        this.modrinthMods = modrinthMods;\n        this.fileDeleter = fileDeleter;\n        this.curseModPackInfo = curseModPackInfo;\n        this.modrinthModPackInfo = modrinthModPackInfo;\n        this.optiFineInfo = optiFineInfo;\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void attachFlowUpdater(@NotNull FlowUpdater flowUpdater)\n    {\n        this.logger = flowUpdater.getLogger();\n        this.vanilla = flowUpdater.getVanillaVersion();\n        this.downloadList = flowUpdater.getDownloadList();\n        this.callback = flowUpdater.getCallback();\n        this.javaPath = flowUpdater.getUpdaterOptions().getJavaPath();\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public List<Mod> getMods()\n    {\n        return this.mods;\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public DownloadList getDownloadList()\n    {\n        return this.downloadList;\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public IProgressCallback getCallback()\n    {\n        return this.callback;\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public List<CurseFileInfo> getCurseMods()\n    {\n        return this.curseMods;\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public List<ModrinthVersionInfo> getModrinthMods()\n    {\n        return this.modrinthMods;\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void setAllCurseMods(List<Mod> allCurseMods)\n    {\n        this.mods.addAll(allCurseMods);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public CurseModPackInfo getCurseModPackInfo()\n    {\n        return this.curseModPackInfo;\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public ModrinthModPackInfo getModrinthModPackInfo()\n    {\n        return this.modrinthModPackInfo;\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public OptiFineInfo getOptiFineInfo()\n    {\n        return this.optiFineInfo;\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void setAllModrinthMods(List<Mod> modrinthMods)\n    {\n        this.mods.addAll(modrinthMods);\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public ILogger getLogger()\n    {\n        return this.logger;\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public String getModLoaderVersion()\n    {\n        return this.modLoaderVersion;\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public ModFileDeleter getFileDeleter()\n    {\n        return this.fileDeleter;\n    }\n\n    @Override\n    public void setModrinthModPack(ModrinthModPack modrinthModPack)\n    {\n        this.modrinthModPack = modrinthModPack;\n    }\n\n    @Override\n    public ModrinthModPack getModrinthModPack()\n    {\n        return this.modrinthModPack;\n    }\n\n    @Override\n    public void installMods(@NotNull Path modsDir) throws Exception\n    {\n        this.callback.step(Step.MODS);\n        this.installAllMods(modsDir);\n\n        final OptiFine ofObj = this.downloadList.getOptiFine();\n\n        if(ofObj != null)\n        {\n            try\n            {\n                final Path optiFineFilePath = modsDir.resolve(ofObj.getName());\n\n                if (Files.notExists(optiFineFilePath) || Files.size(optiFineFilePath) != ofObj.getSize())\n                    IOUtils.copy(this.logger, modsDir.getParent().resolve(\".op\").resolve(ofObj.getName()), optiFineFilePath);\n            } catch (Exception e)\n            {\n                this.logger.printStackTrace(e);\n            }\n            this.downloadList.incrementDownloaded(ofObj.getSize());\n            this.callback.update(this.downloadList.getDownloadInfo());\n        }\n\n        this.fileDeleter.delete(this.logger, modsDir, this.mods, ofObj, this.modrinthModPack);\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/versions/IModLoaderVersion.java",
    "content": "package fr.flowarg.flowupdater.versions;\n\nimport fr.flowarg.flowlogger.ILogger;\nimport fr.flowarg.flowupdater.FlowUpdater;\nimport fr.flowarg.flowupdater.download.DownloadList;\nimport fr.flowarg.flowupdater.download.IProgressCallback;\nimport fr.flowarg.flowupdater.download.Step;\nimport fr.flowarg.flowupdater.download.json.Mod;\nimport fr.flowarg.flowupdater.utils.IOUtils;\nimport fr.flowarg.flowupdater.utils.ModFileDeleter;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.net.URL;\nimport java.nio.file.Path;\nimport java.util.List;\n\npublic interface IModLoaderVersion\n{\n    /**\n     * Attach {@link FlowUpdater} object to mod loaders, allow them to retrieve some information.\n     * @param flowUpdater flow updater object.\n     */\n    void attachFlowUpdater(@NotNull FlowUpdater flowUpdater);\n\n    /**\n     * Check if the current mod loader is already installed.\n     * @param installDir the dir to check.\n     * @return if the current mod loader is already installed.\n     */\n    boolean isModLoaderAlreadyInstalled(@NotNull Path installDir);\n\n    /**\n     * Install the current mod loader in a specified directory.\n     * @param installDir folder where the mod loader is going to be installed.\n     * @throws Exception if an I/O error occurred.\n     */\n    default void install(@NotNull Path installDir) throws Exception\n    {\n        this.getCallback().step(Step.MOD_LOADER);\n        this.getLogger().info(\"Installing \" + this.name() + \", version: \" + this.getModLoaderVersion() + \"...\");\n    }\n\n    /**\n     * Install all mods in the mods' directory.\n     * @param modsDir mods directory.\n     * @throws Exception if an I/O error occurred.\n     */\n    void installMods(@NotNull Path modsDir) throws Exception;\n\n    /**\n     * Get the mod loader version.\n     * @return the mod loader version.\n     */\n    String getModLoaderVersion();\n\n    /**\n     * Get all processed mods / mods to process.\n     * @return all processed mods / mods to process.\n     */\n    List<Mod> getMods();\n\n    /**\n     * Download mods in the mods' folder.\n     * @param modsDir mods' folder\n     */\n    default void installAllMods(@NotNull Path modsDir)\n    {\n        this.getDownloadList().getMods().forEach(mod -> {\n            try\n            {\n                final Path modFilePath = modsDir.resolve(mod.getName());\n                IOUtils.download(this.getLogger(), new URL(mod.getDownloadURL()), modFilePath);\n                this.getCallback().onFileDownloaded(modFilePath);\n            }\n            catch (Exception e)\n            {\n                this.getLogger().printStackTrace(e);\n            }\n            this.getDownloadList().incrementDownloaded(mod.getSize());\n            this.getCallback().update(this.getDownloadList().getDownloadInfo());\n        });\n    }\n\n    /**\n     * Get the {@link DownloadList} object.\n     * @return download info.\n     */\n    DownloadList getDownloadList();\n\n    /**\n     * Get the {@link ILogger} object.\n     * @return the logger.\n     */\n    ILogger getLogger();\n\n    /**\n     * Get the {@link IProgressCallback} object.\n     * @return the progress callback.\n     */\n    IProgressCallback getCallback();\n\n    /**\n     * Get the attached {@link ModFileDeleter} instance;\n     * @return this mod file deleter;\n     */\n    ModFileDeleter getFileDeleter();\n\n    /**\n     * Get the mod loader name.\n     * @return the mod loader name.\n     */\n    String name();\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/versions/ModLoaderUtils.java",
    "content": "package fr.flowarg.flowupdater.versions;\n\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport fr.flowarg.flowio.FileUtils;\nimport org.jetbrains.annotations.Contract;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.net.URL;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class ModLoaderUtils\n{\n    @Contract(pure = true)\n    public static @NotNull String buildJarUrl(String baseUrl, @NotNull String group, String artifact, String version)\n    {\n        return buildJarUrl(baseUrl, group, artifact, version, \"\");\n    }\n\n    @Contract(pure = true)\n    public static @NotNull String buildJarUrl(String baseUrl, @NotNull String group, String artifact, String version, String classifier)\n    {\n        return baseUrl + group.replace(\".\", \"/\") + \"/\" + artifact + \"/\" + version + \"/\" + artifact + \"-\" + version + classifier + \".jar\";\n    }\n\n    public static @NotNull Path buildLibraryPath(@NotNull Path installDir, @NotNull String group, String artifact, String version)\n    {\n        return installDir.resolve(\"libraries\")\n                .resolve(group.replace(\".\", installDir.getFileSystem().getSeparator()))\n                .resolve(artifact)\n                .resolve(version)\n                .resolve(artifact + \"-\" + version + \".jar\");\n    }\n\n    public static void fakeContext(@NotNull Path dirToInstall, String vanilla) throws Exception\n    {\n        final Path fakeProfiles = dirToInstall.resolve(\"launcher_profiles.json\");\n\n        Files.write(fakeProfiles, \"{}\".getBytes(StandardCharsets.UTF_8));\n\n        final Path versions = dirToInstall.resolve(\"versions\");\n        if(Files.notExists(versions))\n            Files.createDirectories(versions);\n\n        final Path vanillaVersion = versions.resolve(vanilla);\n        if(Files.notExists(vanillaVersion))\n            Files.createDirectories(vanillaVersion);\n\n        Files.copy(\n                dirToInstall.resolve(\"client.jar\"),\n                vanillaVersion.resolve(vanilla + \".jar\"),\n                StandardCopyOption.REPLACE_EXISTING\n        );\n    }\n\n    public static void removeFakeContext(@NotNull Path dirToInstall) throws Exception\n    {\n        FileUtils.deleteDirectory(dirToInstall.resolve(\"versions\"));\n        Files.deleteIfExists(dirToInstall.resolve(\"launcher_profiles.json\"));\n    }\n\n    public static @NotNull List<ParsedLibrary> parseNewVersionInfo(Path installDir, @NotNull JsonObject versionInfo) throws Exception\n    {\n        final List<ParsedLibrary> parsedLibraries = new ArrayList<>();\n\n        final JsonArray libraries = versionInfo.getAsJsonArray(\"libraries\");\n\n        for (final JsonElement libraryElement : libraries)\n        {\n            final JsonObject library = libraryElement.getAsJsonObject();\n            final String name = library.get(\"name\").getAsString();\n            final JsonObject downloads = library.getAsJsonObject(\"downloads\");\n            final JsonObject artifact = downloads.getAsJsonObject(\"artifact\");\n\n            final String path = artifact.get(\"path\").getAsString();\n            final String sha1 = artifact.get(\"sha1\").getAsString();\n            final String url = artifact.get(\"url\").getAsString();\n\n            final Path libraryPath = installDir.resolve(\"libraries\")\n                    .resolve(path.replace(\"/\", installDir.getFileSystem().getSeparator()));\n\n            final boolean installed = Files.exists(libraryPath) &&\n                    FileUtils.getSHA1(libraryPath).equalsIgnoreCase(sha1);\n\n            parsedLibraries.add(new ParsedLibrary(libraryPath, url.isEmpty() ? null : new URL(url), name, installed));\n        }\n\n        return parsedLibraries;\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/versions/ModLoaderVersionBuilder.java",
    "content": "package fr.flowarg.flowupdater.versions;\n\nimport fr.flowarg.flowupdater.download.json.*;\nimport fr.flowarg.flowupdater.utils.ModFileDeleter;\nimport fr.flowarg.flowupdater.utils.builderapi.BuilderArgument;\nimport fr.flowarg.flowupdater.utils.builderapi.BuilderException;\nimport fr.flowarg.flowupdater.utils.builderapi.IBuilder;\n\nimport java.net.URL;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.List;\n\n@SuppressWarnings(\"unchecked\")\npublic abstract class ModLoaderVersionBuilder<T extends IModLoaderVersion, B extends ModLoaderVersionBuilder<T, B>> implements IBuilder<T>\n{\n    protected final BuilderArgument<List<Mod>> modsArgument = new BuilderArgument<List<Mod>>(\"Mods\", ArrayList::new).optional();\n    protected final BuilderArgument<List<CurseFileInfo>> curseModsArgument = new BuilderArgument<List<CurseFileInfo>>(\"CurseMods\", ArrayList::new).optional();\n    protected final BuilderArgument<List<ModrinthVersionInfo>> modrinthModsArgument = new BuilderArgument<List<ModrinthVersionInfo>>(\"ModrinthMods\", ArrayList::new).optional();\n    protected final BuilderArgument<ModFileDeleter> fileDeleterArgument = new BuilderArgument<>(\"ModFileDeleter\", () -> new ModFileDeleter(false)).optional();\n    protected final BuilderArgument<CurseModPackInfo> curseModPackArgument = new BuilderArgument<CurseModPackInfo>(\"CurseModPack\").optional();\n    protected final BuilderArgument<ModrinthModPackInfo> modrinthPackArgument = new BuilderArgument<ModrinthModPackInfo>(\"ModrinthModPack\").optional();\n\n    /**\n     * Append a mod list to the version.\n     * @param mods mods to append.\n     * @return the builder.\n     */\n    public B withMods(List<Mod> mods)\n    {\n        this.modsArgument.get().addAll(mods);\n        return (B) this;\n    }\n\n    /**\n     * Append a single mod or a mod array to the version.\n     * @param mods mods to append.\n     * @return the builder.\n     */\n    public B withMods(Mod... mods)\n    {\n        return this.withMods(Arrays.asList(mods));\n    }\n\n    /**\n     * Append mods contained in the provided JSON url.\n     * @param jsonUrl The json URL of mods to append.\n     * @return the builder.\n     */\n    public B withMods(URL jsonUrl)\n    {\n        return this.withMods(Mod.getModsFromJson(jsonUrl));\n    }\n\n    /**\n     * Append mods contained in the provided JSON url.\n     * @param jsonUrl The json URL of mods to append.\n     * @return the builder.\n     */\n    public B withMods(String jsonUrl)\n    {\n        return this.withMods(Mod.getModsFromJson(jsonUrl));\n    }\n\n    /**\n     * Append a mod list to the version.\n     * @param curseMods CurseForge's mods to append.\n     * @return the builder.\n     */\n    public B withCurseMods(Collection<CurseFileInfo> curseMods)\n    {\n        this.curseModsArgument.get().addAll(curseMods);\n        return (B) this;\n    }\n\n    /**\n     * Append a single mod or a mod array to the version.\n     * @param curseMods CurseForge's mods to append.\n     * @return the builder.\n     */\n    public B withCurseMods(CurseFileInfo... curseMods)\n    {\n        return this.withCurseMods(Arrays.asList(curseMods));\n    }\n\n    /**\n     * Append mods contained in the provided JSON url.\n     * @param jsonUrl The json URL of mods to append.\n     * @return the builder.\n     */\n    public B withCurseMods(URL jsonUrl)\n    {\n        return this.withCurseMods(CurseFileInfo.getFilesFromJson(jsonUrl));\n    }\n\n    /**\n     * Append mods contained in the provided JSON url.\n     * @param jsonUrl The json URL of mods to append.\n     * @return the builder.\n     */\n    public B withCurseMods(String jsonUrl)\n    {\n        return this.withCurseMods(CurseFileInfo.getFilesFromJson(jsonUrl));\n    }\n\n    /**\n     * Append a mod list to the version.\n     * @param modrinthMods Modrinth's mods to append.\n     * @return the builder.\n     */\n    public B withModrinthMods(Collection<ModrinthVersionInfo> modrinthMods)\n    {\n        this.modrinthModsArgument.get().addAll(modrinthMods);\n        return (B) this;\n    }\n\n    /**\n     * Append a single mod or a mod array to the version.\n     * @param modrinthMods Modrinth's mods to append.\n     * @return the builder.\n     */\n    public B withModrinthMods(ModrinthVersionInfo... modrinthMods)\n    {\n        return this.withModrinthMods(Arrays.asList(modrinthMods));\n    }\n\n    /**\n     * Append mods contained in the provided JSON url.\n     * @param jsonUrl The json URL of mods to append.\n     * @return the builder.\n     */\n    public B withModrinthMods(URL jsonUrl)\n    {\n        return this.withModrinthMods(ModrinthVersionInfo.getModrinthVersionsFromJson(jsonUrl));\n    }\n\n    /**\n     * Append mods contained in the provided JSON url.\n     * @param jsonUrl The json URL of mods to append.\n     * @return the builder.\n     */\n    public B withModrinthMods(String jsonUrl)\n    {\n        return this.withModrinthMods(ModrinthVersionInfo.getModrinthVersionsFromJson(jsonUrl));\n    }\n\n    /**\n     * Assign to the future forge version a mod pack.\n     * @param modPackInfo the mod pack information to assign.\n     * @return the builder.\n     */\n    public B withCurseModPack(CurseModPackInfo modPackInfo)\n    {\n        this.curseModPackArgument.set(modPackInfo);\n        return (B) this;\n    }\n\n    /**\n     * Assign to the future forge version a mod pack.\n     * @param modPackInfo the mod pack information to assign.\n     * @return the builder.\n     */\n    public B withModrinthModPack(ModrinthModPackInfo modPackInfo)\n    {\n        this.modrinthPackArgument.set(modPackInfo);\n        return (B) this;\n    }\n\n    /**\n     * Append a file deleter to the version.\n     * @param fileDeleter the file deleter to append.\n     * @return the builder.\n     */\n    public B withFileDeleter(ModFileDeleter fileDeleter)\n    {\n        this.fileDeleterArgument.set(fileDeleter);\n        return (B) this;\n    }\n\n    @Override\n    public abstract T build() throws BuilderException;\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/versions/ParsedLibrary.java",
    "content": "package fr.flowarg.flowupdater.versions;\n\nimport fr.flowarg.flowlogger.ILogger;\nimport fr.flowarg.flowupdater.utils.IOUtils;\n\nimport java.net.URL;\nimport java.nio.file.Path;\nimport java.util.Optional;\n\npublic class ParsedLibrary\n{\n    private final Path path;\n    private final URL url;\n    private final String artifact;\n    private final boolean installed;\n\n    public ParsedLibrary(Path path, URL url, String artifact, boolean installed)\n    {\n        this.path = path;\n        this.url = url;\n        this.artifact = artifact;\n        this.installed = installed;\n    }\n\n    public void download(ILogger logger)\n    {\n        if(this.url != null)\n            IOUtils.download(logger, this.url, this.path);\n    }\n\n    public Path getPath()\n    {\n        return this.path;\n    }\n\n    public Optional<URL> getUrl()\n    {\n        return Optional.ofNullable(this.url);\n    }\n\n    public String getArtifact()\n    {\n        return this.artifact;\n    }\n\n    public boolean isInstalled()\n    {\n        return this.installed;\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/versions/VanillaVersion.java",
    "content": "package fr.flowarg.flowupdater.versions;\n\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport fr.flowarg.flowstringer.StringUtils;\nimport fr.flowarg.flowupdater.FlowUpdater;\nimport fr.flowarg.flowupdater.download.json.AssetDownloadable;\nimport fr.flowarg.flowupdater.download.json.AssetIndex;\nimport fr.flowarg.flowupdater.download.json.Downloadable;\nimport fr.flowarg.flowupdater.download.json.MCP;\nimport fr.flowarg.flowupdater.utils.IOUtils;\nimport fr.flowarg.flowupdater.utils.builderapi.BuilderArgument;\nimport fr.flowarg.flowupdater.utils.builderapi.BuilderException;\nimport fr.flowarg.flowupdater.utils.builderapi.IBuilder;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.io.InputStream;\nimport java.net.URL;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicReference;\n\npublic class VanillaVersion\n{\n    /**\n     * Default version. It used when an update doesn't need a Minecraft installation.\n     */\n    public static final VanillaVersion NULL_VERSION = new VanillaVersion(\"no\", null, false,\n                                                                         null, new ArrayList<>(),\n                                                                         new ArrayList<>(), null);\n    \n    private final String name;\n    private final MCP mcp;\n    private final boolean snapshot;\n    private final AssetIndex customAssetIndex;\n    private final List<AssetDownloadable> anotherAssets;\n    private final List<Downloadable> anotherLibraries;\n    private final boolean custom;\n    \n    private JsonElement json = null;\n    private String jsonURL = null;\n    \n    private VanillaVersion(String name, MCP mcp,\n            boolean snapshot,\n            AssetIndex customAssetIndex, List<AssetDownloadable> anotherAssets,\n            List<Downloadable> anotherLibraries, JsonObject customVersionJson)\n    {\n        this.name = name;\n        this.mcp = mcp;\n        this.snapshot = snapshot;\n        this.customAssetIndex = customAssetIndex;\n        this.anotherAssets = anotherAssets;\n        this.anotherLibraries = anotherLibraries;\n        this.custom = customVersionJson != null;\n        if(!this.name.equals(\"no\"))\n            this.json = (customVersionJson == null ? IOUtils.readJson(this.getJsonVersion()) : customVersionJson);\n    }\n\n    /**\n     * Get the JSON array representing all Minecraft's libraries.\n     * @return the libraries in JSON format.\n     */\n    public JsonArray getMinecraftLibrariesJson() \n    {\n        return this.json.getAsJsonObject().getAsJsonArray(\"libraries\");\n    }\n\n    /**\n     * Get the JSON object representing Minecraft's client.\n     * @return the client in JSON format.\n     */\n    public JsonObject getMinecraftClient() \n    {\n        if(!this.custom && this.mcp != null)\n        {\n            final JsonObject result = new JsonObject();\n            final String sha1 = this.mcp.getClientSha1();\n            final String url = this.mcp.getClientURL();\n            final long size = this.mcp.getClientSize();\n            if(StringUtils.checkString(sha1) && StringUtils.checkString(url) && size > 0)\n            {\n                result.addProperty(\"sha1\", sha1);\n                result.addProperty(\"size\", size);\n                result.addProperty(\"url\", url);\n                return result;\n            }\n            else FlowUpdater.DEFAULT_LOGGER.warn(\"Skipped MCP Client\");\n        }\n        return this.json.getAsJsonObject().getAsJsonObject(\"downloads\").getAsJsonObject(\"client\");\n    }\n\n    /**\n     * Get the JSON object representing Minecraft's asset index.\n     * @return the asset index in JSON format.\n     */\n    public JsonObject getMinecraftAssetIndex()\n    {\n        return this.json.getAsJsonObject().getAsJsonObject(\"assetIndex\");\n    }\n    \n    /**\n     * Get the input stream of the wanted version json.\n     */\n    private InputStream getJsonVersion()\n    {\n        final AtomicReference<String> version = new AtomicReference<>(this.getName());\n        final AtomicReference<InputStream> result = new AtomicReference<>(null);\n\n        try\n        {\n            final JsonObject launcherMeta = IOUtils.readJson(\n                    new URL(\"https://piston-meta.mojang.com/mc/game/version_manifest_v2.json\")\n                            .openStream())\n                    .getAsJsonObject();\n\n            if (this.getName().equals(\"latest\"))\n            {\n                final JsonObject latest = launcherMeta.getAsJsonObject(\"latest\");\n                if (this.snapshot)\n                    version.set(latest.get(\"snapshot\").getAsString());\n                else version.set(latest.get(\"release\").getAsString());\n            }\n\n            launcherMeta.getAsJsonArray(\"versions\").forEach(jsonElement ->\n            {\n                if (!jsonElement.getAsJsonObject().get(\"id\").getAsString().equals(version.get())) return;\n                try\n                {\n                    this.jsonURL = jsonElement.getAsJsonObject().get(\"url\").getAsString();\n                    result.set(new URL(this.jsonURL).openStream());\n                } catch (Exception e)\n                {\n                    FlowUpdater.DEFAULT_LOGGER.printStackTrace(e);\n                }\n            });\n        } catch (Exception e)\n        {\n            FlowUpdater.DEFAULT_LOGGER.printStackTrace(e);\n        }\n\n        return result.get();\n    }\n\n    /**\n     * Get the name of the version.\n     * @return the name of the version.\n     */\n    public @NotNull String getName()\n    {\n        return this.name;\n    }\n\n    /**\n     * Get the MCP object of the version.\n     * @return the MCP object of the version.\n     */\n    public MCP getMCP()\n    {\n        return this.mcp;\n    }\n\n    /**\n     * Is the current version a snapshot?\n     * @return if the current version is a snapshot.\n     */\n    public boolean isSnapshot()\n    {\n        return this.snapshot;\n    }\n\n    /**\n     * The custom asset index.\n     * @return the custom asset index.\n     */\n    public AssetIndex getCustomAssetIndex()\n    {\n        return this.customAssetIndex;\n    }\n\n    /**\n     * The list of custom assets.\n     * @return The list of custom assets.\n     */\n    public List<AssetDownloadable> getAnotherAssets()\n    {\n        return this.anotherAssets;\n    }\n\n    /**\n     * The list of custom libraries.\n     * @return The list of custom libraries.\n     */\n    public List<Downloadable> getAnotherLibraries()\n    {\n        return this.anotherLibraries;\n    }\n\n    /**\n     * Get the url of the JSON version.\n     * @return the url of the JSON version.\n     */\n    public String getJsonURL()\n    {\n        return this.jsonURL;\n    }\n\n    /**\n     * A builder for building a vanilla version like {@link FlowUpdater.FlowUpdaterBuilder}\n     * @author FlowArg\n     */\n    public static class VanillaVersionBuilder implements IBuilder<VanillaVersion>\n    {\n        private final BuilderArgument<String> nameArgument = new BuilderArgument<String>(\"Name\").required();\n        private final BuilderArgument<MCP> mcpArgument = new BuilderArgument<MCP>(\"MCP\").optional();\n        private final BuilderArgument<Boolean> snapshotArgument = new BuilderArgument<>(\"Snapshot\", () -> false).optional();\n        private final BuilderArgument<AssetIndex> customAssetIndexArgument = new BuilderArgument<AssetIndex>(\"CustomAssetIndex\").optional();\n        private final BuilderArgument<List<AssetDownloadable>> anotherAssetsArgument = new BuilderArgument<List<AssetDownloadable>>(\"AnotherAssets\", ArrayList::new).optional();\n        private final BuilderArgument<List<Downloadable>> anotherLibrariesArgument = new BuilderArgument<List<Downloadable>>(\"AnotherLibraries\", ArrayList::new).optional();\n        private final BuilderArgument<JsonObject> customVersionJsonArgument = new BuilderArgument<JsonObject>(\"CustomVersionJson\").optional();\n\n        /**\n         * Define the name of the wanted Minecraft version.\n         * @param name wanted Minecraft's version.\n         * @return the builder.\n         */\n        public VanillaVersionBuilder withName(String name)\n        {\n            this.nameArgument.set(name);\n            return this;\n        }\n\n        /**\n         * Append a mcp object to the version\n         * @param mcp the mcp object to append.\n         * @return the builder.\n         */\n        public VanillaVersionBuilder withMCP(MCP mcp)\n        {\n            this.mcpArgument.set(mcp);\n            return this;\n        }\n\n        /**\n         * Append a mcp object to the version\n         * @param mcpJsonUrl the mcp json url of mcp object to append.\n         * @return the builder.\n         */\n        public VanillaVersionBuilder withMCP(URL mcpJsonUrl)\n        {\n            return withMCP(MCP.getMCPFromJson(mcpJsonUrl));\n        }\n\n        /**\n         * Append a mcp object to the version\n         * @param mcpJsonUrl the mcp json url of mcp object to append.\n         * @return the builder.\n         */\n        public VanillaVersionBuilder withMCP(String mcpJsonUrl)\n        {\n            return withMCP(MCP.getMCPFromJson(mcpJsonUrl));\n        }\n\n        /**\n         * Required if you want the latest snapshot version. Otherwise, it's unnecessary.\n         * @param snapshot if the version is a snapshot.\n         * @return the builder.\n         */\n        public VanillaVersionBuilder withSnapshot(boolean snapshot)\n        {\n            this.snapshotArgument.set(snapshot);\n            return this;\n        }\n\n        /**\n         * Add custom asset index to the version.\n         * @param assetIndex the custom asset index to add.\n         * @return the builder.\n         */\n        public VanillaVersionBuilder withCustomAssetIndex(AssetIndex assetIndex)\n        {\n            this.customAssetIndexArgument.set(assetIndex);\n            return this;\n        }\n\n        /**\n         * Add custom assets to the version.\n         * @param anotherAssets custom assets to add.\n         * @return the builder.\n         */\n        public VanillaVersionBuilder withAnotherAssets(Collection<AssetDownloadable> anotherAssets)\n        {\n            this.anotherAssetsArgument.get().addAll(anotherAssets);\n            return this;\n        }\n\n        /**\n         * Add custom assets to the version.\n         * @param anotherAssets custom assets to add.\n         * @return the builder.\n         */\n        public VanillaVersionBuilder withAnotherAssets(AssetDownloadable... anotherAssets)\n        {\n            return withAnotherAssets(Arrays.asList(anotherAssets));\n        }\n\n        /**\n         * Add custom libraries to the version.\n         * @param anotherLibraries custom libraries to add.\n         * @return the builder.\n         */\n        public VanillaVersionBuilder withAnotherLibraries(Collection<Downloadable> anotherLibraries)\n        {\n            this.anotherLibrariesArgument.get().addAll(anotherLibraries);\n            return this;\n        }\n\n        /**\n         * Add custom libraries to the version.\n         * @param anotherLibraries custom libraries to add.\n         * @return the builder.\n         */\n        public VanillaVersionBuilder withAnotherLibraries(Downloadable... anotherLibraries)\n        {\n            return withAnotherLibraries(Arrays.asList(anotherLibraries));\n        }\n\n        /**\n         * Define the version's json.\n         * @param customVersionJson the custom version's json to set.\n         * @return the builder.\n         */\n        public VanillaVersionBuilder withCustomVersionJson(JsonObject customVersionJson)\n        {\n            this.customVersionJsonArgument.set(customVersionJson);\n            return this;\n        }\n\n        /**\n         * Define the version's json.\n         * @param customVersionJsonUrl the custom version's json url to set.\n         * @return the builder.\n         */\n        public VanillaVersionBuilder withCustomVersionJson(URL customVersionJsonUrl)\n        {\n            this.customVersionJsonArgument.set(IOUtils.readJson(customVersionJsonUrl).getAsJsonObject());\n            return this;\n        }\n\n        /**\n         * Build a new {@link VanillaVersion} instance with provided arguments.\n         * @return the freshly created instance.\n         * @throws BuilderException if an error occurred.\n         */\n        @Override\n        public VanillaVersion build() throws BuilderException\n        {\n            return new VanillaVersion(\n                    this.nameArgument.get(),\n                    this.mcpArgument.get(),\n                    this.snapshotArgument.get(),\n                    this.customAssetIndexArgument.get(),\n                    this.anotherAssetsArgument.get(),\n                    this.anotherLibrariesArgument.get(),\n                    this.customVersionJsonArgument.get()\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/versions/fabric/FabricBasedVersion.java",
    "content": "package fr.flowarg.flowupdater.versions.fabric;\n\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport fr.flowarg.flowio.FileUtils;\nimport fr.flowarg.flowupdater.download.json.*;\nimport fr.flowarg.flowupdater.utils.IOUtils;\nimport fr.flowarg.flowupdater.utils.ModFileDeleter;\nimport fr.flowarg.flowupdater.versions.AbstractModLoaderVersion;\nimport fr.flowarg.flowupdater.versions.ModLoaderUtils;\nimport fr.flowarg.flowupdater.versions.ParsedLibrary;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.net.URL;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.Callable;\n\npublic abstract class FabricBasedVersion extends AbstractModLoaderVersion\n{\n    protected final String metaApi;\n    protected String versionId;\n\n    public FabricBasedVersion(String modLoaderVersion, List<Mod> mods, List<CurseFileInfo> curseMods,\n            List<ModrinthVersionInfo> modrinthMods, ModFileDeleter fileDeleter, CurseModPackInfo curseModPackInfo,\n            ModrinthModPackInfo modrinthModPackInfo, OptiFineInfo optiFineInfo, String metaApi)\n    {\n        super(modLoaderVersion, mods, curseMods, modrinthMods, fileDeleter, curseModPackInfo, modrinthModPackInfo, optiFineInfo);\n        this.metaApi = metaApi;\n    }\n\n    @Override\n    public boolean isModLoaderAlreadyInstalled(@NotNull Path installDir)\n    {\n        final Path versionJsonFile = installDir.resolve(this.versionId + \".json\");\n\n        if(Files.notExists(versionJsonFile))\n            return false;\n\n        try {\n            return this.parseLibraries(versionJsonFile, installDir).stream().allMatch(ParsedLibrary::isInstalled);\n        }\n        catch (Exception e)\n        {\n            this.logger.err(\"An error occurred while checking if the mod loader is already installed.\");\n            return false;\n        }\n    }\n\n    /**\n     * {@inheritDoc}\n     */\n    @Override\n    public void install(final @NotNull Path installDir) throws Exception\n    {\n        super.install(installDir);\n\n        final Path versionJsonFile = installDir.resolve(this.versionId + \".json\");\n\n        IOUtils.download(this.logger, new URL(String.format(this.metaApi, this.vanilla.getName(), this.modLoaderVersion)), versionJsonFile);\n\n        try {\n            final List<ParsedLibrary> parsedLibraries = this.parseLibraries(versionJsonFile, installDir);\n            parsedLibraries.stream()\n                    .filter(parsedLibrary -> !parsedLibrary.isInstalled())\n                    .forEach(parsedLibrary -> parsedLibrary.download(this.logger));\n        }\n        catch (Exception e)\n        {\n            this.logger.err(\"An error occurred while installing the mod loader.\");\n        }\n    }\n\n    protected List<ParsedLibrary> parseLibraries(Path versionJsonFile, Path installDir) throws Exception\n    {\n        final List<ParsedLibrary> parsedLibraries = new ArrayList<>();\n        final JsonObject object = JsonParser.parseReader(Files.newBufferedReader(versionJsonFile))\n                .getAsJsonObject();\n        final JsonArray libraries = object.getAsJsonArray(\"libraries\");\n\n        for (final JsonElement libraryElement : libraries)\n        {\n            final JsonObject library = libraryElement.getAsJsonObject();\n            final String url = library.get(\"url\").getAsString();\n            final String completeArtifact = library.get(\"name\").getAsString();\n            final String[] name = completeArtifact.split(\":\");\n            final String group = name[0];\n            final String artifact = name[1];\n            final String version = name[2];\n\n            final String builtJarUrl = ModLoaderUtils.buildJarUrl(url, group, artifact, version);\n            final Path builtLibaryPath = ModLoaderUtils.buildLibraryPath(installDir, group, artifact, version);\n            final Callable<String> sha1 = this.getSha1FromLibrary(library, builtJarUrl);\n            final boolean installed = Files.exists(builtLibaryPath) &&\n                    FileUtils.getSHA1(builtLibaryPath).equalsIgnoreCase(sha1.call());\n\n            parsedLibraries.add(new ParsedLibrary(builtLibaryPath, new URL(builtJarUrl), completeArtifact, installed));\n        }\n        return parsedLibraries;\n    }\n\n    protected Callable<String> getSha1FromLibrary(@NotNull JsonObject library, String builtJarUrl)\n    {\n        final JsonElement sha1Elem = library.get(\"sha1\");\n        if (sha1Elem != null)\n            return sha1Elem::getAsString;\n\n        return () -> IOUtils.getContent(new URL(builtJarUrl + \".sha1\"));\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/versions/fabric/FabricVersion.java",
    "content": "package fr.flowarg.flowupdater.versions.fabric;\n\nimport fr.flowarg.flowupdater.FlowUpdater;\nimport fr.flowarg.flowupdater.download.json.*;\nimport fr.flowarg.flowupdater.utils.ModFileDeleter;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.util.List;\n\n/**\n * The object that contains Fabric's stuff.\n */\npublic class FabricVersion extends FabricBasedVersion\n{\n    private static final String FABRIC_META_API = \"https://meta.fabricmc.net/v2/versions/loader/%s/%s/profile/json\";\n\n    /**\n     * Use {@link FabricVersionBuilder} to instantiate this class.\n     * @param mods        {@link List<Mod>} to install.\n     * @param curseMods   {@link List<CurseFileInfo>} to install.\n     * @param fabricVersion to install.\n     * @param fileDeleter {@link ModFileDeleter} used to clean up mods' dir.\n     * @param curseModPackInfo {@link CurseModPackInfo} the mod pack you want to install.\n     */\n    FabricVersion(String fabricVersion, List<Mod> mods, List<CurseFileInfo> curseMods,\n            List<ModrinthVersionInfo> modrinthMods, ModFileDeleter fileDeleter, CurseModPackInfo curseModPackInfo,\n            ModrinthModPackInfo modrinthModPackInfo, OptiFineInfo optiFineInfo)\n    {\n        super(fabricVersion, mods, curseMods, modrinthMods, fileDeleter, curseModPackInfo, modrinthModPackInfo, optiFineInfo, FABRIC_META_API);\n    }\n\n    @Override\n    public void attachFlowUpdater(@NotNull FlowUpdater flowUpdater)\n    {\n        super.attachFlowUpdater(flowUpdater);\n        this.versionId = \"fabric-loader-\" + this.modLoaderVersion + \"-\" + this.vanilla.getName();\n    }\n\n    @Override\n    public String name()\n    {\n        return \"Fabric\";\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/versions/fabric/FabricVersionBuilder.java",
    "content": "package fr.flowarg.flowupdater.versions.fabric;\n\nimport fr.flowarg.flowupdater.download.json.OptiFineInfo;\nimport fr.flowarg.flowupdater.utils.IOUtils;\nimport fr.flowarg.flowupdater.utils.builderapi.BuilderArgument;\nimport fr.flowarg.flowupdater.utils.builderapi.BuilderException;\nimport fr.flowarg.flowupdater.versions.ModLoaderVersionBuilder;\n\npublic class FabricVersionBuilder extends ModLoaderVersionBuilder<FabricVersion, FabricVersionBuilder>\n{\n    private static final String FABRIC_VERSION_METADATA =\n            \"https://maven.fabricmc.net/net/fabricmc/fabric-loader/maven-metadata.xml\";\n\n    private final BuilderArgument<String> fabricVersionArgument =\n            new BuilderArgument<>(\"FabricVersion\", () ->\n                    IOUtils.getLatestArtifactVersion(FABRIC_VERSION_METADATA))\n                    .optional();\n    private final BuilderArgument<OptiFineInfo> optiFineArgument = new BuilderArgument<OptiFineInfo>(\"OptiFine\").optional();\n\n\n    /**\n     * @param fabricVersion the Fabric version you want to install\n     * (don't use this function if you want to use the latest fabric version).\n     * @return the builder.\n     */\n    public FabricVersionBuilder withFabricVersion(String fabricVersion)\n    {\n        this.fabricVersionArgument.set(fabricVersion);\n        return this;\n    }\n\n    /**\n     * Append some OptiFine download's information.\n     * @param optiFineInfo OptiFine info.\n     * @return the builder.\n     */\n    public FabricVersionBuilder withOptiFine(OptiFineInfo optiFineInfo)\n    {\n        this.optiFineArgument.set(optiFineInfo);\n        return this;\n    }\n\n    /**\n     * Build a new {@link FabricVersion} instance with provided arguments.\n     * @return the freshly created instance.\n     * @throws BuilderException if an error occurred.\n     */\n    @Override\n    public FabricVersion build() throws BuilderException\n    {\n        return new FabricVersion(\n                this.fabricVersionArgument.get(),\n                this.modsArgument.get(),\n                this.curseModsArgument.get(),\n                this.modrinthModsArgument.get(),\n                this.fileDeleterArgument.get(),\n                this.curseModPackArgument.get(),\n                this.modrinthPackArgument.get(),\n                this.optiFineArgument.get()\n        );\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/versions/fabric/QuiltVersion.java",
    "content": "package fr.flowarg.flowupdater.versions.fabric;\n\nimport fr.flowarg.flowupdater.FlowUpdater;\nimport fr.flowarg.flowupdater.download.json.*;\nimport fr.flowarg.flowupdater.utils.ModFileDeleter;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.util.List;\n\n/**\n * The object that contains Quilt's stuff.\n */\npublic class QuiltVersion extends FabricBasedVersion\n{\n    private static final String QUILT_META_API = \"https://meta.quiltmc.org/v3/versions/loader/%s/%s/profile/json\";\n\n    /**\n     * Use {@link QuiltVersionBuilder} to instantiate this class.\n     * @param mods        {@link List<Mod>} to install.\n     * @param curseMods   {@link List<CurseFileInfo>} to install.\n     * @param quiltVersion to install.\n     * @param fileDeleter {@link ModFileDeleter} used to clean up mods' dir.\n     * @param curseModPackInfo {@link CurseModPackInfo} the mod pack you want to install.\n     */\n    QuiltVersion(String quiltVersion, List<Mod> mods, List<CurseFileInfo> curseMods,\n            List<ModrinthVersionInfo> modrinthMods, ModFileDeleter fileDeleter, CurseModPackInfo curseModPackInfo,\n            ModrinthModPackInfo modrinthModPackInfo, OptiFineInfo optiFineInfo)\n    {\n        super(quiltVersion, mods, curseMods, modrinthMods, fileDeleter, curseModPackInfo, modrinthModPackInfo, optiFineInfo, QUILT_META_API);\n    }\n\n    @Override\n    public void attachFlowUpdater(@NotNull FlowUpdater flowUpdater)\n    {\n        super.attachFlowUpdater(flowUpdater);\n        this.versionId = \"quilt-loader-\" + this.modLoaderVersion + \"-\" + this.vanilla.getName();\n    }\n\n    @Override\n    public String name()\n    {\n        return \"Quilt\";\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/versions/fabric/QuiltVersionBuilder.java",
    "content": "package fr.flowarg.flowupdater.versions.fabric;\n\nimport fr.flowarg.flowupdater.download.json.OptiFineInfo;\nimport fr.flowarg.flowupdater.utils.IOUtils;\nimport fr.flowarg.flowupdater.utils.builderapi.BuilderArgument;\nimport fr.flowarg.flowupdater.utils.builderapi.BuilderException;\nimport fr.flowarg.flowupdater.versions.ModLoaderVersionBuilder;\n\npublic class QuiltVersionBuilder extends ModLoaderVersionBuilder<QuiltVersion, QuiltVersionBuilder>\n{\n    private static final String QUILT_VERSION_METADATA =\n            \"https://maven.quiltmc.org/repository/release/org/quiltmc/quilt-loader/maven-metadata.xml\";\n\n    private final BuilderArgument<String> quiltVersionArgument =\n            new BuilderArgument<>(\"QuiltVersion\", () -> IOUtils.getLatestArtifactVersion(QUILT_VERSION_METADATA)).optional();\n    private final BuilderArgument<OptiFineInfo> optiFineArgument = new BuilderArgument<OptiFineInfo>(\"OptiFine\").optional();\n\n    /**\n     * @param quiltVersion the Quilt version you want to install\n     * (don't use this function if you want to use the latest Quilt version).\n     * @return the builder.\n     */\n    public QuiltVersionBuilder withQuiltVersion(String quiltVersion)\n    {\n        this.quiltVersionArgument.set(quiltVersion);\n        return this;\n    }\n\n    /**\n     * Append some OptiFine download's information.\n     * @param optiFineInfo OptiFine info.\n     * @return the builder.\n     */\n    public QuiltVersionBuilder withOptiFine(OptiFineInfo optiFineInfo)\n    {\n        this.optiFineArgument.set(optiFineInfo);\n        return this;\n    }\n\n    /**\n     * Build a new {@link QuiltVersion} instance with provided arguments.\n     * @return the freshly created instance.\n     * @throws BuilderException if an error occurred.\n     */\n    @Override\n    public QuiltVersion build() throws BuilderException\n    {\n        return new QuiltVersion(\n                this.quiltVersionArgument.get(),\n                this.modsArgument.get(),\n                this.curseModsArgument.get(),\n                this.modrinthModsArgument.get(),\n                this.fileDeleterArgument.get(),\n                this.curseModPackArgument.get(),\n                this.modrinthPackArgument.get(),\n                this.optiFineArgument.get()\n        );\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/versions/fabric/package-info.java",
    "content": "/**\n * This package contains all the classes that are used to install Fabric-based mod loaders (Fabric and Quilt at the moment).\n */\npackage fr.flowarg.flowupdater.versions.fabric;"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/versions/forge/ForgeVersion.java",
    "content": "package fr.flowarg.flowupdater.versions.forge;\n\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport fr.flowarg.flowio.FileUtils;\nimport fr.flowarg.flowstringer.StringUtils;\nimport fr.flowarg.flowupdater.download.json.*;\nimport fr.flowarg.flowupdater.integrations.optifineintegration.IOptiFineCompatible;\nimport fr.flowarg.flowupdater.utils.IOUtils;\nimport fr.flowarg.flowupdater.utils.ModFileDeleter;\nimport fr.flowarg.flowupdater.utils.Version;\nimport fr.flowarg.flowupdater.versions.AbstractModLoaderVersion;\nimport fr.flowarg.flowupdater.versions.ModLoaderUtils;\nimport fr.flowarg.flowupdater.versions.ParsedLibrary;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.net.URI;\nimport java.net.URL;\nimport java.nio.file.*;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.concurrent.Callable;\nimport java.util.stream.Collectors;\n\npublic class ForgeVersion extends AbstractModLoaderVersion implements IOptiFineCompatible\n{\n    private final OptiFineInfo optiFineInfo;\n    private final String versionId;\n    private final boolean shouldUseInstaller;\n    private final boolean newInstallerJsonSpec;\n\n    public ForgeVersion(String modLoaderVersion, List<Mod> mods, List<CurseFileInfo> curseMods,\n            List<ModrinthVersionInfo> modrinthMods, ModFileDeleter fileDeleter, CurseModPackInfo curseModPackInfo,\n            ModrinthModPackInfo modrinthModPackInfo, OptiFineInfo optiFineInfo)\n    {\n        super(modLoaderVersion, mods, curseMods, modrinthMods, fileDeleter, curseModPackInfo, modrinthModPackInfo, optiFineInfo);\n        this.optiFineInfo = optiFineInfo;\n\n        final String[] data = this.modLoaderVersion.split(\"-\");\n        final String vanilla = data[0];\n        final String forge = data[1];\n        final Version vanillaVersion = Version.gen(vanilla);\n        if(vanillaVersion.isEqualTo(Version.gen(\"1.20.3\")))\n            throw new IllegalArgumentException(\"Forge 1.20.3 is not supported. (can't even launch through official launcher!).\");\n        final Version forgeVersion = Version.gen(forge);\n\n        if (data.length == 2)\n        {\n            if(forgeVersion.isNewerOrEqualTo(Version.gen(\"14.23.5.2851\")))\n            {\n                this.versionId = vanilla + \"-forge-\" + forge;\n                this.shouldUseInstaller = vanillaVersion.isNewerThan(Version.gen(\"1.12.2\"));\n                this.newInstallerJsonSpec = true;\n            }\n            else\n            {\n                this.versionId = vanilla + \"-forge\" + this.modLoaderVersion;\n                this.shouldUseInstaller = false;\n                this.newInstallerJsonSpec = false;\n            }\n        }\n        else\n        {\n            if(vanillaVersion.isOlderOrEqualTo(Version.gen(\"1.7.10\")))\n                this.versionId = vanilla + \"-Forge\" + forge + \"-\" + data[2];\n            else this.versionId = vanilla + \"-forge\" + this.modLoaderVersion;\n            this.shouldUseInstaller = false;\n            this.newInstallerJsonSpec = false;\n        }\n    }\n\n    @Override\n    public boolean isModLoaderAlreadyInstalled(@NotNull Path installDir)\n    {\n        final Path versionJsonFile = installDir.resolve(this.versionId + \".json\");\n\n        if(Files.notExists(versionJsonFile))\n            return false;\n\n        try {\n            final JsonObject object = JsonParser.parseReader(Files.newBufferedReader(versionJsonFile))\n                    .getAsJsonObject();\n\n            if(this.newInstallerJsonSpec)\n            {\n                final String vanillaVersionStr = this.vanilla.getName();\n                final Version vanillaVersion = Version.gen(vanillaVersionStr);\n                final boolean firstPass = ModLoaderUtils.parseNewVersionInfo(installDir, object).stream().allMatch(ParsedLibrary::isInstalled);\n\n                if(vanillaVersion.isEqualTo(Version.gen(\"1.12.2\")))\n                    return firstPass;\n\n                if(!firstPass)\n                    return false;\n\n                final Path librariesDir = installDir.resolve(\"libraries\");\n\n                // 1.13.2 --> 1.15.2 = minecraft (vanilla : slim + extra) + (vanilla-mcp : srg)\n                // 1.16.1 --> 1.20.2 = minecraft (vanilla-mcp : slim + extra + srg)\n\n                if(vanillaVersion.isBetweenOrEqual(Version.gen(\"1.13.2\"), Version.gen(\"1.15.2\")))\n                {\n                    final String mcpVersion = this.getMcpVersion(object);\n                    final Path minecraftDir = librariesDir\n                            .resolve(\"net\")\n                            .resolve(\"minecraft\")\n                            .resolve(\"client\");\n\n                    final Path vanillaDir = minecraftDir.resolve(vanillaVersionStr);\n                    final Path vanillaMcpDir = minecraftDir.resolve(vanillaVersionStr + \"-\" + mcpVersion);\n                    final Path extraJar = vanillaDir.resolve(\"client-\" + vanillaVersionStr + \"-extra.jar\");\n                    final Path extraJarCache = vanillaDir.resolve(\"client-\" + vanillaVersionStr + \"-extra.jar.cache\");\n                    final Path slimJar = vanillaDir.resolve(\"client-\" + vanillaVersionStr + \"-slim.jar\");\n                    final Path slimJarCache = vanillaDir.resolve(\"client-\" + vanillaVersionStr + \"-slim.jar.cache\");\n                    final Path srgJar = vanillaMcpDir.resolve(\"client-\" + vanillaVersionStr + \"-\" + mcpVersion + \"-srg.jar\");\n\n                    if (this.isSlimOrExtraSha1Wrong(extraJar, extraJarCache, slimJar, slimJarCache, srgJar))\n                        return false;\n                }\n                else if(vanillaVersion.isBetweenOrEqual(Version.gen(\"1.16.1\"), Version.gen(\"1.20.2\")))\n                {\n                    final String mcpVersion = this.getMcpVersion(object);\n                    final String clientId = \"client-\" + vanillaVersionStr + \"-\" + mcpVersion;\n                    final Path vanillaMcpDir = librariesDir\n                            .resolve(\"net\")\n                            .resolve(\"minecraft\")\n                            .resolve(\"client\")\n                            .resolve(vanillaVersionStr + \"-\" + mcpVersion);\n                    final Path extraJar = vanillaMcpDir.resolve(clientId + \"-extra.jar\");\n                    final Path extraJarCache = vanillaMcpDir.resolve(clientId + \"-extra.jar.cache\");\n                    final Path slimJar = vanillaMcpDir.resolve(clientId + \"-slim.jar\");\n                    final Path slimJarCache = vanillaMcpDir.resolve(clientId + \"-slim.jar.cache\");\n                    final Path srgJar = vanillaMcpDir.resolve(clientId + \"-srg.jar\");\n\n                    if (this.isSlimOrExtraSha1Wrong(extraJar, extraJarCache, slimJar, slimJarCache, srgJar))\n                        return false;\n                }\n\n                // 1.12.2 = libs\n                // 1.13.2 --> 1.20.2 = libs + client + universal\n                // 1.20.4 --> 1.21 = libs + shim\n\n                if(vanillaVersion.isBetweenOrEqual(Version.gen(\"1.13.2\"), Version.gen(\"1.20.2\")))\n                {\n                    final Path forgeDir = librariesDir\n                            .resolve(\"net\")\n                            .resolve(\"minecraftforge\")\n                            .resolve(\"forge\")\n                            .resolve(this.modLoaderVersion);\n\n                    final Path universalJar = forgeDir.resolve(\"forge-\" + this.modLoaderVersion + \"-universal.jar\");\n                    final Path clientJar = forgeDir.resolve(\"forge-\" + this.modLoaderVersion + \"-client.jar\");\n\n                    if(Files.notExists(universalJar) || Files.notExists(clientJar))\n                        return false;\n                }\n                else if(vanillaVersion.isNewerOrEqualTo(Version.gen(\"1.20.4\")))\n                {\n                    final Path shimJar = librariesDir\n                            .resolve(\"net\")\n                            .resolve(\"minecraftforge\")\n                            .resolve(\"forge\")\n                            .resolve(this.modLoaderVersion)\n                            .resolve(\"forge-\" + this.modLoaderVersion + \"-shim.jar\");\n\n                    if(Files.notExists(shimJar))\n                        return false;\n                }\n            }\n            else return this.parseOldVersionInfo(installDir, object).stream().allMatch(ParsedLibrary::isInstalled);\n        }\n        catch (Exception e)\n        {\n            this.logger.err(\"An error occurred while checking if the mod loader is already installed.\");\n            return false;\n        }\n\n        return true;\n    }\n\n    private String getMcpVersion(@NotNull JsonObject object)\n    {\n        final List<String> gameArguments = object\n                .getAsJsonObject(\"arguments\")\n                .getAsJsonArray(\"game\")\n                .asList()\n                .stream()\n                .filter(JsonElement::isJsonPrimitive)\n                .map(JsonElement::getAsString)\n                .collect(Collectors.toList());\n        return gameArguments.get(gameArguments.indexOf(\"--fml.mcpVersion\") + 1);\n    }\n\n    private boolean isSlimOrExtraSha1Wrong(Path extraJar, Path extraJarCache, Path slimJar, Path slimJarCache, Path srgJar) throws Exception\n    {\n        if(Files.notExists(extraJar) ||\n                Files.notExists(extraJarCache) ||\n                Files.notExists(slimJar) ||\n                Files.notExists(slimJarCache) ||\n                Files.notExists(srgJar)) return true;\n\n        final String extraJarSha1 = FileUtils.getSHA1(extraJar);\n        final String slimJarSha1 = FileUtils.getSHA1(slimJar);\n\n        String slimJarCacheSha1 = \"\";\n        for (final String line : Files.readAllLines(slimJarCache))\n        {\n            if(line.contains(\"Output: \"))\n            {\n                slimJarCacheSha1 = StringUtils.empty(line, \"Output: \");\n                break;\n            }\n        }\n\n        String extraJarCacheSha1 = \"\";\n        for (final String line : Files.readAllLines(extraJarCache))\n        {\n            if(line.contains(\"Output: \"))\n            {\n                extraJarCacheSha1 = StringUtils.empty(line, \"Output: \");\n                break;\n            }\n        }\n\n        return !extraJarSha1.equalsIgnoreCase(extraJarCacheSha1) || !slimJarSha1.equalsIgnoreCase(slimJarCacheSha1);\n    }\n\n    private @NotNull Callable<String> getSha1FromLibrary(@NotNull JsonObject library, String builtJarUrl)\n    {\n        final JsonElement checksumsElem = library.get(\"checksums\");\n        if (checksumsElem != null)\n        {\n            final JsonElement checksums = checksumsElem.getAsJsonArray().get(0);\n\n            if(checksums != null)\n                return checksums::getAsString;\n        }\n\n        return () -> IOUtils.getContent(new URL(builtJarUrl + \".sha1\"));\n    }\n\n    @Override\n    public void install(@NotNull Path installDir) throws Exception\n    {\n        super.install(installDir);\n\n        final String installerUrl = String.format(\"https://maven.minecraftforge.net/net/minecraftforge/forge/%s/forge-%s-installer.jar\",\n                                                  this.modLoaderVersion, this.modLoaderVersion);\n        final String[] installerUrlParts = installerUrl.split(\"/\");\n        final Path installerFile = installDir.resolve(installerUrlParts[installerUrlParts.length - 1]);\n        IOUtils.download(\n                this.logger,\n                new URL(installerUrl),\n                installerFile\n        );\n\n        if(this.newInstallerJsonSpec)\n        {\n            if(this.shouldUseInstaller)\n                this.useInstaller(installDir, installerFile);\n            else\n            {\n                this.logger.info(\"Installing libraries...\");\n                final URI uri = URI.create(\"jar:\" + installerFile.toAbsolutePath().toUri());\n                try (final FileSystem zipFs = FileSystems.newFileSystem(uri, new HashMap<>()))\n                {\n                    final Path versionFile = zipFs.getPath(\"version.json\");\n                    final Path versionJsonFile = installDir.resolve(this.versionId + \".json\");\n                    Files.copy(versionFile, versionJsonFile, StandardCopyOption.REPLACE_EXISTING);\n\n                    ModLoaderUtils.parseNewVersionInfo(installDir, JsonParser.parseReader(Files.newBufferedReader(versionFile)).getAsJsonObject())\n                            .stream()\n                            .filter(parsedLibrary -> !parsedLibrary.isInstalled())\n                            .forEach(parsedLibrary -> {\n                                if(parsedLibrary.getUrl().isPresent())\n                                    parsedLibrary.download(this.logger);\n                                else\n                                {\n                                    try\n                                    {\n                                        final String[] name = parsedLibrary.getArtifact().split(\":\");\n                                        final String group = name[0].replace('.', '/');\n                                        final String artifact = name[1];\n                                        final boolean hasExtension = name[2].contains(\"@\");\n                                        final String version = name[2].contains(\"@\") ? name[2].split(\"@\")[0] : name[2];\n                                        final String extension = hasExtension ? name[2].split(\"@\")[1] : \"jar\";\n                                        String classifier = \"\";\n                                        if(name.length == 4)\n                                            classifier = \"-\" + name[3];\n                                        Files.createDirectories(parsedLibrary.getPath().getParent());\n                                        Files.copy(zipFs.getPath(\"maven/\" + group + '/' + artifact + '/' + version + '/' + artifact + \"-\" + version + classifier + \".\" + extension), parsedLibrary.getPath(), StandardCopyOption.REPLACE_EXISTING);\n                                    } catch (Exception e)\n                                    {\n                                        this.logger.printStackTrace(e);\n                                    }\n                                }\n                            });\n                } catch (Exception e)\n                {\n                    this.logger.printStackTrace(e);\n                }\n            }\n        }\n        else\n        {\n            this.logger.info(\"Installing libraries...\");\n            final URI uri = URI.create(\"jar:\" + installerFile.toAbsolutePath().toUri());\n            try (final FileSystem zipFs = FileSystems.newFileSystem(uri, new HashMap<>()))\n            {\n                final Path installProfileFile = zipFs.getPath(\"install_profile.json\");\n                final JsonObject versionInfo = JsonParser.parseReader(Files.newBufferedReader(installProfileFile)).getAsJsonObject().getAsJsonObject(\"versionInfo\");\n                final Path versionJsonFile = installDir.resolve(this.versionId + \".json\");\n                Files.write(versionJsonFile, versionInfo.toString().getBytes(), StandardOpenOption.CREATE, StandardOpenOption.WRITE);\n\n                this.parseOldVersionInfo(installDir, versionInfo)\n                        .stream()\n                        .filter(parsedLibrary -> !parsedLibrary.isInstalled())\n                        .forEach(parsedLibrary -> parsedLibrary.download(this.logger));\n            } catch (Exception e)\n            {\n                this.logger.printStackTrace(e);\n            }\n        }\n        Files.deleteIfExists(installerFile);\n    }\n\n    private void useInstaller(Path installDir, @NotNull Path installerFile) throws Exception\n    {\n        this.logger.info(\"Launching installer...\");\n        ModLoaderUtils.fakeContext(installDir, this.vanilla.getName());\n\n        final List<String> command = new ArrayList<>();\n        command.add(this.javaPath);\n        command.add(\"-jar\");\n        command.add(installerFile.toAbsolutePath().toString());\n        command.add(\"--installClient\");\n        command.add(installDir.toAbsolutePath().toString());\n\n        final ProcessBuilder processBuilder = new ProcessBuilder(command);\n\n        processBuilder.directory(installDir.toFile());\n        processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT);\n\n        final Process process = processBuilder.start();\n        process.waitFor();\n\n        Files.copy(\n                installDir.resolve(\"versions\")\n                        .resolve(this.versionId)\n                        .resolve(this.versionId + \".json\"),\n                installDir.resolve(this.versionId + \".json\"),\n                StandardCopyOption.REPLACE_EXISTING\n        );\n\n        ModLoaderUtils.removeFakeContext(installDir);\n    }\n\n    private @NotNull List<ParsedLibrary> parseOldVersionInfo(Path installDir, @NotNull JsonObject versionInfo) throws Exception\n    {\n        final List<ParsedLibrary> parsedLibraries = new ArrayList<>();\n        final JsonArray libraries = versionInfo.getAsJsonArray(\"libraries\");\n\n        for (final JsonElement libraryElement : libraries)\n        {\n            final JsonObject library = libraryElement.getAsJsonObject();\n            final JsonElement clientreqElem = library.get(\"clientreq\");\n            final boolean shouldInstall = clientreqElem == null || clientreqElem.getAsBoolean();\n\n            if(!shouldInstall)\n                continue;\n\n            final JsonElement urlElem = library.get(\"url\");\n            final String baseUrl = urlElem == null ? \"https://libraries.minecraft.net/\" : urlElem.getAsString();\n            final String completeArtifact = library.get(\"name\").getAsString();\n            final String[] name = completeArtifact.split(\":\");\n            final String group = name[0];\n            final String artifact = name[1];\n            final String version = name[2];\n            final String classifier = artifact.equals(\"forge\") ? \"-universal\" : \"\";\n            final Path libraryPath = ModLoaderUtils.buildLibraryPath(installDir, group, artifact, version);\n            final String builtJarUrl = ModLoaderUtils.buildJarUrl(baseUrl, group, artifact, version, classifier);\n            final Callable<String> sha1 = this.getSha1FromLibrary(library, builtJarUrl);\n            final boolean installed = Files.exists(libraryPath) &&\n                    FileUtils.getSHA1(libraryPath).equals(sha1.call());\n\n            parsedLibraries.add(new ParsedLibrary(libraryPath, new URL(builtJarUrl), completeArtifact, installed));\n        }\n\n        return parsedLibraries;\n    }\n\n    @Override\n    public OptiFineInfo getOptiFineInfo()\n    {\n        return this.optiFineInfo;\n    }\n\n    @Override\n    public String name()\n    {\n        return \"Forge\";\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/versions/forge/ForgeVersionBuilder.java",
    "content": "package fr.flowarg.flowupdater.versions.forge;\n\nimport fr.flowarg.flowupdater.download.json.OptiFineInfo;\nimport fr.flowarg.flowupdater.utils.builderapi.BuilderArgument;\nimport fr.flowarg.flowupdater.utils.builderapi.BuilderException;\nimport fr.flowarg.flowupdater.versions.ModLoaderVersionBuilder;\n\n/**\n * Builder for {@link ForgeVersion}\n * @author Flow Arg (FlowArg)\n */\npublic class ForgeVersionBuilder extends ModLoaderVersionBuilder<ForgeVersion, ForgeVersionBuilder>\n{\n    private final BuilderArgument<String> forgeVersionArgument = new BuilderArgument<String>(\"ForgeVersion\").required();\n    private final BuilderArgument<OptiFineInfo> optiFineArgument = new BuilderArgument<OptiFineInfo>(\"OptiFine\").optional();\n\n    /**\n     * @param forgeVersion the Forge version you want to install. You should be very precise with the string you give.\n     * For instance, \"1.18.2-40.2.21\", \"1.12.2-14.23.5.2860\", \"1.8.9-11.15.1.2318-1.8.9\", \"1.7.10-10.13.4.1614-1.7.10\" are correct.\n     * Download an installer and check the name of it to get the correct version you should provide here if you are not sure.\n     * @return the builder.\n     */\n    public ForgeVersionBuilder withForgeVersion(String forgeVersion)\n    {\n        this.forgeVersionArgument.set(forgeVersion);\n        return this;\n    }\n\n    /**\n     * Append some OptiFine download's information.\n     * @param optiFineInfo provided information.\n     * @return the builder.\n     */\n    public ForgeVersionBuilder withOptiFine(OptiFineInfo optiFineInfo)\n    {\n        this.optiFineArgument.set(optiFineInfo);\n        return this;\n    }\n\n    @Override\n    public ForgeVersion build() throws BuilderException\n    {\n        return new ForgeVersion(\n                this.forgeVersionArgument.get(),\n                this.modsArgument.get(),\n                this.curseModsArgument.get(),\n                this.modrinthModsArgument.get(),\n                this.fileDeleterArgument.get(),\n                this.curseModPackArgument.get(),\n                this.modrinthPackArgument.get(),\n                this.optiFineArgument.get()\n        );\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/versions/forge/package-info.java",
    "content": "/**\n * This package contains all the classes that are used to install Forge.\n */\npackage fr.flowarg.flowupdater.versions.forge;"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/versions/neoforge/NeoForgeVersion.java",
    "content": "package fr.flowarg.flowupdater.versions.neoforge;\n\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport fr.flowarg.flowupdater.download.json.*;\nimport fr.flowarg.flowupdater.utils.IOUtils;\nimport fr.flowarg.flowupdater.utils.ModFileDeleter;\nimport fr.flowarg.flowupdater.utils.Version;\nimport fr.flowarg.flowupdater.versions.AbstractModLoaderVersion;\nimport fr.flowarg.flowupdater.versions.ModLoaderUtils;\nimport fr.flowarg.flowupdater.versions.ParsedLibrary;\nimport org.jetbrains.annotations.NotNull;\n\nimport java.net.URL;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.StandardCopyOption;\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class NeoForgeVersion extends AbstractModLoaderVersion\n{\n    private final boolean isOldNeoForge; // 1.20.1 neo forge versions only are \"old\"\n    private final String versionId;\n\n    NeoForgeVersion(String modLoaderVersion, List<Mod> mods, List<CurseFileInfo> curseMods,\n            List<ModrinthVersionInfo> modrinthMods, ModFileDeleter fileDeleter,\n            CurseModPackInfo curseModPackInfo, ModrinthModPackInfo modrinthModPackInfo, OptiFineInfo optiFineInfo)\n    {\n        super(modLoaderVersion, mods, curseMods, modrinthMods, fileDeleter, curseModPackInfo, modrinthModPackInfo, optiFineInfo);\n        this.isOldNeoForge = this.modLoaderVersion.startsWith(\"1.\");\n\n        if(this.isOldNeoForge)\n        {\n            final String[] oldNeoForgeVersionData = this.modLoaderVersion.split(\"-\");\n            final String vanillaVersion = oldNeoForgeVersionData[0];\n            final String oldNeoForgeVersion = oldNeoForgeVersionData[1];\n\n            this.versionId = String.format(\"%s-forge-%s\", vanillaVersion, oldNeoForgeVersion);\n        }\n        else this.versionId = String.format(\"neoforge-%s\", this.modLoaderVersion);\n    }\n\n    @Override\n    public boolean isModLoaderAlreadyInstalled(@NotNull Path installDir)\n    {\n        final Path versionJsonFile = installDir.resolve(this.versionId + \".json\");\n\n        if(Files.notExists(versionJsonFile))\n            return false;\n\n        try {\n            final JsonObject object = JsonParser.parseReader(Files.newBufferedReader(versionJsonFile))\n                    .getAsJsonObject();\n\n            final boolean firstPass = ModLoaderUtils.parseNewVersionInfo(installDir, object).stream().allMatch(ParsedLibrary::isInstalled);\n\n            if(!firstPass)\n                return false;\n        }\n        catch (Exception e)\n        {\n            this.logger.warn(\"An error occurred while checking if the mod loader is already installed.\");\n            return false;\n        }\n\n        final Path neoForgeDirectory = installDir.resolve(\"libraries\")\n                .resolve(\"net\")\n                .resolve(\"neoforged\")\n                .resolve(this.isOldNeoForge ? \"forge\" : \"neoforge\")\n                .resolve(this.modLoaderVersion);\n\n        final Path universalNeoForgeJar = neoForgeDirectory.resolve(this.versionId + \"-universal.jar\");\n        final Path minecraftClientPatchedJar = installDir.resolve(\"libraries\")\n                .resolve(\"net\")\n                .resolve(\"neoforged\")\n                .resolve(\"minecraft-client-patched\")\n                .resolve(this.modLoaderVersion)\n                .resolve(\"minecraft-client-patched-\" + this.modLoaderVersion + \".jar\"); // starting from 21.10.37-beta\n        final Path clientNeoForgeJar = neoForgeDirectory.resolve(this.versionId + \"-client.jar\");\n\n        final Version modLoaderVer = Version.gen(this.modLoaderVersion.split(\"-\")[0]); // skip -beta/alpha etc strings\n\n        return Files.exists(universalNeoForgeJar) && (\n                Files.exists(\n                        modLoaderVer.isNewerOrEqualTo(Version.gen(\"21.10.37\")) ? minecraftClientPatchedJar : clientNeoForgeJar\n                )\n        );\n    }\n\n    @Override\n    public void install(@NotNull Path installDir) throws Exception\n    {\n        super.install(installDir);\n\n        final String installerUrl = String.format(\n                \"https://maven.neoforged.net/releases/net/neoforged/%s/%s/%s-installer.jar\",\n                this.isOldNeoForge ? \"forge\" : \"neoforge\",\n                this.modLoaderVersion,\n                this.isOldNeoForge ? \"forge-\" + this.modLoaderVersion : this.versionId\n        );\n\n        ModLoaderUtils.fakeContext(installDir, this.vanilla.getName());\n\n        final String[] installerUrlParts = installerUrl.split(\"/\");\n        final Path installerFile = installDir.resolve(installerUrlParts[installerUrlParts.length - 1]);\n\n        IOUtils.download(this.logger, new URL(installerUrl), installerFile);\n\n        final List<String> command = new ArrayList<>();\n        command.add(this.javaPath);\n        command.add(\"-jar\");\n        command.add(installerFile.toAbsolutePath().toString());\n        command.add(\"--installClient\");\n        command.add(installDir.toAbsolutePath().toString());\n\n        final ProcessBuilder processBuilder = new ProcessBuilder(command);\n\n        processBuilder.directory(installDir.toFile());\n        processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT);\n\n        final Process process = processBuilder.start();\n        process.waitFor();\n\n        Files.copy(\n                installDir.resolve(\"versions\")\n                        .resolve(this.versionId)\n                        .resolve(this.versionId + \".json\"),\n                installDir.resolve(this.versionId + \".json\"),\n                StandardCopyOption.REPLACE_EXISTING\n        );\n\n        Files.deleteIfExists(installerFile);\n        ModLoaderUtils.removeFakeContext(installDir);\n    }\n\n    @Override\n    public String name()\n    {\n        return \"NeoForge\";\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/versions/neoforge/NeoForgeVersionBuilder.java",
    "content": "package fr.flowarg.flowupdater.versions.neoforge;\n\nimport fr.flowarg.flowupdater.download.json.OptiFineInfo;\nimport fr.flowarg.flowupdater.utils.builderapi.BuilderArgument;\nimport fr.flowarg.flowupdater.utils.builderapi.BuilderException;\nimport fr.flowarg.flowupdater.versions.ModLoaderVersionBuilder;\n\npublic class NeoForgeVersionBuilder extends ModLoaderVersionBuilder<NeoForgeVersion, NeoForgeVersionBuilder>\n{\n    private final BuilderArgument<String> neoForgeVersionArgument = new BuilderArgument<String>(\"NeoForgeVersion\").required();\n    private final BuilderArgument<OptiFineInfo> optiFineArgument = new BuilderArgument<OptiFineInfo>(\"OptiFine\").optional();\n\n    /**\n     * @param neoForgeVersion the NeoForge version you want to install.\n     * For 1.20.1, it should be in the format \"1.20.1-47.1.x\" (vanilla version-NeoForge version). (forge format)\n     * For 1.21 and above, it should only be the NeoForge version (for example: 21.8.31) (NeoForge version only).\n     * @return the builder.\n     */\n    public NeoForgeVersionBuilder withNeoForgeVersion(String neoForgeVersion)\n    {\n        this.neoForgeVersionArgument.set(neoForgeVersion);\n        return this;\n    }\n\n    /**\n     * Append some OptiFine download's information.\n     * @param optiFineInfo OptiFine info.\n     * @return the builder.\n     */\n    public NeoForgeVersionBuilder withOptiFine(OptiFineInfo optiFineInfo)\n    {\n        this.optiFineArgument.set(optiFineInfo);\n        return this;\n    }\n\n    @Override\n    public NeoForgeVersion build() throws BuilderException\n    {\n        return new NeoForgeVersion(\n                this.neoForgeVersionArgument.get(),\n                this.modsArgument.get(),\n                this.curseModsArgument.get(),\n                this.modrinthModsArgument.get(),\n                this.fileDeleterArgument.get(),\n                this.curseModPackArgument.get(),\n                this.modrinthPackArgument.get(),\n                this.optiFineArgument.get()\n        );\n    }\n}\n"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/versions/neoforge/package-info.java",
    "content": "/**\n * This package contains all the classes that are used to install NeoForge.\n */\npackage fr.flowarg.flowupdater.versions.neoforge;"
  },
  {
    "path": "src/main/java/fr/flowarg/flowupdater/versions/package-info.java",
    "content": "/**\n * This package contains all common classes to the versions system.\n */\npackage fr.flowarg.flowupdater.versions;"
  },
  {
    "path": "src/test/java/fr/flowarg/flowupdater/IntegrationTests.java",
    "content": "package fr.flowarg.flowupdater;\n\nimport fr.flowarg.flowio.FileUtils;\nimport fr.flowarg.flowupdater.utils.UpdaterOptions;\nimport org.junit.jupiter.api.*;\n\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\n\nimport static org.junit.jupiter.api.Assertions.assertFalse;\nimport static org.junit.jupiter.api.Assertions.assertTrue;\n\n@TestMethodOrder(MethodOrderer.OrderAnnotation.class)\npublic class IntegrationTests\n{\n    private static final Path UPDATE_DIR = Paths.get(\"testing_directory\");\n\n    @BeforeAll\n    public static void setup() throws Exception\n    {\n        Updates.UPDATE_DIR = UPDATE_DIR;\n        Files.createDirectory(UPDATE_DIR);\n    }\n\n    @AfterAll\n    public static void cleanup() throws Exception\n    {\n        FileUtils.deleteDirectory(UPDATE_DIR);\n    }\n\n    @Order(1)\n    @Test\n    public void testWithVanillaUsage() throws Exception\n    {\n        final Updates.Result result = Updates.vanillaUsage();\n        this.basicAssertions(result.updateDir, result.error, result.version);\n    }\n\n    @Order(2)\n    @Test\n    public void testWithNewForgeUsage() throws Exception\n    {\n        final Updates.Result result = Updates.testWithNewForgeUsage();\n        final String vanillaForge = result.version + \"-\" + result.modLoaderVersion;\n\n        this.basicAssertions(result.updateDir, result.error, result.version);\n\n        assertTrue(Files.exists(result.updateDir.resolve(String.format(\"%s-forge-%s.json\", result.version, result.modLoaderVersion))));\n        assertTrue(Files.exists(result.updateDir.resolve(\"libraries\").resolve(\"net\").resolve(\"minecraftforge\").resolve(\"forge\").resolve(vanillaForge).resolve(\"forge-\" + vanillaForge + \"-universal.jar\")));\n    }\n\n    @Order(3)\n    @Test\n    public void testWithVeryOldForgeUsage() throws Exception\n    {\n        final Updates.Result result = Updates.testWithVeryOldForgeUsage();\n        final String full = result.version + '-' + result.modLoaderVersion + '-' + result.version;\n\n        this.basicAssertions(result.updateDir, result.error, result.version);\n\n        assertTrue(Files.exists(result.updateDir.resolve(result.version + \"-Forge\" + result.modLoaderVersion + \"-\" + result.version + \".json\")));\n        assertTrue(Files.exists(result.updateDir.resolve(\"libraries\").resolve(\"net\").resolve(\"minecraftforge\").resolve(\"forge\").resolve(full).resolve(\"forge-\" + full + \".jar\")));\n    }\n\n    @Order(4)\n    @Test\n    public void testWithOldForgeUsage() throws Exception\n    {\n        final Updates.Result result = Updates.testWithOldForgeUsage();\n        final String full = result.version + '-' + result.modLoaderVersion;\n\n        this.basicAssertions(result.updateDir, result.error, result.version);\n\n        assertTrue(Files.exists(result.updateDir.resolve(result.version + \"-forge\" + full + \".json\")));\n        assertTrue(Files.exists(result.updateDir.resolve(\"libraries\").resolve(\"net\").resolve(\"minecraftforge\").resolve(\"forge\").resolve(full).resolve(\"forge-\" + full + \".jar\")));\n    }\n\n    @Order(5)\n    @Test\n    public void testWithFabric() throws Exception\n    {\n        final Updates.Result result = Updates.testWithFabric();\n\n        this.basicAssertions(result.updateDir, result.error, result.version);\n\n        assertTrue(Files.exists(result.updateDir.resolve(\"libraries\").resolve(\"net\").resolve(\"fabricmc\").resolve(\"fabric-loader\")));\n    }\n\n    @Order(6)\n    @Test\n    public void testWithQuilt() throws Exception\n    {\n        if(Integer.parseInt(System.getProperty(\"java.version\").split(\"\\\\.\")[0]) < 17)\n        {\n            System.out.println(\"Skipping test with Quilt because Java version is < 17\");\n            return;\n        }\n\n        final Updates.Result result = Updates.testWithQuilt(new UpdaterOptions.UpdaterOptionsBuilder().build());\n\n        this.basicAssertions(result.updateDir, result.error, result.version);\n\n        assertTrue(Files.exists(result.updateDir.resolve(\"libraries\").resolve(\"org\").resolve(\"quiltmc\").resolve(\"quilt-loader\")));\n    }\n\n    @Order(6)\n    @Test\n    public void testWithFabric119() throws Exception\n    {\n        final Updates.Result result = Updates.testWithFabric119();\n\n        this.basicAssertions(result.updateDir, result.error, result.version, false);\n        assertTrue(Files.exists(result.updateDir.resolve(\"libraries\").resolve(\"net\").resolve(\"fabricmc\").resolve(\"fabric-loader\")));\n    }\n\n    @Order(8)\n    @Test\n    public void testWithNeoForgeUsage() throws Exception\n    {\n        final Updates.Result result = Updates.testWithNeoForgeUsage();\n\n        this.basicAssertions(result.updateDir, result.error, result.version, false);\n\n        assertTrue(Files.exists(result.updateDir.resolve(String.format(\"neoforge-%s.json\", result.modLoaderVersion))));\n        assertTrue(Files.exists(result.updateDir.resolve(\"libraries\").resolve(\"net\").resolve(\"neoforged\").resolve(\"neoforge\").resolve(result.modLoaderVersion).resolve(\"neoforge-\" + result.modLoaderVersion + \"-universal.jar\")));\n    }\n\n    private void basicAssertions(Path updateDir, boolean error, String version) throws Exception\n    {\n        this.basicAssertions(updateDir, error, version, true);\n    }\n\n    private void basicAssertions(Path updateDir, boolean error, String version, boolean natives) throws Exception\n    {\n        assertFalse(error);\n        assertTrue(Files.exists(updateDir.resolve(version + \".json\")));\n        assertTrue(Files.exists(updateDir.resolve(\"client.jar\")));\n\n        if(natives)\n        {\n            final Path nativesDir = updateDir.resolve(\"natives\");\n            assertTrue(Files.exists(nativesDir));\n            assertTrue(Files.isDirectory(nativesDir));\n            assertFalse(FileUtils.list(nativesDir).isEmpty());\n        }\n\n        final Path librariesDir = updateDir.resolve(\"libraries\");\n        assertTrue(Files.exists(librariesDir));\n        assertTrue(Files.isDirectory(librariesDir));\n        assertFalse(FileUtils.list(librariesDir).isEmpty());\n        FileUtils.list(librariesDir).forEach(path -> assertTrue(Files.isDirectory(path)));\n        assertTrue(FileUtils.list(updateDir.resolve(\"assets\").resolve(\"objects\")).size() > 200);\n    }\n}\n"
  },
  {
    "path": "src/test/java/fr/flowarg/flowupdater/Updates.java",
    "content": "package fr.flowarg.flowupdater;\n\nimport fr.flowarg.flowupdater.utils.UpdaterOptions;\nimport fr.flowarg.flowupdater.versions.VanillaVersion;\nimport fr.flowarg.flowupdater.versions.fabric.FabricVersion;\nimport fr.flowarg.flowupdater.versions.fabric.FabricVersionBuilder;\nimport fr.flowarg.flowupdater.versions.fabric.QuiltVersion;\nimport fr.flowarg.flowupdater.versions.fabric.QuiltVersionBuilder;\nimport fr.flowarg.flowupdater.versions.forge.ForgeVersion;\nimport fr.flowarg.flowupdater.versions.forge.ForgeVersionBuilder;\nimport fr.flowarg.flowupdater.versions.neoforge.NeoForgeVersion;\nimport fr.flowarg.flowupdater.versions.neoforge.NeoForgeVersionBuilder;\n\nimport java.nio.file.Path;\n\npublic class Updates\n{\n    public static Path UPDATE_DIR;\n\n    public static Result vanillaUsage()\n    {\n        final String version = \"1.18.2\";\n        final Path updateDir = UPDATE_DIR.resolve(\"vanilla-\" + version);\n\n        boolean error = false;\n\n        try\n        {\n            final VanillaVersion vanillaVersion = new VanillaVersion.VanillaVersionBuilder()\n                    .withName(version)\n                    .build();\n\n            final FlowUpdater updater = new FlowUpdater.FlowUpdaterBuilder()\n                    .withVanillaVersion(vanillaVersion)\n                    .build();\n\n            updater.update(updateDir);\n        }\n        catch (Exception e)\n        {\n            error = true;\n            e.printStackTrace();\n        }\n\n        return new Result(updateDir, error, version);\n    }\n\n    public static Result testWithNewForgeUsage()\n    {\n        boolean error = false;\n        final String vanilla = \"1.18.2\";\n        final String forge = \"40.2.21\";\n        final String vanillaForge = vanilla + \"-\" + forge;\n        final Path updateDir = UPDATE_DIR.resolve(\"new_forge-\" + vanillaForge);\n\n        try\n        {\n            final VanillaVersion version = new VanillaVersion.VanillaVersionBuilder()\n                    .withName(vanilla)\n                    .build();\n\n            final ForgeVersion forgeVersion = new ForgeVersionBuilder()\n                    .withForgeVersion(vanillaForge)\n                    .build();\n\n            final FlowUpdater updater = new FlowUpdater.FlowUpdaterBuilder()\n                    .withVanillaVersion(version)\n                    .withModLoaderVersion(forgeVersion)\n                    .build();\n\n            updater.update(updateDir);\n        }\n        catch (Exception e)\n        {\n            error = true;\n            e.printStackTrace();\n        }\n\n        return new Result(updateDir, error, vanilla, forge);\n    }\n\n    public static Result testWithVeryOldForgeUsage()\n    {\n        boolean error = false;\n        final String vanilla = \"1.7.10\";\n        final String forge = \"10.13.4.1614\";\n        final String full = vanilla + '-' + forge + '-' + vanilla;\n        final Path updateDir = UPDATE_DIR.resolve(\"forge-\" + vanilla);\n\n        try\n        {\n            final VanillaVersion vanillaVersion = new VanillaVersion.VanillaVersionBuilder()\n                    .withName(vanilla)\n                    .build();\n\n            final ForgeVersion forgeVersion = new ForgeVersionBuilder()\n                    .withForgeVersion(full)\n                    .build();\n\n            final FlowUpdater updater = new FlowUpdater.FlowUpdaterBuilder()\n                    .withVanillaVersion(vanillaVersion)\n                    .withModLoaderVersion(forgeVersion)\n                    .build();\n\n            updater.update(updateDir);\n        }\n        catch (Exception e)\n        {\n            error = true;\n            e.printStackTrace();\n        }\n\n        return new Result(updateDir, error, vanilla, forge);\n    }\n\n    public static Result testWithOldForgeUsage()\n    {\n        boolean error = false;\n        final String vanilla = \"1.8.9\";\n        final String forge = \"11.15.1.2318-\" + vanilla;\n        final Path updateDir = UPDATE_DIR.resolve(\"forge-\" + vanilla);\n\n        try\n        {\n            final VanillaVersion vanillaVersion = new VanillaVersion.VanillaVersionBuilder()\n                    .withName(vanilla)\n                    .build();\n\n            final ForgeVersion forgeVersion = new ForgeVersionBuilder()\n                    .withForgeVersion(vanilla + '-' + forge)\n                    .build();\n\n            final FlowUpdater updater = new FlowUpdater.FlowUpdaterBuilder()\n                    .withVanillaVersion(vanillaVersion)\n                    .withModLoaderVersion(forgeVersion)\n                    .build();\n\n            updater.update(updateDir);\n        }\n        catch (Exception e)\n        {\n            error = true;\n            e.printStackTrace();\n        }\n\n        return new Result(updateDir, error, vanilla, forge);\n    }\n\n    public static Result testWithFabric()\n    {\n        boolean error = false;\n        final String version = \"1.18.2\";\n        final Path updateDir = UPDATE_DIR.resolve(\"fabric-\" + version);\n\n        String fabric = \"\";\n\n        try\n        {\n            final VanillaVersion vanillaVersion = new VanillaVersion.VanillaVersionBuilder()\n                    .withName(version)\n                    .build();\n\n            final FabricVersion fabricVersion = new FabricVersionBuilder()\n                    .build();\n\n            fabric = fabricVersion.getModLoaderVersion();\n\n            final FlowUpdater updater = new FlowUpdater.FlowUpdaterBuilder()\n                    .withVanillaVersion(vanillaVersion)\n                    .withModLoaderVersion(fabricVersion)\n                    .build();\n\n            updater.update(updateDir);\n        }\n        catch (Exception e)\n        {\n            error = true;\n            e.printStackTrace();\n        }\n\n        return new Result(updateDir, error, version, fabric);\n    }\n\n    public static Result testWithQuilt(UpdaterOptions opts)\n    {\n        boolean error = false;\n        String version = \"1.18.2\";\n        final Path updateDir = UPDATE_DIR.resolve(\"quilt-\" + version);\n\n        String quilt = \"\";\n\n        try\n        {\n            final VanillaVersion vanillaVersion = new VanillaVersion.VanillaVersionBuilder()\n                    .withName(version)\n                    .build();\n\n            final QuiltVersion quiltVersion = new QuiltVersionBuilder()\n                    .build();\n\n            quilt = quiltVersion.getModLoaderVersion();\n\n            final FlowUpdater updater = new FlowUpdater.FlowUpdaterBuilder()\n                    .withVanillaVersion(vanillaVersion)\n                    .withModLoaderVersion(quiltVersion)\n                    .withUpdaterOptions(opts)\n                    .build();\n\n            updater.update(updateDir);\n        }\n        catch (Exception e)\n        {\n            error = true;\n            e.printStackTrace();\n        }\n\n        return new Result(updateDir, error, version, quilt);\n    }\n\n    public static Result testWithFabric119()\n    {\n        boolean error = false;\n        String version = \"1.19.4\";\n        final Path updateDir = UPDATE_DIR.resolve(\"fabric-\" + version);\n\n        String fabric = \"\";\n\n        try\n        {\n            final VanillaVersion vanillaVersion = new VanillaVersion.VanillaVersionBuilder()\n                    .withName(version)\n                    .build();\n\n            final FabricVersion fabricVersion = new FabricVersionBuilder()\n                    .build();\n\n            fabric = fabricVersion.getModLoaderVersion();\n\n            final FlowUpdater updater = new FlowUpdater.FlowUpdaterBuilder()\n                    .withVanillaVersion(vanillaVersion)\n                    .withModLoaderVersion(fabricVersion)\n                    .build();\n\n            updater.update(updateDir);\n        }\n        catch (Exception e)\n        {\n            error = true;\n            e.printStackTrace();\n        }\n\n        return new Result(updateDir, error, version, fabric);\n    }\n\n    public static Result testWithNeoForgeUsage()\n    {\n        boolean error = false;\n        final String vanilla = \"1.20.4\";\n        final String neoForge = \"20.4.235\";\n        final Path updateDir = UPDATE_DIR.resolve(\"neo_forge-\" + vanilla);\n\n        try\n        {\n            final VanillaVersion version = new VanillaVersion.VanillaVersionBuilder()\n                    .withName(vanilla)\n                    .build();\n\n            final NeoForgeVersion neoForgeVersion = new NeoForgeVersionBuilder()\n                    .withNeoForgeVersion(neoForge)\n                    .build();\n\n            final FlowUpdater updater = new FlowUpdater.FlowUpdaterBuilder()\n                    .withVanillaVersion(version)\n                    .withModLoaderVersion(neoForgeVersion)\n                    .build();\n\n            updater.update(updateDir);\n        }\n        catch (Exception e)\n        {\n            error = true;\n            e.printStackTrace();\n        }\n\n        return new Result(updateDir, error, vanilla, neoForge);\n    }\n\n    public static Result testWithNeoForgeUsage2()\n    {\n        boolean error = false;\n        final String vanilla = \"1.21.1\";\n        final String neoForge = \"21.1.18\";\n        final Path updateDir = UPDATE_DIR.resolve(\"neo_forge-\" + vanilla);\n\n        try\n        {\n            final VanillaVersion version = new VanillaVersion.VanillaVersionBuilder()\n                    .withName(vanilla)\n                    .build();\n\n            final NeoForgeVersion neoForgeVersion = new NeoForgeVersionBuilder()\n                    .withNeoForgeVersion(neoForge)\n                    .build();\n\n            final FlowUpdater updater = new FlowUpdater.FlowUpdaterBuilder()\n                    .withVanillaVersion(version)\n                    .withModLoaderVersion(neoForgeVersion)\n                    .build();\n\n            updater.update(updateDir);\n        }\n        catch (Exception e)\n        {\n            error = true;\n            e.printStackTrace();\n        }\n\n        return new Result(updateDir, error, vanilla, neoForge);\n    }\n\n    public static class Result\n    {\n        public final Path updateDir;\n        public final boolean error;\n        public final String version;\n        public final String modLoaderVersion;\n\n        public Result(Path updateDir, boolean error, String version, String modLoaderVersion)\n        {\n            this.updateDir = updateDir;\n            this.error = error;\n            this.version = version;\n            this.modLoaderVersion = modLoaderVersion;\n        }\n\n        public Result(Path updateDir, boolean error, String version)\n        {\n            this.updateDir = updateDir;\n            this.error = error;\n            this.version = version;\n            this.modLoaderVersion = null;\n        }\n    }\n\n    public static Result testWithLast1122Forge()\n    {\n        boolean error = false;\n        final String vanilla = \"1.12.2\";\n        final String forge = \"14.23.5.2860\";\n        final String vanillaForge = vanilla + \"-\" + forge;\n        final Path updateDir = UPDATE_DIR.resolve(\"last_1122_forge-\" + vanillaForge);\n\n        try\n        {\n            final VanillaVersion version = new VanillaVersion.VanillaVersionBuilder()\n                    .withName(vanilla)\n                    .build();\n\n            final ForgeVersion forgeVersion = new ForgeVersionBuilder()\n                    .withForgeVersion(vanillaForge)\n                    .build();\n\n            final FlowUpdater updater = new FlowUpdater.FlowUpdaterBuilder()\n                    .withVanillaVersion(version)\n                    .withModLoaderVersion(forgeVersion)\n                    .build();\n\n            updater.update(updateDir);\n        }\n        catch (Exception e)\n        {\n            error = true;\n            e.printStackTrace();\n        }\n\n        return new Result(updateDir, error, vanilla, forge);\n    }\n\n    public static Result testWith121Forge()\n    {\n        boolean error = false;\n        final String vanilla = \"1.21\";\n        final String forge = \"51.0.29\";\n        final String vanillaForge = vanilla + \"-\" + forge;\n        final Path updateDir = UPDATE_DIR.resolve(\"121_forge-\" + vanillaForge);\n\n        try\n        {\n            final VanillaVersion version = new VanillaVersion.VanillaVersionBuilder()\n                    .withName(vanilla)\n                    .build();\n\n            final ForgeVersion forgeVersion = new ForgeVersionBuilder()\n                    .withForgeVersion(vanillaForge)\n                    .build();\n\n            final FlowUpdater updater = new FlowUpdater.FlowUpdaterBuilder()\n                    .withVanillaVersion(version)\n                    .withModLoaderVersion(forgeVersion)\n                    .build();\n\n            updater.update(updateDir);\n        }\n        catch (Exception e)\n        {\n            error = true;\n            e.printStackTrace();\n        }\n\n        return new Result(updateDir, error, vanilla, forge);\n    }\n}\n"
  },
  {
    "path": "src/test/java/fr/flowarg/flowupdater/utils/VersionTest.java",
    "content": "package fr.flowarg.flowupdater.utils;\n\nimport org.junit.jupiter.api.Test;\n\nimport java.util.Arrays;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class VersionTest\n{\n    @Test\n    public void testVersionCompareWithSameSize()\n    {\n        final Version version = Version.gen(\"1.0.0\");\n        final Version version2 = Version.gen(\"1.0.0\");\n        final Version version3 = Version.gen(\"1.0.1\");\n        final Version version4 = Version.gen(\"1.1.0\");\n        final Version version5 = Version.gen(\"2.0.0\");\n        final Version version6 = Version.gen(\"0.1.0\");\n        final Version version7 = new Version(Arrays.asList(1, 1, 1));\n\n        assertEquals(0, version.compareTo(version2));\n        assertEquals(-1, version.compareTo(version3));\n        assertEquals(-1, version.compareTo(version4));\n        assertEquals(-1, version.compareTo(version5));\n        assertEquals(1, version.compareTo(version6));\n        assertEquals(-1, version.compareTo(version7));\n    }\n\n    @Test\n    public void testVersionCompareWithDifferentSize()\n    {\n        final Version version = Version.gen(\"1.0.0\");\n        final Version version2 = Version.gen(\"1.1\");\n        final Version version3 = Version.gen(\"1.0\");\n        final Version version4 = Version.gen(\"3\");\n        final Version version5 = Version.gen(\"0\");\n        final Version version6 = Version.gen(\"0.1\");\n        final Version version7 = new Version(Arrays.asList(1, 2, 3, 4, 5));\n\n        assertEquals(-1, version.compareTo(version2));\n        assertEquals(1, version.compareTo(version3));\n        assertEquals(-1, version.compareTo(version4));\n        assertEquals(1, version.compareTo(version5));\n        assertEquals(1, version.compareTo(version6));\n        assertEquals(-1, version.compareTo(version7));\n    }\n\n    @Test\n    public void testVersionBetween()\n    {\n        final Version version = Version.gen(\"1.0.0\");\n        final Version version2 = Version.gen(\"1.1\");\n        final Version version3 = Version.gen(\"1.0\");\n        final Version version4 = Version.gen(\"1.0.1\");\n        final Version version5 = Version.gen(\"1.0.2\");\n\n        assertTrue(version.isBetweenOrEqual(version3, version2));\n        assertTrue(version4.isBetweenOrEqual(version, version5));\n    }\n\n    @Test\n    public void testVersionEmpty()\n    {\n        assertThrows(IllegalArgumentException.class, () -> Version.gen(\"\"));\n    }\n}\n"
  },
  {
    "path": "src/test/java/fr/flowarg/flowupdater/utils/builderapi/BuilderAPITest.java",
    "content": "package fr.flowarg.flowupdater.utils.builderapi;\n\nimport org.jetbrains.annotations.Contract;\nimport org.jetbrains.annotations.NotNull;\nimport org.junit.jupiter.api.Test;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\npublic class BuilderAPITest\n{\n    @Test\n    public void shouldFailBecauseMissingRequiredArgument()\n    {\n        assertThrows(BuilderException.class, () -> new TestBuilder().build());\n    }\n\n    @Test\n    public void shouldWorkBecauseRequiredArgumentIsFilled()\n    {\n        final TestObject object = new TestBuilder().withAnArgument(\"AnArgument\").build();\n        assertEquals(\"AnArgument\", object.str);\n    }\n\n    @Test\n    public void shouldFailBecauseOfBadObject()\n    {\n        assertThrows(BuilderException.class, () -> new TestBuilder().withAnArgument(\"AnArgument\").withAnInt(-1).build());\n    }\n\n    @Test\n    public void shouldFailBecauseUndefinedParentArgument()\n    {\n        assertThrows(BuilderException.class, () -> new AnotherTestBuilder().withAnotherBoolean(true).build());\n    }\n\n    @Test\n    public void shouldWorkBecauseDefinedParentArgument()\n    {\n        final AnotherTestObject anotherTestObject = new AnotherTestBuilder().withAnotherBoolean(true).withABoolean(false).build();\n        assertTrue(anotherTestObject.anotherBoolean);\n        assertFalse(anotherTestObject.aBoolean);\n    }\n\n    private static class TestObject\n    {\n        public final String str;\n        public final int anInt;\n\n        public TestObject(String str, int anInt)\n        {\n            this.str = str;\n            this.anInt = anInt;\n        }\n    }\n\n    private static class AnotherTestObject\n    {\n        public final boolean aBoolean;\n        public final boolean anotherBoolean;\n\n        public AnotherTestObject(boolean aBoolean, boolean anotherBoolean)\n        {\n            this.aBoolean = aBoolean;\n            this.anotherBoolean = anotherBoolean;\n        }\n    }\n\n    private static class TestBuilder implements IBuilder<TestObject>\n    {\n        private final BuilderArgument<String> anArgument = new BuilderArgument<String>(\"AnArgument\").required();\n        private final BuilderArgument<Integer> anIntArgument = new BuilderArgument<>(\"AnIntArgument\", () -> 0, () -> -1).optional();\n\n        public TestBuilder withAnArgument(String anArgument)\n        {\n            this.anArgument.set(anArgument);\n            return this;\n        }\n\n        public TestBuilder withAnInt(int anInt)\n        {\n            this.anIntArgument.set(anInt);\n            return this;\n        }\n\n        @Contract(\" -> new\")\n        @Override\n        public @NotNull TestObject build() throws BuilderException\n        {\n            return new TestObject(\n                    this.anArgument.get(),\n                    this.anIntArgument.get()\n            );\n        }\n    }\n\n    private static class AnotherTestBuilder implements IBuilder<AnotherTestObject>\n    {\n        private final BuilderArgument<Boolean> aBooleanArgument = new BuilderArgument<Boolean>(\"ABooleanArgument\").optional();\n        private final BuilderArgument<Boolean> anotherBooleanArgument = new BuilderArgument<Boolean>(\"AnotherBooleanArgument\").require(this.aBooleanArgument).optional();\n\n        public AnotherTestBuilder withABoolean(boolean aBoolean)\n        {\n            this.aBooleanArgument.set(aBoolean);\n            return this;\n        }\n\n        public AnotherTestBuilder withAnotherBoolean(boolean anotherBoolean)\n        {\n            this.anotherBooleanArgument.set(anotherBoolean);\n            return this;\n        }\n\n        @Contract(\" -> new\")\n        @Override\n        public @NotNull AnotherTestObject build() throws BuilderException\n        {\n            return new AnotherTestObject(\n                    this.aBooleanArgument.get(),\n                    this.anotherBooleanArgument.get()\n            );\n        }\n    }\n}\n"
  }
]