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