├── .github └── workflows │ ├── build.yml │ ├── codeql.yml │ └── docs.yml.disabled ├── LICENSE ├── README.md ├── build-logic ├── build.gradle.kts ├── settings.gradle.kts └── src │ └── main │ └── kotlin │ ├── CreateRelocationData.kt │ ├── config-kotlin.gradle.kts │ ├── config-publish.gradle.kts │ └── config-shade.gradle.kts ├── build.gradle.kts ├── gradle-plugin ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── net │ └── weavemc │ └── gradle │ ├── WeaveGradle.kt │ ├── configuration │ ├── DependencyManager.kt │ └── WeaveMinecraftExtension.kt │ └── util │ ├── Constants.kt │ └── DownloadUtil.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── internals ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── net │ └── weavemc │ └── internals │ ├── GameInfo.kt │ ├── InsnDsl.kt │ ├── MappingsRetrieval.kt │ ├── Mods.kt │ └── Util.kt ├── loader ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── net │ └── weavemc │ └── loader │ ├── api │ ├── Hook.kt │ ├── ModInitializer.kt │ ├── Tweaker.kt │ ├── bytecode │ │ └── InsnDslExtras.kt │ └── event │ │ ├── Event.kt │ │ ├── EventBus.kt │ │ └── SubscribeEvent.kt │ └── impl │ ├── InjectionHandler.kt │ ├── WeaveLoader.kt │ ├── bootstrap │ ├── Agent.kt │ ├── Bootstrap.kt │ ├── WeaveLogAppender.kt │ └── transformer │ │ ├── ApplicationWrapper.kt │ │ ├── ArgumentSanitizer.kt │ │ ├── ClassLoaderTransformer.kt │ │ ├── ModInitializerHook.kt │ │ └── SafeTransformer.kt │ ├── mixin │ ├── MixinSandbox.kt │ └── Services.kt │ └── util │ ├── Analytics.kt │ ├── FileManager.kt │ ├── Mappings.kt │ ├── MultiMC.kt │ └── Utility.kt └── settings.gradle.kts /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | pull_request: 5 | release: 6 | types: [ published ] 7 | 8 | concurrency: 9 | # Maximum of one running workflow per pull request on source branches. 10 | # Cancel an old run if an action is re-run. 11 | group: ${{ github.head_ref || format('{0}-{1}', github.ref, github.run_number) }} 12 | cancel-in-progress: true 13 | 14 | permissions: 15 | contents: write 16 | 17 | jobs: 18 | build: 19 | name: "Build with Gradle" 20 | runs-on: "ubuntu-latest" 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | 26 | - name: Validate Gradle Wrapper 27 | uses: gradle/actions/wrapper-validation@v3 28 | 29 | - name: Set up JDK 8 30 | uses: actions/setup-java@v4 31 | with: 32 | java-version: "8" 33 | distribution: "temurin" 34 | 35 | - uses: actions/cache@v4 36 | with: 37 | path: | 38 | ~/.gradle/caches 39 | ~/.gradle/wrapper 40 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 41 | restore-keys: ${{ runner.os }}-gradle- 42 | 43 | - name: Set gradlew permissions 44 | run: chmod +x ./gradlew 45 | 46 | - name: Build with Gradle 47 | run: ./gradlew build --no-daemon 48 | 49 | - name: Upload Build Output 50 | uses: actions/upload-artifact@v4 51 | with: 52 | name: weaveloader-artifacts 53 | path: | 54 | api/build/libs/ 55 | loader/build/libs/ 56 | internals/build/libs/ 57 | gradle-plugin/build/libs/ 58 | 59 | - name: Create release 60 | if: github.event_name == 'release' 61 | working-directory: loader/build/libs 62 | run: | 63 | agent=weave-loader-${{github.event.release.name}}-all.jar 64 | sha256sum $agent > $agent.sha256 65 | gh release upload ${{github.ref_name}} $agent $agent.sha256 66 | env: 67 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | pull_request: 17 | 18 | jobs: 19 | analyze: 20 | name: "Analyze" 21 | runs-on: "ubuntu-latest" 22 | permissions: 23 | actions: read 24 | contents: read 25 | security-events: write 26 | strategy: 27 | fail-fast: false 28 | matrix: 29 | language: [ 'java' ] 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | 34 | - name: Validate Gradle Wrapper 35 | uses: gradle/actions/wrapper-validation@v3 36 | 37 | - name: Set up JDK 8 38 | uses: actions/setup-java@v4 39 | with: 40 | java-version: "8" 41 | distribution: "temurin" 42 | 43 | - uses: actions/cache@v4 44 | with: 45 | path: | 46 | $HOME/.gradle/caches 47 | $HOME/.gradle/wrapper 48 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 49 | restore-keys: ${{ runner.os }}-gradle- 50 | 51 | - name: Set gradlew permissions 52 | run: chmod +x ./gradlew 53 | 54 | # Initializes the CodeQL tools for scanning. 55 | - name: Initialize CodeQL 56 | uses: github/codeql-action/init@v2 57 | with: 58 | debug: true 59 | languages: ${{ matrix.language }} 60 | # If you wish to specify custom queries, you can do so here or in a config file. 61 | # By default, queries listed here will override any specified in a config file. 62 | # Prefix the list here with "+" to use these queries and those in the config file. 63 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 64 | # queries: security-extended,security-and-quality 65 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 66 | 67 | - name: Autobuild 68 | uses: github/codeql-action/autobuild@v2 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v2 72 | with: 73 | category: "/language:${{matrix.language}}" 74 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml.disabled: -------------------------------------------------------------------------------- 1 | name: Deploy dokka docs 2 | 3 | on: 4 | # Runs on pushes targeting the default branch 5 | push: 6 | branches: [ "master" ] 7 | paths-ignore: 8 | - '.gitignore' 9 | - '.gitattributes' 10 | - '.editorconfig' 11 | 12 | # Allows you to run this workflow manually from the Actions tab 13 | workflow_dispatch: 14 | 15 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 16 | permissions: 17 | contents: read 18 | pages: write 19 | id-token: write 20 | 21 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 22 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 23 | concurrency: 24 | group: "pages" 25 | cancel-in-progress: false 26 | 27 | jobs: 28 | # Single deploy job since we're just deploying 29 | deploy: 30 | environment: 31 | name: github-pages 32 | url: ${{ steps.deployment.outputs.page_url }} 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: Checkout 36 | uses: actions/checkout@v4 37 | 38 | - name: Validate Gradle Wrapper 39 | uses: gradle/actions/wrapper-validation@v3 40 | 41 | - name: Set up JDK 8 42 | uses: actions/setup-java@v4 43 | with: 44 | java-version: "8" 45 | distribution: "temurin" 46 | 47 | - uses: actions/cache@v3.2.6 48 | with: 49 | path: | 50 | $HOME/.gradle/caches 51 | $HOME/.gradle/wrapper 52 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 53 | restore-keys: ${{ runner.os }}-gradle- 54 | 55 | - name: Set gradlew permissions 56 | run: chmod +x ./gradlew 57 | 58 | - name: Build (javadoc) with Gradle 59 | run: ./gradlew dokkaHtml --no-daemon 60 | 61 | - name: Setup Pages 62 | uses: actions/configure-pages@v3 63 | 64 | - name: Upload Pages artifact 65 | uses: actions/upload-pages-artifact@v1 66 | with: 67 | path: 'build/dokka/html/' 68 | 69 | - name: Deploy to GitHub Pages 70 | id: deployment 71 | uses: actions/deploy-pages@v2 72 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | title 5 | 6 | # Weave Loader, the Universal Minecraft Mod Loader 7 | 8 | Weave Loader is a tool designed to simplify the process of modding Minecraft. It allows 9 | developers to easily create mods by providing a simple and intuitive API for interacting with Minecraft's codebase, 10 | while supporting injection into clients that are somewhat closed-off to developers. 11 | 12 | ## Supported Clients / Versions 13 | 14 | 15 | 16 | 41 |
Supported ClientsSupported Versions
17 | 18 | | Client | Supported | 19 | |---------|:------------------:| 20 | | Vanilla | :white_check_mark: | 21 | | Forge | :white_check_mark: | 22 | | Lunar | :white_check_mark: | 23 | | Badlion | :x: | 24 | | Feather | :x: | 25 | | Labymod | :white_check_mark: | 26 | 27 | 28 | 29 | | Version | Supported | 30 | |---------|:------------------:| 31 | | 1.7 | :white_check_mark: | 32 | | 1.8 | :white_check_mark: | 33 | | 1.12 | :white_check_mark: | 34 | | 1.16 | :white_check_mark: | 35 | | 1.17 | :white_check_mark: | 36 | | 1.18 | :white_check_mark: | 37 | | 1.19 | :white_check_mark: | 38 | | 1.20 | :white_check_mark: | 39 | 40 |
42 | 43 | 44 | ## Installation 45 | 46 | To install Weave-Loader, you can either download a pre-built release from 47 | the [releases page](https://github.com/Weave-MC/Weave-Loader/releases), or build it yourself from source. 48 | 49 | ### Building with Gradle 50 | 51 | - First clone the project with [Git][git] then `cd` into the project directory: 52 | 53 | ```bash 54 | git clone --recursive https://github.com/Weave-MC/Weave-Loader 55 | cd Weave-Loader 56 | ``` 57 | 58 | You then need to give permission to the Gradle wrapper and run the `agent` task. This can be done a bit differently 59 | depending on your operating system: 60 | 61 |
62 | UN*X (Linux, BSDs, macOS, etc.) 63 | 64 | ```bash 65 | chmod +x ./gradlew 66 | ./gradlew agent 67 | ``` 68 |
69 | 70 |
71 | Windows 72 | 73 | ```powershell 74 | .\gradlew.bat agent 75 | ``` 76 |
77 | 78 | ## Usage 79 | 80 | To use Weave-Loader, you have two options: 81 | - Use [Weave-Manager](https://github.com/Weave-MC/Weave-Manager) to handle the process of attaching Weave to your preferred Minecraft client automatically. 82 | - Manually add the Weave-Loader agent mentioned in the previous step to the JVM arguments when launching Minecraft. 83 | - You will need to include the following argument: `-javaagent:$PATH_TO_AGENT` 84 | 85 | Weave will automatically load mods from `~/.weave/mods/`. 86 | 87 | ## Contributing 88 | 89 | We welcome contributions from anyone interested in improving Weave-Loader. If you find a bug or have an idea for a new 90 | feature, feel free to submit a pull request. 91 | 92 | --- 93 | 94 |
95 | 96 | Weave-Loader is licensed under the [GNU General Public License v3.0][license]. 97 | 98 |
99 | 100 | [git]: https://git-scm.com/ 101 | 102 | [license]: https://github.com/Weave-MC/Weave-Loader/blob/master/LICENSE 103 | -------------------------------------------------------------------------------- /build-logic/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.KotlinVersion 2 | 3 | plugins { 4 | `kotlin-dsl` 5 | } 6 | 7 | repositories { 8 | gradlePluginPortal() 9 | } 10 | 11 | dependencies { 12 | implementation(libs.bundles.asm) 13 | implementation(libs.bundles.kotlin.plugins) 14 | implementation(libs.gradle.shadow) 15 | } 16 | 17 | kotlin { 18 | compilerOptions { 19 | languageVersion = KotlinVersion.KOTLIN_2_0 20 | apiVersion = KotlinVersion.KOTLIN_2_0 21 | } 22 | } -------------------------------------------------------------------------------- /build-logic/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | versionCatalogs { 3 | create("libs") { 4 | from(files("../gradle/libs.versions.toml")) 5 | } 6 | } 7 | } 8 | 9 | rootProject.name = "build-logic" -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/CreateRelocationData.kt: -------------------------------------------------------------------------------- 1 | import org.gradle.api.DefaultTask 2 | import org.gradle.api.file.RegularFileProperty 3 | import org.gradle.api.provider.ListProperty 4 | import org.gradle.api.provider.Property 5 | import org.gradle.api.tasks.Input 6 | import org.gradle.api.tasks.OutputFile 7 | import org.gradle.api.tasks.TaskAction 8 | import java.util.* 9 | 10 | open class CreateRelocationData : DefaultTask() { 11 | @get:Input 12 | val shadedPackage: Property = 13 | project.objects.property(String::class.java) 14 | 15 | @get:Input 16 | val relocationList: ListProperty = 17 | project.objects.listProperty(String::class.java) 18 | 19 | @get:OutputFile 20 | val propertiesFile: RegularFileProperty = 21 | project.objects.fileProperty().convention( 22 | project.layout.buildDirectory.file("tmp/weave-relocation-data.properties") 23 | ) 24 | 25 | @TaskAction 26 | fun createRelocationData() { 27 | val properties = propertiesOf( 28 | "target" to shadedPackage.get(), 29 | "packages" to relocationList.get().joinToString(";") 30 | ) 31 | 32 | propertiesFile.get().asFile.outputStream().use { 33 | properties.store(it, "Relocation data for Weave-Loader") 34 | } 35 | } 36 | } 37 | 38 | private fun propertiesOf(vararg props: Pair) = 39 | Properties().also { it += props } 40 | -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/config-kotlin.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.KotlinVersion 2 | 3 | plugins { 4 | `java-library` 5 | id("org.jetbrains.kotlin.jvm") 6 | id("org.jetbrains.kotlin.plugin.serialization") 7 | } 8 | 9 | base { 10 | archivesName = "weave-${project.name}" 11 | } 12 | 13 | java.withSourcesJar() 14 | 15 | kotlin { 16 | compilerOptions { 17 | freeCompilerArgs.add("-Xjvm-default=all") 18 | languageVersion = KotlinVersion.KOTLIN_2_0 19 | apiVersion = KotlinVersion.KOTLIN_2_0 20 | } 21 | 22 | jvmToolchain(8) 23 | } 24 | -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/config-publish.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.kotlin.dsl.`maven-publish` 2 | 3 | plugins { 4 | `maven-publish` 5 | } 6 | 7 | publishing { 8 | repositories { 9 | maven { 10 | name = "WeaveMC" 11 | url = uri("https://repo.weavemc.dev/releases") 12 | credentials(PasswordCredentials::class) 13 | authentication { 14 | create("basic") 15 | } 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/config-shade.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar 2 | 3 | plugins { 4 | id("com.github.johnrengelman.shadow") 5 | } 6 | 7 | repositories { 8 | mavenCentral() 9 | } 10 | 11 | val shade: Configuration by configurations.creating 12 | val api: Configuration by configurations.getting 13 | api.extendsFrom(shade) 14 | 15 | val targetPackage = "net.weavemc.loader.impl.shaded" 16 | val packagesList = listOf( 17 | "com.grappenmaker.mappings", 18 | "org.objectweb.asm", 19 | "org.spongepowered", 20 | "kotlin", 21 | "kotlinx", 22 | ) 23 | 24 | tasks { 25 | val createRelocationData by creating(CreateRelocationData::class) { 26 | shadedPackage = targetPackage 27 | relocationList.addAll(packagesList) 28 | } 29 | 30 | val shadowJar by getting(ShadowJar::class) { 31 | archiveClassifier.set("all") 32 | 33 | configurations = listOf(shade) 34 | exclude( 35 | // Unwanted libraries 36 | "org/intellij/lang/annotations/**", "org/jetbrains/annotations/**", 37 | 38 | // Unwanted metadata files 39 | "META-INF/maven/**", "META-INF/proguard/**", "META-INF/com.android.tools/**", 40 | 41 | // Kotlin metadata files. It is essentially useless since we relocate the entire stdlib. 42 | // The only drawback is that kotlin-reflect will not work in certain cases. 43 | "META-INF/**/*.kotlin_module", "**/*.kotlin_builtins", 44 | 45 | // Vague license files from other libraries 46 | // Note: This is just to not have random licenses not linked to their 47 | // project in the final jar. The acknowledgements for the used libraries 48 | // will be provided somewhere else. 49 | "LICENSE.txt", "LICENSE", "NOTICE.txt", "NOTICE", 50 | 51 | // Signature files 52 | "META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA", 53 | // OSGI-related metadata 54 | "OSGI-INF/**", "*.profile", 55 | 56 | // Mixin external services 57 | "META-INF/services/org.spongepowered.*", 58 | "META-INF/services/cpw.mods.modlauncher.*", 59 | 60 | /// Remove classes built with J9+ so that Legacy Forge doesn't spam logs 61 | // Module info + Multi-version classes 62 | "module-info.class", "META-INF/versions/**", 63 | // Mixin classes for integrating with ModLauncher 9+ 64 | "org/spongepowered/asm/launch/MixinLaunchPlugin.class", 65 | "org/spongepowered/asm/launch/MixinTransformationService.class", 66 | "org/spongepowered/asm/launch/platform/container/ContainerHandleModLauncherEx.class", 67 | "org/spongepowered/asm/launch/platform/container/ContainerHandleModLauncherEx\$SecureJarResource.class", 68 | ) 69 | 70 | from(createRelocationData) 71 | 72 | packagesList.forEach { pkg -> 73 | val packageName = pkg.substringAfterLast(".") 74 | val relocated = "$targetPackage.$packageName." 75 | relocate("$pkg.", relocated) 76 | } 77 | 78 | mergeServiceFiles() 79 | 80 | afterEvaluate { 81 | val jarTask = tasks.getByName("jar") 82 | manifest.inheritFrom(jarTask.manifest) 83 | 84 | // Fix mixin whining about a missing version 85 | manifest.attributes(mapOf( 86 | "Implementation-Version" to "9.7" 87 | ), "${targetPackage.replace('.', '/')}/asm/") 88 | } 89 | } 90 | 91 | val build by getting { 92 | dependsOn(shadowJar) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlin.jvm) apply false 3 | alias(libs.plugins.serialization.dsl) apply false 4 | } 5 | 6 | subprojects { 7 | group = "net.weavemc" 8 | version = "1.0.0-b.3" 9 | 10 | repositories { 11 | mavenCentral() 12 | maven("https://repo.weavemc.dev/releases") 13 | } 14 | } -------------------------------------------------------------------------------- /gradle-plugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("config-kotlin") 3 | `java-gradle-plugin` 4 | id("config-publish") 5 | } 6 | 7 | dependencies { 8 | compileOnly(gradleApi()) 9 | compileOnly(gradleKotlinDsl()) 10 | 11 | implementation(libs.bundles.asm) 12 | implementation(libs.kxser.json) 13 | implementation(libs.mappings) 14 | implementation(projects.internals) 15 | } 16 | 17 | gradlePlugin { 18 | plugins { 19 | val weave by creating { 20 | id = "net.weavemc.gradle" 21 | displayName = "Weave-Gradle" 22 | description = "Provides various utilities for Weave modders to develop and package their mods" 23 | implementationClass = "net.weavemc.gradle.WeaveGradle" 24 | } 25 | } 26 | } 27 | 28 | base.archivesName = "Weave-Gradle" -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/net/weavemc/gradle/WeaveGradle.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.gradle 2 | 3 | import kotlinx.serialization.encodeToString 4 | import net.weavemc.gradle.configuration.WeaveMinecraftExtension 5 | import net.weavemc.gradle.configuration.pullDeps 6 | import net.weavemc.gradle.util.Constants 7 | import net.weavemc.gradle.util.localCache 8 | import net.weavemc.gradle.util.minecraftJarCache 9 | import net.weavemc.internals.MappingsRetrieval 10 | import net.weavemc.internals.MinecraftVersion 11 | import org.gradle.api.DefaultTask 12 | import org.gradle.api.GradleException 13 | import org.gradle.api.Plugin 14 | import org.gradle.api.Project 15 | import org.gradle.api.plugins.JavaPlugin 16 | import org.gradle.api.tasks.Delete 17 | import org.gradle.api.tasks.OutputFile 18 | import org.gradle.api.tasks.SourceSetContainer 19 | import org.gradle.api.tasks.TaskAction 20 | import org.gradle.api.tasks.bundling.Jar 21 | import org.gradle.kotlin.dsl.* 22 | 23 | /** 24 | * Gradle build system plugin used to automate the setup of a modding environment. 25 | */ 26 | class WeaveGradle : Plugin { 27 | /** 28 | * [Plugin.apply] 29 | * 30 | * @param project The target project. 31 | */ 32 | override fun apply(project: Project) { 33 | // Applying our default plugins 34 | project.pluginManager.apply(JavaPlugin::class) 35 | 36 | val ext = project.extensions.create("weave", WeaveMinecraftExtension::class) 37 | 38 | project.afterEvaluate { 39 | if (!ext.configuration.isPresent) throw GradleException( 40 | "Configuration is missing, make sure to add a configuration through the weave {} block!" 41 | ) 42 | 43 | if (!ext.version.isPresent) throw GradleException( 44 | "Set a Minecraft version through the weave {} block!" 45 | ) 46 | 47 | val version = ext.version.getOrElse(MinecraftVersion.V1_8_9) 48 | it.pullDeps(version, ext.configuration.get().namespace) 49 | } 50 | 51 | val writeModConfig = project.tasks.register("writeModConfig") 52 | 53 | project.tasks.named("jar") { 54 | dependsOn(writeModConfig) 55 | from(writeModConfig) 56 | } 57 | 58 | project.tasks.named("clean") { delete(writeModConfig) } 59 | } 60 | 61 | open class WriteModConfig : DefaultTask() { 62 | @get:OutputFile 63 | val output = project.localCache().map { it.file("weave.mod.json") } 64 | 65 | @TaskAction 66 | fun run() { 67 | val config = project.extensions.getByName("minecraft").configuration.get() 68 | output.get().asFile.writeText(Constants.JSON.encodeToString(config)) 69 | } 70 | } 71 | } 72 | 73 | fun MinecraftVersion.loadMergedMappings() = 74 | MappingsRetrieval.loadMergedWeaveMappings(versionName, minecraftJarCache).mappings 75 | 76 | 77 | val Project.sourceSets get() = extensions.getByName("sourceSets") 78 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/net/weavemc/gradle/configuration/DependencyManager.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.gradle.configuration 2 | 3 | import com.grappenmaker.mappings.* 4 | import kotlinx.serialization.Serializable 5 | import net.weavemc.gradle.loadMergedMappings 6 | import net.weavemc.gradle.sourceSets 7 | import net.weavemc.gradle.util.* 8 | import net.weavemc.internals.MinecraftVersion 9 | import org.gradle.api.Project 10 | import org.gradle.api.logging.LogLevel 11 | import org.gradle.kotlin.dsl.get 12 | import org.gradle.kotlin.dsl.maven 13 | import org.objectweb.asm.ClassVisitor 14 | import java.io.File 15 | import java.net.URL 16 | 17 | private inline fun String?.decodeJSON() = 18 | if (this != null) Constants.JSON.decodeFromString(this) else null 19 | 20 | /** 21 | * Pulls dependencies from [addMinecraftAssets] and [addMappedMinecraft] 22 | */ 23 | fun Project.pullDeps(version: MinecraftVersion, namespace: String) { 24 | addMinecraftAssets(version) 25 | addMappedMinecraft(version, namespace) 26 | } 27 | 28 | /** 29 | * Adds Minecraft as a dependency by providing the jar to the projects file tree. 30 | */ 31 | private fun Project.addMinecraftAssets(version: MinecraftVersion) { 32 | val manifest = DownloadUtil.fetch(Constants.VERSION_MANIFEST).decodeJSON() ?: return 33 | val versionEntry = manifest.versions.find { it.id == version.versionName } ?: return 34 | val versionInfo = DownloadUtil.fetch(versionEntry.url).decodeJSON() ?: return 35 | 36 | val client = versionInfo.downloads.client 37 | DownloadUtil.checksumAndDownload(URL(client.url), client.sha1, version.minecraftJarCache.toPath()) 38 | 39 | repositories.maven("https://libraries.minecraft.net/") 40 | 41 | versionInfo.libraries.filter { "twitch-platform" !in it.name && "twitch-external" !in it.name } 42 | .forEach { dependencies.add("compileOnly", it.name) } 43 | } 44 | 45 | private fun Project.retrieveWideners(): List { 46 | // Cursed code 47 | val ext = extensions["minecraft"] as WeaveMinecraftExtension 48 | val wideners = ext.configuration.get().accessWideners.toHashSet() 49 | val widenerFiles = mutableListOf() 50 | 51 | a@ for (set in project.sourceSets) { 52 | for (dir in set.resources.sourceDirectories) { 53 | wideners.removeIf { left -> 54 | val f = dir.resolve(left) 55 | f.exists().also { if (it) widenerFiles += f } 56 | } 57 | 58 | if (wideners.isEmpty()) break@a 59 | } 60 | } 61 | 62 | require(wideners.isEmpty()) { "Could not resolve access wideners $wideners! Double-check if the file exists" } 63 | return widenerFiles 64 | } 65 | 66 | private fun File.loadWidener() = loadAccessWidener(readText().trim().lines()) 67 | 68 | private fun Project.addMappedMinecraft(version: MinecraftVersion, namespace: String) = runCatching { 69 | val fullMappings = version.loadMergedMappings() 70 | val allWideners = retrieveWideners().map { it.loadWidener().remap(fullMappings, namespace) } 71 | val joinedWideners = allWideners.takeIf { it.isNotEmpty() }?.join() 72 | val joinedFile = localGradleCache().file("joined.accesswidener").asFile 73 | val mapped = mappedJarCache(namespace, version) 74 | 75 | if ( 76 | // should be able to simplify this, right? 77 | !mapped.exists() || 78 | (allWideners.isNotEmpty() xor joinedFile.exists()) || 79 | (allWideners.isNotEmpty() && joinedFile.loadWidener() != joinedWideners) 80 | ) { 81 | logger.log(LogLevel.LIFECYCLE, "Remapping vanilla jar to $namespace for version ${version.versionName}") 82 | mapped.parentFile.mkdirs() // TODO use NIO api? 83 | 84 | val visitor = joinedWideners?.toTree()?.let { { parent: ClassVisitor -> AccessWidenerVisitor(parent, it) } } 85 | remapJar(fullMappings, version.minecraftJarCache, mapped, to = namespace, visitor = visitor) 86 | } 87 | 88 | joinedWideners?.write()?.let { joinedFile.writeText(it.joinToString("\n")) } ?: joinedFile.delete() 89 | 90 | dependencies.add("compileOnly", project.files(mapped)) 91 | }.onFailure { it.printStackTrace() } 92 | 93 | @Serializable 94 | private data class VersionManifest(val versions: List) 95 | 96 | @Serializable 97 | private data class ManifestVersion(val id: String, val url: String) 98 | 99 | @Serializable 100 | private data class VersionInfo(val downloads: VersionDownloads, val libraries: List) 101 | 102 | @Serializable 103 | private data class VersionDownloads(val client: VersionDownload) 104 | 105 | @Serializable 106 | private data class VersionDownload(val url: String, val sha1: String) 107 | 108 | @Serializable 109 | private data class Library(val name: String) 110 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/net/weavemc/gradle/configuration/WeaveMinecraftExtension.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.gradle.configuration 2 | 3 | import net.weavemc.internals.MappingsType 4 | import net.weavemc.internals.MinecraftVersion 5 | import net.weavemc.internals.ModConfig 6 | import org.gradle.api.provider.Property 7 | import kotlin.properties.Delegates 8 | 9 | interface WeaveMinecraftExtension { 10 | val version: Property 11 | val configuration: Property 12 | 13 | fun version(versionString: String) = version.set( 14 | MinecraftVersion.fromVersionName(versionString) ?: error("Unknown version $versionString") 15 | ) 16 | 17 | fun configure(block: ConfigurationBuilder.() -> Unit) = configuration.set(buildConfiguration(block)) 18 | } 19 | 20 | inline fun buildConfiguration(block: ConfigurationBuilder.() -> Unit) = ConfigurationBuilder().also(block).backing 21 | 22 | class ConfigurationBuilder { 23 | @PublishedApi 24 | internal var backing = ModConfig("Unnamed mod", "unnamed-mod", namespace = "official") 25 | 26 | private fun mutatingProperty(initial: T, mut: ModConfig.(T) -> ModConfig) = 27 | Delegates.observable(initial) { _, _, n -> backing = backing.mut(n) } 28 | 29 | var name by mutatingProperty(backing.name) { copy(name = it) } 30 | var modId by mutatingProperty(backing.modId) { copy(modId = it) } 31 | var namespace by mutatingProperty(backing.namespace) { copy(namespace = it) } 32 | var dependencies by mutatingProperty(backing.dependencies) { copy(dependencies = it) } 33 | var entryPoints by mutatingProperty(backing.entryPoints) { copy(entryPoints = it) } 34 | var tweakers by mutatingProperty(backing.tweakers) { copy(tweakers = it) } 35 | var accessWideners by mutatingProperty(backing.accessWideners) { copy(accessWideners = it) } 36 | var hooks by mutatingProperty(backing.hooks) { copy(hooks = it) } 37 | var mixinConfigs by mutatingProperty(backing.mixinConfigs) { copy(mixinConfigs = it) } 38 | 39 | fun yarnMappings() { 40 | namespace = MappingsType.YARN.named 41 | } 42 | 43 | fun mojangMappings() { 44 | namespace = MappingsType.MOJANG.named 45 | } 46 | 47 | fun mcpMappings() { 48 | namespace = MappingsType.MCP.named 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/net/weavemc/gradle/util/Constants.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.gradle.util 2 | 3 | import kotlinx.serialization.json.Json 4 | import net.weavemc.internals.MinecraftVersion 5 | import org.gradle.api.Project 6 | import java.io.File 7 | 8 | /** 9 | * A class containing constant mnemonic values to 10 | * be referenced throughout the project. 11 | */ 12 | object Constants { 13 | /** 14 | * The gradle cache directory. 15 | * 16 | * * Windows: `"%USERPROFILE%\.gradle\caches\weave\"` 17 | * * Linux: `"${HOME}/.gradle/caches/weave/"` 18 | * * Mac: `"${HOME}/.gradle/caches/weave/"` 19 | */ 20 | val CACHE_DIR = File(System.getProperty("user.home"), ".gradle/caches/weave") 21 | 22 | /** 23 | * The global JSON serializer 24 | */ 25 | val JSON = Json { ignoreUnknownKeys = true } 26 | 27 | /** 28 | * The version manifest URL 29 | */ 30 | const val VERSION_MANIFEST = "https://launchermeta.mojang.com/mc/game/version_manifest_v2.json" 31 | } 32 | 33 | val MinecraftVersion.cacheDirectory get() = Constants.CACHE_DIR.resolve("cache-${versionName}").also { it.mkdirs() } 34 | val MinecraftVersion.minecraftJarCache get() = cacheDirectory.resolve("client.jar") 35 | 36 | fun Project.mappedJarCache(namespace: String, version: MinecraftVersion) = 37 | localGradleCache().file("${version.minecraftJarCache.nameWithoutExtension}-$namespace.jar").asFile 38 | 39 | fun Project.localGradleCache() = layout.projectDirectory.dir(".gradle").dir("weave") 40 | fun Project.localCache() = layout.buildDirectory.dir("weave") 41 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/net/weavemc/gradle/util/DownloadUtil.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.gradle.util 2 | 3 | import java.io.IOException 4 | import java.net.URL 5 | import java.nio.file.Files 6 | import java.nio.file.Path 7 | import java.nio.file.Paths 8 | import java.nio.file.StandardCopyOption 9 | import java.security.MessageDigest 10 | import java.security.NoSuchAlgorithmException 11 | import kotlin.io.path.exists 12 | import kotlin.io.path.inputStream 13 | 14 | object DownloadUtil { 15 | /** 16 | * Returns the SHA1 checksum of the file as a [String] 17 | * 18 | * @param file The file to check. 19 | * @return the SHA1 checksum of the file. 20 | */ 21 | private fun checksum(file: Path) = try { 22 | if (!file.exists()) null 23 | else { 24 | val digest = MessageDigest.getInstance("SHA-1") 25 | file.inputStream().use { input -> 26 | val buffer = ByteArray(DEFAULT_BUFFER_SIZE) 27 | var read: Int 28 | 29 | while (input.read(buffer).also { read = it } >= 0) { 30 | digest.update(buffer, 0, read) 31 | } 32 | } 33 | 34 | digest.digest().joinToString { "%02x".format(it) } 35 | } 36 | } catch (ex: IOException) { 37 | ex.printStackTrace() 38 | null 39 | } catch (ignored: NoSuchAlgorithmException) { 40 | null 41 | } 42 | 43 | /** 44 | * Downloads a file from any URL 45 | * 46 | * @param url The URL to download from. 47 | * @param path The path to download to. 48 | */ 49 | private fun download(url: URL, path: Path) { 50 | runCatching { 51 | url.openStream().use { input -> 52 | Files.createDirectories(path.parent) 53 | Files.copy(input, path, StandardCopyOption.REPLACE_EXISTING) 54 | } 55 | }.onFailure { it.printStackTrace() } 56 | } 57 | 58 | fun download(url: String, path: String) = download(URL(url), Paths.get(path)) 59 | 60 | /** 61 | * Fetches data from any URL 62 | * 63 | * @param url The URL to download from 64 | */ 65 | private fun fetch(url: URL) = runCatching { url.openStream().readBytes().decodeToString() } 66 | .onFailure { it.printStackTrace() }.getOrNull() 67 | 68 | fun fetch(url: String) = fetch(URL(url)) 69 | 70 | /** 71 | * Downloads and checksums a file. 72 | * 73 | * @param url The URL to download from. 74 | * @param checksum The checksum to compare to. 75 | * @param path The path to download to. 76 | */ 77 | fun checksumAndDownload(url: URL, checksum: String, path: Path) { 78 | if (checksum(path) != checksum) download(url, path) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.caching=true 2 | org.gradle.parallel=true 3 | org.gradle.configureondemand=true 4 | org.gradle.parallel.threads=4 5 | org.gradle.dependency.verification=strict 6 | org.gradle.kotlin.dsl.allWarningsAsErrors=true 7 | org.gradle.configuration-cache=true 8 | org.gradle.jvmargs=-Xmx4G -Dfile.encoding=UTF-8 9 | org.gradle.kotlin.dsl.precompiled.accessors.strict=true 10 | kotlin.code.style=official 11 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | asm = "9.7" 3 | mixin = "0.13.4+mixin.0.8.5" 4 | 5 | kotlin = "2.0.0" 6 | kxser = "1.7.1" 7 | mappings-util = "0.1.6" 8 | gradle-shadow = "8.1.1" 9 | klog = "0.0.5" 10 | 11 | [libraries] 12 | asm = { module = "org.ow2.asm:asm", version.ref = "asm" } 13 | asm-tree = { module = "org.ow2.asm:asm-tree", version.ref = "asm" } 14 | asm-commons = { module = "org.ow2.asm:asm-commons", version.ref = "asm" } 15 | asm-util = { module = "org.ow2.asm:asm-util", version.ref = "asm" } 16 | klog = { module = "me.xtrm:klog", version.ref = "klog" } 17 | mixin = { module = "net.fabricmc:sponge-mixin", version.ref = "mixin" } 18 | kxser-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kxser" } 19 | mappings = { module = "io.github.770grappenmaker:mappings-util", version.ref = "mappings-util" } 20 | gradle-shadow = { module = "com.github.johnrengelman.shadow:com.github.johnrengelman.shadow.gradle.plugin", version.ref = "gradle-shadow" } 21 | kotlin-compiler = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } 22 | kotlin-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" } 23 | 24 | [plugins] 25 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 26 | serialization-dsl = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 27 | 28 | [bundles] 29 | asm = ["asm", "asm-tree", "asm-commons", "asm-util"] 30 | kotlin-plugins = ["kotlin-compiler", "kotlin-serialization"] -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Weave-MC/Weave-Loader/d1675a500bcc98dce995b9065e0fa3d8a03dfca3/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /internals/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("config-kotlin") 3 | id("config-publish") 4 | } 5 | 6 | dependencies { 7 | api(libs.bundles.asm) 8 | implementation(libs.kxser.json) 9 | implementation(libs.mappings) 10 | } 11 | 12 | publishing { 13 | publications { 14 | create("maven") { 15 | from(components["java"]) 16 | groupId = "net.weavemc" 17 | artifactId = "internals" 18 | version = project.version.toString() 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /internals/src/main/kotlin/net/weavemc/internals/GameInfo.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.internals 2 | 3 | import kotlin.properties.ReadOnlyProperty 4 | 5 | enum class MinecraftVersion( 6 | val protocol: Int, 7 | val versionName: String, 8 | val mappingName: String, 9 | vararg val aliases: String, 10 | ) { 11 | V1_7_10(5, "1.7.10", "1.7", "1.7"), 12 | V1_8_9(47, "1.8.9", "1.8", "1.8"), 13 | V1_12_2(340, "1.12.2", "1.12", "1.12"), 14 | V1_16_5(754, "1.16.5", "1.16", "1.16"), 15 | V1_20_1(763, "1.20.1", "1.20","1.20"); 16 | 17 | companion object { 18 | fun fromProtocol(protocol: Int): MinecraftVersion? = entries.find { it.protocol == protocol } 19 | fun fromVersionName(versionName: String): MinecraftVersion? = 20 | entries.find { versionName.contains(it.versionName) } 21 | ?: entries.find { it.aliases.any { alias -> versionName.contains(alias) } } 22 | 23 | fun fromAlias(alias: String): MinecraftVersion? = entries.find { it.aliases.contains(alias) } 24 | } 25 | } 26 | 27 | enum class MinecraftClient( 28 | val clientName: String, 29 | vararg val aliases: String, 30 | ) { 31 | VANILLA("Vanilla"), 32 | FORGE("Forge", "MinecraftForge", "Minecraft Forge"), 33 | LABYMOD("LabyMod", "Laby"), 34 | LUNAR("Lunar Client", "Lunar", "LunarClient"), 35 | BADLION("Badlion Client", "BLC", "Badlion", "BadlionClient"); 36 | 37 | companion object { 38 | fun fromClientName(clientName: String): MinecraftClient? = 39 | entries.find { it.clientName.equals(clientName, ignoreCase = true) } 40 | ?: entries.find { it.aliases.any { alias -> alias.equals(clientName, ignoreCase = true) } } 41 | } 42 | } 43 | 44 | object GameInfo { 45 | @Suppress("UNCHECKED_CAST") 46 | val rawGameInfo: Map 47 | get() = System.getProperties()["weave.game.info"] as? Map 48 | ?: error("Failed to retrieve Minecraft arguments") 49 | 50 | val commandLineArgs = System.getProperty("sun.java.command") 51 | ?: error("Failed to retrieve command line arguments, this should never happen.") 52 | 53 | val versionString: String by lazy { 54 | rawGameInfo["version"]?.lowercase() ?: error("Could not parse version from arguments") 55 | } 56 | 57 | val version: MinecraftVersion by lazy { 58 | versionString.let(MinecraftVersion::fromVersionName) ?: error("Could not find game version") 59 | } 60 | 61 | val clientString: String by lazy { 62 | rawGameInfo["client"]?.lowercase() ?: error("Could not parse client from arguments") 63 | } 64 | 65 | val client: MinecraftClient by lazy { 66 | clientString.let(MinecraftClient::fromClientName) ?: error("Could not find game client") 67 | } 68 | } 69 | 70 | enum class MappingsType(val id: String) { 71 | MOJANG("mojmap"), 72 | MCP("mcp"), 73 | YARN("yarn"), 74 | MERGED("merged"); 75 | 76 | companion object { 77 | /** 78 | * Converts the mapping String into a [MappingsType] enum. 79 | * 80 | * @param mappings The mappings String. 81 | * @return The [MappingsType] corresponding [id]. 82 | * @throws IllegalArgumentException If there is no mappings which corresponds with the [id]. 83 | */ 84 | @JvmStatic 85 | fun fromString(mappings: String) = 86 | enumValues().find { it.id == mappings } ?: error("No such mappings: $mappings") 87 | } 88 | 89 | private fun resolvingProperty() = ReadOnlyProperty { _, prop -> resolve(prop.name) } 90 | val named by resolvingProperty() 91 | val srg by resolvingProperty() 92 | val obf by resolvingProperty() 93 | val intermediary by resolvingProperty() 94 | 95 | fun resolve(type: String) = "$id-$type" 96 | } -------------------------------------------------------------------------------- /internals/src/main/kotlin/net/weavemc/internals/InsnDsl.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.internals 2 | 3 | import org.objectweb.asm.* 4 | import org.objectweb.asm.tree.* 5 | import java.io.FileOutputStream 6 | import kotlin.reflect.KClass 7 | 8 | @Suppress( 9 | "PropertyName", 10 | "unused", 11 | "FunctionName", 12 | "SpellCheckingInspection", 13 | "MemberVisibilityCanBePrivate", 14 | ) 15 | sealed class InsnBuilder { 16 | abstract operator fun AbstractInsnNode.unaryPlus() 17 | abstract operator fun InsnList.unaryPlus() 18 | 19 | fun getstatic(owner: String, name: String, desc: String) = +FieldInsnNode(Opcodes.GETSTATIC, owner, name, desc) 20 | fun putstatic(owner: String, name: String, desc: String) = +FieldInsnNode(Opcodes.PUTSTATIC, owner, name, desc) 21 | fun getfield(owner: String, name: String, desc: String) = +FieldInsnNode(Opcodes.GETFIELD, owner, name, desc) 22 | fun putfield(owner: String, name: String, desc: String) = +FieldInsnNode(Opcodes.PUTFIELD, owner, name, desc) 23 | 24 | fun iinc(`var`: Int, incr: Int) = +IincInsnNode(`var`, incr) 25 | 26 | @get:JvmName("nop") val nop get() = +InsnNode(Opcodes.NOP) 27 | @get:JvmName("aconst_null") val aconst_null get() = +InsnNode(Opcodes.ACONST_NULL) 28 | @get:JvmName("iconst_m1") val iconst_m1 get() = +InsnNode(Opcodes.ICONST_M1) 29 | @get:JvmName("iconst_0") val iconst_0 get() = +InsnNode(Opcodes.ICONST_0) 30 | @get:JvmName("iconst_1") val iconst_1 get() = +InsnNode(Opcodes.ICONST_1) 31 | @get:JvmName("iconst_2") val iconst_2 get() = +InsnNode(Opcodes.ICONST_2) 32 | @get:JvmName("iconst_3") val iconst_3 get() = +InsnNode(Opcodes.ICONST_3) 33 | @get:JvmName("iconst_4") val iconst_4 get() = +InsnNode(Opcodes.ICONST_4) 34 | @get:JvmName("iconst_5") val iconst_5 get() = +InsnNode(Opcodes.ICONST_5) 35 | @get:JvmName("lconst_0") val lconst_0 get() = +InsnNode(Opcodes.LCONST_0) 36 | @get:JvmName("lconst_1") val lconst_1 get() = +InsnNode(Opcodes.LCONST_1) 37 | @get:JvmName("fconst_0") val fconst_0 get() = +InsnNode(Opcodes.FCONST_0) 38 | @get:JvmName("fconst_1") val fconst_1 get() = +InsnNode(Opcodes.FCONST_1) 39 | @get:JvmName("fconst_2") val fconst_2 get() = +InsnNode(Opcodes.FCONST_2) 40 | @get:JvmName("dconst_0") val dconst_0 get() = +InsnNode(Opcodes.DCONST_0) 41 | @get:JvmName("dconst_1") val dconst_1 get() = +InsnNode(Opcodes.DCONST_1) 42 | @get:JvmName("iaload") val iaload get() = +InsnNode(Opcodes.IALOAD) 43 | @get:JvmName("laload") val laload get() = +InsnNode(Opcodes.LALOAD) 44 | @get:JvmName("faload") val faload get() = +InsnNode(Opcodes.FALOAD) 45 | @get:JvmName("daload") val daload get() = +InsnNode(Opcodes.DALOAD) 46 | @get:JvmName("aaload") val aaload get() = +InsnNode(Opcodes.AALOAD) 47 | @get:JvmName("baload") val baload get() = +InsnNode(Opcodes.BALOAD) 48 | @get:JvmName("caload") val caload get() = +InsnNode(Opcodes.CALOAD) 49 | @get:JvmName("saload") val saload get() = +InsnNode(Opcodes.SALOAD) 50 | @get:JvmName("iastore") val iastore get() = +InsnNode(Opcodes.IASTORE) 51 | @get:JvmName("lastore") val lastore get() = +InsnNode(Opcodes.LASTORE) 52 | @get:JvmName("fastore") val fastore get() = +InsnNode(Opcodes.FASTORE) 53 | @get:JvmName("dastore") val dastore get() = +InsnNode(Opcodes.DASTORE) 54 | @get:JvmName("aastore") val aastore get() = +InsnNode(Opcodes.AASTORE) 55 | @get:JvmName("bastore") val bastore get() = +InsnNode(Opcodes.BASTORE) 56 | @get:JvmName("castore") val castore get() = +InsnNode(Opcodes.CASTORE) 57 | @get:JvmName("sastore") val sastore get() = +InsnNode(Opcodes.SASTORE) 58 | @get:JvmName("pop") val pop get() = +InsnNode(Opcodes.POP) 59 | @get:JvmName("pop2") val pop2 get() = +InsnNode(Opcodes.POP2) 60 | @get:JvmName("dup") val dup get() = +InsnNode(Opcodes.DUP) 61 | @get:JvmName("dup_x1") val dup_x1 get() = +InsnNode(Opcodes.DUP_X1) 62 | @get:JvmName("dup_x2") val dup_x2 get() = +InsnNode(Opcodes.DUP_X2) 63 | @get:JvmName("dup2") val dup2 get() = +InsnNode(Opcodes.DUP2) 64 | @get:JvmName("dup2_x1") val dup2_x1 get() = +InsnNode(Opcodes.DUP2_X1) 65 | @get:JvmName("dup2_x2") val dup2_x2 get() = +InsnNode(Opcodes.DUP2_X2) 66 | @get:JvmName("swap") val swap get() = +InsnNode(Opcodes.SWAP) 67 | @get:JvmName("iadd") val iadd get() = +InsnNode(Opcodes.IADD) 68 | @get:JvmName("ladd") val ladd get() = +InsnNode(Opcodes.LADD) 69 | @get:JvmName("fadd") val fadd get() = +InsnNode(Opcodes.FADD) 70 | @get:JvmName("dadd") val dadd get() = +InsnNode(Opcodes.DADD) 71 | @get:JvmName("isub") val isub get() = +InsnNode(Opcodes.ISUB) 72 | @get:JvmName("lsub") val lsub get() = +InsnNode(Opcodes.LSUB) 73 | @get:JvmName("fsub") val fsub get() = +InsnNode(Opcodes.FSUB) 74 | @get:JvmName("dsub") val dsub get() = +InsnNode(Opcodes.DSUB) 75 | @get:JvmName("imul") val imul get() = +InsnNode(Opcodes.IMUL) 76 | @get:JvmName("lmul") val lmul get() = +InsnNode(Opcodes.LMUL) 77 | @get:JvmName("fmul") val fmul get() = +InsnNode(Opcodes.FMUL) 78 | @get:JvmName("dmul") val dmul get() = +InsnNode(Opcodes.DMUL) 79 | @get:JvmName("idiv") val idiv get() = +InsnNode(Opcodes.IDIV) 80 | @get:JvmName("ldiv") val ldiv get() = +InsnNode(Opcodes.LDIV) 81 | @get:JvmName("fdiv") val fdiv get() = +InsnNode(Opcodes.FDIV) 82 | @get:JvmName("ddiv") val ddiv get() = +InsnNode(Opcodes.DDIV) 83 | @get:JvmName("irem") val irem get() = +InsnNode(Opcodes.IREM) 84 | @get:JvmName("lrem") val lrem get() = +InsnNode(Opcodes.LREM) 85 | @get:JvmName("frem") val frem get() = +InsnNode(Opcodes.FREM) 86 | @get:JvmName("drem") val drem get() = +InsnNode(Opcodes.DREM) 87 | @get:JvmName("ineg") val ineg get() = +InsnNode(Opcodes.INEG) 88 | @get:JvmName("lneg") val lneg get() = +InsnNode(Opcodes.LNEG) 89 | @get:JvmName("fneg") val fneg get() = +InsnNode(Opcodes.FNEG) 90 | @get:JvmName("dneg") val dneg get() = +InsnNode(Opcodes.DNEG) 91 | @get:JvmName("ishl") val ishl get() = +InsnNode(Opcodes.ISHL) 92 | @get:JvmName("lshl") val lshl get() = +InsnNode(Opcodes.LSHL) 93 | @get:JvmName("ishr") val ishr get() = +InsnNode(Opcodes.ISHR) 94 | @get:JvmName("lshr") val lshr get() = +InsnNode(Opcodes.LSHR) 95 | @get:JvmName("iushr") val iushr get() = +InsnNode(Opcodes.IUSHR) 96 | @get:JvmName("lushr") val lushr get() = +InsnNode(Opcodes.LUSHR) 97 | @get:JvmName("iand") val iand get() = +InsnNode(Opcodes.IAND) 98 | @get:JvmName("land") val land get() = +InsnNode(Opcodes.LAND) 99 | @get:JvmName("ior") val ior get() = +InsnNode(Opcodes.IOR) 100 | @get:JvmName("lor") val lor get() = +InsnNode(Opcodes.LOR) 101 | @get:JvmName("ixor") val ixor get() = +InsnNode(Opcodes.IXOR) 102 | @get:JvmName("lxor") val lxor get() = +InsnNode(Opcodes.LXOR) 103 | @get:JvmName("i2l") val i2l get() = +InsnNode(Opcodes.I2L) 104 | @get:JvmName("i2f") val i2f get() = +InsnNode(Opcodes.I2F) 105 | @get:JvmName("i2d") val i2d get() = +InsnNode(Opcodes.I2D) 106 | @get:JvmName("l2i") val l2i get() = +InsnNode(Opcodes.L2I) 107 | @get:JvmName("l2f") val l2f get() = +InsnNode(Opcodes.L2F) 108 | @get:JvmName("l2d") val l2d get() = +InsnNode(Opcodes.L2D) 109 | @get:JvmName("f2i") val f2i get() = +InsnNode(Opcodes.F2I) 110 | @get:JvmName("f2l") val f2l get() = +InsnNode(Opcodes.F2L) 111 | @get:JvmName("f2d") val f2d get() = +InsnNode(Opcodes.F2D) 112 | @get:JvmName("d2i") val d2i get() = +InsnNode(Opcodes.D2I) 113 | @get:JvmName("d2l") val d2l get() = +InsnNode(Opcodes.D2L) 114 | @get:JvmName("d2f") val d2f get() = +InsnNode(Opcodes.D2F) 115 | @get:JvmName("i2b") val i2b get() = +InsnNode(Opcodes.I2B) 116 | @get:JvmName("i2c") val i2c get() = +InsnNode(Opcodes.I2C) 117 | @get:JvmName("i2s") val i2s get() = +InsnNode(Opcodes.I2S) 118 | @get:JvmName("lcmp") val lcmp get() = +InsnNode(Opcodes.LCMP) 119 | @get:JvmName("fcmpl") val fcmpl get() = +InsnNode(Opcodes.FCMPL) 120 | @get:JvmName("fcmpg") val fcmpg get() = +InsnNode(Opcodes.FCMPG) 121 | @get:JvmName("dcmpl") val dcmpl get() = +InsnNode(Opcodes.DCMPL) 122 | @get:JvmName("dcmpg") val dcmpg get() = +InsnNode(Opcodes.DCMPG) 123 | @get:JvmName("ireturn") val ireturn get() = +InsnNode(Opcodes.IRETURN) 124 | @get:JvmName("lreturn") val lreturn get() = +InsnNode(Opcodes.LRETURN) 125 | @get:JvmName("freturn") val freturn get() = +InsnNode(Opcodes.FRETURN) 126 | @get:JvmName("dreturn") val dreturn get() = +InsnNode(Opcodes.DRETURN) 127 | @get:JvmName("areturn") val areturn get() = +InsnNode(Opcodes.ARETURN) 128 | @get:JvmName("_return") val _return get() = +InsnNode(Opcodes.RETURN) 129 | @get:JvmName("arraylength") val arraylength get() = +InsnNode(Opcodes.ARRAYLENGTH) 130 | @get:JvmName("athrow") val athrow get() = +InsnNode(Opcodes.ATHROW) 131 | @get:JvmName("monitorenter") val monitorenter get() = +InsnNode(Opcodes.MONITORENTER) 132 | @get:JvmName("monitorexit") val monitorexit get() = +InsnNode(Opcodes.MONITOREXIT) 133 | 134 | fun bipush(n: Int) = +IntInsnNode(Opcodes.BIPUSH, n) 135 | fun sipush(n: Int) = +IntInsnNode(Opcodes.SIPUSH, n) 136 | fun newarray(type: Int) = +IntInsnNode(Opcodes.NEWARRAY, type) 137 | 138 | fun ldc(cst: Any) = +LdcInsnNode(cst) 139 | 140 | fun ifeq(label: LabelNode) = +JumpInsnNode(Opcodes.IFEQ, label) 141 | fun ifne(label: LabelNode) = +JumpInsnNode(Opcodes.IFNE, label) 142 | fun iflt(label: LabelNode) = +JumpInsnNode(Opcodes.IFLT, label) 143 | fun ifge(label: LabelNode) = +JumpInsnNode(Opcodes.IFGE, label) 144 | fun ifgt(label: LabelNode) = +JumpInsnNode(Opcodes.IFGT, label) 145 | fun ifle(label: LabelNode) = +JumpInsnNode(Opcodes.IFLE, label) 146 | fun if_icmpeq(label: LabelNode) = +JumpInsnNode(Opcodes.IF_ICMPEQ, label) 147 | fun if_icmpne(label: LabelNode) = +JumpInsnNode(Opcodes.IF_ICMPNE, label) 148 | fun if_icmplt(label: LabelNode) = +JumpInsnNode(Opcodes.IF_ICMPLT, label) 149 | fun if_icmpge(label: LabelNode) = +JumpInsnNode(Opcodes.IF_ICMPGE, label) 150 | fun if_icmpgt(label: LabelNode) = +JumpInsnNode(Opcodes.IF_ICMPGT, label) 151 | fun if_icmple(label: LabelNode) = +JumpInsnNode(Opcodes.IF_ICMPLE, label) 152 | fun if_acmpeq(label: LabelNode) = +JumpInsnNode(Opcodes.IF_ACMPEQ, label) 153 | fun if_acmpne(label: LabelNode) = +JumpInsnNode(Opcodes.IF_ACMPNE, label) 154 | fun goto(label: LabelNode) = +JumpInsnNode(Opcodes.GOTO, label) 155 | fun ifnull(label: LabelNode) = +JumpInsnNode(Opcodes.IFNULL, label) 156 | fun ifnonnull(label: LabelNode) = +JumpInsnNode(Opcodes.IFNONNULL, label) 157 | 158 | fun invokedynamic(name: String, desc: String, bsm: Handle, vararg bsmArgs: Any) = 159 | +InvokeDynamicInsnNode(name, desc, bsm, *bsmArgs) 160 | 161 | fun invokevirtual(owner: String, name: String, desc: String) = 162 | +MethodInsnNode(Opcodes.INVOKEVIRTUAL, owner, name, desc, false) 163 | 164 | fun invokespecial(owner: String, name: String, desc: String) = 165 | +MethodInsnNode(Opcodes.INVOKESPECIAL, owner, name, desc, false) 166 | 167 | fun invokestatic(owner: String, name: String, desc: String) = 168 | +MethodInsnNode(Opcodes.INVOKESTATIC, owner, name, desc, false) 169 | 170 | fun invokeinterface(owner: String, name: String, desc: String) = 171 | +MethodInsnNode(Opcodes.INVOKEINTERFACE, owner, name, desc, true) 172 | 173 | fun new(type: String) = +TypeInsnNode(Opcodes.NEW, type) 174 | fun anewarray(type: String) = +TypeInsnNode(Opcodes.ANEWARRAY, type) 175 | fun checkcast(type: String) = +TypeInsnNode(Opcodes.CHECKCAST, type) 176 | fun instanceof(type: String) = +TypeInsnNode(Opcodes.INSTANCEOF, type) 177 | 178 | fun iload(`var`: Int) = +VarInsnNode(Opcodes.ILOAD, `var`) 179 | fun lload(`var`: Int) = +VarInsnNode(Opcodes.LLOAD, `var`) 180 | fun fload(`var`: Int) = +VarInsnNode(Opcodes.FLOAD, `var`) 181 | fun dload(`var`: Int) = +VarInsnNode(Opcodes.DLOAD, `var`) 182 | fun aload(`var`: Int) = +VarInsnNode(Opcodes.ALOAD, `var`) 183 | fun istore(`var`: Int) = +VarInsnNode(Opcodes.ISTORE, `var`) 184 | fun lstore(`var`: Int) = +VarInsnNode(Opcodes.LSTORE, `var`) 185 | fun fstore(`var`: Int) = +VarInsnNode(Opcodes.FSTORE, `var`) 186 | fun dstore(`var`: Int) = +VarInsnNode(Opcodes.DSTORE, `var`) 187 | fun astore(`var`: Int) = +VarInsnNode(Opcodes.ASTORE, `var`) 188 | 189 | fun f_new(numLocal: Int, local: Array?, numStack: Int, stack: Array?) = 190 | +FrameNode(Opcodes.F_NEW, numLocal, local, numStack, stack) 191 | 192 | fun f_full(numLocal: Int, local: Array?, numStack: Int, stack: Array?) = 193 | +FrameNode(Opcodes.F_FULL, numLocal, local, numStack, stack) 194 | 195 | fun f_append(numLocal: Int, local: Array) = 196 | +FrameNode(Opcodes.F_APPEND, numLocal, local, 0, null) 197 | 198 | fun f_chop(numLocal: Int) = 199 | +FrameNode(Opcodes.F_CHOP, numLocal, null, 0, null) 200 | 201 | fun f_same() = 202 | +FrameNode(Opcodes.F_SAME, 0, null, 0, null) 203 | 204 | fun f_same1(stack: Any) = 205 | +FrameNode(Opcodes.F_SAME1, 0, null, 1, arrayOf(stack)) 206 | 207 | fun int(n: Int) = when (n) { 208 | in -1..5 -> +InsnNode(Opcodes.ICONST_0 + n) 209 | in Byte.MIN_VALUE..Byte.MAX_VALUE -> bipush(n) 210 | in Short.MIN_VALUE..Short.MAX_VALUE -> sipush(n) 211 | else -> ldc(n) 212 | } 213 | } 214 | 215 | private class InsnListBuilder : InsnBuilder() { 216 | 217 | val list = InsnList() 218 | override fun AbstractInsnNode.unaryPlus() = list.add(this) 219 | override fun InsnList.unaryPlus() = list.add(this) 220 | } 221 | 222 | private class VisitorInsnBuilder(private val parent: MethodVisitor) : InsnBuilder() { 223 | 224 | override fun AbstractInsnNode.unaryPlus() = accept(parent) 225 | override fun InsnList.unaryPlus() = accept(parent) 226 | } 227 | 228 | public fun asm(block: InsnBuilder.() -> Unit): InsnList = 229 | InsnListBuilder().apply(block).list 230 | 231 | public fun MethodVisitor.visitAsm(block: InsnBuilder.() -> Unit) { 232 | VisitorInsnBuilder(this).run(block) 233 | } 234 | 235 | public fun List.named(name: String) = find { it.name == name }!! 236 | public fun List.search(name: String, desc: String) = find { it.name == name && it.desc == desc }!! 237 | public fun List.search(name: String, returnType: String, vararg args: String) = find { it.name == name && it.desc == "(${args.joinToString("")})$returnType" }!! 238 | public fun List.named(name: String) = find { it.name == name }!! 239 | public fun List.search(name: String, type: String) = find { it.name == name && it.desc == type }!! 240 | 241 | public inline fun internalNameOf(): String = Type.getInternalName(T::class.java) 242 | public fun internalNameOf(javaClass: KClass<*>): String = Type.getInternalName(javaClass.java) 243 | 244 | public inline fun AbstractInsnNode.next(p: (T) -> Boolean = { true }): T? { 245 | return generateSequence(next) { it.next }.filterIsInstance().find(p) 246 | } 247 | 248 | public inline fun AbstractInsnNode.prev(p: (T) -> Boolean = { true }): T? { 249 | return generateSequence(previous) { it.previous }.filterIsInstance().find(p) 250 | } 251 | 252 | public fun ByteArray.dump(file: String) = FileOutputStream(file).use { it.write(this) } 253 | 254 | public fun ClassNode.dump(file: String) { 255 | val cw = ClassWriter(0) 256 | accept(cw) 257 | FileOutputStream(file).use { it.write(cw.toByteArray()) } 258 | } 259 | 260 | public inline fun InsnBuilder.getSingleton() = 261 | getstatic(internalNameOf(), "INSTANCE", "L${internalNameOf()};") 262 | 263 | public fun InsnBuilder.println() { 264 | getstatic("java/lang/System", "out", "Ljava/io/PrintStream;") 265 | swap 266 | invokevirtual("java/io/PrintStream", "println", "(Ljava/lang/Object;)V") 267 | } 268 | -------------------------------------------------------------------------------- /internals/src/main/kotlin/net/weavemc/internals/MappingsRetrieval.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.internals 2 | 3 | import com.grappenmaker.mappings.* 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | import kotlinx.serialization.json.Json 7 | import net.weavemc.internals.MappingsType.* 8 | import java.io.File 9 | import java.io.InputStream 10 | import java.net.URL 11 | import java.net.URLEncoder 12 | import java.nio.file.Path 13 | import java.nio.file.Paths 14 | import java.util.jar.JarFile 15 | import java.util.zip.ZipInputStream 16 | import kotlin.io.path.* 17 | 18 | fun getVanillaMinecraftJar(version: String): File { 19 | val os = System.getProperty("os.name").lowercase() 20 | val minecraftPath = Paths.get( 21 | System.getProperty("user.home"), *when { 22 | os.contains("win") -> arrayOf("AppData", "Roaming", ".minecraft") 23 | os.contains("mac") -> arrayOf("Library", "Application Support", "minecraft") 24 | os.contains("nix") || os.contains("nux") || os.contains("aix") -> arrayOf(".minecraft") 25 | else -> error("Failed to retrieve Vanilla Minecraft Jar due to an unsupported OS.") 26 | } 27 | ) 28 | 29 | return minecraftPath.resolve("versions").resolve(version).resolve("$version.jar").toFile() 30 | } 31 | 32 | object MappingsRetrieval { 33 | private val forgeMavenRoot = "https://maven.minecraftforge.net/de/oceanlabs/mcp" 34 | private val yarnMavenRoot = "https://maven.fabricmc.net/net/fabricmc/yarn" 35 | 36 | data class MCPVersion( 37 | val version: String, 38 | val snapshot: String?, 39 | val fullVersion: String, 40 | val channel: String 41 | ) 42 | 43 | fun MCPVersion.mcpNamesStream(): ZipInputStream = ZipInputStream( 44 | URL("$forgeMavenRoot/mcp_$channel/$fullVersion/mcp_$channel-$fullVersion.zip").openStream() 45 | ) 46 | 47 | fun MCPVersion.srgNamesStream(useNew: Boolean): ZipInputStream = 48 | if (useNew) ZipInputStream(URL("$forgeMavenRoot/mcp_config/$version/mcp_config-$version.zip").openStream()) 49 | else ZipInputStream(URL("$forgeMavenRoot/mcp/$version/mcp-$version-srg.zip").openStream()) 50 | 51 | private fun List.asNamesMapping(): Map { 52 | val meaning = first().split(',') 53 | val fromIdx = meaning.indexOf("searge") 54 | val toIdx = meaning.indexOf("name") 55 | 56 | return drop(1).map { it.split(',') }.associate { it[fromIdx] to it[toIdx] } 57 | } 58 | 59 | fun Mappings.fixSRGNamespaces(): Mappings = if (namespaces.size == 2) renameNamespaces("obf", "srg") else this 60 | 61 | fun Mappings.mergeSRGWithMCP(methods: List, fields: List): GenericMappings { 62 | require("named" !in namespaces) 63 | 64 | val methodsMapping = methods.asNamesMapping() 65 | val fieldsMapping = fields.asNamesMapping() 66 | 67 | return GenericMappings( 68 | namespaces = namespaces + "named", 69 | classes = classes.map { oc -> 70 | oc.copy( 71 | names = oc.names + oc.names.last(), 72 | methods = oc.methods.map { 73 | val last = it.names.last() 74 | it.copy(names = it.names + (methodsMapping[last] ?: last)) 75 | }, 76 | fields = oc.fields.map { 77 | val last = it.names.last() 78 | it.copy(names = it.names + (fieldsMapping[last] ?: last)) 79 | }, 80 | ) 81 | } 82 | ) 83 | } 84 | 85 | fun mcpMappingsStream(version: String, gameJar: File): InputStream? { 86 | val versionDecimal = version.substringAfter("1.").toDouble() 87 | 88 | // TODO: figure out 1.12.2 89 | if (versionDecimal == 12.2) return null 90 | val joinedMappingsPath = if (versionDecimal >= 13) "config/joined.tsrg" else "joined.srg" 91 | val mappingsChannel = if (versionDecimal >= 15.1) "config" else "snapshot" 92 | if (versionDecimal >= 16) return null 93 | 94 | return mappingsCache(MCP, version).getOrPut { 95 | val url = "$forgeMavenRoot/mcp_$mappingsChannel/maven-metadata.xml" 96 | val mcVersion = parseMCPVersions(url)[version] ?: error("Could not find version $version in $url") 97 | val srgMappingsContent = mcVersion.srgNamesStream(versionDecimal >= 13).readEntries() 98 | val mcpMappingsContent = mcVersion.mcpNamesStream().readEntries() 99 | 100 | val joinedMappings = srgMappingsContent[joinedMappingsPath] 101 | ?: error("Failed to find $joinedMappingsPath in SRG mappings zip") 102 | 103 | val originalMappings = JarFile(gameJar).use { jar -> 104 | MappingsLoader.loadMappings(joinedMappings.decodeToString().nonBlankLines()) 105 | .fixSRGNamespaces() 106 | .removeRedundancy(jar) 107 | .recoverFieldDescriptors(jar) 108 | } 109 | 110 | val finalMappings = originalMappings.mergeSRGWithMCP( 111 | methods = mcpMappingsContent["methods.csv"]?.decodeToString()?.nonBlankLines() 112 | ?: error("Failed to find methods.csv in MCP mappings zip"), 113 | fields = mcpMappingsContent["fields.csv"]?.decodeToString()?.nonBlankLines() 114 | ?: error("Failed to find fields.csv in MCP mappings zip"), 115 | ) 116 | 117 | finalMappings.renameNamespaces("official", "srg", "named").asTinyMappings(v2 = true).write() 118 | } 119 | } 120 | 121 | fun String.nonBlankLines() = lines().filter { it.isNotBlank() } 122 | fun ZipInputStream.readEntries() = generateSequence { nextEntry }.associate { it.name to readBytes() } 123 | 124 | private val versionRegex = """(.*?)""".toRegex() 125 | fun String.parseXMLVersions() = versionRegex.findAll(this).map { it.groupValues[1] }.toList() 126 | 127 | fun parseMCPVersions(url: String): Map { 128 | val text = URL(url).readText() 129 | val versionsString = text.parseXMLVersions() 130 | val versions: List = versionsString.map { l -> 131 | val (before, after) = l.splitAround('-') 132 | when { 133 | "snapshot" in url -> MCPVersion(after, before, l, "snapshot") 134 | else -> MCPVersion(before, after.substringBefore('.').takeIf { it.isNotEmpty() }, l, "config") 135 | } 136 | } 137 | 138 | return versions.groupBy { it.version }.mapValues { (_, v) -> 139 | v.find { it.snapshot == null } ?: v.maxBy { it.snapshot!! } 140 | } 141 | } 142 | 143 | fun yarnMappingsStream(version: String, gameJar: File): InputStream? { 144 | return mappingsCache(YARN, version).getOrPut { 145 | val versions = URL("$yarnMavenRoot/maven-metadata.xml").readText().parseXMLVersions() 146 | val targetVersion = versions 147 | .filter { it.substringBefore('+') == version } 148 | .maxByOrNull { it.substringAfterLast('.').toInt() } 149 | ?: return null 150 | 151 | val versionEncoded = URLEncoder.encode(targetVersion, "UTF-8") 152 | val url = "$yarnMavenRoot/$versionEncoded/yarn-$versionEncoded.jar" 153 | val pathInJar = "mappings/mappings.tiny" 154 | val entries = ZipInputStream(URL(url).openStream()).readEntries() 155 | 156 | JarFile(gameJar).use { jar -> 157 | MappingsLoader.loadMappings(entries.getValue(pathInJar).decodeToString().lines()) 158 | .removeRedundancy(jar) 159 | .asTinyMappings(v2 = true) 160 | .write() 161 | } 162 | } 163 | } 164 | 165 | fun mappingsCache(type: MappingsType, version: String) = 166 | Path(System.getProperty("user.home"), ".weave", ".cache", "mappings", "${type.id}_$version", "mappings.tiny") 167 | 168 | inline fun Path.getOrPut(block: () -> Iterable): InputStream { 169 | createParentDirectories() 170 | 171 | if (!exists()) writeLines(block()) 172 | require(exists()) 173 | 174 | return inputStream() 175 | } 176 | 177 | internal val json = Json { ignoreUnknownKeys = true } 178 | internal inline fun String.decodeJSON() = json.decodeFromString(this) 179 | 180 | private fun mojangMappingsStream(version: String, gameJar: File): InputStream? { 181 | return mappingsCache(MOJANG, version).getOrPut { 182 | val manifest = URL("https://launchermeta.mojang.com/mc/game/version_manifest_v2.json") 183 | .readText().decodeJSON() 184 | 185 | val versionEntry = manifest.versions.find { it.id == version } ?: return null 186 | val versionInfo = URL(versionEntry.url).readText().decodeJSON() 187 | val mappings = versionInfo.downloads.mappings ?: return null 188 | require(mappings.size != -1) { "Invalid mappings entry for version $version: $mappings" } 189 | 190 | JarFile(gameJar).use { jar -> 191 | MappingsLoader.loadMappings(URL(mappings.url).readText().trim().lines()) 192 | .filterClasses { it.names.first().substringAfterLast('/') != "package-info" } 193 | .reorderNamespaces("official", "named") 194 | .removeRedundancy(jar) 195 | .asTinyMappings(v2 = true).write() 196 | } 197 | } 198 | } 199 | 200 | @Serializable 201 | private data class VersionManifest(val versions: List) 202 | 203 | @Serializable 204 | private data class ManifestVersion(val id: String, val url: String) 205 | 206 | @Serializable 207 | private data class VersionInfo(val downloads: VersionDownloads) 208 | 209 | @Serializable 210 | private data class VersionDownloads( 211 | val client: VersionDownload, 212 | @SerialName("client_mappings") val mappings: ClientMappings? = null 213 | ) 214 | 215 | @Serializable 216 | private data class ClientMappings( 217 | val url: String, 218 | val sha1: String, 219 | val size: Int = -1 220 | ) 221 | 222 | @Serializable 223 | private data class VersionDownload(val url: String, val sha1: String) 224 | 225 | private fun allMappings(version: String, gameJar: File) = 226 | (MappingsType.entries - MERGED).mapNotNull { loadWeaveMappings(it, version, gameJar) } 227 | 228 | private fun mergedMappingsStream(version: String, gameJar: File): InputStream = 229 | mappingsCache(MERGED, version).getOrPut { 230 | val joined = allMappings(version, gameJar).map { (id, mappings) -> 231 | mappings.renameNamespaces(mappings.namespaces.map { if (it == "official") it else id.resolve(it) }) 232 | }.join("official") 233 | 234 | val otherNs = joined.namespaces - "official" 235 | joined.reorderNamespaces(listOf("official") + otherNs).asTinyMappings(v2 = true).write() 236 | } 237 | 238 | fun loadWeaveMappings(mappings: MappingsType, version: String, gameJar: File) = when (mappings) { 239 | YARN -> yarnMappingsStream(version, gameJar) 240 | MCP -> mcpMappingsStream(version, gameJar) 241 | MOJANG -> mojangMappingsStream(version, gameJar) 242 | MERGED -> mergedMappingsStream(version, gameJar) 243 | }?.let { WeaveMappings(mappings, MappingsLoader.loadMappings(it.readBytes().decodeToString().lines())) } 244 | 245 | fun loadMergedWeaveMappings(version: String, gameJar: File) = loadWeaveMappings(MERGED, version, gameJar)!! 246 | } 247 | 248 | data class WeaveMappings(val type: MappingsType, val mappings: Mappings) -------------------------------------------------------------------------------- /internals/src/main/kotlin/net/weavemc/internals/Mods.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.internals 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | /** 6 | * The data class that is read from a mod's `weave.mod.json`. 7 | * 8 | * @property name The loaded name of the mod, if this field is not found, the game will crash 9 | * @property modId The loaded mod ID of the mod, if this field is not found, the game will crash 10 | * @property mixinConfigs The loaded mixin configs of the mod. 11 | * @property hooks The loaded hooks of the mod. 12 | * @property tweakers Names of classes that can be loaded as Tweakers 13 | * @property accessWideners Paths to configurations embedded within mods for access widening 14 | * @property entryPoints The loaded [ModInitializer] entry points of the mod. 15 | * @property namespace The mappings namespace this mod was created with. 16 | * @property dependencies The dependencies this mod requires, at a list of [modId]s. 17 | * @property compiledFor The version id this mod was compiled for 18 | */ 19 | @Serializable 20 | data class ModConfig( 21 | val name: String, 22 | val modId: String, 23 | val entryPoints: List = emptyList(), 24 | val mixinConfigs: List = emptyList(), 25 | val hooks: List = emptyList(), 26 | val tweakers: List = emptyList(), 27 | val accessWideners: List = emptyList(), 28 | val namespace: String, 29 | val dependencies: List = emptyList(), 30 | val compiledFor: String? = null, 31 | ) -------------------------------------------------------------------------------- /internals/src/main/kotlin/net/weavemc/internals/Util.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.internals 2 | 3 | internal fun String.splitAround(c: Char): Pair = 4 | substringBefore(c) to substringAfter(c, "") -------------------------------------------------------------------------------- /loader/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("VulnerableLibrariesLocal") 2 | 3 | plugins { 4 | id("config-kotlin") 5 | id("config-shade") 6 | id("config-publish") 7 | } 8 | 9 | repositories { 10 | maven("https://maven.fabricmc.net/") 11 | } 12 | 13 | kotlin { 14 | compilerOptions { 15 | explicitApi() 16 | optIn.add("net.weavemc.loader.impl.bootstrap.PublicButInternal") 17 | } 18 | } 19 | 20 | dependencies { 21 | shade(projects.internals) 22 | shade(libs.klog) 23 | shade(libs.kxser.json) 24 | shade(libs.bundles.asm) 25 | shade(libs.mappings) 26 | shade(libs.mixin) { 27 | exclude(group = "com.google.guava") 28 | exclude(group = "com.google.code.gson") 29 | } 30 | } 31 | 32 | tasks { 33 | jar { 34 | manifest.attributes( 35 | "Premain-Class" to "net.weavemc.loader.impl.bootstrap.AgentKt", 36 | "Main-Class" to "net.weavemc.loader.impl.bootstrap.AgentKt", 37 | "Can-Retransform-Classes" to "true", 38 | ) 39 | 40 | manifest.attributes( 41 | mapOf( 42 | "Specification-Title" to "Weave Loader API", 43 | "Specification-Version" to "0", 44 | "Specification-Vendor" to "WeaveMC", 45 | "Implementation-Title" to "Weave Loader", 46 | "Implementation-Version" to "${project.version}", 47 | "Implementation-Vendor" to "WeaveMC", 48 | ), "net.weavemc.loader.api" 49 | ) 50 | manifest.attributes( 51 | mapOf( 52 | "Specification-Title" to "Weave Loader", 53 | "Specification-Version" to "0", // we're still in beta, so this is 0 54 | "Specification-Vendor" to "WeaveMC", 55 | "Implementation-Title" to "Weave Loader", 56 | "Implementation-Version" to "${project.version}", 57 | "Implementation-Vendor" to "WeaveMC", 58 | ), "net.weavemc.loader.impl" 59 | ) 60 | } 61 | } 62 | 63 | publishing { 64 | publications { 65 | create("maven") { 66 | from(components["java"]) 67 | groupId = "net.weavemc" 68 | artifactId = "loader" 69 | version = "${project.version}" 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /loader/src/main/kotlin/net/weavemc/loader/api/Hook.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.loader.api 2 | 3 | import org.objectweb.asm.tree.ClassNode 4 | 5 | /** 6 | * A hook is a type of weave mod intrinsic that allows modders to change the bytecode of any class. 7 | * Implementors are expected to have a no-args public constructor 8 | * 9 | * @property targets the internal names of the targets this [Hook] wants to modify, in the namespace of the mod 10 | */ 11 | public abstract class Hook(public vararg val targets: String) { 12 | /** 13 | * Receives all transformed [ClassNode]s in the namespace of the mod that match [targets]. 14 | * The class writer parameters can be altered using the [AssemblerConfig]. 15 | */ 16 | public abstract fun transform(node: ClassNode, cfg: AssemblerConfig) 17 | 18 | /** 19 | * Allows modders to alter class writer parameters 20 | */ 21 | public interface AssemblerConfig { 22 | /** 23 | * Enables [org.objectweb.asm.ClassWriter.COMPUTE_FRAMES] 24 | */ 25 | public fun computeFrames() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /loader/src/main/kotlin/net/weavemc/loader/api/ModInitializer.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.loader.api 2 | 3 | import java.lang.instrument.Instrumentation 4 | 5 | /** 6 | * When Weave is loading mods, it will create a new instance of the class that implements this interface. 7 | * This occurs before the game loads. 8 | * 9 | * Override [preInit()][preInit] to initialize your mod. 10 | * Implementors are expected to have a no-args public constructor 11 | */ 12 | @JvmDefaultWithCompatibility 13 | public interface ModInitializer { 14 | /** 15 | * Invoked before Minecraft is initialized. 16 | * 17 | * @param inst Instrumentation object which can be used to register custom transformers 18 | * @since Weave-Loader 1.0.0-beta.1 19 | */ 20 | @Deprecated("", replaceWith = ReplaceWith("init()")) 21 | public fun preInit(inst: Instrumentation) {} 22 | 23 | /** 24 | * Invoked when Minecraft is initialized as to prevent premature class loading of Minecraft related classes. 25 | * 26 | * Should be overridden to initialize your mod. 27 | * @since Weave-Loader 1.0.0-beta.2 28 | */ 29 | public fun init() {} 30 | } -------------------------------------------------------------------------------- /loader/src/main/kotlin/net/weavemc/loader/api/Tweaker.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.loader.api 2 | 3 | import java.lang.instrument.Instrumentation 4 | 5 | /** 6 | * A hook is a type of weave mod intrinsic that allows modders to run code before the actual main method, similar 7 | * to a premain method. Implementors are expected to have a no-args public constructor 8 | */ 9 | public interface Tweaker { 10 | /** 11 | * This function will be called in premain() 12 | */ 13 | public fun tweak(inst: Instrumentation) 14 | } -------------------------------------------------------------------------------- /loader/src/main/kotlin/net/weavemc/loader/api/bytecode/InsnDslExtras.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.loader.api.bytecode 2 | 3 | import net.weavemc.loader.api.event.Event 4 | import net.weavemc.loader.api.event.EventBus 5 | import net.weavemc.internals.InsnBuilder 6 | import net.weavemc.internals.internalNameOf 7 | 8 | /** 9 | * Pops an [Event] off the stack and posts the [Event] to the [EventBus] 10 | */ 11 | public fun InsnBuilder.postEvent() { 12 | invokestatic( 13 | internalNameOf(), 14 | "postEvent", 15 | "(L${internalNameOf()};)V" 16 | ) 17 | } -------------------------------------------------------------------------------- /loader/src/main/kotlin/net/weavemc/loader/api/event/Event.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.loader.api.event 2 | 3 | /** 4 | * This is the base class for all events provided by the Weave Loader. 5 | */ 6 | public abstract class Event 7 | 8 | /** 9 | * This is the base class for all *cancellable* events provided by the 10 | * Weave Loader, extending [Event]. 11 | */ 12 | public abstract class CancellableEvent : Event() { 13 | /** 14 | * This field defines whether the event is cancelled or not. Any mod can cancel and 15 | * un-cancel an event. What an event does when cancelled is event-specific, and noted in 16 | * that event's documentation. 17 | */ 18 | @get:JvmName("isCancelled") 19 | public var cancelled: Boolean = false 20 | } 21 | -------------------------------------------------------------------------------- /loader/src/main/kotlin/net/weavemc/loader/api/event/EventBus.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.loader.api.event 2 | 3 | import java.lang.reflect.Method 4 | import java.util.concurrent.ConcurrentHashMap 5 | import java.util.concurrent.CopyOnWriteArrayList 6 | import java.util.function.Consumer 7 | 8 | /** 9 | * The event bus handles events and event listeners. 10 | * 11 | * @see Event 12 | * 13 | * @see SubscribeEvent 14 | */ 15 | @Suppress("unused") 16 | public object EventBus { 17 | private val map: MutableMap, MutableList>> = ConcurrentHashMap() 18 | 19 | /** 20 | * Subscribe an object to the event bus, turning methods defined in it into listeners using @SubscribeEvent. 21 | * 22 | * @param obj The object to subscribe. 23 | * @see SubscribeEvent 24 | */ 25 | @JvmStatic 26 | public fun subscribe(obj: Any) { 27 | obj.javaClass.declaredMethods 28 | .filter { it.isAnnotationPresent(SubscribeEvent::class.java) && it.parameterCount == 1 } 29 | .forEach { getListeners(it.parameterTypes.first()) += ReflectEventConsumer(obj, it) } 30 | } 31 | 32 | /** 33 | * Subscribe a listener to the event bus. 34 | * 35 | * @param event The class of the event to subscribe to. 36 | * @param handler The Consumer to handle that event. 37 | */ 38 | @JvmStatic 39 | public fun subscribe(event: Class, handler: Consumer) { 40 | getListeners(event) += handler 41 | } 42 | 43 | /** 44 | * Post an event for all the listeners listening for it. 45 | * 46 | * @param event The event to call. 47 | */ 48 | @JvmStatic 49 | public fun postEvent(event: T) { 50 | var curr: Class<*> = event.javaClass 51 | 52 | while (curr != Any::class.java) { 53 | getListeners(curr).filterIsInstance>().forEach(Consumer { it.accept(event) }) 54 | curr = curr.superclass 55 | } 56 | } 57 | 58 | /** 59 | * Unsubscribe a listener from the event bus. 60 | * 61 | * @param consumer The Consumer to unsubscribe. 62 | */ 63 | @JvmStatic 64 | public fun unsubscribe(consumer: Consumer) { 65 | map.values.forEach { it.removeIf { c -> c === consumer } } 66 | } 67 | 68 | /** 69 | * Unsubscribe an object from the event bus, which unsubscribes all of its listeners. 70 | * 71 | * @param obj The object to unsubscribe. 72 | */ 73 | @JvmStatic 74 | public fun unsubscribe(obj: Any) { 75 | map.values.forEach { it.removeIf { c -> c is ReflectEventConsumer && c.obj === obj } } 76 | } 77 | 78 | /** 79 | * Returns a list of listeners alongside its event class. 80 | * 81 | * @param event The corresponding event class to grab the listeners from. 82 | * @return a list of listeners corresponding to the event class. 83 | */ 84 | private fun getListeners(event: Class<*>) = map.computeIfAbsent(event) { CopyOnWriteArrayList() } 85 | 86 | private class ReflectEventConsumer(val obj: Any, val method: Method) : Consumer { 87 | override fun accept(event: Event?) { 88 | method.invoke(obj, event) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /loader/src/main/kotlin/net/weavemc/loader/api/event/SubscribeEvent.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.loader.api.event 2 | 3 | /** 4 | * Annotate methods with this to make them event listeners. 5 | * To work, methods annotated with this annotation must have 1 parameter, 6 | * that parameter being the event type they listen for. 7 | * 8 | * @see Event 9 | */ 10 | @Retention(AnnotationRetention.RUNTIME) 11 | @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) 12 | public annotation class SubscribeEvent 13 | -------------------------------------------------------------------------------- /loader/src/main/kotlin/net/weavemc/loader/impl/InjectionHandler.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.loader.impl 2 | 3 | import com.grappenmaker.mappings.LambdaAwareRemapper 4 | import me.xtrm.klog.dsl.klog 5 | import com.grappenmaker.mappings.remap 6 | import net.weavemc.loader.api.Hook 7 | import net.weavemc.internals.dump 8 | import net.weavemc.loader.impl.bootstrap.transformer.SafeTransformer 9 | import net.weavemc.loader.impl.util.* 10 | import net.weavemc.loader.impl.util.asClassNode 11 | import net.weavemc.loader.impl.util.asClassReader 12 | import org.objectweb.asm.ClassReader 13 | import org.objectweb.asm.ClassWriter 14 | import org.objectweb.asm.Opcodes 15 | import org.objectweb.asm.commons.SimpleRemapper 16 | import org.objectweb.asm.tree.ClassNode 17 | import kotlin.io.path.createDirectories 18 | import kotlin.random.Random 19 | import kotlin.random.nextUInt 20 | 21 | public object InjectionHandler : SafeTransformer { 22 | /** 23 | * JVM argument to dump bytecode to disk. Can be enabled by adding 24 | * `-DdumpBytecode=true` to your JVM arguments when launching with Weave. 25 | * 26 | * Defaults to `false`. 27 | */ 28 | public val dumpBytecode: Boolean = System.getProperty("dumpBytecode")?.toBoolean() ?: false 29 | 30 | private val modifiers = mutableListOf() 31 | 32 | public fun registerModifier(modifier: Modifier) { 33 | modifiers += modifier 34 | } 35 | 36 | private fun ClassNode.remap(from: String, to: String) { 37 | if (from != to) remap(MappingsHandler.mapper(from, to)) 38 | } 39 | 40 | override fun transform( 41 | loader: ClassLoader?, 42 | className: String, 43 | originalClass: ByteArray 44 | ): ByteArray? { 45 | val groupedModifiers = modifiers.filter { className in it.targets }.groupBy { it.namespace } 46 | if (groupedModifiers.isEmpty()) return null 47 | 48 | with(MappingsHandler) { 49 | val classReader = originalClass.asClassReader() 50 | val node = classReader.asClassNode() 51 | 52 | // Hack: temporarily preserve already @MixinMerged methods, such that collisions will never occur 53 | val potentialConflicts = node.methods.filter { it.hasMixinAnnotation("MixinMerged") } 54 | val conflictsMapping = hashMapOf() 55 | val inverseConflictsMapping = hashMapOf() 56 | 57 | for (m in potentialConflicts) { 58 | val tempName = "potentialConflict${Random.nextUInt()}" 59 | conflictsMapping["${node.name}.${m.name}${m.desc}"] = tempName 60 | inverseConflictsMapping["${node.name}.${tempName}${m.desc}"] = m.name 61 | } 62 | 63 | // Hack: SimpleRemapper.map() can return null, and that breaks remap() 64 | node.remap(object : SimpleRemapper(conflictsMapping) { 65 | override fun map(key: String): String { 66 | return super.map(key) ?: key.run { 67 | // for an unknown reason, `key` isn't just internal name only 68 | // for example, . is sometimes passed to this method 69 | 70 | if (contains('.')) { 71 | if (contains('(')) substringAfter('.').substringBefore('(') 72 | else substringAfter('.') 73 | } else this 74 | } 75 | } 76 | }) 77 | 78 | val hookConfig = AssemblerConfigImpl() 79 | val modNs = groupedModifiers.keys 80 | 81 | // first FROM env namespace to all other namespaces to apply modifiers, 82 | // then finally remap to env namespace and apply its modifiers, instantly done. 83 | val nsOrder = (listOf(environmentNamespace) + (modNs - environmentNamespace)) + environmentNamespace 84 | 85 | nsOrder.windowed(2).forEach { (last, curr) -> 86 | node.remap(last, curr) 87 | groupedModifiers[curr]?.forEach { it.apply(node, hookConfig) } 88 | } 89 | 90 | val classWriter = InjectionClassWriter(hookConfig.classWriterFlags, classReader) 91 | node.accept(LambdaAwareRemapper(classWriter, SimpleRemapper(inverseConflictsMapping))) 92 | 93 | if (dumpBytecode) { 94 | val bytecodeOut = FileManager.DUMP_DIRECTORY.resolve("$className.class") 95 | .also { it.parent.createDirectories() }.toFile() 96 | 97 | runCatching { 98 | classWriter.toByteArray().dump(bytecodeOut.absolutePath) 99 | }.onFailure { klog.error("Failed to dump bytecode for $bytecodeOut", it) } 100 | } 101 | 102 | return classWriter.toByteArray() 103 | } 104 | } 105 | } 106 | 107 | public interface Modifier { 108 | public val namespace: String 109 | public val targets: Set 110 | public fun apply(node: ClassNode, cfg: Hook.AssemblerConfig) 111 | } 112 | 113 | /** 114 | * @param hook Hook class 115 | */ 116 | public data class ModHook( 117 | override val namespace: String, 118 | val hook: Hook, 119 | // TODO: jank 120 | override val targets: Set = hook.targets.mapTo(hashSetOf()) { 121 | MappingsHandler.mapper(namespace, MappingsHandler.environmentNamespace).map(it) 122 | } 123 | ): Modifier { 124 | override fun apply(node: ClassNode, cfg: Hook.AssemblerConfig): Unit = hook.transform(node, cfg) 125 | } 126 | 127 | public class AssemblerConfigImpl : Hook.AssemblerConfig { 128 | public var computeFrames: Boolean = false 129 | 130 | override fun computeFrames() { 131 | computeFrames = true 132 | } 133 | 134 | public val classWriterFlags: Int 135 | get() = if (computeFrames) ClassWriter.COMPUTE_FRAMES else ClassWriter.COMPUTE_MAXS 136 | } 137 | 138 | private class InjectionClassWriter( 139 | flags: Int, 140 | reader: ClassReader? = null, 141 | ) : ClassWriter(reader, flags) { 142 | val bytesProvider = MappingsHandler.classLoaderBytesProvider(MappingsHandler.environmentNamespace) 143 | 144 | private fun ClassNode.isInterface(): Boolean = (this.access and Opcodes.ACC_INTERFACE) != 0 145 | private fun ClassReader.isAssignableFrom(target: ClassReader): Boolean { 146 | val classes = ArrayDeque(listOf(target)) 147 | 148 | while (classes.isNotEmpty()) { 149 | val cl = classes.removeFirst() 150 | if (cl.className == className) return true 151 | 152 | classes.addAll( 153 | (listOfNotNull(cl.superName) + cl.interfaces).map { ClassReader(bytesProvider(it)) } 154 | ) 155 | } 156 | 157 | return false 158 | } 159 | 160 | override fun getCommonSuperClass(type1: String, type2: String): String { 161 | var class1 = bytesProvider(type1)?.asClassReader() ?: error("Failed to find type1 $type1") 162 | val class2 = bytesProvider(type2)?.asClassReader() ?: error("Failed to find type2 $type2") 163 | 164 | return when { 165 | class1.isAssignableFrom(class2) -> type1 166 | class2.isAssignableFrom(class1) -> type2 167 | class1.asClassNode().isInterface() || class2.asClassNode().isInterface() -> "java/lang/Object" 168 | else -> { 169 | while (!class1.isAssignableFrom(class2)) 170 | class1 = bytesProvider(class1.superName)!!.asClassReader() 171 | 172 | return class1.className 173 | } 174 | } 175 | } 176 | } -------------------------------------------------------------------------------- /loader/src/main/kotlin/net/weavemc/loader/impl/WeaveLoader.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.loader.impl 2 | 3 | import com.grappenmaker.mappings.* 4 | import me.xtrm.klog.Logger 5 | import net.weavemc.loader.api.Hook 6 | import net.weavemc.loader.api.ModInitializer 7 | import net.weavemc.internals.GameInfo 8 | import net.weavemc.internals.ModConfig 9 | import net.weavemc.loader.impl.bootstrap.PublicButInternal 10 | import net.weavemc.loader.impl.bootstrap.transformer.URLClassLoaderAccessor 11 | import net.weavemc.loader.impl.util.* 12 | import net.weavemc.loader.impl.util.fatalError 13 | import net.weavemc.loader.impl.util.launchStart 14 | import net.weavemc.loader.impl.util.updateLaunchTimes 15 | import net.weavemc.loader.impl.mixin.SandboxedMixinLoader 16 | import org.objectweb.asm.tree.ClassNode 17 | import java.io.File 18 | import java.lang.instrument.Instrumentation 19 | import java.util.jar.JarFile 20 | 21 | /** 22 | * The main class of the Weave Loader. 23 | */ 24 | public class WeaveLoader( 25 | private val classLoader: URLClassLoaderAccessor, 26 | private val instrumentation: Instrumentation, 27 | private val mappedModJars: List 28 | ) { 29 | private val logger = Logger(WeaveLoader::class.java.name) 30 | 31 | /** 32 | * Stored list of [WeaveMod]s. 33 | * 34 | * @see ModConfig 35 | */ 36 | private val mods = mutableListOf( 37 | // Fake base game mod so dependencies can be made on them 38 | // TODO: proper versioned dependencies, eg. optional, version greater/less than... 39 | WeaveMod( 40 | modId = "minecraft", config = ModConfig( 41 | name = "Minecraft", 42 | modId = "minecraft", 43 | entryPoints = emptyList(), 44 | mixinConfigs = emptyList(), 45 | hooks = emptyList(), 46 | tweakers = emptyList(), 47 | namespace = MappingsHandler.environmentNamespace, 48 | dependencies = emptyList(), 49 | compiledFor = GameInfo.version.versionName 50 | ) 51 | ) 52 | ) 53 | 54 | private val mixinInstances = mutableMapOf() 55 | 56 | public companion object { 57 | private var INSTANCE: WeaveLoader? = null 58 | 59 | @JvmStatic 60 | public fun getInstance(): WeaveLoader = INSTANCE 61 | ?: fatalError("Attempted to retrieve WeaveLoader instance before it has been instantiated!") 62 | } 63 | 64 | init { 65 | logger.info("Initializing Weave") 66 | 67 | INSTANCE = this 68 | launchStart = System.currentTimeMillis() 69 | instrumentation.addTransformer(InjectionHandler) 70 | 71 | finalize() 72 | } 73 | 74 | private fun finalize() { 75 | logger.trace("Finalizing Weave loading...") 76 | mappedModJars.forEach { it.registerAsMod() } 77 | logger.trace("Verifying dependencies") 78 | verifyDependencies() 79 | logger.trace("Populating mixin modifiers") 80 | populateMixinModifiers() 81 | logger.trace("Setting up access wideners") 82 | setupAccessWideners() 83 | 84 | logger.trace("Calling preInit() for mods") 85 | // TODO remove 86 | // Invoke preInit() once everything is done. 87 | mods.forEach { weaveMod -> 88 | weaveMod.config.entryPoints.forEach { entrypoint -> 89 | runCatching { 90 | logger.debug("Calling $entrypoint#preInit") 91 | instantiate(entrypoint) 92 | }.onFailure { 93 | logger.error("Failed to instantiate $entrypoint#preInit", it) 94 | }.onSuccess { 95 | runCatching { 96 | @Suppress("DEPRECATION") 97 | it.preInit(instrumentation) 98 | }.onFailure { 99 | logger.error("Exception thrown when invoking $entrypoint#preInit", it) 100 | } 101 | } 102 | } 103 | } 104 | 105 | logger.info("Weave initialized in ${System.currentTimeMillis() - launchStart}ms") 106 | updateLaunchTimes() 107 | } 108 | 109 | /** 110 | * Invokes Weave Mods' init. @see net.weavemc.api.ModInitializer 111 | * Invoked at the head of Minecraft's main method. 112 | * 113 | * @see [net.weavemc.loader.impl.bootstrap.transformer.ModInitializerHook] 114 | */ 115 | @Suppress("unused") 116 | @PublicButInternal 117 | public fun initializeMods() { 118 | mods.forEach { weaveMod -> 119 | weaveMod.config.entryPoints.forEach { entrypoint -> 120 | runCatching { 121 | instantiate(entrypoint) 122 | }.onFailure { 123 | logger.error("Failed to instantiate $entrypoint#init", it) 124 | }.onSuccess { 125 | runCatching { 126 | it.init() 127 | }.onFailure { 128 | logger.error("Exception thrown when invoking $entrypoint#init", it) 129 | } 130 | } 131 | } 132 | } 133 | } 134 | 135 | private fun verifyDependencies() { 136 | val duplicates = mods.groupingBy { it.modId }.eachCount().filterValues { it > 1 }.keys 137 | if (duplicates.isNotEmpty()) fatalError("Duplicate mods ${duplicates.joinToString()} were found") 138 | 139 | val dependencyGraph = mods.associate { it.modId to it.config.dependencies } 140 | val seen = hashSetOf() 141 | dependencyGraph.keys.forEach { toDetermine -> 142 | // soFar = List to keep order 143 | // Supposedly the list will be small enough such that a linear search is efficient enough 144 | fun verify(curr: String, soFar: List) { 145 | if (curr in soFar) fatalError( 146 | "Circular dependency: $toDetermine's dependency graph eventually " + 147 | "meets $curr again through ${(soFar + curr).joinToString(" -> ")}" 148 | ) 149 | 150 | if (!seen.add(curr)) return 151 | val deps = dependencyGraph[curr] ?: fatalError("Dependency $curr for mod $toDetermine is not available") 152 | deps.forEach { verify(it, soFar + curr) } 153 | } 154 | 155 | verify(toDetermine, emptyList()) 156 | } 157 | } 158 | 159 | private fun mixinForNamespace(namespace: String) = mixinInstances.getOrPut(namespace) { 160 | logger.debug("Creating a new SandboxedMixinLoader for namespace $namespace") 161 | val parent = classLoader.weaveBacking 162 | SandboxedMixinLoader( 163 | parent = parent, 164 | loader = ClasspathLoaders.fromLoader(parent) 165 | .remappingNames(MappingsHandler.mergedMappings.mappings, "official", namespace), 166 | ).apply { state.initialize() } 167 | } 168 | 169 | private fun populateMixinModifiers() { 170 | for (ns in MappingsHandler.mergedMappings.mappings.namespaces) { 171 | val state = mixinForNamespace(ns).state 172 | val targets = state.findTargets(state.transformer) 173 | if (targets.isEmpty()) continue 174 | 175 | val mapper = MappingsHandler.mapper(ns, MappingsHandler.environmentNamespace) 176 | InjectionHandler.registerModifier(object : Modifier { 177 | override val namespace = ns 178 | override val targets = targets.mapTo(hashSetOf()) { mapper.map(it.replace('.', '/')) } 179 | override fun apply(node: ClassNode, cfg: Hook.AssemblerConfig) { 180 | cfg.computeFrames() 181 | state.transform(node.name, node) 182 | } 183 | }) 184 | } 185 | } 186 | 187 | private fun setupAccessWideners() { 188 | val tree = mods.asSequence().flatMap { it.config.accessWideners }.mapNotNull { aw -> 189 | val res = javaClass.classLoader.getResourceAsStream(aw) ?: return@mapNotNull let { 190 | println("[Weave] Could not load access widener configuration $aw") 191 | null 192 | } 193 | 194 | loadAccessWidener(res.readBytes().decodeToString().trim().lines()) 195 | .remap(MappingsHandler.mergedMappings.mappings, MappingsHandler.environmentNamespace) 196 | }.reduceOrNull { acc, curr -> acc + curr }?.toTree() ?: return 197 | 198 | InjectionHandler.registerModifier(object : Modifier { 199 | override val namespace = MappingsHandler.environmentNamespace 200 | override val targets = tree.classes.mapTo(hashSetOf()) { it.key } 201 | 202 | override fun apply(node: ClassNode, cfg: Hook.AssemblerConfig) = node.applyWidener(tree) 203 | }) 204 | } 205 | 206 | /** 207 | * Registers mod's hooks and mixins then adds to mods list for later instantiation 208 | */ 209 | private fun File.registerAsMod() { 210 | logger.trace("Registering mod $name") 211 | classLoader.addWeaveURL(this.toURI().toURL()) 212 | 213 | JarFile(this).use { jar -> 214 | val config = jar.configOrFatal() 215 | val modId = config.modId 216 | 217 | // Added a backup classloader search for precautionary measures 218 | instrumentation.appendToSystemClassLoaderSearch(jar) 219 | 220 | config.hooks.forEach { hook -> 221 | logger.trace("Registering hook $hook") 222 | InjectionHandler.registerModifier(ModHook(config.namespace, instantiate(hook))) 223 | } 224 | 225 | val state = mixinForNamespace(config.namespace).state 226 | config.mixinConfigs.forEach { state.registerMixin(modId, it) } 227 | 228 | mods += WeaveMod(modId, config) 229 | logger.trace("Registered mod $name") 230 | } 231 | } 232 | } -------------------------------------------------------------------------------- /loader/src/main/kotlin/net/weavemc/loader/impl/bootstrap/Agent.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.loader.impl.bootstrap 2 | 3 | import me.xtrm.klog.Level 4 | import me.xtrm.klog.dsl.klog 5 | import me.xtrm.klog.dsl.klogConfig 6 | import net.weavemc.loader.api.Tweaker 7 | import net.weavemc.internals.GameInfo 8 | import net.weavemc.internals.MinecraftVersion 9 | import net.weavemc.internals.ModConfig 10 | import net.weavemc.loader.impl.WeaveLoader 11 | import net.weavemc.loader.impl.bootstrap.transformer.ArgumentSanitizer 12 | import net.weavemc.loader.impl.bootstrap.transformer.ModInitializerHook 13 | import net.weavemc.loader.impl.bootstrap.transformer.URLClassLoaderTransformer 14 | import net.weavemc.loader.impl.util.* 15 | import net.weavemc.loader.impl.util.JSON 16 | import net.weavemc.loader.impl.util.fatalError 17 | import net.weavemc.loader.impl.util.setGameInfo 18 | import java.awt.GraphicsEnvironment 19 | import java.io.File 20 | import java.lang.instrument.Instrumentation 21 | import java.net.URL 22 | import java.net.URLClassLoader 23 | import java.util.jar.JarFile 24 | 25 | private val logger by klog 26 | 27 | /** 28 | * The JavaAgent's `premain()` method, this is where initialization of Weave Loader begins. 29 | * Weave Loader's initialization begins by instantiating [WeaveLoader] 30 | */ 31 | @Suppress("UNUSED_PARAMETER") 32 | public fun premain(opt: String?, inst: Instrumentation) { 33 | klogConfig { 34 | defaultLevel = Level.INFO 35 | appenders = mutableListOf(WeaveLogAppender) 36 | } 37 | 38 | logger.info("Attached Weave") 39 | 40 | setGameInfo() 41 | 42 | val mods = retrieveMods() 43 | callTweakers(inst, mods) 44 | 45 | inst.addTransformer(URLClassLoaderTransformer) 46 | inst.addTransformer(ModInitializerHook) 47 | 48 | inst.addTransformer(ArgumentSanitizer, true) 49 | inst.retransformClasses(Class.forName("sun.management.RuntimeImpl", false, ClassLoader.getSystemClassLoader())) 50 | inst.removeTransformer(ArgumentSanitizer) 51 | 52 | // Prevent ichor prebake 53 | System.setProperty("ichor.prebakeClasses", "false") 54 | 55 | // Hack: sometimes the state is improperly initialized, which causes Swing to feel like it is headless? 56 | // Calling this solves the problem 57 | GraphicsEnvironment.isHeadless() 58 | 59 | // initialize bootstrap 60 | Bootstrap.bootstrap(inst, mods) 61 | } 62 | 63 | private class TweakerClassLoader(urls: List) : URLClassLoader( 64 | /* urls = */ urls.toTypedArray(), 65 | /* parent = */ ClassLoader.getSystemClassLoader() 66 | ) 67 | 68 | private fun callTweakers(inst: Instrumentation, mods: List) { 69 | logger.info("Calling tweakers") 70 | 71 | val tweakers = mods 72 | .mapNotNull { runCatching { JarFile(it).use { it.fetchModConfig(JSON) } }.getOrNull() } 73 | .flatMap(ModConfig::tweakers) 74 | 75 | // TODO: could tweakers have dependencies on other mods? 76 | val loader = TweakerClassLoader(mods.map { it.toURI().toURL() }) 77 | 78 | for (tweaker in tweakers) { 79 | logger.trace("Calling tweaker: $tweaker") 80 | instantiate(tweaker, loader).tweak(inst) 81 | } 82 | } 83 | 84 | private fun FileManager.ModJar.parseAndMap(): File { 85 | val fileName = file.name.substringBeforeLast('.') 86 | 87 | return JarFile(file).use { 88 | val config = it.configOrFatal() 89 | val compiledFor = config.compiledFor 90 | 91 | if (compiledFor != null && GameInfo.version != MinecraftVersion.fromVersionName(compiledFor)) { 92 | val extra = if (!isSpecific) { 93 | " Hint: this mod was placed in the general mods folder. Consider putting mods in a version-specific mods folder" 94 | } else "" 95 | 96 | fatalError( 97 | "Mod ${config.modId} was compiled for version $compiledFor, current version is ${GameInfo.version.versionName}.$extra" 98 | ) 99 | } 100 | 101 | if (!MappingsHandler.isNamespaceAvailable(config.namespace)) { 102 | fatalError("Mod ${config.modId} was mapped in namespace ${config.namespace}, which is not available!") 103 | } 104 | 105 | file.createRemappedTemp(fileName, config) 106 | } 107 | } 108 | 109 | private fun retrieveMods() = FileManager.getMods().map { it.parseAndMap() } 110 | 111 | public fun main() { 112 | fatalError("This is not how you use Weave! Please refer to the readme for instructions.") 113 | } 114 | 115 | @Retention 116 | @RequiresOptIn("This member is public but internal Weave API! Do not use.") 117 | public annotation class PublicButInternal 118 | -------------------------------------------------------------------------------- /loader/src/main/kotlin/net/weavemc/loader/impl/bootstrap/Bootstrap.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.loader.impl.bootstrap 2 | 3 | import me.xtrm.klog.dsl.klog 4 | import net.weavemc.internals.GameInfo 5 | import net.weavemc.loader.impl.bootstrap.transformer.ApplicationWrapper 6 | import net.weavemc.loader.impl.bootstrap.transformer.SafeTransformer 7 | import net.weavemc.loader.impl.bootstrap.transformer.URLClassLoaderAccessor 8 | import net.weavemc.loader.impl.bootstrap.transformer.URLClassLoaderTransformer 9 | import net.weavemc.loader.impl.util.exit 10 | import net.weavemc.loader.impl.util.fatalError 11 | import java.io.File 12 | import java.lang.instrument.Instrumentation 13 | 14 | internal object Bootstrap { 15 | private val logger by klog 16 | 17 | fun bootstrap(inst: Instrumentation, mods: List) = inst.addTransformer(object: SafeTransformer { 18 | override fun transform(loader: ClassLoader?, className: String, originalClass: ByteArray): ByteArray? { 19 | if (className != "net/minecraft/client/main/Main") return null 20 | if (loader == ClassLoader.getSystemClassLoader()) 21 | return ApplicationWrapper.insertWrapper(className, originalClass) 22 | 23 | printBootstrap(loader) 24 | 25 | // remove bootstrap transformers 26 | inst.removeTransformer(this) 27 | inst.removeTransformer(URLClassLoaderTransformer) 28 | 29 | val clAccessor = if (loader is URLClassLoaderAccessor) loader 30 | else fatalError("Failed to transform URLClassLoader to implement URLClassLoaderAccessor. Impossible to recover") 31 | 32 | runCatching { 33 | clAccessor.addWeaveURL(javaClass.protectionDomain.codeSource.location) 34 | }.onFailure { 35 | it.printStackTrace() 36 | fatalError("Failed to deliberately add Weave to the target classloader") 37 | } 38 | 39 | logger.info("Bootstrapping complete, initializing loader...") 40 | 41 | runCatching { 42 | loader.loadClass("net.weavemc.loader.impl.WeaveLoader").getConstructor( 43 | URLClassLoaderAccessor::class.java, 44 | Instrumentation::class.java, 45 | java.util.List::class.java 46 | ).newInstance(clAccessor, inst, mods) 47 | }.onFailure { 48 | logger.fatal("Failed to instantiate WeaveLoader", it) 49 | exit(-1) 50 | } 51 | 52 | return null 53 | } 54 | }) 55 | 56 | private fun printBootstrap(loader: ClassLoader?) { 57 | logger.info("Bootstrapping Weave Loader...") 58 | logger.debug(" - Version: ${GameInfo.version.versionName}") 59 | logger.debug(" - Client: ${GameInfo.client.clientName}") 60 | logger.debug(" - Loader: $loader") 61 | } 62 | } -------------------------------------------------------------------------------- /loader/src/main/kotlin/net/weavemc/loader/impl/bootstrap/WeaveLogAppender.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.loader.impl.bootstrap 2 | 3 | import me.xtrm.klog.Appender 4 | import me.xtrm.klog.LogContext 5 | import me.xtrm.klog.dsl.klog 6 | import net.weavemc.loader.impl.util.getOrCreateDirectory 7 | import java.io.FileOutputStream 8 | import java.io.PrintStream 9 | import java.nio.file.Files 10 | import java.time.LocalDateTime 11 | import java.time.format.DateTimeFormatter 12 | import kotlin.io.path.deleteIfExists 13 | 14 | /** 15 | * We have to do a bit of gymnastics to ensure we don't reinitialize our logfile, create two logfiles, 16 | * or overwrite stuff we don't want to. 17 | * 18 | * Since this class is loaded on two different classloaders, we need to pass it some state (here `initialized`) 19 | * to determine whether to start a new logging session or to continue an existing one. 20 | */ 21 | internal object WeaveLogAppender : Appender { 22 | private val newline = System.lineSeparator() 23 | private val formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy_HH-mm-ss") 24 | private val logDir = getOrCreateDirectory("logs") 25 | private val logFile = logDir.resolve("weave-loader-${LocalDateTime.now().format(formatter)}.log") 26 | 27 | private val stdoutStream = WrappingStream(System.out) 28 | private val stderrStream = WrappingStream(System.err) 29 | private val logFileStream = PrintStream(FileOutputStream(logFile.toFile(), true), true) 30 | 31 | private val logger by klog 32 | 33 | init { 34 | symlinkLatest() 35 | } 36 | 37 | override fun append(context: LogContext, finalMessage: String) { 38 | stdoutStream.println(finalMessage) 39 | stdoutStream.flush() 40 | logFileStream.println(finalMessage) 41 | logFileStream.flush() 42 | 43 | val args = context.args 44 | if (args.isNotEmpty()) { 45 | val last = args.last() 46 | if (last is Throwable) { 47 | last.printStackTrace(stderrStream) 48 | last.printStackTrace(logFileStream) 49 | } 50 | } 51 | } 52 | 53 | private fun symlinkLatest() { 54 | val latest = logDir.resolve("latest.log") 55 | latest.deleteIfExists() 56 | runCatching { 57 | Files.createLink(latest, logFile) 58 | }.onFailure { e1 -> 59 | runCatching { 60 | Files.createSymbolicLink(latest, logFile) 61 | }.onFailure { e2 -> 62 | e1.addSuppressed(e2) 63 | logger.error("Failed to create (sym)link to latest log", e1) 64 | } 65 | } 66 | } 67 | 68 | internal class WrappingStream(private val stream: PrintStream) : PrintStream(stream) { 69 | override fun println(x: Any?) { 70 | // Use print with a manual newline to prevent Legacy Forge from redirecting to log4j 71 | stream.print(x.toString() + newline) 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /loader/src/main/kotlin/net/weavemc/loader/impl/bootstrap/transformer/ApplicationWrapper.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.loader.impl.bootstrap.transformer 2 | 3 | import me.xtrm.klog.dsl.klog 4 | import net.weavemc.internals.asm 5 | import net.weavemc.loader.impl.bootstrap.PublicButInternal 6 | import net.weavemc.loader.impl.mixin.LoaderClassWriter 7 | import net.weavemc.loader.impl.util.asClassNode 8 | import net.weavemc.loader.impl.util.asClassReader 9 | import net.weavemc.loader.impl.util.fatalError 10 | import net.weavemc.loader.impl.util.illegalToReload 11 | import org.objectweb.asm.ClassWriter 12 | import org.objectweb.asm.Type 13 | import org.objectweb.asm.tree.LabelNode 14 | import java.lang.invoke.MethodHandles 15 | import java.lang.invoke.MethodType 16 | import java.lang.invoke.WrongMethodTypeException 17 | import java.net.URLClassLoader 18 | 19 | // Makes sure to run the application within some notion of a "custom" ClassLoader, 20 | // such that signing integrity errors will not occur 21 | @PublicButInternal 22 | public object ApplicationWrapper { 23 | internal fun insertWrapper(className: String, originalClass: ByteArray): ByteArray { 24 | val reader = originalClass.asClassReader() 25 | val node = reader.asClassNode() 26 | 27 | val methodNode = node.methods.find { it.name == "main" } 28 | ?: fatalError("Failed to find the main method in $className whilst inserting wrapper") 29 | 30 | methodNode.instructions.insert(asm { 31 | ldc(Type.getObjectType(node.name)) 32 | invokevirtual("java/lang/Class", "getClassLoader", "()Ljava/lang/ClassLoader;") 33 | invokestatic("java/lang/ClassLoader", "getSystemClassLoader", "()Ljava/lang/ClassLoader;") 34 | 35 | val label = LabelNode() 36 | if_acmpne(label) 37 | 38 | ldc(node.name.replace('/', '.')) 39 | aload(0) 40 | invokestatic( 41 | "net/weavemc/loader/impl/bootstrap/transformer/ApplicationWrapper", 42 | "wrap", 43 | "(Ljava/lang/String;[Ljava/lang/String;)V" 44 | ) 45 | 46 | // just in case 47 | _return 48 | 49 | +label 50 | 51 | ldc(className) 52 | 53 | ldc(Type.getObjectType(className)) 54 | invokevirtual("java/lang/Class", "getClassLoader", "()Ljava/lang/ClassLoader;") 55 | 56 | aload(0) 57 | invokestatic( 58 | "net/weavemc/loader/impl/bootstrap/BootstrapContainer", 59 | "finishBootstrap", 60 | "(Ljava/lang/String;Ljava/lang/ClassLoader;[Ljava/lang/String;)V" 61 | ) 62 | }) 63 | 64 | return LoaderClassWriter(ClassLoader.getSystemClassLoader(), reader, ClassWriter.COMPUTE_FRAMES) 65 | .also { node.accept(it) }.toByteArray() 66 | } 67 | 68 | @JvmStatic 69 | @Suppress("unused") 70 | public fun wrap(targetMain: String, args: Array) { 71 | val logger by klog 72 | logger.info("Minecraft Main was directly invoked, which potentially blocks transformation") 73 | logger.info( 74 | "This is normal to happen on Vanilla Minecraft pre-launchwrapper. " + 75 | "Therefore, the game will be wrapped into a new ClassLoader" 76 | ) 77 | 78 | val mainClass = WrappingLoader().loadClass(targetMain) 79 | 80 | try { 81 | val type = MethodType.methodType(Void::class.javaPrimitiveType, args::class.java) 82 | MethodHandles.lookup().findStatic(mainClass, "main", type).invokeExact(args) as Unit 83 | } catch (e: Throwable) { 84 | when (e) { 85 | is WrongMethodTypeException, 86 | is NoSuchMethodException, 87 | is IllegalAccessException, 88 | is ClassNotFoundException -> { 89 | // Some error occurred within reflective access 90 | e.printStackTrace() 91 | 92 | logger.warn("Failed to wrap game using java.lang.invoke, using Reflection fallback") 93 | mainClass.getMethod("main", args::class.java)(null, args) 94 | } 95 | 96 | else -> throw e 97 | } 98 | } 99 | } 100 | 101 | public class WrappingLoader : URLClassLoader(emptyArray(), getSystemClassLoader()) { 102 | override fun loadClass(name: String, resolve: Boolean): Class<*> = 103 | findClass(name).also { if (resolve) resolveClass(it) } 104 | 105 | override fun findClass(name: String): Class<*> { 106 | if (name == javaClass.name) return javaClass 107 | findLoadedClass(name)?.let { return it } 108 | 109 | if ( 110 | illegalToReload.any { name.startsWith(it) } || 111 | name == "net.weavemc.loader.impl.bootstrap.BootstrapContainer" 112 | ) return parent.loadClass(name) 113 | 114 | val internalName = name.replace('.', '/') 115 | val bytes = getResourceAsStream("$internalName.class")?.readBytes() ?: throw ClassNotFoundException() 116 | 117 | // bye-bye protectiondomain! 118 | // also we need a urlclassloader 119 | return defineClass(name, bytes, 0, bytes.size) 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /loader/src/main/kotlin/net/weavemc/loader/impl/bootstrap/transformer/ArgumentSanitizer.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.loader.impl.bootstrap.transformer 2 | 3 | import net.weavemc.internals.asm 4 | import org.objectweb.asm.ClassReader 5 | import org.objectweb.asm.ClassWriter 6 | import org.objectweb.asm.Opcodes 7 | import org.objectweb.asm.tree.ClassNode 8 | import org.objectweb.asm.tree.LabelNode 9 | 10 | internal object ArgumentSanitizer : SafeTransformer { 11 | override fun transform(loader: ClassLoader?, className: String, originalClass: ByteArray): ByteArray? { 12 | if (className != "sun/management/RuntimeImpl") return null 13 | 14 | val node = ClassNode().also { ClassReader(originalClass).accept(it, 0) } 15 | val writer = ClassWriter(ClassWriter.COMPUTE_FRAMES) 16 | 17 | with(node.methods.first { it.name == "getInputArguments" }) { 18 | val insn = instructions.first { it.opcode == Opcodes.ARETURN } 19 | instructions.insertBefore(insn, asm { 20 | invokeinterface("java/lang/Iterable", "iterator", "()Ljava/util/Iterator;") 21 | astore(2) 22 | new("java/util/ArrayList") 23 | dup 24 | invokespecial("java/util/ArrayList", "", "()V") 25 | astore(3) 26 | 27 | val loop = LabelNode() 28 | val end = LabelNode() 29 | 30 | +loop 31 | aload(2) 32 | invokeinterface("java/util/Iterator", "hasNext", "()Z") 33 | ifeq(end) 34 | 35 | aload(2) 36 | invokeinterface("java/util/Iterator", "next", "()Ljava/lang/Object;") 37 | checkcast("java/lang/String") 38 | dup 39 | astore(4) 40 | ldc("javaagent") 41 | invokevirtual("java/lang/String", "contains", "(Ljava/lang/CharSequence;)Z") 42 | ifne(loop) 43 | 44 | aload(3) 45 | aload(4) 46 | invokeinterface("java/util/List", "add", "(Ljava/lang/Object;)Z") 47 | pop 48 | 49 | goto(loop) 50 | +end 51 | 52 | aload(3) 53 | }) 54 | } 55 | 56 | node.accept(writer) 57 | 58 | return writer.toByteArray() 59 | } 60 | } -------------------------------------------------------------------------------- /loader/src/main/kotlin/net/weavemc/loader/impl/bootstrap/transformer/ClassLoaderTransformer.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.loader.impl.bootstrap.transformer 2 | 3 | import net.weavemc.internals.asm 4 | import net.weavemc.internals.internalNameOf 5 | import net.weavemc.internals.visitAsm 6 | import net.weavemc.loader.impl.bootstrap.PublicButInternal 7 | import net.weavemc.loader.impl.mixin.LoaderClassWriter 8 | import org.objectweb.asm.ClassReader 9 | import org.objectweb.asm.ClassWriter 10 | import org.objectweb.asm.Opcodes 11 | import org.objectweb.asm.tree.ClassNode 12 | import org.objectweb.asm.tree.LabelNode 13 | import java.net.URL 14 | import java.net.URLClassLoader 15 | 16 | @PublicButInternal 17 | public interface URLClassLoaderAccessor { 18 | public val weaveBacking: ClassLoader 19 | public fun addWeaveURL(url: URL) 20 | } 21 | 22 | @PublicButInternal 23 | public object URLClassLoaderTransformer : SafeTransformer { 24 | override fun transform(loader: ClassLoader?, className: String, originalClass: ByteArray): ByteArray? { 25 | if (loader == null) return null 26 | 27 | val reader = ClassReader(originalClass) 28 | if (reader.superName != internalNameOf()) return null 29 | 30 | val node = ClassNode() 31 | reader.accept(node, 0) 32 | 33 | node.interfaces.add(internalNameOf()) 34 | 35 | // public URLClassLoader getWeaveBacking() 36 | node.visitMethod(Opcodes.ACC_PUBLIC, "getWeaveBacking", "()Ljava/lang/ClassLoader;", null, null).visitAsm { 37 | aload(0) 38 | areturn 39 | } 40 | 41 | val defaultIgnoredPackages = listOf( 42 | "net.weavemc.loader.impl.bootstrap.", 43 | "me.xtrm.klog.", 44 | "kotlin." 45 | ) 46 | 47 | // private final List weave$ignoredPackages = new ArrayList<>(); 48 | node.visitField( 49 | Opcodes.ACC_PRIVATE or Opcodes.ACC_FINAL, 50 | "weave\$ignoredPackages", 51 | "Ljava/util/List;", 52 | null, 53 | null 54 | ) 55 | 56 | node.methods.filter { it.name == "" }.forEach { methodNode -> 57 | methodNode.instructions.insert(asm { 58 | aload(0) 59 | 60 | new("java/util/ArrayList") 61 | dup 62 | invokespecial("java/util/ArrayList", "", "()V") 63 | 64 | defaultIgnoredPackages.forEach { pkg -> 65 | dup 66 | ldc(pkg) 67 | invokeinterface("java/util/List", "add", "(Ljava/lang/Object;)Z") 68 | pop 69 | } 70 | 71 | putfield(node.name, "weave\$ignoredPackages", "Ljava/util/List;") 72 | }) 73 | } 74 | 75 | node.visitMethod( 76 | Opcodes.ACC_PRIVATE or Opcodes.ACC_FINAL, 77 | "weave\$shouldIgnore", 78 | "(Ljava/lang/String;)Z", 79 | null, 80 | null 81 | ).visitAsm { 82 | val loopStart = LabelNode() 83 | val loopEnd = LabelNode() 84 | 85 | // Iterator iter = weave$ignoredPackages.iterator(); 86 | aload(0) 87 | getfield(node.name, "weave\$ignoredPackages", "Ljava/util/List;") 88 | invokeinterface("java/util/List", "iterator", "()Ljava/util/Iterator;") 89 | astore(2) 90 | 91 | // while (iter.hasNext()) 92 | +loopStart 93 | aload(2) 94 | invokeinterface("java/util/Iterator", "hasNext", "()Z") 95 | ifeq(loopEnd) 96 | 97 | // if (iter.next().startsWith(pkg)) 98 | aload(1) 99 | aload(2) 100 | invokeinterface("java/util/Iterator", "next", "()Ljava/lang/Object;") 101 | checkcast("java/lang/String") 102 | invokevirtual("java/lang/String", "startsWith", "(Ljava/lang/String;)Z") 103 | ifeq(loopStart) 104 | 105 | iconst_1 106 | ireturn 107 | 108 | +loopEnd 109 | iconst_0 110 | ireturn 111 | } 112 | 113 | // public void addWeaveURL(URL url) 114 | node.visitMethod(Opcodes.ACC_PUBLIC, "addWeaveURL", "(Ljava/net/URL;)V", null, null).visitAsm { 115 | aload(0) 116 | aload(1) 117 | invokevirtual(node.name, "addURL", "(Ljava/net/URL;)V") 118 | _return 119 | } 120 | 121 | val loadClassInject = asm { 122 | val notIgnored = LabelNode() 123 | 124 | // if (weave$shouldIgnore(name)) 125 | aload(0) 126 | aload(1) 127 | invokespecial(node.name, "weave\$shouldIgnore", "(Ljava/lang/String;)Z") 128 | ifeq(notIgnored) 129 | 130 | // return ClassLoader.getSystemClassLoader().loadClass(name); 131 | invokestatic("java/lang/ClassLoader", "getSystemClassLoader", "()Ljava/lang/ClassLoader;") 132 | aload(1) 133 | invokevirtual("java/lang/ClassLoader", "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;") 134 | areturn 135 | 136 | +notIgnored 137 | } 138 | 139 | listOf("loadClass", "findClass") 140 | .mapNotNull { t -> node.methods.find { it.name == t } } 141 | .forEach { it.instructions.insert(loadClassInject) } 142 | 143 | return LoaderClassWriter(loader, reader, ClassWriter.COMPUTE_FRAMES).also { node.accept(it) }.toByteArray() 144 | } 145 | } -------------------------------------------------------------------------------- /loader/src/main/kotlin/net/weavemc/loader/impl/bootstrap/transformer/ModInitializerHook.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.loader.impl.bootstrap.transformer 2 | 3 | import net.weavemc.internals.asm 4 | import net.weavemc.internals.internalNameOf 5 | import net.weavemc.loader.impl.WeaveLoader 6 | import net.weavemc.loader.impl.mixin.LoaderClassWriter 7 | import net.weavemc.loader.impl.util.asClassNode 8 | import net.weavemc.loader.impl.util.fatalError 9 | import org.objectweb.asm.ClassReader 10 | import org.objectweb.asm.ClassWriter 11 | 12 | /** 13 | * Transformer meant to start the initialization phase for Weave Mods by hooking [net.minecraft.client.main.Main.main]. 14 | * 15 | * @see [net.weavemc.api.ModInitializer.init] 16 | */ 17 | internal object ModInitializerHook : SafeTransformer { 18 | override fun transform(loader: ClassLoader?, className: String, originalClass: ByteArray): ByteArray? { 19 | if (className != "net/minecraft/client/main/Main") return null 20 | 21 | val reader = ClassReader(originalClass) 22 | val node = reader.asClassNode() 23 | 24 | val main = node.methods.find { it.name == "main" } ?: fatalError("Failed to find main method in $className") 25 | main.instructions.insert(asm { 26 | invokestatic(internalNameOf(), "getInstance", "()L${internalNameOf()};") 27 | invokevirtual(internalNameOf(), "initializeMods", "()V") 28 | }) 29 | 30 | return LoaderClassWriter(loader, reader, ClassWriter.COMPUTE_MAXS).also { node.accept(it) }.toByteArray() 31 | } 32 | } -------------------------------------------------------------------------------- /loader/src/main/kotlin/net/weavemc/loader/impl/bootstrap/transformer/SafeTransformer.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.loader.impl.bootstrap.transformer 2 | 3 | import me.xtrm.klog.dsl.klog 4 | import net.weavemc.loader.impl.util.exit 5 | import org.objectweb.asm.ClassReader 6 | import java.lang.instrument.ClassFileTransformer 7 | import java.security.ProtectionDomain 8 | 9 | private val checkBytecode = java.lang.Boolean.getBoolean("weave.loader.checkTransformedBytecode") 10 | private val logger by klog 11 | 12 | internal interface SafeTransformer : ClassFileTransformer { 13 | /** 14 | * @param loader The ClassLoader responsible for loading the class, can be null if the loader is the Bootstrap Loader 15 | * @param className The name of the class 16 | * @param originalClass The class file bytes 17 | */ 18 | fun transform(loader: ClassLoader?, className: String, originalClass: ByteArray): ByteArray? 19 | 20 | override fun transform( 21 | loader: ClassLoader?, 22 | className: String, 23 | classBeingRedefined: Class<*>?, 24 | protectionDomain: ProtectionDomain?, 25 | classfileBuffer: ByteArray 26 | ) = runCatching { 27 | transform(loader, className, classfileBuffer) 28 | ?.also { if (checkBytecode) verifyBytes(className, classfileBuffer, it) } 29 | }.getOrElse { 30 | it.printStackTrace() 31 | logger.fatal("An error occurred while transforming {} (from {})", className, javaClass.name, it) 32 | exit(1) 33 | } 34 | 35 | private fun verifyBytes(className: String, original: ByteArray, transformed: ByteArray) { 36 | val reader = ClassReader(transformed) 37 | val transformedName = reader.className 38 | 39 | val originalVersion = (original[6].toInt() and 0xFF shl 8) or (original[7].toInt() and 0xFF) 40 | val transformedVersion = reader.readShort(6).toInt() 41 | 42 | check(transformedName == className) { "Class name mismatch: expected $className, got $transformedName" } 43 | check(transformedVersion <= originalVersion) { 44 | "Class version mismatch: expected $originalVersion or lower, got $transformedVersion" 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /loader/src/main/kotlin/net/weavemc/loader/impl/mixin/MixinSandbox.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.loader.impl.mixin 2 | 3 | import com.grappenmaker.mappings.ClasspathLoaders 4 | import me.xtrm.klog.dsl.klog 5 | import net.weavemc.internals.asm 6 | import net.weavemc.internals.internalNameOf 7 | import net.weavemc.internals.named 8 | import net.weavemc.loader.impl.util.asClassNode 9 | import net.weavemc.loader.impl.util.asClassReader 10 | import net.weavemc.loader.impl.util.illegalToReload 11 | import org.objectweb.asm.ClassReader 12 | import org.objectweb.asm.ClassWriter 13 | import org.objectweb.asm.Opcodes.ACC_INTERFACE 14 | import org.objectweb.asm.tree.ClassNode 15 | import org.objectweb.asm.tree.MethodInsnNode 16 | import org.spongepowered.asm.launch.MixinBootstrap 17 | import org.spongepowered.asm.mixin.MixinEnvironment 18 | import org.spongepowered.asm.mixin.MixinEnvironment.Phase 19 | import org.spongepowered.asm.mixin.Mixins 20 | import org.spongepowered.asm.mixin.extensibility.IMixinConfig 21 | import org.spongepowered.asm.mixin.transformer.IMixinTransformer 22 | import org.spongepowered.asm.service.MixinService 23 | import java.io.ByteArrayInputStream 24 | import java.io.File 25 | import java.io.InputStream 26 | import java.lang.instrument.ClassFileTransformer 27 | import java.net.URL 28 | import java.security.ProtectionDomain 29 | import java.util.* 30 | import kotlin.properties.ReadOnlyProperty 31 | import kotlin.reflect.KProperty 32 | 33 | private val logger by klog 34 | 35 | /** 36 | * Implements a [ClassFileTransformer] that passes all loaded classes through the wrapped [mixin] state 37 | */ 38 | public class SandboxedMixinTransformer( 39 | private val mixin: SandboxedMixinState 40 | ) : ClassFileTransformer { 41 | override fun transform( 42 | loader: ClassLoader?, 43 | className: String, 44 | classBeingRedefined: Class<*>?, 45 | protectionDomain: ProtectionDomain?, 46 | classfileBuffer: ByteArray 47 | ): ByteArray? = mixin.transformOrNull(className, classfileBuffer) 48 | } 49 | 50 | /** 51 | * A [ClassLoader] that is capable of "sandboxing" Spongepowered Mixin. The [parent] loader can be specified, 52 | * which will be used to load non-sandboxed classes. A [loader] can be specified, which is responsible for providing 53 | * class bytes by a given name (forward slashes are the package separator). 54 | */ 55 | public class SandboxedMixinLoader( 56 | private val parent: ClassLoader = getSystemClassLoader(), 57 | private val loader: (name: String) -> ByteArray? = ClasspathLoaders.fromLoader(parent), 58 | ) : ClassLoader(parent) { 59 | private val injectedResources = mutableMapOf() 60 | private val tempFiles = mutableMapOf() 61 | private val loaderExclusions = mutableSetOf("net.weavemc.loader.impl.mixin.MixinAccess") 62 | 63 | private val systemClasses = illegalToReload + setOf( 64 | "kotlin.", "kotlinx.", "org.objectweb.asm.", 65 | "me.xtrm.klog.", "net.weavemc.loader.impl.mixin.SandboxedMixinState" 66 | ) 67 | 68 | /** 69 | * Responsible for managing the state of the sandboxed mixin environment 70 | */ 71 | public val state: SandboxedMixinState = SandboxedMixinState(this) 72 | 73 | private fun transform(internalName: String, bytes: ByteArray): ByteArray? { 74 | if (internalName != "org/spongepowered/asm/util/Constants") return null 75 | 76 | val reader = bytes.asClassReader() 77 | val node = reader.asClassNode() 78 | require(node.name == internalName) 79 | 80 | val target = node.methods.named("") 81 | val call = target.instructions.filterIsInstance() 82 | .first { it.name == "getName" && it.owner == internalNameOf() } 83 | 84 | target.instructions.insert(call, asm { 85 | pop 86 | ldc("org.spongepowered.asm.mixin") 87 | }) 88 | 89 | target.instructions.remove(call) 90 | 91 | val writer = ClassWriter(reader, 0) 92 | node.accept(writer) 93 | return writer.toByteArray() 94 | } 95 | 96 | override fun findClass(name: String): Class<*> { 97 | if (name == javaClass.name) return javaClass 98 | 99 | return findLoadedClass(name) ?: if (shouldLoadParent(name)) parent.loadClass(name) else { 100 | val internalName = name.replace('.', '/') 101 | val bytes = getResourceAsStream("$internalName.class")?.readBytes() 102 | ?: throw ClassNotFoundException("Could not sandbox mixin: resource unavailable: $internalName") 103 | 104 | val resultBytes = transform(internalName, bytes) ?: bytes 105 | defineClass(name, resultBytes, 0, resultBytes.size) 106 | } 107 | } 108 | 109 | override fun loadClass(name: String): Class<*> = findClass(name) 110 | private fun shouldLoadParent(name: String) = name in loaderExclusions || systemClasses.any { name.startsWith(it) } 111 | 112 | /** 113 | * Allows you to add an "injected resource" to this loader, that is, when a resource with [name] is requested, 114 | * a stream (or temporary file) with [bytes] is returned (instead of delegating to the parent loader) 115 | */ 116 | public fun injectResource(name: String, bytes: ByteArray) { 117 | injectedResources[name] = bytes 118 | } 119 | 120 | override fun getResource(name: String): URL? = tempFiles[name] ?: injectedResources[name]?.let { 121 | File.createTempFile("injected-resource", null).run { 122 | writeBytes(injectedResources.getValue(name)) 123 | deleteOnExit() 124 | toURI().toURL().also { tempFiles[name] = it } 125 | } 126 | } ?: super.getResource(name) 127 | 128 | override fun getResources(name: String): Enumeration = 129 | if (name in injectedResources) Collections.enumeration(listOf(getResource(name))) 130 | else super.getResources(name) 131 | 132 | override fun getResourceAsStream(name: String): InputStream? = 133 | injectedResources[name]?.let { ByteArrayInputStream(it) } ?: super.getResourceAsStream(name) 134 | 135 | /** 136 | * Allows you to prevent a certain class with a given binary [name] from being sandboxed 137 | */ 138 | public fun addLoaderExclusion(name: String) { 139 | loaderExclusions += name 140 | } 141 | 142 | internal fun getClassBytes(name: String) = loader(name) ?: getResourceAsStream("$name.class")?.readBytes() 143 | } 144 | 145 | private fun createMixinAccessor(loader: ClassLoader) = runCatching { 146 | loader 147 | .loadClass("net.weavemc.loader.impl.mixin.MixinAccessImpl") 148 | .getField("INSTANCE").also { it.isAccessible = true }[null] as MixinAccess 149 | }.onFailure { 150 | logger.error("Failed to create a mixin access instance", it) 151 | 152 | val dummy = object {} 153 | logger.debug( 154 | "Creating accessor within $loader, which is nested within ${loader.parent}, " + 155 | "while this method is called within ${dummy.javaClass.classLoader}, " + 156 | "and MixinAccess is accessed from within ${MixinAccess::class.java.classLoader}" 157 | ) 158 | }.getOrThrow() 159 | 160 | /** 161 | * Keeps track of the state of the mixin environment. Allows you to interact with the sandbox. 162 | * 163 | * [initialize] must be invoked before calling any other method 164 | */ 165 | public class SandboxedMixinState( 166 | private val loader: SandboxedMixinLoader 167 | ) : MixinAccess by createMixinAccessor(loader) { 168 | // be careful to not load classes in the wrong context 169 | internal lateinit var transformer: Any 170 | 171 | /** 172 | * Whether this [SandboxedMixinState] has been initialized 173 | */ 174 | public var initialized: Boolean = false 175 | private set 176 | 177 | /** 178 | * Initializes this [SandboxedMixinState], does nothing when [initialized] is true 179 | */ 180 | public fun initialize() { 181 | if (initialized) return 182 | 183 | injectService( 184 | "org.spongepowered.asm.service.IMixinService", 185 | "net.weavemc.loader.impl.mixin.SandboxedMixinService" 186 | ) 187 | 188 | injectService( 189 | "org.spongepowered.asm.service.IMixinServiceBootstrap", 190 | "net.weavemc.loader.impl.mixin.DummyServiceBootstrap" 191 | ) 192 | 193 | injectService( 194 | "org.spongepowered.asm.service.IGlobalPropertyService", 195 | "net.weavemc.loader.impl.mixin.DummyPropertyService" 196 | ) 197 | 198 | bootstrap() 199 | validate() 200 | gotoDefault() 201 | 202 | require(this::transformer.isInitialized) { "Why did I not get a transformer?" } 203 | initialized = true 204 | } 205 | 206 | /** 207 | * Registers a mixin configuration, available on the classpath of the parent loader with a given [resourceName] 208 | */ 209 | public fun registerMixin(modId: String, resourceName: String) { 210 | val resource = loader.getResourceAsStream(resourceName) ?: error("Mixin config $resourceName does not exist!") 211 | 212 | // This is quite jank: we relocate the resources such that we can later find out what mod it was from 213 | val relocatedName = "weave-mod-mixin/$modId/${resourceName}" 214 | loader.injectResource(relocatedName, resource.use { it.readBytes() }) 215 | 216 | addMixin(relocatedName) 217 | } 218 | 219 | /** 220 | * Transforms a class represented by given [bytes] with a certain [internalName] using the mixin environment 221 | */ 222 | public fun transform(internalName: String, bytes: ByteArray): ByteArray = 223 | transform(transformer, internalName.replace('/', '.'), bytes) 224 | 225 | /** 226 | * Transforms a class represented by given [bytes] with a certain [internalName] using the mixin environment. 227 | * 228 | * Returns null when mixin was not interested in visiting this class 229 | */ 230 | public fun transformOrNull(internalName: String, bytes: ByteArray): ByteArray? = 231 | transform(internalName, bytes).takeIf { it !== bytes } 232 | 233 | /** 234 | * Transforms a class represented by given [node] with a certain [internalName] using the mixin environment 235 | * Returns whether a transformation has been performed 236 | */ 237 | public fun transform(internalName: String, node: ClassNode): Boolean = 238 | transform(transformer, internalName.replace('/', '.'), node) 239 | 240 | private fun injectService(name: String, value: String) = 241 | loader.injectResource("META-INF/services/$name", value.encodeToByteArray()) 242 | } 243 | 244 | internal sealed interface MixinAccess { 245 | fun bootstrap() 246 | fun validate() 247 | fun gotoDefault() 248 | fun addMixin(name: String) 249 | fun transform(transformer: Any, internalName: String, bytes: ByteArray): ByteArray 250 | fun transform(transformer: Any, internalName: String, node: ClassNode): Boolean 251 | fun generateClass(transformer: Any, internalName: String): ByteArray? 252 | fun produceInjectedClasses(transformer: Any): Map 253 | fun findTargets(transformer: Any): Set 254 | } 255 | 256 | @Suppress("unused") 257 | private data object MixinAccessImpl : MixinAccess { 258 | private val env get() = MixinEnvironment.getDefaultEnvironment() 259 | private var hasForcedSelect = false 260 | private var injectedClasses: Map? = null 261 | 262 | override fun bootstrap() = MixinBootstrap.init() 263 | override fun validate() { 264 | val service = MixinService.getService() 265 | require(service is SandboxedMixinService) { "Invalid mixin service: $service" } 266 | } 267 | 268 | override fun gotoDefault() { 269 | // Usually does nothing, but the internal state kind of expects this to be the case 270 | MixinEnvironment.init(Phase.DEFAULT) 271 | MixinEnvironment::class.java.getDeclaredMethod("gotoPhase", Phase::class.java) 272 | .also { it.isAccessible = true }(null, Phase.DEFAULT) 273 | } 274 | 275 | override fun addMixin(name: String) = Mixins.addConfiguration(name) 276 | override fun transform(transformer: Any, internalName: String, bytes: ByteArray): ByteArray = 277 | (transformer as IMixinTransformer).transformClass(env, internalName, bytes) 278 | 279 | override fun transform(transformer: Any, internalName: String, node: ClassNode): Boolean = 280 | (transformer as IMixinTransformer).transformClass(env, internalName, node) 281 | 282 | override fun generateClass(transformer: Any, internalName: String): ByteArray? = 283 | (transformer as IMixinTransformer).generateClass(env, internalName) 284 | 285 | // TODO: properly append these to the game classloader (reflective defineClass?) 286 | override fun produceInjectedClasses(transformer: Any): Map { 287 | injectedClasses?.let { return it } 288 | 289 | checkForcedSelect(transformer) 290 | val registry = (transformer as IMixinTransformer).extensions.syntheticClassRegistry 291 | val classes = registry.javaClass.getDeclaredField("classes") 292 | .also { it.isAccessible = true }[registry] as Map<*, *> 293 | 294 | return buildMap { 295 | classes.keys.forEach { 296 | if (it is String) generateClass(transformer, it)?.let { bytes -> put(it, bytes) } 297 | } 298 | }.also { injectedClasses = it } 299 | } 300 | 301 | private fun checkForcedSelect(transformer: Any) { 302 | if (hasForcedSelect) return 303 | 304 | val processor = retrieveProcessor(transformer) 305 | processor.javaClass.getDeclaredMethod("checkSelect", MixinEnvironment::class.java) 306 | .also { it.isAccessible = true }(processor, env) 307 | 308 | hasForcedSelect = true 309 | } 310 | 311 | private fun retrieveProcessor(transformer: Any) = 312 | transformer.javaClass.getDeclaredField("processor").also { it.isAccessible = true }[transformer] 313 | 314 | override fun findTargets(transformer: Any): Set { 315 | checkForcedSelect(transformer) 316 | 317 | val processor = retrieveProcessor(transformer) 318 | val configs = (processor.javaClass.getDeclaredField("configs") 319 | .also { it.isAccessible = true }[processor] as List<*>) 320 | .filterIsInstance() 321 | 322 | val mxnConfig = Class.forName("org.spongepowered.asm.mixin.transformer.MixinConfig") 323 | val getClasses = mxnConfig.getMethod("getClasses").also { it.isAccessible = true } 324 | 325 | @Suppress("UNCHECKED_CAST") 326 | return configs.filter { it.name.startsWith("weave-mod-mixin/") }.flatMapTo(hashSetOf()) { 327 | it.targets + if (mxnConfig.isInstance(it)) getClasses(it) as List else emptyList() 328 | } 329 | } 330 | } 331 | 332 | private fun counter() = object : ReadOnlyProperty { 333 | private var underlying = 0 334 | override fun getValue(thisRef: R, property: KProperty<*>) = underlying++ 335 | } 336 | 337 | /** 338 | * Utility that ensures that asm can find inheritance info when writing a class. 339 | * [useBytecodeInheritance] determines if inheritance info should be derived from bytecode instead of reflection, if possible 340 | */ 341 | // TODO: generalize 342 | public class LoaderClassWriter( 343 | private val loader: ClassLoader?, 344 | reader: ClassReader? = null, 345 | flags: Int = 0, 346 | private val useBytecodeInheritance: Boolean = true, 347 | ) : ClassWriter(reader, flags) { 348 | private fun getStream(name: String) = loader?.getResourceAsStream(name) 349 | ?: LoaderClassWriter::class.java.getResourceAsStream(name) 350 | 351 | private fun String.load() = 352 | getStream("$this.class")?.readBytes()?.asClassReader()?.asClassNode() 353 | 354 | private val ClassNode.isInterface: Boolean 355 | get() = access and ACC_INTERFACE != 0 356 | 357 | override fun getCommonSuperClass(type1: String, type2: String): String { 358 | if (!useBytecodeInheritance) return super.getCommonSuperClass(type1, type2) 359 | return when { 360 | type1 == "java/lang/Object" || type2 == "java/lang/Object" -> "java/lang/Object" 361 | type1 == type2 -> type1 362 | else -> { 363 | val node1 = type1.load() ?: return super.getCommonSuperClass(type1, type2) 364 | val node2 = type2.load() ?: return super.getCommonSuperClass(type1, type2) 365 | 366 | when { 367 | node1.isInterface || node2.isInterface -> "java/lang/Object" 368 | else -> { 369 | node1.getAllParents().intersect(node2.getAllParents().toSet()) 370 | .firstOrNull() ?: return super.getCommonSuperClass(type1, type2) 371 | } 372 | } 373 | } 374 | } 375 | } 376 | 377 | private fun ClassNode.getAllParents() = listOf(name) + getParents() 378 | private fun ClassNode.getParents(): List = when (name) { 379 | "java/lang/Object" -> emptyList() 380 | else -> listOf(superName) + (superName.load()?.getParents() ?: emptyList()) 381 | } 382 | 383 | override fun getClassLoader(): ClassLoader? = loader 384 | } -------------------------------------------------------------------------------- /loader/src/main/kotlin/net/weavemc/loader/impl/mixin/Services.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.loader.impl.mixin 2 | 3 | import me.xtrm.klog.dsl.klog 4 | import net.weavemc.loader.impl.bootstrap.WeaveLogAppender 5 | import net.weavemc.loader.impl.util.asClassNode 6 | import net.weavemc.loader.impl.util.asClassReader 7 | import net.weavemc.loader.impl.util.getJavaVersion 8 | import org.spongepowered.asm.launch.platform.container.ContainerHandleVirtual 9 | import org.spongepowered.asm.launch.platform.container.IContainerHandle 10 | import org.spongepowered.asm.logging.Level 11 | import org.spongepowered.asm.logging.LoggerAdapterAbstract 12 | import org.spongepowered.asm.mixin.MixinEnvironment 13 | import org.spongepowered.asm.mixin.transformer.IMixinTransformerFactory 14 | import org.spongepowered.asm.service.* 15 | import org.spongepowered.asm.util.Constants 16 | import org.spongepowered.asm.util.ReEntranceLock 17 | import java.io.InputStream 18 | import java.net.URL 19 | 20 | internal object SandboxedClassProvider : IClassProvider, IClassBytecodeProvider { 21 | private val sandboxLoader get() = javaClass.classLoader as? SandboxedMixinLoader 22 | 23 | @Deprecated("Deprecated in Java", ReplaceWith("emptyArray()")) 24 | override fun getClassPath() = emptyArray() 25 | override fun findClass(name: String?) = findClass(name, false) 26 | override fun findAgentClass(name: String?, initialize: Boolean) = findClass(name, initialize) 27 | override fun findClass(name: String?, initialize: Boolean): Class<*> = 28 | Class.forName(name, initialize, javaClass.classLoader) 29 | 30 | override fun getClassNode(name: String) = getClassNode(name, false) 31 | override fun getClassNode(name: String, runTransformers: Boolean) = 32 | sandboxLoader?.getClassBytes(name.replace('.', '/'))?.asClassReader()?.asClassNode() 33 | ?: throw ClassNotFoundException(name.replace('/', '.')) 34 | } 35 | 36 | internal class SandboxedMixinService : IMixinService { 37 | private val logger by klog 38 | 39 | private val lock = ReEntranceLock(1) 40 | private val sandboxLoader get() = javaClass.classLoader as? SandboxedMixinLoader 41 | 42 | override fun getName() = "Weave Mixin (within ${sandboxLoader?.parent})" 43 | override fun isValid() = true 44 | override fun prepare() {} 45 | override fun getInitialPhase(): MixinEnvironment.Phase = MixinEnvironment.Phase.PREINIT 46 | 47 | override fun offer(internal: IMixinInternal) { 48 | if (internal is IMixinTransformerFactory) { 49 | if (sandboxLoader == null) { 50 | logger.warn("SandboxedMixinService was loaded within illegal parent loader $sandboxLoader") 51 | } 52 | 53 | sandboxLoader?.state?.transformer = internal.createTransformer() 54 | } 55 | } 56 | 57 | override fun init() {} 58 | override fun beginPhase() {} 59 | override fun checkEnv(bootSource: Any?) {} 60 | override fun getReEntranceLock() = lock 61 | override fun getClassProvider() = SandboxedClassProvider 62 | override fun getBytecodeProvider() = SandboxedClassProvider 63 | override fun getTransformerProvider() = null 64 | override fun getClassTracker() = null 65 | override fun getAuditTrail() = null 66 | override fun getPlatformAgents() = emptyList() 67 | override fun getPrimaryContainer() = ContainerHandleVirtual(name) 68 | override fun getMixinContainers() = emptyList() 69 | override fun getResourceAsStream(name: String): InputStream? = javaClass.classLoader.getResourceAsStream(name) 70 | override fun getSideName() = Constants.SIDE_CLIENT 71 | override fun getMinCompatibilityLevel() = MixinEnvironment.CompatibilityLevel.JAVA_7 72 | override fun getMaxCompatibilityLevel() = runCatching { 73 | MixinEnvironment.CompatibilityLevel.valueOf("JAVA_${getJavaVersion()}") 74 | }.getOrNull() 75 | 76 | override fun getLogger(name: String) = WeaveLoggerAdapter(name) 77 | } 78 | 79 | @Suppress("unused") 80 | internal class DummyServiceBootstrap : IMixinServiceBootstrap { 81 | override fun getName() = "Weave Mixin Bootstrap" 82 | override fun getServiceClassName(): String = SandboxedMixinService::class.java.name 83 | override fun bootstrap() {} 84 | } 85 | 86 | @Suppress("unused") 87 | internal class DummyPropertyService : IGlobalPropertyService { 88 | private val properties = mutableMapOf() 89 | 90 | data class Key(val name: String) : IPropertyKey 91 | 92 | override fun resolveKey(name: String) = Key(name) 93 | 94 | @Suppress("UNCHECKED_CAST") 95 | override fun getProperty(key: IPropertyKey) = properties[key as Key] as T? 96 | 97 | @Suppress("UNCHECKED_CAST") 98 | override fun getProperty(key: IPropertyKey, defaultValue: T) = 99 | properties[key as Key] as? T? ?: defaultValue 100 | 101 | override fun setProperty(key: IPropertyKey, value: Any?) { 102 | properties[key as Key] = value 103 | } 104 | 105 | override fun getPropertyString(key: IPropertyKey, defaultValue: String) = getProperty(key, defaultValue) 106 | } 107 | 108 | private typealias KlogLevel = me.xtrm.klog.Level 109 | 110 | internal class WeaveLoggerAdapter(name: String) : LoggerAdapterAbstract(name) { 111 | private val logger by klog(name) 112 | private val stdout = WeaveLogAppender.WrappingStream(System.out) 113 | 114 | override fun getType() = "Default Console Logger" 115 | override fun catching(level: Level, t: Throwable) = logger.warn("Catching {}: {}", t.javaClass.name, t.message, t) 116 | override fun log(level: Level, message: String, vararg params: Any) { 117 | val formatted = FormattedMessage(message, *params) 118 | logger.log(level.toKlog(), formatted.message) 119 | if (formatted.hasThrowable()) formatted.throwable.printStackTrace(stdout) 120 | } 121 | 122 | override fun log(level: Level, message: String, t: Throwable) { 123 | logger.log(level.toKlog(), message) 124 | t.printStackTrace(stdout) 125 | } 126 | 127 | override fun throwing(throwable: T?): T? { 128 | log( 129 | Level.WARN, "Throwing {}: {}", 130 | throwable?.javaClass?.getName() ?: "null", 131 | throwable?.message ?: "null", 132 | throwable ?: Throwable("No Throwable") 133 | ) 134 | 135 | return throwable 136 | } 137 | 138 | private fun Level.toKlog(): KlogLevel = when (this) { 139 | Level.TRACE -> KlogLevel.TRACE 140 | Level.DEBUG -> KlogLevel.DEBUG 141 | Level.INFO -> KlogLevel.INFO 142 | Level.WARN -> KlogLevel.WARN 143 | Level.ERROR -> KlogLevel.ERROR 144 | Level.FATAL -> KlogLevel.FATAL 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /loader/src/main/kotlin/net/weavemc/loader/impl/util/Analytics.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.loader.impl.util 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | import kotlinx.serialization.encodeToString 6 | import kotlinx.serialization.json.Json 7 | import java.nio.file.Path 8 | import java.nio.file.Paths 9 | import kotlin.io.path.createFile 10 | import kotlin.io.path.exists 11 | import kotlin.io.path.readText 12 | import kotlin.io.path.writeText 13 | 14 | private fun Iterable.atMost(capacity: Int) = when (this) { 15 | is Collection -> if (size <= capacity) this else take(capacity) 16 | else -> take(capacity) 17 | } 18 | 19 | // I don't know why I decided to overengineer this 20 | // Could also make it a pointer but then the order does not mean that much anymore 21 | private class CircularBuffer( 22 | private val capacity: Int, 23 | private val backing: MutableList = mutableListOf() 24 | ) : MutableList by backing { 25 | init { 26 | require(backing.size <= capacity) { "Backing list has too large of an initial size!" } 27 | } 28 | 29 | override fun add(element: T): Boolean { 30 | if (size >= capacity) removeFirst() 31 | backing.add(element) 32 | return true 33 | } 34 | 35 | override fun add(index: Int, element: T) { 36 | if (size >= capacity) removeFirst() 37 | backing.add(index, element) 38 | } 39 | 40 | private fun removeN(n: Int) { 41 | if (n <= 0) return 42 | repeat(minOf(capacity, n)) { removeFirst() } 43 | } 44 | 45 | // Not recommended, does not make a lot of sense for a circular buffer 46 | // For that reason, the implementation is basic/inefficient 47 | override fun addAll(index: Int, elements: Collection): Boolean { 48 | if (elements.isEmpty()) return false 49 | elements.forEach { add(index, it) } 50 | return true 51 | } 52 | 53 | override fun addAll(elements: Collection): Boolean { 54 | if (elements.isEmpty()) return false 55 | 56 | removeN(elements.size - capacity) 57 | backing.addAll(elements.atMost(capacity)) 58 | 59 | return true 60 | } 61 | 62 | fun toList() = backing.toList() 63 | } 64 | 65 | private fun Iterable.toCircularBuffer(capacity: Int) = CircularBuffer(capacity, atMost(capacity).toMutableList()) 66 | 67 | internal var launchStart = 0L 68 | 69 | internal fun updateLaunchTimes() { 70 | val now = System.currentTimeMillis() 71 | val time = now - launchStart 72 | 73 | val analytics = getOrCreateAnalyticsFile() 74 | val json = analytics.readText() 75 | 76 | val launchData = if (json.isNotEmpty()) Json.decodeFromString(json) else Analytics() 77 | val launchTimes = launchData.launchTimes.toCircularBuffer(capacity = 10) 78 | launchTimes += time 79 | 80 | analytics.writeText( 81 | Json.encodeToString( 82 | launchData.copy( 83 | launchTimes = launchTimes, 84 | averageLaunchTime = (launchTimes.average() / 1000).toFloat() 85 | ) 86 | ) 87 | ) 88 | } 89 | 90 | private fun getOrCreateAnalyticsFile(): Path { 91 | val file = Paths.get(System.getProperty("user.home"), ".weave", "analytics.json") 92 | if (!file.exists()) file.createFile() 93 | 94 | return file 95 | } 96 | 97 | @Serializable 98 | private data class Analytics( 99 | @SerialName("launch_times") val launchTimes: List = emptyList(), 100 | @SerialName("time_played") val timePlayed: Long = 0L, 101 | @SerialName("average_launch_time") val averageLaunchTime: Float = 0f 102 | ) 103 | -------------------------------------------------------------------------------- /loader/src/main/kotlin/net/weavemc/loader/impl/util/FileManager.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.loader.impl.util 2 | 3 | import me.xtrm.klog.dsl.klog 4 | import net.weavemc.internals.GameInfo 5 | import java.io.File 6 | import java.nio.file.Path 7 | import kotlin.io.path.* 8 | 9 | internal object FileManager { 10 | private val logger by klog 11 | val MODS_DIRECTORY = getOrCreateDirectory("mods") 12 | val DUMP_DIRECTORY = getOrCreateDirectory(".bytecode.out") 13 | 14 | fun getVanillaMinecraftJar(): File { 15 | logger.trace("Searching for vanilla jar") 16 | 17 | val os = System.getProperty("os.name").lowercase() 18 | run { 19 | val userHome = System.getProperty("user.home", System.getenv("HOME") ?: System.getenv("USERPROFILE")) 20 | val minecraftPath = when { 21 | os.contains("win") -> Path("AppData", "Roaming", ".minecraft") 22 | os.contains("mac") -> Path("Library", "Application Support", "minecraft") 23 | os.contains("nix") || os.contains("nux") || os.contains("aix") -> Path(".minecraft") 24 | else -> return@run 25 | } 26 | 27 | val fullPath = Path(userHome).resolve(minecraftPath) 28 | val regularPath = fullPath.resolve("versions") 29 | .resolve(GameInfo.version.versionName) 30 | .resolve("${GameInfo.version.versionName}.jar") 31 | if (regularPath.exists()) { 32 | return regularPath.toFile() 33 | } 34 | } 35 | 36 | logger.trace("Trying to find vanilla jar in classpath") 37 | val gameVersion = GameInfo.version.versionName 38 | val mclPath = Path("versions", gameVersion, "$gameVersion.jar") 39 | val mmcPath = Path("libraries", "com", "mojang", "minecraft", gameVersion, "minecraft-$gameVersion-client.jar") 40 | val classpath = System.getProperty("java.class.path") 41 | val paths = classpath?.split(File.pathSeparator)?.map { Path(it) } 42 | return paths?.find { it.endsWith(mclPath) || it.endsWith(mmcPath) }?.toFile() 43 | ?: fatalError("Could not find vanilla jar for version $gameVersion") 44 | } 45 | 46 | /** 47 | * Gets all mods in the `~/.weave/mods` directory. 48 | */ 49 | fun getMods(): List { 50 | val mods = mutableListOf() 51 | 52 | logger.trace("Searching for mods in $MODS_DIRECTORY") 53 | mods += MODS_DIRECTORY.walkMods() 54 | 55 | val specificVersionDirectory = MODS_DIRECTORY.resolve(GameInfo.version.versionName) 56 | if (specificVersionDirectory.exists() && specificVersionDirectory.isDirectory()) { 57 | logger.trace("Searching for mods in $specificVersionDirectory") 58 | mods += specificVersionDirectory.walkMods(true) 59 | } 60 | 61 | logger.info("Discovered ${mods.size} mod files") 62 | 63 | return mods 64 | } 65 | 66 | private fun Path.walkMods(isSpecific: Boolean = false) = listDirectoryEntries("*.jar") 67 | .filter { it.isRegularFile() } 68 | .map { ModJar(it.toFile(), isSpecific) } 69 | 70 | data class ModJar(val file: File, val isSpecific: Boolean) 71 | } 72 | -------------------------------------------------------------------------------- /loader/src/main/kotlin/net/weavemc/loader/impl/util/Mappings.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.loader.impl.util 2 | 3 | import com.grappenmaker.mappings.* 4 | import me.xtrm.klog.dsl.klog 5 | import net.weavemc.internals.* 6 | import net.weavemc.internals.MappingsType.MCP 7 | import net.weavemc.internals.MappingsType.MOJANG 8 | import net.weavemc.loader.impl.WeaveLoader 9 | import org.objectweb.asm.* 10 | import org.objectweb.asm.commons.ClassRemapper 11 | import org.objectweb.asm.commons.Remapper 12 | import org.objectweb.asm.commons.SimpleRemapper 13 | import java.io.File 14 | import java.util.* 15 | import java.util.jar.JarFile 16 | import kotlin.system.measureTimeMillis 17 | 18 | public object MappingsHandler { 19 | private val logger by klog 20 | public val relocationRemapper: Remapper? by lazy { createRelocationRemapper() } 21 | public val vanillaJar: File by lazy { FileManager.getVanillaMinecraftJar() } 22 | 23 | public val mergedMappings: WeaveMappings by lazy { 24 | logger.info("Loading merged mappings for ${GameInfo.version.versionName}") 25 | logger.debug("Vanilla jar: $vanillaJar") 26 | 27 | val mappings: WeaveMappings 28 | measureTimeMillis { 29 | mappings = MappingsRetrieval.loadMergedWeaveMappings(GameInfo.version.versionName, vanillaJar) 30 | }.let { logger.info("Took ${it}ms to load mappings") } 31 | 32 | mappings 33 | } 34 | 35 | public val environmentNamespace: String by lazy { 36 | System.getProperty("weave.environment.namespace") ?: when (GameInfo.client) { 37 | MinecraftClient.LUNAR -> if (GameInfo.version < MinecraftVersion.V1_16_5) MCP.named else MOJANG.named 38 | MinecraftClient.FORGE -> MCP.srg 39 | MinecraftClient.VANILLA, MinecraftClient.LABYMOD, MinecraftClient.BADLION -> "official" 40 | } 41 | } 42 | 43 | internal fun classLoaderBytesProvider(expectedNamespace: String): (String) -> ByteArray? { 44 | val names = if (expectedNamespace != "official") mergedMappings.mappings.asASMMapping( 45 | from = expectedNamespace, 46 | to = "official", 47 | includeMethods = false, 48 | includeFields = false 49 | ) else emptyMap() 50 | 51 | val mapper = SimpleRemapper(names.toList().associate { (k, v) -> v to k }) 52 | val callback = ClasspathLoaders.fromLoader(WeaveLoader::class.java.classLoader) 53 | 54 | return { name -> callback(names[name] ?: name)?.remap(mapper) } 55 | } 56 | 57 | private val cachedMappers = mutableMapOf, MappingsRemapper>() 58 | 59 | public fun mapper(from: String, to: String): MappingsRemapper = cachedMappers.getOrPut(from to to) { 60 | MappingsRemapper(mergedMappings.mappings, from, to, loader = classLoaderBytesProvider(from)) 61 | } 62 | 63 | internal val environmentRemapper = mapper("official", environmentNamespace) 64 | internal val environmentUnmapper = environmentRemapper.reverse() 65 | 66 | private val mappable by lazy { 67 | val id = mergedMappings.mappings.namespace("official") 68 | mergedMappings.mappings.classes.mapTo(hashSetOf()) { it.names[id] } 69 | } 70 | 71 | internal fun ByteArray.remap(remapper: Remapper, bypassMappableCheck: Boolean = false): ByteArray { 72 | val reader = ClassReader(this) 73 | if (reader.className !in mappable && !bypassMappableCheck) return this 74 | 75 | val writer = ClassWriter(reader, 0) 76 | reader.accept(MinecraftRemapper(writer, remapper), 0) 77 | 78 | return writer.toByteArray() 79 | } 80 | 81 | private class SimpleAnnotationNode( 82 | parent: AnnotationVisitor?, 83 | val descriptor: String? 84 | ) : AnnotationVisitor(Opcodes.ASM9, parent) { 85 | private val values: MutableList> = mutableListOf() 86 | 87 | override fun visit(name: String?, value: Any?) { 88 | values += name to value 89 | super.visit(name, value) 90 | } 91 | } 92 | 93 | // Remapper meant to be used with loaded Minecraft classes. 94 | // It will ignore MixinMerged methods (an issue with LabyMod) 95 | private class MinecraftRemapper( 96 | parent: ClassVisitor, 97 | remapper: Remapper 98 | ) : ClassRemapper(Opcodes.ASM9, parent, remapper) { 99 | private val annotationNodes: MutableList = mutableListOf() 100 | 101 | override fun createAnnotationRemapper( 102 | descriptor: String?, 103 | parent: AnnotationVisitor? 104 | ): AnnotationVisitor { 105 | val node = SimpleAnnotationNode(parent, descriptor) 106 | annotationNodes += node 107 | return node 108 | } 109 | 110 | override fun createMethodRemapper(parent: MethodVisitor): MethodVisitor = 111 | MinecraftMethodRemapper(annotationNodes.map { it.descriptor ?: "" }, parent, remapper) 112 | } 113 | 114 | private class MinecraftMethodRemapper( 115 | private val annotations: List, 116 | private val parent: MethodVisitor, 117 | remapper: Remapper 118 | ) : LambdaAwareMethodRemapper(parent, remapper) { 119 | override fun visitInvokeDynamicInsn(name: String, descriptor: String, handle: Handle, vararg args: Any) { 120 | if (annotations.contains("org/spongepowered/asm/mixin/transformer/meta/MixinMerged")) 121 | parent.visitInvokeDynamicInsn(name, descriptor, handle, args) 122 | else 123 | super.visitInvokeDynamicInsn(name, descriptor, handle, *args) 124 | } 125 | } 126 | 127 | internal fun remapModJar( 128 | mappings: Mappings, 129 | input: File, 130 | output: File, 131 | from: String = "official", 132 | to: String = environmentNamespace, 133 | classpath: List = listOf(), 134 | ) { 135 | val jarsToUse = (classpath + input).map { JarFile(it) } 136 | 137 | remapJar( 138 | mappings, input, output, from, to, ClasspathLoaders.fromJars(jarsToUse).remappingNames( 139 | mappings = mergedMappings.mappings, 140 | from = "official", 141 | to = from, 142 | ) 143 | ) { if (relocationRemapper != null) ClassRemapper(it, relocationRemapper) else it } 144 | 145 | jarsToUse.forEach { it.close() } 146 | } 147 | 148 | public fun isNamespaceAvailable(ns: String): Boolean = ns in mergedMappings.mappings.namespaces 149 | } 150 | 151 | private val relocationData: Properties by lazy { 152 | val url = MappingsHandler::class.java.classLoader.getResource("weave-relocation-data.properties") 153 | val stream = url?.openStream() 154 | 155 | Properties().apply { stream?.use { load(it) } } 156 | } 157 | 158 | private fun createRelocationRemapper(): Remapper? { 159 | if (relocationData.isEmpty) { 160 | klog.warn("Relocation data not found, skipping remapping.") 161 | return null 162 | } 163 | 164 | val relocatePrefix = relocationData["target"].toString().replace(".", "/") 165 | val packages = relocationData["packages"].toString().split(";").map { it.replace(".", "/") } 166 | 167 | // Note the missing trailing slash in those packages, this makes it so that 168 | // shadowJar won't rewrite them during relocation (since it's configured to target 169 | // `org.objectweb.asm.`, not `org.objectweb.asm` for example) 170 | val mapping = packages.associateWith { "$relocatePrefix/${it.substringAfterLast("/")}" } 171 | fun findMapping(name: String) = mapping.entries.find { (k) -> name.startsWith("$k/") } 172 | 173 | return object : Remapper() { 174 | override fun map(name: String) = findMapping(name)?.let { (k, v) -> name.replaceFirst(k, v) } ?: name 175 | } 176 | } -------------------------------------------------------------------------------- /loader/src/main/kotlin/net/weavemc/loader/impl/util/MultiMC.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.loader.impl.util 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | internal data class MultiMCInstance( 7 | val components: List 8 | ) 9 | 10 | @Serializable 11 | internal data class MultiMCComponent( 12 | val cachedName: String, 13 | val uid: String, 14 | val version: String? = null 15 | ) -------------------------------------------------------------------------------- /loader/src/main/kotlin/net/weavemc/loader/impl/util/Utility.kt: -------------------------------------------------------------------------------- 1 | package net.weavemc.loader.impl.util 2 | 3 | import kotlinx.serialization.json.Json 4 | import me.xtrm.klog.dsl.klog 5 | import net.weavemc.internals.GameInfo 6 | import net.weavemc.internals.ModConfig 7 | import org.objectweb.asm.ClassReader 8 | import org.objectweb.asm.tree.ClassNode 9 | import org.objectweb.asm.tree.MethodNode 10 | import java.io.File 11 | import java.nio.file.Files 12 | import java.nio.file.Path 13 | import java.nio.file.Paths 14 | import java.security.AccessController 15 | import java.security.PrivilegedAction 16 | import java.util.jar.JarFile 17 | import javax.swing.JOptionPane 18 | import kotlin.io.path.* 19 | 20 | /** 21 | * Grabs the directory for the specified directory, creating it if it doesn't exist. 22 | * If the file exists as a file and not a directory, it will be deleted. 23 | * 24 | * @param directory The directory to grab. 25 | * @return The specified directory: `"~/.weave/"` 26 | */ 27 | internal fun getOrCreateDirectory(directory: String): Path { 28 | val dir = Paths.get(System.getProperty("user.home"), ".weave", directory) 29 | if (dir.exists() && !dir.isDirectory()) Files.delete(dir) 30 | if (!dir.exists()) dir.createDirectories() 31 | return dir 32 | } 33 | 34 | internal fun ByteArray.asClassReader(): ClassReader = ClassReader(this) 35 | internal fun ClassReader.asClassNode(): ClassNode = ClassNode().also { this.accept(it, 0) } 36 | 37 | internal val JSON = Json { ignoreUnknownKeys = true } 38 | 39 | internal fun MethodNode.hasMixinAnnotation(name: String): Boolean { 40 | val annotation = "spongepowered/asm/mixin/transformer/meta/$name;" 41 | return visibleAnnotations?.any { it.desc.endsWith(annotation) } == true 42 | } 43 | 44 | internal inline fun instantiate(className: String): T = 45 | Class.forName(className) 46 | .getConstructor() 47 | .newInstance() as? T 48 | ?: error("$className does not implement ${T::class.java.simpleName}!") 49 | 50 | internal inline fun instantiate(className: String, loader: ClassLoader?): T = 51 | Class.forName(className, false, loader) 52 | .getConstructor() 53 | .newInstance() as? T 54 | ?: error("$className does not implement ${T::class.java.simpleName}!") 55 | 56 | internal fun fatalError(message: String): Nothing { 57 | klog.fatal("An error occurred: $message") 58 | JOptionPane.showMessageDialog( 59 | /* parentComponent = */ null, 60 | /* message = */ "An error occurred: $message", 61 | /* title = */ "Weave Loader error", 62 | /* messageType = */ JOptionPane.ERROR_MESSAGE 63 | ) 64 | 65 | exit(-1) 66 | } 67 | 68 | /** 69 | * Exits the JVM with the given error code, escaping any SecurityManager 70 | * in place. 71 | * 72 | * @param errorCode the error code to exit with 73 | */ 74 | internal fun exit(errorCode: Int): Nothing { 75 | runCatching { 76 | val clazz = Class.forName("java.lang.Shutdown") 77 | clazz.getDeclaredMethod("exit", Int::class.javaPrimitiveType).apply { 78 | isAccessible = true 79 | }(null, errorCode) 80 | }.onFailure { e0 -> 81 | runCatching { 82 | exitRuntime(errorCode) 83 | }.onFailure { e1 -> 84 | if (getJavaVersion() <= 19) { 85 | @Suppress("DEPRECATION") 86 | AccessController.doPrivileged(PrivilegedAction { 87 | exitRuntime(errorCode) 88 | null 89 | }) 90 | } else { 91 | e1.addSuppressed(e0) 92 | throw RuntimeException("Exiting the JVM, no errors to report here.", e1) 93 | } 94 | } 95 | } 96 | 97 | throw IllegalStateException("This should never be reached") 98 | } 99 | 100 | private fun exitRuntime(errorCode: Int) { 101 | val clazz = Class.forName("java.lang.Runtime") 102 | val runtime = clazz.getDeclaredMethod("getRuntime").also { it.isAccessible = true }(null) 103 | clazz.getDeclaredMethod("exit", Int::class.javaPrimitiveType).also { it.isAccessible = true }(runtime, errorCode) 104 | } 105 | 106 | internal fun getJavaVersion(): Int { 107 | val version = System.getProperty("java.version", "1.6.0") 108 | val part = if (version.startsWith("1.")) version.split(".")[1] else version.substringBefore(".") 109 | return part.toInt() 110 | } 111 | 112 | internal fun JarFile.configOrFatal() = runCatching { fetchModConfig(JSON) }.onFailure { 113 | klog.error("Possibly non-weave mod failed to load:") 114 | it.printStackTrace() 115 | 116 | fatalError("Mod file ${this.name} is possibly not a Weave mod!") 117 | }.getOrThrow() 118 | 119 | internal fun JarFile.fetchModConfig(json: Json): ModConfig { 120 | val configEntry = getEntry("weave.mod.json") ?: error("${this.name} does not contain a weave.mod.json!") 121 | return json.decodeFromString(getInputStream(configEntry).readBytes().decodeToString()) 122 | } 123 | 124 | internal fun File.createRemappedTemp(name: String, config: ModConfig): File { 125 | val temp = File.createTempFile(name, "-weavemod.jar") 126 | MappingsHandler.remapModJar( 127 | mappings = MappingsHandler.mergedMappings.mappings, 128 | input = this, 129 | output = temp, 130 | classpath = listOf(FileManager.getVanillaMinecraftJar()), 131 | from = config.namespace 132 | ) 133 | 134 | temp.deleteOnExit() 135 | return temp 136 | } 137 | 138 | internal fun setGameInfo() { 139 | val cwd = Path(System.getProperty("user.dir")) 140 | val version = System.getProperty("weave.environment.version") 141 | ?: cwd.takeIf { "instances" in it.pathString }?.run { 142 | val instance = cwd.parent 143 | runCatching { 144 | val instanceData = JSON.decodeFromString( 145 | instance.resolve("mmc-pack.json").toFile().readText() 146 | ) 147 | 148 | instanceData.components.find { it.uid == "net.minecraft" }?.version 149 | }.getOrNull() 150 | } ?: """--version\s+(\S+)""".toRegex() 151 | .find(System.getProperty("sun.java.command")) 152 | ?.groupValues?.get(1) 153 | ?: fatalError("Could not determine game version") 154 | 155 | fun classExists(name: String): Boolean = 156 | GameInfo::class.java.classLoader.getResourceAsStream("${name.replace('.', '/')}.class") != null 157 | 158 | val client = when { 159 | classExists("com.moonsworth.lunar.genesis.Genesis") -> "lunar client" 160 | classExists("net.minecraftforge.fml.common.Loader") -> "forge" 161 | GameInfo.commandLineArgs.contains("labymod") -> "labymod" 162 | else -> "vanilla" 163 | } 164 | 165 | System.getProperties()["weave.game.info"] = mapOf( 166 | "version" to version, 167 | "client" to client 168 | ) 169 | } 170 | 171 | // TODO: give this a good place 172 | internal val illegalToReload = setOf( 173 | "java.", "javax.", "org.xml.", "org.w3c.", "sun.", "jdk.", 174 | "com.sun.management.", "org.apache.", "org.slf4j." 175 | ) 176 | 177 | internal data class WeaveMod(val modId: String, val config: ModConfig) -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 2 | 3 | pluginManagement { 4 | repositories { 5 | mavenCentral() 6 | gradlePluginPortal() 7 | maven("https://repo.weavemc.dev") 8 | } 9 | } 10 | 11 | plugins { 12 | id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" 13 | } 14 | 15 | rootProject.name = "Weave-Loader" 16 | 17 | includeBuild("build-logic") 18 | include("loader", "internals", "gradle-plugin") 19 | --------------------------------------------------------------------------------