├── .gitattributes ├── .github └── workflows │ └── publish.yml ├── .gitignore ├── LICENSE-header.txt ├── LICENSE.txt ├── README.md ├── build.gradle ├── coremods-test-jar ├── build.gradle └── src │ └── main │ └── java │ ├── module-info.java │ └── net │ └── minecraftforge │ └── coremod │ └── testjar │ ├── Counter.java │ ├── TestClass.java │ └── TestMarker.java ├── coremods-test ├── build.gradle └── src │ └── test │ ├── java │ ├── module-info.java │ └── net │ │ └── minecraftforge │ │ └── coremod │ │ └── test │ │ ├── CoreModTests.java │ │ ├── JSFileLoader.java │ │ ├── TestLaunchTransformerBase.java │ │ └── TestTransformerService.java │ ├── javascript │ ├── test_insert_after.js │ ├── test_insert_before.js │ ├── test_insert_remove.js │ ├── test_load_data.js │ ├── test_load_data.json │ ├── test_load_file.js │ ├── test_load_file_extra.js │ └── test_method_manual.js │ └── resources │ └── log4j2.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main ├── java ├── module-info.java └── net │ └── minecraftforge │ └── coremod │ ├── CoreMod.java │ ├── CoreModEngine.java │ ├── CoreModProvider.java │ ├── CoreModTracker.java │ ├── NashornFactory.java │ ├── api │ └── ASMAPI.java │ └── transformer │ ├── CoreModBaseTransformer.java │ ├── CoreModClassTransformer.java │ ├── CoreModFieldTransformer.java │ └── CoreModMethodTransformer.java └── resources └── META-INF └── services └── net.minecraftforge.forgespi.coremod.ICoreModProvider /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.bat text eol=crlf 3 | *.patch text eol=lf 4 | *.java text eol=lf 5 | *.gradle text eol=crlf 6 | *.png binary 7 | *.gif binary 8 | *.exe binary 9 | *.dll binary 10 | *.jar binary 11 | *.lzma binary 12 | *.zip binary 13 | *.pyd binary 14 | *.cfg text eol=lf 15 | *.py text eol=lf 16 | *.tsrg text eol=lf 17 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | build: 12 | uses: MinecraftForge/SharedActions/.github/workflows/gradle.yml@main 13 | with: 14 | java: 17 15 | gradle_tasks: "publish" 16 | artifact_name: "coremods" 17 | secrets: 18 | DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} 19 | PROMOTE_ARTIFACT_WEBHOOK: ${{ secrets.PROMOTE_ARTIFACT_WEBHOOK }} 20 | PROMOTE_ARTIFACT_USERNAME: ${{ secrets.PROMOTE_ARTIFACT_USERNAME }} 21 | PROMOTE_ARTIFACT_PASSWORD: ${{ secrets.PROMOTE_ARTIFACT_PASSWORD }} 22 | MAVEN_USER: ${{ secrets.MAVEN_USER }} 23 | MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #occupational hazards 2 | /logs/ 3 | /out/ 4 | /repo/ 5 | 6 | #eclipse 7 | **/bin 8 | **/.settings 9 | **/.classpath 10 | *.project 11 | 12 | #idea 13 | /.idea 14 | /classes 15 | *.iml 16 | 17 | #gradle 18 | **/build 19 | **/.gradle 20 | /coremods-test/logs/ 21 | -------------------------------------------------------------------------------- /LICENSE-header.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) Forge Development LLC 2 | SPDX-License-Identifier: LGPL-2.1-only -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 489 | 490 | Also add information on how to contact you by electronic and paper mail. 491 | 492 | You should also get your employer (if you work as a programmer) or your 493 | school, if any, to sign a "copyright disclaimer" for the library, if 494 | necessary. Here is a sample; alter the names: 495 | 496 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 497 | library `Frob' (a library for tweaking knobs) written by James Random Hacker. 498 | 499 | , 1 April 1990 500 | Ty Coon, President of Vice 501 | 502 | That's all there is to it! 503 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CoreMods 2 | 3 | CoreMods is a JavaScript-based system that acts as a wrapper around ObjectWeb ASM. 4 | 5 | ## Purpose 6 | 7 | CoreMods need to be sandboxed, or otherwise isolated, in their own environments so that they are not able to cause early 8 | class-loading. They transform classes as only as they are loaded and do not have access to objects outside of the 9 | sandbox given to them. This helps prevent issues that would otherwise arise from CoreMods written traditionally in Java. 10 | 11 | Since CoreMods integrates with ModLauncher's transformation system, it is easier to manage the lifecycle as CoreMods is 12 | only responsible for managing the transformation as ModLauncher is instead the one responsible for providing the class 13 | loading system. 14 | 15 | ## Usage 16 | 17 | CoreMods are JavaScript files that are sandboxed by the limitations provided within the CoreMod engine. It is only able 18 | to access a limited set of classes and packages. ASMAPI, included within CoreMods, exists to provide several helpful 19 | tools for writing CoreMods. You can view this class yourself to see its usages, or you can find examples of it in other 20 | CoreMods. 21 | 22 | The best way to find examples for CoreMods is to look at Forge itself, since it includes complex examples that utilize 23 | much of the functionality within the sandbox. 24 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | import net.minecraftforge.gradleutils.PomUtils 2 | 3 | plugins { 4 | id 'idea' 5 | id 'eclipse' 6 | id 'java-library' 7 | id 'maven-publish' 8 | alias libs.plugins.license 9 | //alias libs.plugins.versions 10 | alias libs.plugins.gradleutils 11 | } 12 | 13 | group = 'net.minecraftforge' 14 | version = gradleutils.tagOffsetVersion 15 | println "Version: $version" 16 | 17 | java { 18 | toolchain.languageVersion = JavaLanguageVersion.of(17) 19 | withSourcesJar() 20 | } 21 | 22 | repositories { 23 | mavenCentral() 24 | maven gradleutils.forgeMaven 25 | mavenLocal() 26 | } 27 | 28 | changelog { 29 | from '1.0.0' 30 | } 31 | 32 | license { 33 | header = file('LICENSE-header.txt') 34 | newLine = false 35 | } 36 | 37 | dependencies { 38 | compileOnly(libs.modlauncher) 39 | compileOnly(libs.securemodules) 40 | compileOnly(libs.log4j.api) 41 | compileOnly(libs.nulls) 42 | compileOnly(libs.forgespi) 43 | 44 | api(libs.bundles.asm) 45 | implementation(libs.nashorn) 46 | } 47 | 48 | tasks.named('jar', Jar) { 49 | manifest { 50 | attributes([ 51 | 'Specification-Title': 'coremods', 52 | 'Specification-Vendor': 'Forge Development LLC', 53 | 'Specification-Version': '1', // TODO: Use the tag 54 | 'Implementation-Title': project.name, 55 | 'Implementation-Version': project.version, 56 | 'Implementation-Vendor' :'Forge Development LLC' 57 | ], 'net/minecraftforge/coremod/') 58 | } 59 | } 60 | 61 | publishing { 62 | publications.register('mavenJava', MavenPublication) { 63 | pom { 64 | from components.java 65 | artifactId = 'coremods' 66 | name = 'Core Mods' 67 | description = 'JavaScript based core modding framework for use with Forge' 68 | url = 'https://github.com/MinecraftForge/CoreMods' 69 | PomUtils.setGitHubDetails(pom, 'CoreMods') 70 | license PomUtils.Licenses.LGPLv2_1 71 | 72 | developers { 73 | developer PomUtils.Developers.LexManos 74 | developer PomUtils.Developers.cpw 75 | } 76 | } 77 | } 78 | 79 | repositories { 80 | maven gradleutils.publishingForgeMaven 81 | } 82 | } 83 | 84 | allprojects { 85 | ext.VALID_VMS = [ 86 | 'Adoptium': (17..21), 87 | 'Amazon': (17..21), 88 | 'Azul': (17..21), 89 | 'BellSoft': (17..21), 90 | 'Graal_VM': [17, 19, 20, 21], 91 | 'IBM': [17, 18, 19, 20 ], 92 | 'Microsoft': [17, 21], 93 | 'Oracle': (17..21), 94 | 'SAP': (17..20) 95 | ] 96 | ext.VALID_VMS = [ 'Adoptium': [17] ] 97 | } 98 | 99 | idea.module { 100 | downloadJavadoc = downloadSources = true 101 | } 102 | -------------------------------------------------------------------------------- /coremods-test-jar/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'eclipse' 3 | id 'java-library' 4 | alias libs.plugins.license 5 | //alias libs.plugins.versions 6 | alias libs.plugins.gradleutils 7 | } 8 | 9 | repositories { 10 | mavenCentral() 11 | maven gradleutils.forgeMaven 12 | mavenLocal() 13 | } 14 | 15 | java { 16 | toolchain.languageVersion = JavaLanguageVersion.of(17) 17 | } 18 | 19 | license { 20 | header = rootProject.file("LICENSE-header.txt") 21 | } 22 | 23 | // Hack eclipse into knowing that the gradle deps are modules 24 | eclipse.classpath { 25 | containers 'org.eclipse.buildship.core.gradleclasspathcontainer' 26 | file.whenMerged { entries.findAll { it.kind == 'lib' || it.path == 'org.eclipse.buildship.core.gradleclasspathcontainer' }.each { it.entryAttributes['module'] = 'true' } } 27 | } 28 | -------------------------------------------------------------------------------- /coremods-test-jar/src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Forge Development LLC 3 | * SPDX-License-Identifier: LGPL-2.1-only 4 | */ 5 | 6 | open module net.minecraftforge.coremod.testjar { 7 | exports net.minecraftforge.coremod.testjar; 8 | } 9 | -------------------------------------------------------------------------------- /coremods-test-jar/src/main/java/net/minecraftforge/coremod/testjar/Counter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Forge Development LLC 3 | * SPDX-License-Identifier: LGPL-2.1-only 4 | */ 5 | 6 | package net.minecraftforge.coremod.testjar; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | public class Counter { 12 | private final List counts = new ArrayList<>(); 13 | 14 | public Counter() { 15 | push(); 16 | } 17 | 18 | public int[] getCounts() { 19 | return counts.stream().mapToInt(Integer::intValue).toArray(); 20 | } 21 | 22 | public void push() { 23 | counts.add(0); 24 | } 25 | 26 | public void increment() { 27 | counts.set(counts.size() - 1, counts.get(counts.size() - 1) + 1); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /coremods-test-jar/src/main/java/net/minecraftforge/coremod/testjar/TestClass.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Forge Development LLC 3 | * SPDX-License-Identifier: LGPL-2.1-only 4 | */ 5 | 6 | package net.minecraftforge.coremod.testjar; 7 | 8 | public class TestClass { 9 | public static boolean False() { 10 | return false; 11 | } 12 | 13 | public static int[] testCounter() { 14 | var counts = new Counter(); 15 | counts.push(); 16 | return counts.getCounts(); 17 | } 18 | 19 | public static String testString() { 20 | return "raw"; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /coremods-test-jar/src/main/java/net/minecraftforge/coremod/testjar/TestMarker.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Forge Development LLC 3 | * SPDX-License-Identifier: LGPL-2.1-only 4 | */ 5 | 6 | package net.minecraftforge.coremod.testjar; 7 | 8 | // Marker class to let the arms length find the test resources 9 | public class TestMarker { 10 | } 11 | -------------------------------------------------------------------------------- /coremods-test/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'eclipse' 3 | id 'java-library' 4 | alias libs.plugins.license 5 | alias libs.plugins.modules 6 | //alias libs.plugins.versions 7 | alias libs.plugins.gradleutils 8 | } 9 | 10 | repositories { 11 | mavenCentral() 12 | maven gradleutils.forgeMaven 13 | mavenLocal() 14 | } 15 | 16 | java { 17 | toolchain.languageVersion = rootProject.java.toolchain.languageVersion 18 | } 19 | 20 | license { 21 | header = rootProject.file("LICENSE-header.txt") 22 | newLine = false 23 | } 24 | 25 | test { 26 | useJUnitPlatform() 27 | reports.html.outputLocation = rootProject.file("build/reports/") 28 | reports.junitXml.outputLocation = rootProject.file("build/test-results/") 29 | } 30 | 31 | test { 32 | //exclude '**/*' 33 | useJUnitPlatform() 34 | } 35 | 36 | compileTestJava { 37 | //exclude '**/*' 38 | } 39 | 40 | sourceSets { 41 | test { 42 | resources { 43 | srcDir 'src/test/javascript/' 44 | } 45 | } 46 | } 47 | 48 | dependencies { 49 | testImplementation(rootProject) 50 | testImplementation(project(':coremods-test-jar')) 51 | testImplementation(libs.junit.api) 52 | testImplementation(libs.log4j.api) 53 | testImplementation(libs.modlauncher) 54 | testImplementation(libs.securemodules) 55 | testImplementation(libs.forgespi) 56 | testImplementation(libs.unsafe) 57 | testRuntimeOnly(libs.bundles.junit.runtime) 58 | testCompileOnly(libs.nulls) 59 | } 60 | 61 | extraJavaModuleInfo { 62 | failOnMissingModuleInfo = false 63 | automaticModule('jopt-simple-5.0.4.jar', 'jopt.simple') 64 | } 65 | 66 | // If we are being told a specific vendor then we are probably being run in parallel 67 | if (project.hasProperty('javaVendor') && project.hasProperty('javaVersion')) { 68 | test.javaLauncher.set(javaToolchains.launcherFor { 69 | it.vendor.set(JvmVendorSpec."${project.property('javaVendor').toUpperCase(Locale.ROOT)}" as JvmVendorSpec) 70 | it.languageVersion.set(JavaLanguageVersion.of(project.property('javaVersion') as int)) 71 | it.implementation.set(JvmImplementation.VENDOR_SPECIFIC) 72 | }) 73 | } else if (!project.hasProperty('disable_bulk_tests')) { 74 | configurations { 75 | groovyScript 76 | } 77 | 78 | dependencies { 79 | groovyScript libs.ivy 80 | groovyScript libs.groovy 81 | } 82 | 83 | tasks.register('collectTests', JavaExec) { 84 | classpath = configurations.groovyScript 85 | mainClass = 'groovy.ui.GroovyMain' 86 | args '.github/workflows/aggregate-junit-tests.groovy' 87 | workingDir rootProject.projectDir 88 | } 89 | 90 | ((Map>) VALID_VMS).forEach { javaVendor, javaVersions -> 91 | for (Integer javaVersion in javaVersions) { 92 | var task = tasks.register("test${javaVendor}${javaVersion}", Test) { 93 | testClassesDirs = testing.suites.test.sources.output.classesDirs 94 | classpath = testing.suites.test.sources.runtimeClasspath 95 | useJUnitPlatform() 96 | javaLauncher = javaToolchains.launcherFor { 97 | vendor = JvmVendorSpec."${javaVendor.toUpperCase(Locale.ROOT)}" as JvmVendorSpec 98 | languageVersion = JavaLanguageVersion.of(javaVersion) 99 | implementation = JvmImplementation.VENDOR_SPECIFIC 100 | } 101 | reports.html.outputLocation = rootProject.file("build/test_artifacts/test-reports-${javaVendor}-${javaVersion}/") 102 | reports.junitXml.outputLocation = rootProject.file("build/test_artifacts/test-results-${javaVendor}-${javaVersion}/") 103 | } 104 | test.dependsOn(task) 105 | collectTests.mustRunAfter(task) 106 | } 107 | } 108 | } 109 | 110 | // Hack eclipse into knowing that the gradle deps are modules 111 | eclipse.classpath { 112 | containers 'org.eclipse.buildship.core.gradleclasspathcontainer' 113 | file.whenMerged { entries.findAll { it.kind == 'lib' || it.path == 'org.eclipse.buildship.core.gradleclasspathcontainer' }.each { it.entryAttributes['module'] = 'true' } } 114 | } 115 | -------------------------------------------------------------------------------- /coremods-test/src/test/java/module-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Forge Development LLC 3 | * SPDX-License-Identifier: LGPL-2.1-only 4 | */ 5 | open module net.minecraftforge.coremods.test { 6 | requires cpw.mods.modlauncher; 7 | requires net.minecraftforge.forgespi; 8 | requires net.minecraftforge.coremod; 9 | requires org.junit.jupiter.api; 10 | requires org.apache.logging.log4j; 11 | requires org.objectweb.asm.tree; 12 | requires static org.jetbrains.annotations; 13 | requires net.minecraftforge.unsafe; 14 | requires static net.minecraftforge.coremod.testjar; 15 | requires cpw.mods.securejarhandler; 16 | 17 | provides cpw.mods.modlauncher.api.ITransformationService 18 | with net.minecraftforge.coremod.test.TestTransformerService; 19 | } 20 | -------------------------------------------------------------------------------- /coremods-test/src/test/java/net/minecraftforge/coremod/test/CoreModTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Forge Development LLC 3 | * SPDX-License-Identifier: LGPL-2.1-only 4 | */ 5 | package net.minecraftforge.coremod.test; 6 | 7 | import org.junit.jupiter.api.Test; 8 | 9 | import net.minecraftforge.coremod.testjar.TestClass; 10 | 11 | import static org.junit.jupiter.api.Assertions.*; 12 | 13 | public class CoreModTests extends TestLaunchTransformerBase { 14 | 15 | @Test 16 | public void testModifyMethod() throws Exception { 17 | loadCoremod("test_method_manual.js"); 18 | 19 | if (!isTransformed()) return; 20 | 21 | // Manually iterates the instructions and replace CONST_0 with CONST_1 22 | assertTrue(TestClass.False(), "Transformer failed"); 23 | } 24 | 25 | @Test 26 | public void testInsertRemove() throws Exception { 27 | loadCoremod("test_insert_remove.js"); 28 | 29 | if (!isTransformed()) return; 30 | 31 | // Replaces the push() with increment(), which will make the return {1} 32 | assertArrayEquals(new int[]{1}, TestClass.testCounter(), "Transformer failed"); 33 | } 34 | 35 | @Test 36 | public void testInsertBefore() throws Exception { 37 | loadCoremod("test_insert_before.js"); 38 | 39 | if (!isTransformed()) return; 40 | 41 | // Inserts increment() before the push() call, which will make the return {1, 0} 42 | assertArrayEquals(new int[]{1, 0}, TestClass.testCounter(), "Transformer failed"); 43 | } 44 | 45 | @Test 46 | public void testInsertAfter() throws Exception { 47 | loadCoremod("test_insert_after.js"); 48 | 49 | if (!isTransformed()) return; 50 | 51 | // Inserts increment() after the push() call, which will make the return {0, 1} 52 | assertArrayEquals(new int[]{0, 1}, TestClass.testCounter(), "Transformer failed"); 53 | } 54 | 55 | @Test 56 | public void testLoadData() throws Exception { 57 | loadCoremod("test_load_data.js"); 58 | 59 | if (!isTransformed()) return; 60 | 61 | // Loads a json file containing a string 62 | assertEquals("transformed", TestClass.testString(), "Transformer failed"); 63 | } 64 | 65 | @Test 66 | public void testLoadFile() throws Exception { 67 | loadCoremod("test_load_file.js"); 68 | 69 | if (!isTransformed()) return; 70 | 71 | // Loads a javascript file and uses a function from it 72 | assertEquals("transformed", TestClass.testString(), "Transformer failed"); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /coremods-test/src/test/java/net/minecraftforge/coremod/test/JSFileLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Forge Development LLC 3 | * SPDX-License-Identifier: LGPL-2.1-only 4 | */ 5 | package net.minecraftforge.coremod.test; 6 | 7 | import net.minecraftforge.forgespi.coremod.ICoreModFile; 8 | 9 | import java.io.IOException; 10 | import java.io.Reader; 11 | import java.net.URISyntaxException; 12 | import java.nio.file.Files; 13 | import java.nio.file.Path; 14 | 15 | class JSFileLoader implements ICoreModFile { 16 | private final Path path; 17 | 18 | JSFileLoader(final String path) { 19 | this.path = getPathFromResource(path); 20 | } 21 | 22 | @Override 23 | public Reader readCoreMod() { 24 | try { 25 | return Files.newBufferedReader(this.path); 26 | } catch (IOException e) { 27 | throw new RuntimeException(e); 28 | } 29 | } 30 | 31 | @Override 32 | public Path getPath() { 33 | return this.path; 34 | } 35 | 36 | @Override 37 | public Reader getAdditionalFile(final String fileName) throws IOException { 38 | return Files.newBufferedReader(this.path.getParent().resolve(fileName)); 39 | } 40 | 41 | @Override 42 | public String getOwnerId() { 43 | return "dummy"; 44 | } 45 | 46 | protected static Path getPathFromResource(String resource) { 47 | var cl = JSFileLoader.class.getClassLoader(); 48 | var url = cl.getResource(resource); 49 | if (url == null) 50 | throw new IllegalStateException("Could not find " + resource + " in classloader " + cl); 51 | 52 | try { 53 | return Path.of(url.toURI()); 54 | } catch (URISyntaxException e) { 55 | return sneak(e); 56 | } 57 | } 58 | 59 | @SuppressWarnings("unchecked") 60 | private static R sneak(Throwable e) throws E { 61 | throw (E)e; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /coremods-test/src/test/java/net/minecraftforge/coremod/test/TestLaunchTransformerBase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Forge Development LLC 3 | * SPDX-License-Identifier: LGPL-2.1-only 4 | */ 5 | package net.minecraftforge.coremod.test; 6 | 7 | import cpw.mods.modlauncher.Launcher; 8 | import cpw.mods.modlauncher.api.ITransformer; 9 | import cpw.mods.modlauncher.api.ServiceRunner; 10 | import net.minecraftforge.coremod.CoreModEngine; 11 | import net.minecraftforge.coremod.testjar.TestMarker; 12 | import net.minecraftforge.forgespi.coremod.ICoreModFile; 13 | import net.minecraftforge.securemodules.SecureModuleClassLoader; 14 | import net.minecraftforge.unsafe.UnsafeHacks; 15 | 16 | import org.junit.jupiter.api.AfterEach; 17 | import org.junit.jupiter.api.Assertions; 18 | import org.junit.jupiter.api.BeforeEach; 19 | import org.junit.jupiter.api.TestInfo; 20 | 21 | import java.lang.invoke.MethodHandles; 22 | import java.lang.invoke.MethodType; 23 | import java.nio.file.Path; 24 | import java.util.List; 25 | import java.util.function.BiConsumer; 26 | import java.util.stream.Collectors; 27 | 28 | public class TestLaunchTransformerBase { 29 | private static final String CLASS_NAME = "test.class"; 30 | private static final String METHOD_NAME = "test.method"; 31 | private static final String TRANSFORMED = "test.transformed"; 32 | 33 | private static CoreModEngine cme; 34 | public static List> getTransformers() { 35 | return cme.initializeCoreMods(); 36 | } 37 | 38 | protected static BiConsumer LOAD_CORE_MOD = getLoadCoreMod(); 39 | protected static Runnable RELAUNCH = getRelaunch(); 40 | 41 | @BeforeEach 42 | public void setup(TestInfo testInfo) { 43 | System.setProperty(CLASS_NAME, testInfo.getTestClass().get().getName()); 44 | System.setProperty(METHOD_NAME, testInfo.getTestMethod().get().getName()); 45 | cme = new CoreModEngine(); 46 | } 47 | 48 | @AfterEach 49 | public void teardown() { 50 | System.clearProperty(CLASS_NAME); 51 | System.clearProperty(METHOD_NAME); 52 | cme = null; 53 | } 54 | 55 | protected static BiConsumer getLoadCoreMod() { 56 | try { 57 | var loadCoreMod = CoreModEngine.class.getDeclaredMethod("loadCoreMod", ICoreModFile.class); 58 | UnsafeHacks.setAccessible(loadCoreMod); 59 | return (engine, name) -> { 60 | try { 61 | loadCoreMod.invoke(engine, new JSFileLoader(name)); 62 | } catch (Throwable e) { 63 | sneak(e); 64 | } 65 | }; 66 | } catch (Throwable e) { 67 | return sneak(e); 68 | } 69 | } 70 | 71 | protected static Runnable getRelaunch() { 72 | try { 73 | // Bypass some logging, if log4j.xml doesn't override, cuz it feels like being stupid. 74 | var ctr = Launcher.class.getDeclaredConstructor(); 75 | UnsafeHacks.setAccessible(ctr); 76 | var run = Launcher.class.getDeclaredMethod("run", String[].class); 77 | UnsafeHacks.setAccessible(run); 78 | return () -> { 79 | try { 80 | run.invoke(ctr.newInstance(), (Object)new String[] {"--version", "1.0", "--launchTarget", "testharness"}); 81 | } catch (Throwable e) { 82 | sneak(e); 83 | } 84 | }; 85 | } catch (Throwable e) { 86 | return sneak(e); 87 | } 88 | } 89 | 90 | protected void loadCoremod(String name) { 91 | if (!getTransformed()) 92 | LOAD_CORE_MOD.accept(cme, name); 93 | } 94 | 95 | private static final String getPath(Class clz) { 96 | try { 97 | return Path.of(clz.getProtectionDomain().getCodeSource().getLocation().toURI()).toAbsolutePath().toString(); 98 | } catch (Throwable e) { 99 | return sneak(e); 100 | } 101 | } 102 | 103 | protected boolean isTransformed() { 104 | if (getTransformed()) 105 | return true; 106 | 107 | var jars = List.of( 108 | getPath(TestTransformerService.class), 109 | getPath(TestMarker.class) 110 | ); 111 | 112 | System.setProperty("test.harness.game", jars.stream().collect(Collectors.joining(","))); 113 | System.setProperty("test.harness.callable", Callback.class.getName()); 114 | 115 | try { 116 | System.setProperty(TRANSFORMED, "true"); 117 | System.out.println(System.getProperty(CLASS_NAME) + "." + System.getProperty(METHOD_NAME)); 118 | RELAUNCH.run(); 119 | } catch (Throwable e) { 120 | sneak(e); 121 | } finally { 122 | System.clearProperty(TRANSFORMED); 123 | } 124 | 125 | return false; 126 | } 127 | 128 | @SuppressWarnings("unchecked") 129 | protected Class getTransformedClass(String name) { 130 | var ccl = Thread.currentThread().getContextClassLoader(); 131 | Assertions.assertInstanceOf(SecureModuleClassLoader.class, ccl, "ContextClassLoader is not TransformingClassLoader"); 132 | try { 133 | return (Class) Class.forName(name, false, ccl); 134 | } catch (Throwable e) { 135 | return sneak(e); 136 | } 137 | } 138 | 139 | private boolean getTransformed() { 140 | return System.getProperty(TRANSFORMED) != null; 141 | } 142 | 143 | public static final class Callback { 144 | public static ServiceRunner supplier() { 145 | return () -> { 146 | try { 147 | var ccl = Thread.currentThread().getContextClassLoader(); 148 | var cls = Class.forName(System.getProperty(CLASS_NAME), true, ccl); 149 | var inst = cls.getDeclaredConstructor().newInstance(); 150 | var handle = MethodHandles.lookup().findVirtual(cls, System.getProperty(METHOD_NAME), MethodType.methodType(void.class)); 151 | handle.invoke(inst); 152 | } catch (Throwable e) { 153 | sneak(e); 154 | } 155 | }; 156 | } 157 | } 158 | 159 | @SuppressWarnings("unchecked") 160 | private static R sneak(Throwable e) throws E { 161 | throw (E)e; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /coremods-test/src/test/java/net/minecraftforge/coremod/test/TestTransformerService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Forge Development LLC 3 | * SPDX-License-Identifier: LGPL-2.1-only 4 | */ 5 | package net.minecraftforge.coremod.test; 6 | 7 | import cpw.mods.jarhandling.SecureJar; 8 | import cpw.mods.modlauncher.api.IEnvironment; 9 | import cpw.mods.modlauncher.api.IModuleLayerManager; 10 | import cpw.mods.modlauncher.api.ITransformationService; 11 | import cpw.mods.modlauncher.api.ITransformer; 12 | import cpw.mods.modlauncher.api.IncompatibleEnvironmentException; 13 | import org.jetbrains.annotations.NotNull; 14 | import java.nio.file.FileSystems; 15 | import java.util.Arrays; 16 | import java.util.List; 17 | import java.util.Set; 18 | 19 | public class TestTransformerService implements ITransformationService { 20 | @NotNull 21 | @Override 22 | public String name() { 23 | return "coremodtest"; 24 | } 25 | 26 | @Override 27 | public void initialize(final IEnvironment environment) { 28 | } 29 | 30 | @Override 31 | public void onLoad(final IEnvironment env, final Set otherServices) throws IncompatibleEnvironmentException { 32 | } 33 | 34 | @SuppressWarnings({ "rawtypes", "unchecked" }) 35 | @NotNull 36 | @Override 37 | public List transformers() { 38 | return (List)TestLaunchTransformerBase.getTransformers(); 39 | } 40 | 41 | @SuppressWarnings("resource") 42 | @Override 43 | public List beginScanning(IEnvironment environment) { 44 | if (System.getProperty("test.harness.plugin")!=null) { 45 | return List.of(new Resource(IModuleLayerManager.Layer.PLUGIN, 46 | Arrays.stream(System.getProperty("test.harness.plugin").split(",")) 47 | .map(FileSystems.getDefault()::getPath) 48 | .map(SecureJar::from) 49 | .toList())); 50 | } else if (System.getProperty("test.harness.service")!=null) { 51 | return List.of(new Resource(IModuleLayerManager.Layer.SERVICE, 52 | Arrays.stream(System.getProperty("test.harness.service").split(",")) 53 | .map(FileSystems.getDefault()::getPath) 54 | .map(SecureJar::from) 55 | .toList())); 56 | } else { 57 | return List.of(); 58 | } 59 | } 60 | 61 | @SuppressWarnings("resource") 62 | @Override 63 | public List completeScan(IModuleLayerManager layerManager) { 64 | if (System.getProperty("test.harness.game")!=null) { 65 | return List.of(new Resource(IModuleLayerManager.Layer.GAME, 66 | Arrays.stream(System.getProperty("test.harness.game").split(",")) 67 | .map(FileSystems.getDefault()::getPath) 68 | .map(SecureJar::from) 69 | .toList())); 70 | } else { 71 | return List.of(); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /coremods-test/src/test/javascript/test_insert_after.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Forge Development LLC 3 | * SPDX-License-Identifier: LGPL-2.1-only 4 | */ 5 | function initializeCoreMod() { 6 | return { 7 | 'coremodmethod': { 8 | 'target': { 9 | 'type': 'METHOD', 10 | 'class': 'net.minecraftforge.coremod.testjar.TestClass', 11 | 'methodName': 'testCounter', 12 | 'methodDesc': '()[I' 13 | }, 14 | 'transformer': function(method) { 15 | var Counter = "net/minecraftforge/coremod/testjar/Counter"; 16 | var ASMAPI = Java.type('net.minecraftforge.coremod.api.ASMAPI'); 17 | var VarInsnNode = Java.type('org.objectweb.asm.tree.VarInsnNode'); 18 | var Opcodes = Java.type('org.objectweb.asm.Opcodes'); 19 | 20 | var lst = ASMAPI.listOf( 21 | new VarInsnNode(Opcodes.ALOAD, 0), 22 | ASMAPI.buildMethodCall(Counter, "increment", "()V", ASMAPI.MethodType.VIRTUAL) 23 | ); 24 | 25 | if (ASMAPI.insertInsnList(method, ASMAPI.MethodType.VIRTUAL, Counter, "push", "()V", lst, ASMAPI.InsertMode.INSERT_AFTER) === false) { 26 | throw "MethodInsnNode for push not found!"; 27 | } 28 | 29 | print("Inserted increment after push"); 30 | 31 | return method; 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /coremods-test/src/test/javascript/test_insert_before.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Forge Development LLC 3 | * SPDX-License-Identifier: LGPL-2.1-only 4 | */ 5 | function initializeCoreMod() { 6 | return { 7 | 'coremodmethod': { 8 | 'target': { 9 | 'type': 'METHOD', 10 | 'class': 'net.minecraftforge.coremod.testjar.TestClass', 11 | 'methodName': 'testCounter', 12 | 'methodDesc': '()[I' 13 | }, 14 | 'transformer': function(method) { 15 | var Counter = "net/minecraftforge/coremod/testjar/Counter"; 16 | var ASMAPI = Java.type('net.minecraftforge.coremod.api.ASMAPI'); 17 | var VarInsnNode = Java.type('org.objectweb.asm.tree.VarInsnNode'); 18 | var Opcodes = Java.type('org.objectweb.asm.Opcodes'); 19 | 20 | var lst = ASMAPI.listOf( 21 | ASMAPI.buildMethodCall(Counter, "increment", "()V", ASMAPI.MethodType.VIRTUAL), 22 | new VarInsnNode(Opcodes.ALOAD, 0) 23 | ); 24 | 25 | if (ASMAPI.insertInsnList(method, ASMAPI.MethodType.VIRTUAL, Counter, "push", "()V", lst, ASMAPI.InsertMode.INSERT_BEFORE) === false) { 26 | throw "MethodInsnNode for push not found!"; 27 | } 28 | 29 | print("Inserted increment before push"); 30 | 31 | return method; 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /coremods-test/src/test/javascript/test_insert_remove.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Forge Development LLC 3 | * SPDX-License-Identifier: LGPL-2.1-only 4 | */ 5 | function initializeCoreMod() { 6 | return { 7 | 'coremodmethod': { 8 | 'target': { 9 | 'type': 'METHOD', 10 | 'class': 'net.minecraftforge.coremod.testjar.TestClass', 11 | 'methodName': 'testCounter', 12 | 'methodDesc': '()[I' 13 | }, 14 | 'transformer': function(method) { 15 | var Counter = "net/minecraftforge/coremod/testjar/Counter"; 16 | var ASMAPI = Java.type('net.minecraftforge.coremod.api.ASMAPI'); 17 | var VarInsnNode = Java.type('org.objectweb.asm.tree.VarInsnNode'); 18 | var Opcodes = Java.type('org.objectweb.asm.Opcodes'); 19 | 20 | var lst = ASMAPI.listOf( 21 | ASMAPI.buildMethodCall(Counter, "increment", "()V", ASMAPI.MethodType.VIRTUAL) 22 | ); 23 | 24 | if (ASMAPI.insertInsnList(method, ASMAPI.MethodType.VIRTUAL, Counter, "push", "()V", lst, ASMAPI.InsertMode.REMOVE_ORIGINAL) === false) { 25 | throw "MethodInsnNode for push not found!"; 26 | } 27 | print("Replaced push with increment"); 28 | 29 | return method; 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /coremods-test/src/test/javascript/test_load_data.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Forge Development LLC 3 | * SPDX-License-Identifier: LGPL-2.1-only 4 | */ 5 | function initializeCoreMod() { 6 | var ASMAPI = Java.type('net.minecraftforge.coremod.api.ASMAPI'); 7 | // Why is this arbitrarily limited to being loaded at start? 8 | var data = ASMAPI.loadData('test_load_data.json') 9 | if (data == null) 10 | throw "Could not load data"; 11 | 12 | return { 13 | 'coremodmethod': { 14 | 'target': { 15 | 'type': 'METHOD', 16 | 'class': 'net.minecraftforge.coremod.testjar.TestClass', 17 | 'methodName': 'testString', 18 | 'methodDesc': '()Ljava/lang/String;' 19 | }, 20 | 'transformer': function(method) { 21 | var Opcodes = Java.type('org.objectweb.asm.Opcodes'); 22 | var LdcInsnNode = Java.type('org.objectweb.asm.tree.LdcInsnNode'); 23 | 24 | var arrayLength = method.instructions.size(); 25 | for (var i = 0; i < arrayLength; ++i) { 26 | var instruction = method.instructions.get(i); 27 | if (instruction.getOpcode() == Opcodes.LDC) { 28 | var newInstruction = new LdcInsnNode(data.value); 29 | method.instructions.insertBefore(instruction, newInstruction); 30 | method.instructions.remove(instruction); 31 | print("Manually replaced string with " + data.value); 32 | break; 33 | } 34 | } 35 | return method; 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /coremods-test/src/test/javascript/test_load_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "value": "transformed" 3 | } -------------------------------------------------------------------------------- /coremods-test/src/test/javascript/test_load_file.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Forge Development LLC 3 | * SPDX-License-Identifier: LGPL-2.1-only 4 | */ 5 | function initializeCoreMod() { 6 | var ASMAPI = Java.type('net.minecraftforge.coremod.api.ASMAPI'); 7 | // Why is this arbitrarily limited to being loaded at start? 8 | if (!ASMAPI.loadFile('test_load_file_extra.js')) 9 | throw "Could not load extra script"; 10 | 11 | return { 12 | 'coremodmethod': { 13 | 'target': { 14 | 'type': 'METHOD', 15 | 'class': 'net.minecraftforge.coremod.testjar.TestClass', 16 | 'methodName': 'testString', 17 | 'methodDesc': '()Ljava/lang/String;' 18 | }, 19 | 'transformer': function(method) { 20 | var Opcodes = Java.type('org.objectweb.asm.Opcodes'); 21 | var LdcInsnNode = Java.type('org.objectweb.asm.tree.LdcInsnNode'); 22 | 23 | var arrayLength = method.instructions.size(); 24 | for (var i = 0; i < arrayLength; ++i) { 25 | var instruction = method.instructions.get(i); 26 | if (instruction.getOpcode() == Opcodes.LDC) { 27 | var newInstruction = new LdcInsnNode(extraFunction()); 28 | method.instructions.insertBefore(instruction, newInstruction); 29 | method.instructions.remove(instruction); 30 | print("Manually replaced string with " + extraFunction()); 31 | break; 32 | } 33 | } 34 | return method; 35 | } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /coremods-test/src/test/javascript/test_load_file_extra.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Forge Development LLC 3 | * SPDX-License-Identifier: LGPL-2.1-only 4 | */ 5 | function extraFunction() { 6 | return "transformed"; 7 | } -------------------------------------------------------------------------------- /coremods-test/src/test/javascript/test_method_manual.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Forge Development LLC 3 | * SPDX-License-Identifier: LGPL-2.1-only 4 | */ 5 | function initializeCoreMod() { 6 | return { 7 | 'coremodmethod': { 8 | 'target': { 9 | 'type': 'METHOD', 10 | 'class': 'net.minecraftforge.coremod.testjar.TestClass', 11 | 'methodName': 'False', 12 | 'methodDesc': '()Z' 13 | }, 14 | 'transformer': function(method) { 15 | var Opcodes = Java.type('org.objectweb.asm.Opcodes'); 16 | var arrayLength = method.instructions.size(); 17 | for (var i = 0; i < arrayLength; ++i) { 18 | var instruction = method.instructions.get(i); 19 | if (instruction.getOpcode() == Opcodes.ICONST_0) { 20 | var InsnNode = Java.type('org.objectweb.asm.tree.InsnNode'); 21 | var newInstruction = new InsnNode(Opcodes.ICONST_1); 22 | method.instructions.insertBefore(instruction, newInstruction); 23 | method.instructions.remove(instruction); 24 | print("Manually replaced false with true"); 25 | break; 26 | } 27 | } 28 | return method; 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /coremods-test/src/test/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.caching=true 2 | org.gradle.parallel=true 3 | org.gradle.configuration-cache=true 4 | org.gradle.configureondemand=true 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinecraftForge/CoreMods/298ff2fdb9e49e9315b35c49f736a48e25af47bc/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.12-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 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | maven { url = 'https://maven.minecraftforge.net/' } 5 | } 6 | } 7 | 8 | plugins { 9 | id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' 10 | } 11 | 12 | dependencyResolutionManagement { 13 | versionCatalogs { 14 | libs { 15 | plugin('license', 'net.minecraftforge.licenser').version('1.0.1') 16 | plugin('gradleutils', 'net.minecraftforge.gradleutils').version('[2.3,2.4)') 17 | plugin('modules', 'org.gradlex.extra-java-module-info').version('1.9') 18 | plugin('versions', 'com.github.ben-manes.versions').version('0.51.0') 19 | 20 | version('asm', '9.7.1') 21 | library('asm', 'org.ow2.asm', 'asm' ).versionRef('asm') 22 | library('asm-tree', 'org.ow2.asm', 'asm-tree' ).versionRef('asm') 23 | library('asm-commons', 'org.ow2.asm', 'asm-commons').versionRef('asm') 24 | bundle('asm', ['asm', 'asm-tree', 'asm-commons']) 25 | 26 | version('junit', '5.11.4') 27 | library('junit-api', 'org.junit.jupiter', 'junit-jupiter-api').versionRef('junit') 28 | library('junit-engine', 'org.junit.jupiter', 'junit-jupiter-engine').versionRef('junit') 29 | library('junit-platform-launcher', 'org.junit.platform:junit-platform-launcher:1.11.4') 30 | bundle('junit-runtime', ['junit-engine', 'junit-platform-launcher']) 31 | 32 | library('unsafe', 'net.minecraftforge:unsafe:0.9.2') 33 | library('securemodules', 'net.minecraftforge:securemodules:2.2.20') // Needs unsafe 34 | /* 35 | library('gson', 'com.google.code.gson:gson:2.10.1') 36 | library('jopt-simple', 'net.sf.jopt-simple:jopt-simple:5.0.4') 37 | */ 38 | 39 | library('forgespi', 'net.minecraftforge:forgespi:7.1.0') 40 | library('modlauncher', 'net.minecraftforge:modlauncher:10.1.1') // Needs securemodules 41 | library('nulls', 'org.jetbrains:annotations:26.0.1') 42 | library('nashorn', 'org.openjdk.nashorn:nashorn-core:15.4') // Needed by coremods, because the JRE no longer ships JS 43 | 44 | // Used by our test aggrigator scripts 45 | library('ivy', 'org.apache.ivy:ivy:2.5.3') 46 | library('groovy', 'org.codehaus.groovy:groovy-all:3.0.23') 47 | 48 | version('log4j', '2.17.0') // Needs to be kept in step with modlauncher because of its config.. TODO: Figure out why? 49 | library('log4j-api', 'org.apache.logging.log4j', 'log4j-api' ).versionRef('log4j') 50 | library('log4j-core', 'org.apache.logging.log4j', 'log4j-core').versionRef('log4j') 51 | } 52 | } 53 | } 54 | 55 | rootProject.name = 'CoreMods' 56 | include 'coremods-test' 57 | include 'coremods-test-jar' 58 | -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Forge Development LLC 3 | * SPDX-License-Identifier: LGPL-2.1-only 4 | */ 5 | module net.minecraftforge.coremod { 6 | // CoreMods framework 7 | exports net.minecraftforge.coremod; 8 | // ASMAPI 9 | exports net.minecraftforge.coremod.api; 10 | 11 | requires cpw.mods.modlauncher; 12 | requires net.minecraftforge.forgespi; 13 | requires org.apache.logging.log4j; 14 | requires org.openjdk.nashorn; 15 | requires org.objectweb.asm.util; 16 | 17 | requires static org.jetbrains.annotations; 18 | 19 | provides net.minecraftforge.forgespi.coremod.ICoreModProvider 20 | with net.minecraftforge.coremod.CoreModProvider; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/net/minecraftforge/coremod/CoreMod.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Forge Development LLC 3 | * SPDX-License-Identifier: LGPL-2.1-only 4 | */ 5 | package net.minecraftforge.coremod; 6 | 7 | import cpw.mods.modlauncher.api.ITransformer; 8 | import net.minecraftforge.coremod.api.ASMAPI; 9 | import net.minecraftforge.coremod.transformer.CoreModClassTransformer; 10 | import net.minecraftforge.coremod.transformer.CoreModFieldTransformer; 11 | import net.minecraftforge.coremod.transformer.CoreModMethodTransformer; 12 | import net.minecraftforge.forgespi.coremod.ICoreModFile; 13 | import org.apache.logging.log4j.Level; 14 | import org.apache.logging.log4j.LogManager; 15 | import org.apache.logging.log4j.Logger; 16 | import org.apache.logging.log4j.Marker; 17 | import org.apache.logging.log4j.MarkerManager; 18 | import org.jetbrains.annotations.Nullable; 19 | 20 | import javax.script.Bindings; 21 | import javax.script.Invocable; 22 | import javax.script.ScriptEngine; 23 | import javax.script.ScriptException; 24 | import java.io.IOException; 25 | import java.io.Reader; 26 | import java.nio.file.Path; 27 | import java.util.Collections; 28 | import java.util.List; 29 | import java.util.Map; 30 | import java.util.Set; 31 | import java.util.function.Function; 32 | import java.util.stream.Collectors; 33 | import java.util.stream.Stream; 34 | 35 | /** 36 | * Represents a coremod. 37 | */ 38 | public class CoreMod { 39 | /** 40 | * Used to log events inside coremods. 41 | * 42 | * @see ASMAPI#log(String, String, Object...) 43 | */ 44 | @SuppressWarnings("exports") 45 | public static final Marker COREMODLOG = MarkerManager.getMarker("COREMODLOG").addParents(MarkerManager.getMarker("COREMOD")); 46 | 47 | private final ICoreModFile file; 48 | private final ScriptEngine scriptEngine; 49 | private Map javaScript; 50 | private boolean loaded = false; 51 | private Exception error; 52 | private Logger logger; 53 | 54 | CoreMod(final ICoreModFile file, final ScriptEngine scriptEngine) { 55 | this.file = file; 56 | this.scriptEngine = scriptEngine; 57 | } 58 | 59 | /** 60 | * Gets the path to the coremod file. 61 | * 62 | * @return The path 63 | */ 64 | public Path getPath() { 65 | return this.file.getPath(); 66 | } 67 | 68 | @SuppressWarnings("unchecked") 69 | void initialize() { 70 | this.logger = LogManager.getLogger("net.minecraftforge.coremod.CoreMod." + this.file.getOwnerId()); 71 | try { 72 | // TODO would it be better to setCoreMod before eval? this would allow loadFile and loadData to be run in the global context 73 | this.scriptEngine.eval(this.file.readCoreMod()); 74 | CoreModTracker.setCoreMod(this); 75 | this.javaScript = (Map) ((Invocable) this.scriptEngine).invokeFunction("initializeCoreMod"); 76 | CoreModTracker.clearCoreMod(); 77 | this.loaded = true; 78 | } catch (IOException | ScriptException | NoSuchMethodException e) { 79 | this.loaded = false; 80 | this.error = e; 81 | } 82 | } 83 | 84 | 85 | List> buildTransformers() { 86 | if (!this.loaded) return Collections.emptyList(); 87 | return this.javaScript.entrySet().stream().map(this::buildCore).collect(Collectors.toList()); 88 | } 89 | 90 | @SuppressWarnings("unchecked") 91 | private ITransformer buildCore(Map.Entry entry) { 92 | final String coreName = entry.getKey(); 93 | final Bindings data = entry.getValue(); 94 | final Map targetData = (Map) data.get("target"); 95 | final ITransformer.TargetType targetType = ITransformer.TargetType.valueOf((String) targetData.get("type")); 96 | final Set targets; 97 | final Bindings function = (Bindings) data.get("transformer"); 98 | switch (targetType) { 99 | case CLASS: 100 | if (targetData.containsKey("names")) { 101 | Function, Map> names = NashornFactory.getFunction((Bindings) targetData.get("names")); 102 | targets = names.apply(targetData).values().stream().map(o -> (String) o).map(ITransformer.Target::targetClass).collect(Collectors.toSet()); 103 | } else 104 | targets = Stream.of(ITransformer.Target.targetClass((String) targetData.get("name"))).collect(Collectors.toSet()); 105 | return new CoreModClassTransformer(this, coreName, targets, NashornFactory.getFunction(function)); 106 | case METHOD: 107 | targets = Collections.singleton(ITransformer.Target.targetMethod( 108 | (String) targetData.get("class"), ASMAPI.mapMethod((String) targetData.get("methodName")), (String) targetData.get("methodDesc"))); 109 | return new CoreModMethodTransformer(this, coreName, targets, NashornFactory.getFunction(function)); 110 | case FIELD: 111 | targets = Collections.singleton(ITransformer.Target.targetField( 112 | (String) targetData.get("class"), ASMAPI.mapField((String) targetData.get("fieldName")))); 113 | return new CoreModFieldTransformer(this, coreName, targets, NashornFactory.getFunction(function)); 114 | default: 115 | throw new RuntimeException("Unimplemented target type " + targetData); 116 | } 117 | } 118 | 119 | /** 120 | * Returns whether the coremod has an error. Usually paired with {@link #getError()}. 121 | * 122 | * @return {@code true} if the coremod has an error 123 | */ 124 | public boolean hasError() { 125 | return !this.loaded; 126 | } 127 | 128 | /** 129 | * Returns the error that occurred while loading the coremod. Only consider valid if {@link #hasError()} returns 130 | * {@code true}. 131 | * 132 | * @return The error that occurred 133 | */ 134 | public Exception getError() { 135 | return this.error; 136 | } 137 | 138 | /** 139 | * Gets the coremod file. 140 | * 141 | * @return The coremod file 142 | */ 143 | @SuppressWarnings("exports") 144 | public ICoreModFile getFile() { 145 | return this.file; 146 | } 147 | 148 | /** 149 | * Loads an additional file into the script engine. 150 | * 151 | * @param fileName The file to load 152 | * @return {@code true} if loading was successful 153 | * 154 | * @throws ScriptException If the script engine encounters an error, usually due to a syntax error in the script 155 | * @throws IOException If an I/O error occurs while reading the file, usually due to a corrupt or missing file 156 | */ 157 | public boolean loadAdditionalFile(final String fileName) throws ScriptException, IOException { 158 | // why does this method return a boolean if we're going to crash anyways on load failure? 159 | // it looks like the case of the coremod not being tracked is never reached 160 | if (this.loaded) return false; 161 | 162 | Reader additional = this.file.getAdditionalFile(fileName); 163 | this.scriptEngine.eval(additional); 164 | return true; 165 | } 166 | 167 | /** 168 | * Loads additional JSON data from a file into an {@link Object}. 169 | * 170 | * @param fileName The file to load 171 | * @return The loaded JSON data if successful, or {@code null} if not 172 | * 173 | * @throws ScriptException If the parsed JSON data is malformed 174 | * @throws IOException If an I/O error occurs while reading the file, usually due to a corrupt or missing file 175 | */ 176 | @Nullable 177 | public Object loadAdditionalData(final String fileName) throws ScriptException, IOException { 178 | // again with this shit, dude! why are we going to return null?? 179 | // isn't the coremod always going to be tracked if it calls ASMAPI.loadData from itself? 180 | if (this.loaded) return null; 181 | 182 | Reader additional = this.file.getAdditionalFile(fileName); 183 | 184 | char[] buf = new char[4096]; 185 | StringBuilder builder = new StringBuilder(); 186 | int numChars; 187 | while ((numChars = additional.read(buf)) >= 0) 188 | builder.append(buf, 0, numChars); 189 | String str = builder.toString(); 190 | 191 | return this.scriptEngine.eval("tmp_json_loading_variable = " + str + ";"); 192 | } 193 | 194 | /** 195 | * Logs a message from the coremod. 196 | * 197 | * @param level The log level 198 | * @param message The message 199 | * @param args Any formatting arguments 200 | * @see ASMAPI#log(String, String, Object...) 201 | */ 202 | public void logMessage(final String level, final String message, final Object[] args) { 203 | this.logger.log(Level.getLevel(level), COREMODLOG, message, args); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/main/java/net/minecraftforge/coremod/CoreModEngine.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Forge Development LLC 3 | * SPDX-License-Identifier: LGPL-2.1-only 4 | */ 5 | package net.minecraftforge.coremod; 6 | 7 | import cpw.mods.modlauncher.Launcher; 8 | import cpw.mods.modlauncher.api.ITransformer; 9 | import cpw.mods.modlauncher.api.TypesafeMap; 10 | import net.minecraftforge.forgespi.coremod.ICoreModFile; 11 | import org.apache.logging.log4j.LogManager; 12 | import org.apache.logging.log4j.Logger; 13 | import org.apache.logging.log4j.Marker; 14 | import org.apache.logging.log4j.MarkerManager; 15 | 16 | import javax.script.ScriptContext; 17 | import javax.script.ScriptEngine; 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | import java.util.Set; 21 | import java.util.stream.Collectors; 22 | 23 | /** 24 | * The global coremod engine, responsible for loading and initializing coremods. 25 | */ 26 | public class CoreModEngine { 27 | private static final Logger LOGGER = LogManager.getLogger(); 28 | private static final Marker COREMOD = MarkerManager.getMarker("COREMOD"); 29 | private List coreMods = new ArrayList<>(); 30 | 31 | static final Set ALLOWED_PACKAGES = Set.of( 32 | "java.util", 33 | "java.util.function", 34 | "org.objectweb.asm.util" // ASM util has nice debugging things like Trace visitors 35 | ); 36 | 37 | static final Set ALLOWED_CLASSES = Set.of( 38 | "net.minecraftforge.coremod.api.ASMAPI", "org.objectweb.asm.Opcodes", 39 | 40 | // Editing the code of methods 41 | "org.objectweb.asm.tree.AbstractInsnNode", "org.objectweb.asm.tree.FieldInsnNode", 42 | "org.objectweb.asm.tree.FrameNode", "org.objectweb.asm.tree.IincInsnNode", 43 | "org.objectweb.asm.tree.InsnNode", "org.objectweb.asm.tree.IntInsnNode", 44 | "org.objectweb.asm.tree.InsnList", "org.objectweb.asm.tree.InvokeDynamicInsnNode", 45 | "org.objectweb.asm.tree.JumpInsnNode", "org.objectweb.asm.tree.LabelNode", 46 | "org.objectweb.asm.tree.LdcInsnNode", "org.objectweb.asm.tree.LineNumberNode", 47 | "org.objectweb.asm.tree.LocalVariableAnnotationNode", "org.objectweb.asm.tree.LocalVariableNode", 48 | "org.objectweb.asm.tree.LookupSwitchInsnNode", "org.objectweb.asm.tree.MethodInsnNode", 49 | "org.objectweb.asm.tree.MultiANewArrayInsnNode", "org.objectweb.asm.tree.TableSwitchInsnNode", 50 | "org.objectweb.asm.tree.TryCatchBlockNode", "org.objectweb.asm.tree.TypeAnnotationNode", 51 | "org.objectweb.asm.tree.TypeInsnNode", "org.objectweb.asm.tree.VarInsnNode", 52 | 53 | // Adding new fields to classes 54 | "org.objectweb.asm.tree.FieldNode", 55 | 56 | // Adding new methods to classes 57 | "org.objectweb.asm.tree.MethodNode", "org.objectweb.asm.tree.ParameterNode", 58 | 59 | // Misc stuff referenced in above classes that's probably useful 60 | "org.objectweb.asm.Attribute", "org.objectweb.asm.Handle", 61 | "org.objectweb.asm.Label", "org.objectweb.asm.Type", 62 | "org.objectweb.asm.TypePath", "org.objectweb.asm.TypeReference" 63 | ); 64 | 65 | /** 66 | * Whether to preserve the legacy behavior of 67 | * {@link net.minecraftforge.coremod.api.ASMAPI#findFirstInstructionBefore(org.objectweb.asm.tree.MethodNode, int, 68 | * int)} for backwards-compatibility. 69 | *

70 | * In Forge's case, this is set by FML in Minecraft 1.21.1 and earlier, but not in 1.21.3 and later. 71 | * 72 | * @see net.minecraftforge.coremod.api.ASMAPI#findFirstInstructionBefore(org.objectweb.asm.tree.MethodNode, int, 73 | * int) 74 | */ 75 | public static final boolean DO_NOT_FIX_INSNBEFORE = shouldntFixInsnBefore(); 76 | 77 | private static final boolean shouldntFixInsnBefore() { 78 | if (Launcher.INSTANCE == null) 79 | return false; 80 | 81 | var blackboardVar = Launcher.INSTANCE.blackboard().get(TypesafeMap.Key.getOrCreate(Launcher.INSTANCE.blackboard(), "coremods.use_old_findFirstInstructionBefore", Boolean.class)); 82 | if (blackboardVar.isPresent() && blackboardVar.get()) { 83 | LOGGER.debug("CoreMods will preserve legacy behavior of ASMAPI.findFirstInstructionBefore for backwards-compatibility"); 84 | return true; 85 | } 86 | return false; 87 | } 88 | 89 | void loadCoreMod(ICoreModFile coremod) { 90 | // We have a factory per coremod, to provide namespace and functional isolation between coremods 91 | final ScriptEngine scriptEngine = NashornFactory.createEngine(); 92 | final ScriptContext jsContext = scriptEngine.getContext(); 93 | // remove the load, loadWithNewGlobal, exit and quit methods from javascript. 94 | // They don't serve a useful purpose and can cause annoying holes in what is 95 | // meant to be a sandboxed environment. 96 | jsContext.removeAttribute("load", jsContext.getAttributesScope("load")); 97 | jsContext.removeAttribute("quit", jsContext.getAttributesScope("quit")); 98 | jsContext.removeAttribute("loadWithNewGlobal", jsContext.getAttributesScope("loadWithNewGlobal")); 99 | jsContext.removeAttribute("exit", jsContext.getAttributesScope("exit")); 100 | this.coreMods.add(new CoreMod(coremod, scriptEngine)); 101 | } 102 | 103 | static boolean checkClass(String s) { 104 | return ALLOWED_CLASSES.contains(s) || (s.lastIndexOf('.') != -1 && ALLOWED_PACKAGES.contains(s.substring(0, s.lastIndexOf('.')))); 105 | } 106 | 107 | /** 108 | * Initializes all coremods that were added by any {@link CoreModProvider}s. 109 | * 110 | * @return A list of transformers given by the coremods 111 | */ 112 | public List> initializeCoreMods() { 113 | for (CoreMod coreMod : this.coreMods) this.initialize(coreMod); 114 | return this.coreMods.stream().map(CoreMod::buildTransformers).flatMap(List::stream).collect(Collectors.toList()); 115 | } 116 | 117 | private void initialize(final CoreMod coreMod) { 118 | LOGGER.debug(COREMOD, "Loading CoreMod from {}", coreMod.getPath()); 119 | coreMod.initialize(); 120 | if (coreMod.hasError()) { 121 | LOGGER.error(COREMOD, "Error occurred initializing CoreMod", coreMod.getError()); 122 | } else { 123 | LOGGER.debug(COREMOD, "CoreMod loaded successfully"); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/net/minecraftforge/coremod/CoreModProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Forge Development LLC 3 | * SPDX-License-Identifier: LGPL-2.1-only 4 | */ 5 | package net.minecraftforge.coremod; 6 | 7 | import cpw.mods.modlauncher.api.ITransformer; 8 | import net.minecraftforge.forgespi.coremod.ICoreModFile; 9 | import net.minecraftforge.forgespi.coremod.ICoreModProvider; 10 | 11 | import java.util.List; 12 | 13 | /** 14 | * Exposes the CoreMods system to external systems via ForgeSPI (i.e. FML). 15 | */ 16 | public class CoreModProvider implements ICoreModProvider { 17 | private final CoreModEngine engine = new CoreModEngine(); 18 | 19 | @SuppressWarnings("exports") 20 | @Override 21 | public void addCoreMod(final ICoreModFile file) { 22 | this.engine.loadCoreMod(file); 23 | } 24 | 25 | @Override 26 | public List> getCoreModTransformers() { 27 | return this.engine.initializeCoreMods(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/net/minecraftforge/coremod/CoreModTracker.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Forge Development LLC 3 | * SPDX-License-Identifier: LGPL-2.1-only 4 | */ 5 | package net.minecraftforge.coremod; 6 | 7 | import org.jetbrains.annotations.Nullable; 8 | 9 | import javax.script.ScriptException; 10 | import java.io.IOException; 11 | 12 | /** 13 | * Tracks the current coremod being processed. 14 | */ 15 | public class CoreModTracker { 16 | private static final ThreadLocal LOCAL = ThreadLocal.withInitial(CoreModTracker::new); 17 | 18 | private CoreMod tracked; 19 | 20 | /** 21 | * Sets the coremod currently being processed. 22 | * 23 | * @param coreMod The coremod to track 24 | */ 25 | public static void setCoreMod(CoreMod coreMod) { 26 | LOCAL.get().tracked = coreMod; 27 | } 28 | 29 | /** 30 | * Clears the coremod currently being processed. 31 | */ 32 | public static void clearCoreMod() { 33 | LOCAL.get().tracked = null; 34 | } 35 | 36 | /** 37 | * Loads a script file by name. This will be loaded relative to the coremod's path. 38 | * 39 | * @param file The file to load 40 | * @return True if the file was loaded, false otherwise 41 | * 42 | * @throws ScriptException If the script engine encounters an error, usually due to a syntax error in the script 43 | * @throws IOException If an I/O error occurs while reading the file, usually due to a corrupt or missing file 44 | * @see net.minecraftforge.coremod.api.ASMAPI#loadFile(String) 45 | */ 46 | public static boolean loadFileByName(final String file) throws ScriptException, IOException { 47 | final CoreMod tracked = LOCAL.get().tracked; 48 | if (tracked != null) { 49 | return tracked.loadAdditionalFile(file); 50 | } 51 | return false; 52 | } 53 | 54 | /** 55 | * Loads a JSON data file by name. This will be loaded relative to the coremod's path. 56 | * 57 | * @param file The file to load 58 | * @return The loaded JSON data if successful, or {@code null} if not 59 | * 60 | * @throws ScriptException If the parsed JSON data is malformed 61 | * @throws IOException If an I/O error occurs while reading the file, usually due to a corrupt or missing file 62 | * @see net.minecraftforge.coremod.api.ASMAPI#loadData(String) 63 | */ 64 | @Nullable 65 | public static Object loadDataByName(final String file) throws ScriptException, IOException { 66 | final CoreMod tracked = LOCAL.get().tracked; 67 | if (tracked != null) { 68 | return tracked.loadAdditionalData(file); 69 | } 70 | return null; 71 | } 72 | 73 | /** 74 | * Logs a message from the coremod. 75 | * 76 | * @param level The log level 77 | * @param message The message 78 | * @param args Any formatting arguments 79 | * @see net.minecraftforge.coremod.api.ASMAPI#log(String, String, Object...) 80 | */ 81 | public static void log(final String level, final String message, final Object[] args) { 82 | final CoreMod tracked = LOCAL.get().tracked; 83 | if (tracked != null) { 84 | tracked.logMessage(level, message, args); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/net/minecraftforge/coremod/NashornFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Forge Development LLC 3 | * SPDX-License-Identifier: LGPL-2.1-only 4 | */ 5 | package net.minecraftforge.coremod; 6 | 7 | import org.openjdk.nashorn.api.scripting.NashornScriptEngineFactory; 8 | import org.openjdk.nashorn.api.scripting.ScriptObjectMirror; 9 | 10 | import javax.script.Bindings; 11 | import javax.script.ScriptEngine; 12 | import java.util.Objects; 13 | import java.util.function.Function; 14 | 15 | class NashornFactory { 16 | // TODO for CoreMods 5.3 or 6.0: Consider args that improve performance 17 | // https://github.com/openjdk/nashorn/blob/2eb88e4024023ee8e9baacb7736f914e3aa68aa4/src/org.openjdk.nashorn/share/classes/org/openjdk/nashorn/internal/runtime/resources/Options.properties 18 | private static final String[] NASHORN_ARGS = new String[] { 19 | "--language=es6" 20 | }; 21 | 22 | static ScriptEngine createEngine() { 23 | return new NashornScriptEngineFactory().getScriptEngine(NASHORN_ARGS, getAppClassLoader(), CoreModEngine::checkClass); 24 | } 25 | 26 | /** @see NashornScriptEngineFactory#getAppClassLoader() */ 27 | @SuppressWarnings("JavadocReference") 28 | private static ClassLoader getAppClassLoader() { 29 | return Objects.requireNonNullElseGet( 30 | Thread.currentThread().getContextClassLoader(), 31 | NashornScriptEngineFactory.class::getClassLoader 32 | ); 33 | } 34 | 35 | @SuppressWarnings("unchecked") 36 | static Function getFunction(Bindings obj) { 37 | return a -> (R) ((ScriptObjectMirror) obj).call(obj, a); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/net/minecraftforge/coremod/api/ASMAPI.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Forge Development LLC 3 | * SPDX-License-Identifier: LGPL-2.1-only 4 | */ 5 | package net.minecraftforge.coremod.api; 6 | 7 | import cpw.mods.modlauncher.Launcher; 8 | import cpw.mods.modlauncher.api.INameMappingService; 9 | import net.minecraftforge.coremod.CoreModEngine; 10 | import net.minecraftforge.coremod.CoreModTracker; 11 | import org.jetbrains.annotations.Nullable; 12 | import org.objectweb.asm.Opcodes; 13 | import org.objectweb.asm.tree.*; 14 | import org.objectweb.asm.util.Textifier; 15 | import org.objectweb.asm.util.TraceClassVisitor; 16 | import org.objectweb.asm.util.TraceMethodVisitor; 17 | 18 | import javax.script.ScriptException; 19 | import java.io.IOException; 20 | import java.io.PrintWriter; 21 | import java.io.StringWriter; 22 | import java.lang.reflect.Modifier; 23 | import java.util.ArrayList; 24 | import java.util.ListIterator; 25 | import java.util.Objects; 26 | import java.util.Optional; 27 | import java.util.function.Function; 28 | 29 | /** 30 | * Helper methods for working with ASM. The goal of this class is to provide several assisting methods for common tasks 31 | * to prevent boilerplate code, excessive imports, unnecessary loops, and to provide a more user-friendly API for 32 | * coremod developers. 33 | */ 34 | @SuppressWarnings({"unused", "exports"}) // annoying IDE warnings 35 | public class ASMAPI { 36 | /* BUILDING INSTRUCTION LISTS */ 37 | 38 | /** 39 | * Builds a new {@link InsnList} out of the specified {@link AbstractInsnNode}s. 40 | * 41 | * @param nodes The instructions you want to add 42 | * @return A new list with the instructions 43 | * 44 | * @apiNote Due to a bug in Nashorn, invoking this method from your CoreMod with a very large array (or varargs) 45 | * will not work due to it being cast to {@code Object[]}. In that case, you will need to wrap this method 46 | * like this: {@code ASMAPI.listOf(Java.to([...], Java.type('org.objectweb.asm.tree.AbstractInsnNode[]')))}. 47 | */ 48 | public static InsnList listOf(AbstractInsnNode... nodes) { 49 | InsnList list = new InsnList(); 50 | for (AbstractInsnNode node : nodes) list.add(node); 51 | return list; 52 | } 53 | 54 | 55 | /* INSTRUCTION INJECTION */ 56 | 57 | /** 58 | * The mode in which the given code should be inserted. 59 | */ 60 | public enum InsertMode { 61 | INSERT_BEFORE, INSERT_AFTER, REMOVE_ORIGINAL 62 | } 63 | 64 | /** 65 | * Inserts/replaces an instruction, with respect to the given {@link InsertMode}, on the given instruction. 66 | * 67 | * @param method The method to insert the instruction into 68 | * @param insn The old instruction where the new instruction should be inserted into 69 | * @param toInsert The instruction to be inserted 70 | * @param mode How the instruction should be inserted 71 | * @return {@code true} if the list was inserted, {@code false} otherwise 72 | */ 73 | public static boolean insertInsn(MethodNode method, AbstractInsnNode insn, AbstractInsnNode toInsert, InsertMode mode) { 74 | if (!method.instructions.contains(insn)) return false; 75 | 76 | switch (mode) { 77 | case INSERT_BEFORE -> method.instructions.insertBefore(insn, toInsert); 78 | case INSERT_AFTER -> method.instructions.insert(insn, toInsert); 79 | case REMOVE_ORIGINAL -> method.instructions.set(insn, toInsert); 80 | } 81 | 82 | return true; 83 | } 84 | 85 | /** 86 | * Inserts/replaces an instruction, with respect to the given {@link InsertMode}, on the given index. 87 | * 88 | * @param method The method to insert the instruction into 89 | * @param index The index where the new instruction should be inserted into 90 | * @param toInsert The instruction to be inserted 91 | * @param mode How the instruction should be inserted 92 | * @return {@code true} if the list was inserted, {@code false} otherwise 93 | */ 94 | public static boolean insertInsn(MethodNode method, int index, AbstractInsnNode toInsert, InsertMode mode) { 95 | index = clamp(index, 0, method.instructions.size()); 96 | var insn = method.instructions.get(index); 97 | return insertInsn(method, insn, toInsert, mode); 98 | } 99 | 100 | /** 101 | * Inserts/replaces an instruction, with respect to the given {@link InsertMode}, on the first 102 | * {@link MethodInsnNode} that matches the parameters of these functions in the method provided. Only the first 103 | * matching node is targeted, all other matches are ignored. 104 | * 105 | * @param method The method to insert the instruction into 106 | * @param type The type of the method call to search for 107 | * @param owner The owner of the method call to search for 108 | * @param name The name of the method call to search for (you may want to use {@link #mapMethod(String)} if this 109 | * is a srg name) 110 | * @param desc The desc of the method call to search for 111 | * @param toInsert The instruction to be inserted 112 | * @param mode How the instruction should be inserted 113 | * @return {@code true} if the method call was found and the list was inserted, {@code false} otherwise 114 | * 115 | * @apiNote This method mostly exists for the sake of backwards-compatibility. It may be prudent to use 116 | * {@link #insertInsn(MethodNode, AbstractInsnNode, AbstractInsnNode, InsertMode)} instead if you want to value 117 | * readability. 118 | */ 119 | public static boolean insertInsn(MethodNode method, MethodType type, String owner, String name, String desc, AbstractInsnNode toInsert, InsertMode mode) { 120 | var insn = findFirstMethodCall(method, type, owner, name, desc); 121 | if (insn == null) return false; 122 | 123 | return insertInsn(method, insn, toInsert, mode); 124 | } 125 | 126 | /** 127 | * Inserts/replaces an instruction list, with respect to the given {@link InsertMode}, on the given instruction. 128 | * 129 | * @param method The method to insert the list into 130 | * @param insn The instruction where the list should be inserted into 131 | * @param list The list to be inserted 132 | * @param mode How the list should be inserted 133 | * @return {@code true} if the list was inserted, {@code false} otherwise 134 | */ 135 | public static boolean insertInsnList(MethodNode method, AbstractInsnNode insn, InsnList list, InsertMode mode) { 136 | if (!method.instructions.contains(insn)) return false; 137 | 138 | if (mode == InsertMode.INSERT_BEFORE) 139 | method.instructions.insertBefore(insn, list); 140 | else 141 | method.instructions.insert(insn, list); 142 | 143 | if (mode == InsertMode.REMOVE_ORIGINAL) 144 | method.instructions.remove(insn); 145 | 146 | return true; 147 | } 148 | 149 | /** 150 | * Inserts/replaces an instruction list, with respect to the given {@link InsertMode}, on the given index. 151 | * 152 | * @param method The method to insert the list into 153 | * @param index The index where the list should be inserted into 154 | * @param list The list to be inserted 155 | * @param mode How the list should be inserted 156 | * @return {@code true} if the list was inserted, {@code false} otherwise 157 | */ 158 | public static boolean insertInsnList(MethodNode method, int index, InsnList list, InsertMode mode) { 159 | return insertInsnList(method, method.instructions.get(clamp(index, 0, method.instructions.size())), list, mode); 160 | } 161 | 162 | /** 163 | * Inserts/replaces an instruction list, with respect to the given {@link InsertMode}, on the first 164 | * {@link MethodInsnNode} that matches the parameters of these functions in the method provided. Only the first 165 | * matching node is targeted, all other matches are ignored. 166 | * 167 | * @param method The method to insert the list into 168 | * @param type The type of the method call to search for 169 | * @param owner The owner of the method call to search for 170 | * @param name The name of the method call to search for (you may want to use {@link #mapMethod(String)} if this 171 | * is a srg name) 172 | * @param desc The desc of the method call to search for 173 | * @param list The list to be inserted 174 | * @param mode How the list should be inserted 175 | * @return {@code true} if the method call was found and the list was inserted, {@code false} otherwise 176 | * 177 | * @apiNote This method mostly exists for the sake of backwards-compatibility. It may be prudent to use 178 | * {@link #insertInsnList(MethodNode, AbstractInsnNode, InsnList, InsertMode)} instead if you want to value 179 | * readability. 180 | */ 181 | public static boolean insertInsnList(MethodNode method, MethodType type, String owner, String name, String desc, InsnList list, InsertMode mode) { 182 | var insn = findFirstMethodCall(method, type, owner, name, desc); 183 | if (insn == null) return false; 184 | 185 | return insertInsnList(method, insn, list, mode); 186 | } 187 | 188 | /** 189 | * Injects a method call to the beginning of the given method. 190 | * 191 | * @param method The method to inject the method call into 192 | * @param insn The method call to inject 193 | */ 194 | public static void injectMethodCall(MethodNode method, MethodInsnNode insn) { 195 | ASMAPI.insertInsn(method, method.instructions.getFirst(), insn, InsertMode.INSERT_BEFORE); 196 | } 197 | 198 | /** 199 | * Injects a method call to the beginning of the given method. 200 | * 201 | * @param method The method to inject the call into 202 | * @param insn The method call to inject 203 | * @deprecated Renamed to {@link #injectMethodCall(MethodNode, MethodInsnNode)} 204 | */ 205 | @Deprecated(forRemoval = true, since = "6.0") 206 | public static void appendMethodCall(MethodNode method, MethodInsnNode insn) { 207 | injectMethodCall(method, insn); 208 | } 209 | 210 | 211 | /* INSTRUCTION SEARCHING */ 212 | 213 | /** 214 | * The type of instruction. Useful for searching for a specfic instruction, especially for those that use opcode 215 | * {@code -1} or you need to ignore matching the opcode. 216 | * 217 | * @see AbstractInsnNode 218 | */ 219 | public enum InsnType { 220 | INSN(AbstractInsnNode.INSN), 221 | INT_INSN(AbstractInsnNode.INT_INSN), 222 | VAR_INSN(AbstractInsnNode.VAR_INSN), 223 | TYPE_INSN(AbstractInsnNode.TYPE_INSN), 224 | FIELD_INSN(AbstractInsnNode.FIELD_INSN), 225 | METHOD_INSN(AbstractInsnNode.METHOD_INSN), 226 | INVOKE_DYNAMIC_INSN(AbstractInsnNode.INVOKE_DYNAMIC_INSN), 227 | JUMP_INSN(AbstractInsnNode.JUMP_INSN), 228 | LABEL(AbstractInsnNode.LABEL), 229 | LDC_INSN(AbstractInsnNode.LDC_INSN), 230 | IINC_INSN(AbstractInsnNode.IINC_INSN), 231 | TABLESWITCH_INSN(AbstractInsnNode.TABLESWITCH_INSN), 232 | LOOKUPSWITCH_INSN(AbstractInsnNode.LOOKUPSWITCH_INSN), 233 | MULTIANEWARRAY_INSN(AbstractInsnNode.MULTIANEWARRAY_INSN), 234 | FRAME(AbstractInsnNode.FRAME), 235 | LINE(AbstractInsnNode.LINE); 236 | 237 | private final int type; 238 | 239 | InsnType(int type) { 240 | this.type = type; 241 | } 242 | 243 | /** 244 | * Gets the type of the instruction represented in {@link AbstractInsnNode}. 245 | * 246 | * @return The type 247 | */ 248 | public int get() { 249 | return type; 250 | } 251 | } 252 | 253 | /** 254 | * Finds the first instruction with matching opcode. 255 | * 256 | * @param method the method to search in 257 | * @param opcode the opcode to search for 258 | * @return the found instruction or {@code null} if none matched 259 | */ 260 | public static @Nullable AbstractInsnNode findFirstInstruction(MethodNode method, int opcode) { 261 | return findFirstInstructionAfter(method, opcode, null, 0); 262 | } 263 | 264 | /** 265 | * Finds the first instruction with matching instruction type. 266 | * 267 | * @param method the method to search in 268 | * @param type the instruction type to search for 269 | * @return the found instruction node or {@code null} if none matched 270 | */ 271 | public static @Nullable AbstractInsnNode findFirstInstruction(MethodNode method, InsnType type) { 272 | return findFirstInstructionAfter(method, -2, type, 0); 273 | } 274 | 275 | /** 276 | * Finds the first instruction with matching opcode and instruction type. 277 | * 278 | * @param method the method to search in 279 | * @param opcode the opcode to search for 280 | * @param type the instruction type to search for 281 | * @return the found instruction node or {@code null} if none matched 282 | */ 283 | public static @Nullable AbstractInsnNode findFirstInstruction(MethodNode method, int opcode, InsnType type) { 284 | return findFirstInstructionAfter(method, opcode, type, 0); 285 | } 286 | 287 | /** 288 | * Finds the first instruction with matching opcode after the given start index. 289 | * 290 | * @param method the method to search in 291 | * @param opcode the opcode to search for 292 | * @param startIndex the index to start search after (inclusive) 293 | * @return the found instruction node or {@code null} if none matched after the given index 294 | */ 295 | public static @Nullable AbstractInsnNode findFirstInstructionAfter(MethodNode method, int opcode, int startIndex) { 296 | return findFirstInstructionAfter(method, opcode, null, startIndex); 297 | } 298 | 299 | /** 300 | * Finds the first instruction with matching opcode after the given start instruction. 301 | * 302 | * @param method the method to search in 303 | * @param opcode the opcode to search for 304 | * @param startInsn the instruction to start search after (inclusive) 305 | * @return the found instruction node or {@code null} if none matched after the given index 306 | */ 307 | public static @Nullable AbstractInsnNode findFirstInstructionAfter(MethodNode method, int opcode, AbstractInsnNode startInsn) { 308 | return findFirstInstructionAfter(method, opcode, method.instructions.indexOf(startInsn)); 309 | } 310 | 311 | /** 312 | * Finds the first instruction with matching instruction type after the given start index. 313 | * 314 | * @param method the method to search in 315 | * @param type the instruction type to search for 316 | * @param startIndex the index to start search after (inclusive) 317 | * @return the found instruction node or {@code null} if none matched after the given index 318 | */ 319 | public static @Nullable AbstractInsnNode findFirstInstructionAfter(MethodNode method, InsnType type, int startIndex) { 320 | return findFirstInstructionAfter(method, -2, type, startIndex); 321 | } 322 | 323 | /** 324 | * Finds the first instruction with matching instruction type after the given start instruction. 325 | * 326 | * @param method the method to search in 327 | * @param type the instruction type to search for 328 | * @param startInsn the instruction to start search after (inclusive) 329 | * @return the found instruction node or {@code null} if none matched after the given index 330 | */ 331 | public static @Nullable AbstractInsnNode findFirstInstructionAfter(MethodNode method, InsnType type, AbstractInsnNode startInsn) { 332 | return findFirstInstructionAfter(method, type, method.instructions.indexOf(startInsn)); 333 | } 334 | 335 | /** 336 | * Finds the first instruction with matching opcode and instruction type after the given start index. 337 | * 338 | * @param method the method to search in 339 | * @param opcode the opcode to search for 340 | * @param type the instruction type to search for 341 | * @param startIndex the index to start search after (inclusive) 342 | * @return the found instruction node or {@code null} if none matched after the given index 343 | */ 344 | public static @Nullable AbstractInsnNode findFirstInstructionAfter(MethodNode method, int opcode, @Nullable InsnType type, int startIndex) { 345 | for (int i = Math.max(0, startIndex); i < method.instructions.size(); i++) { 346 | AbstractInsnNode ain = method.instructions.get(i); 347 | 348 | boolean opcodeMatch = opcode < -1 || ain.getOpcode() == opcode; 349 | boolean typeMatch = type == null || type.get() == ain.getType(); 350 | if (opcodeMatch && typeMatch) return ain; 351 | } 352 | 353 | return null; 354 | } 355 | 356 | /** 357 | * Finds the first instruction with matching opcode and instruction type after the given start instruction. 358 | * 359 | * @param method the method to search in 360 | * @param opcode the opcode to search for 361 | * @param type the instruction type to search for 362 | * @param startInsn the instruction to start search after (inclusive) 363 | * @return the found instruction node or {@code null} if none matched after the given index 364 | */ 365 | public static @Nullable AbstractInsnNode findFirstInstructionAfter(MethodNode method, int opcode, @Nullable InsnType type, AbstractInsnNode startInsn) { 366 | return findFirstInstructionAfter(method, opcode, type, method.instructions.indexOf(startInsn)); 367 | } 368 | 369 | /** 370 | * Finds the first instruction with matching opcode before the given index in reverse search. 371 | * 372 | * @param method the method to search in 373 | * @param opcode the opcode to search for 374 | * @param startIndex the index at which to start searching (inclusive) 375 | * @return the found instruction node or {@code null} if none matched before the given startIndex 376 | * 377 | * @apiNote In Minecraft 1.21.1 and earlier, this method contains broken logic that ignores the 378 | * {@code startIndex} parameter and searches for the requested instruction at the end of the method. This 379 | * behavior is preserved to not disrupt older coremods. If you are on one of these older versions and need to 380 | * use the fixed logic, please use {@link #findFirstInstructionBefore(MethodNode, int, int, boolean)}. 381 | */ 382 | public static @Nullable AbstractInsnNode findFirstInstructionBefore(MethodNode method, int opcode, int startIndex) { 383 | return findFirstInstructionBefore(method, opcode, null, startIndex); 384 | } 385 | 386 | /** 387 | * Finds the first instruction with matching opcode before the given instruction in reverse search. 388 | * 389 | * @param method the method to search in 390 | * @param opcode the opcode to search for 391 | * @param startInsn the instruction at which to start searching (inclusive) 392 | * @return the found instruction node or {@code null} if none matched before the given startIndex 393 | * 394 | * @apiNote Since this method is new, it will automatically apply the fixed logic in 395 | * {@link #findFirstInstructionBefore(MethodNode, int, InsnType, int, boolean)}. 396 | */ 397 | public static @Nullable AbstractInsnNode findFirstInstructionBefore(MethodNode method, int opcode, AbstractInsnNode startInsn) { 398 | return findFirstInstructionBefore(method, opcode, null, startInsn); 399 | } 400 | 401 | /** 402 | * Finds the first instruction with matching instruction type before the given index in reverse search. 403 | * 404 | * @param method the method to search in 405 | * @param type the instruction type to search for 406 | * @param startIndex the index at which to start searching (inclusive) 407 | * @return the found instruction node or {@code null} if none matched before the given startIndex 408 | * 409 | * @apiNote In Minecraft 1.21.1 and earlier, this method contains broken logic that ignores the 410 | * {@code startIndex} parameter and searches for the requested instruction at the end of the method. This 411 | * behavior is preserved to not disrupt older coremods. If you are on one of these older versions and need to 412 | * use the fixed logic, please use {@link #findFirstInstructionBefore(MethodNode, int, int, boolean)}. 413 | */ 414 | public static @Nullable AbstractInsnNode findFirstInstructionBefore(MethodNode method, InsnType type, int startIndex) { 415 | return findFirstInstructionBefore(method, -2, type, startIndex); 416 | } 417 | 418 | /** 419 | * Finds the first instruction with matching instruction type before the given instruction in reverse search. 420 | * 421 | * @param method the method to search in 422 | * @param type the instruction type to search for 423 | * @param startInsn the index at which to start searching (inclusive) 424 | * @return the found instruction node or {@code null} if none matched before the given startIndex 425 | * 426 | * @apiNote Since this method is new, it will automatically apply the fixed logic in 427 | * {@link #findFirstInstructionBefore(MethodNode, int, InsnType, int, boolean)}. 428 | */ 429 | public static @Nullable AbstractInsnNode findFirstInstructionBefore(MethodNode method, InsnType type, AbstractInsnNode startInsn) { 430 | return findFirstInstructionBefore(method, -2, type, startInsn); 431 | } 432 | 433 | /** 434 | * Finds the first instruction with matching opcode before the given index in reverse search. 435 | * 436 | * @param method the method to search in 437 | * @param opCode the opcode to search for 438 | * @param startIndex the index at which to start searching (inclusive) 439 | * @param fixLogic whether to use the fixed logic for finding instructions before the given startIndex 440 | * ({@code true} by default on versions since 1.21.3, {@code false} otherwise) 441 | * @return the found instruction node or {@code null} if none matched before the given startIndex 442 | */ 443 | public static @Nullable AbstractInsnNode findFirstInstructionBefore(MethodNode method, int opCode, int startIndex, boolean fixLogic) { 444 | return findFirstInstructionBefore(method, opCode, null, startIndex, fixLogic); 445 | } 446 | 447 | /** 448 | * Finds the first instruction with matching instruction type before the given index in reverse search. 449 | * 450 | * @param method the method to search in 451 | * @param type the instruction type to search for 452 | * @param startIndex the index at which to start searching (inclusive) 453 | * @param fixLogic whether to use the fixed logic for finding instructions before the given startIndex 454 | * ({@code true} by default on versions since 1.21.3, {@code false} otherwise) 455 | * @return the found instruction node or {@code null} if none matched before the given startIndex 456 | */ 457 | public static @Nullable AbstractInsnNode findFirstInstructionBefore(MethodNode method, InsnType type, int startIndex, boolean fixLogic) { 458 | return findFirstInstructionBefore(method, -2, type, startIndex, fixLogic); 459 | } 460 | 461 | /** 462 | * Finds the first instruction with matching opcode and instruction type before the given index in reverse search. 463 | * 464 | * @param method the method to search in 465 | * @param opCode the opcode to search for 466 | * @param startIndex the index at which to start searching (inclusive) 467 | * @return the found instruction node or {@code null} if none matched before the given startIndex 468 | * 469 | * @apiNote In Minecraft 1.21.1 and earlier, this method contains broken logic that ignores the 470 | * {@code startIndex} parameter and searches for the requested instruction at the end of the method. This 471 | * behavior is preserved to not disrupt older coremods. If you are on one of these older versions and need to 472 | * use the fixed logic, please use 473 | * {@link #findFirstInstructionBefore(MethodNode, int, InsnType, int, boolean)}. 474 | */ 475 | public static @Nullable AbstractInsnNode findFirstInstructionBefore(MethodNode method, int opCode, @Nullable InsnType type, int startIndex) { 476 | return findFirstInstructionBefore(method, opCode, type, startIndex, !CoreModEngine.DO_NOT_FIX_INSNBEFORE); 477 | } 478 | 479 | /** 480 | * Finds the first instruction with matching opcode and instruction type before the given instruction in reverse 481 | * search. 482 | * 483 | * @param method the method to search in 484 | * @param opCode the opcode to search for 485 | * @param startInsn the instruction at which to start searching (inclusive) 486 | * @return the found instruction node or {@code null} if none matched before the given startIndex 487 | * 488 | * @apiNote Since this method is new, it will automatically apply the fixed logic in 489 | * {@link #findFirstInstructionBefore(MethodNode, int, InsnType, int, boolean)}. 490 | */ 491 | public static @Nullable AbstractInsnNode findFirstInstructionBefore(MethodNode method, int opCode, @Nullable InsnType type, AbstractInsnNode startInsn) { 492 | return findFirstInstructionBefore(method, opCode, type, method.instructions.indexOf(startInsn), true); 493 | } 494 | 495 | /** 496 | * Finds the first instruction with matching opcode and instruction type before the given index in reverse search. 497 | * 498 | * @param method the method to search in 499 | * @param opCode the opcode to search for 500 | * @param startIndex the index at which to start searching (inclusive) 501 | * @param fixLogic whether to use the fixed logic for finding instructions before the given startIndex 502 | * ({@code true} by default on versions since 1.21.3, {@code false} otherwise) 503 | * @return the found instruction node or {@code null} if none matched before the given startIndex 504 | */ 505 | public static @Nullable AbstractInsnNode findFirstInstructionBefore(MethodNode method, int opCode, @Nullable InsnType type, int startIndex, boolean fixLogic) { 506 | for (int i = fixLogic ? Math.min(method.instructions.size() - 1, startIndex) : startIndex; i >= 0; i--) { 507 | AbstractInsnNode ain = method.instructions.get(i); 508 | 509 | boolean opcodeMatch = opCode < -1 || ain.getOpcode() == opCode; 510 | boolean typeMatch = type == null || type.get() == ain.getType(); 511 | if (opcodeMatch && typeMatch) return ain; 512 | } 513 | 514 | return null; 515 | } 516 | 517 | /** 518 | * Finds the first method call in the given method matching the given type, owner, name and descriptor. 519 | * 520 | * @param method the method to search in 521 | * @param type the type of method call to search for 522 | * @param owner the method call's owner to search for 523 | * @param name the method call's name 524 | * @param descriptor the method call's descriptor 525 | * @return the found method call node or {@code null} if none matched 526 | */ 527 | public static @Nullable MethodInsnNode findFirstMethodCall(MethodNode method, MethodType type, String owner, String name, String descriptor) { 528 | return findFirstMethodCallAfter(method, type, owner, name, descriptor, 0); 529 | } 530 | 531 | /** 532 | * Finds the first method call in the given method matching the given type, owner, name and descriptor after the 533 | * given index. 534 | * 535 | * @param method the method to search in 536 | * @param type the type of method call to search for 537 | * @param owner the method call's owner to search for 538 | * @param name the method call's name 539 | * @param descriptor the method call's descriptor 540 | * @param index the index after which to start searching (inclusive) 541 | * @return the found method call node, {@code null} if none matched after the given index 542 | */ 543 | public static @Nullable MethodInsnNode findFirstMethodCallAfter(MethodNode method, MethodType type, String owner, String name, String descriptor, int index) { 544 | for (int i = Math.max(0, index); i < method.instructions.size(); i++) { 545 | if (method.instructions.get(i) instanceof MethodInsnNode insn 546 | && insn.getOpcode() == type.toOpcode() 547 | && Objects.equals(insn.owner, owner) 548 | && Objects.equals(insn.name, name) 549 | && Objects.equals(insn.desc, descriptor)) { 550 | return insn; 551 | } 552 | } 553 | 554 | return null; 555 | } 556 | 557 | /** 558 | * Finds the first method call in the given method matching the given type, owner, name and descriptor after the 559 | * given instruction. 560 | * 561 | * @param method the method to search in 562 | * @param type the type of method call to search for 563 | * @param owner the method call's owner to search for 564 | * @param name the method call's name 565 | * @param descriptor the method call's descriptor 566 | * @param insn the instruction after which to start searching (inclusive) 567 | * @return the found method call node, {@code null} if none matched after the given index 568 | */ 569 | public static @Nullable MethodInsnNode findFirstMethodCallAfter(MethodNode method, MethodType type, String owner, String name, String descriptor, AbstractInsnNode insn) { 570 | return findFirstMethodCallAfter(method, type, owner, name, descriptor, method.instructions.indexOf(insn)); 571 | } 572 | 573 | /** 574 | * Finds the first method call in the given method matching the given type, owner, name and descriptor before the 575 | * given index in reverse search. 576 | * 577 | * @param method the method to search in 578 | * @param type the type of method call to search for 579 | * @param owner the method call's owner to search for 580 | * @param name the method call's name 581 | * @param descriptor the method call's descriptor 582 | * @param index the index at which to start searching (inclusive) 583 | * @return the found method call node or {@code null} if none matched before the given startIndex 584 | */ 585 | public static @Nullable MethodInsnNode findFirstMethodCallBefore(MethodNode method, MethodType type, String owner, String name, String descriptor, int index) { 586 | for (int i = Math.min(method.instructions.size() - 1, index); i >= 0; i--) { 587 | if (method.instructions.get(i) instanceof MethodInsnNode insn 588 | && insn.getOpcode() == type.toOpcode() 589 | && Objects.equals(insn.owner, owner) 590 | && Objects.equals(insn.name, name) 591 | && Objects.equals(insn.desc, descriptor)) { 592 | return insn; 593 | } 594 | } 595 | 596 | return null; 597 | } 598 | 599 | /** 600 | * Finds the first method call in the given method matching the given type, owner, name and descriptor before the 601 | * given instruction in reverse search. 602 | * 603 | * @param method the method to search in 604 | * @param type the type of method call to search for 605 | * @param owner the method call's owner to search for 606 | * @param name the method call's name 607 | * @param descriptor the method call's descriptor 608 | * @param insn the instruction at which to start searching (inclusive) 609 | * @return the found method call node or {@code null} if none matched before the given startIndex 610 | */ 611 | public static @Nullable MethodInsnNode findFirstMethodCallBefore(MethodNode method, MethodType type, String owner, String name, String descriptor, AbstractInsnNode insn) { 612 | return findFirstMethodCallBefore(method, type, owner, name, descriptor, method.instructions.indexOf(insn)); 613 | } 614 | 615 | /** 616 | * Finds the first field call in the given method matching the given opcode, owner, name and descriptor. 617 | * 618 | * @param method the method to search in 619 | * @param opcode the opcode of field call to search for 620 | * @param owner the method call's owner to search for 621 | * @param name the method call's name 622 | * @param descriptor the method call's descriptor 623 | * @return the found method call node or {@code null} if none matched 624 | */ 625 | public static @Nullable FieldInsnNode findFirstFieldCall(MethodNode method, int opcode, String owner, String name, String descriptor) { 626 | return findFirstFieldCallAfter(method, opcode, owner, name, descriptor, 0); 627 | } 628 | 629 | /** 630 | * Finds the first field call in the given method matching the given opcode, owner, name and descriptor after the 631 | * given index. 632 | * 633 | * @param method the method to search in 634 | * @param opcode the opcode of field call to search for 635 | * @param owner the method call's owner to search for 636 | * @param name the method call's name 637 | * @param descriptor the method call's descriptor 638 | * @param startIndex the index after which to start searching (inclusive) 639 | * @return the found method call node, {@code null} if none matched after the given index 640 | */ 641 | public static @Nullable FieldInsnNode findFirstFieldCallAfter(MethodNode method, int opcode, String owner, String name, String descriptor, int startIndex) { 642 | for (int i = Math.max(0, startIndex); i < method.instructions.size(); i++) { 643 | if (method.instructions.get(i) instanceof FieldInsnNode insn 644 | && insn.getOpcode() == opcode 645 | && insn.owner.equals(owner) 646 | && insn.name.equals(name) 647 | && insn.desc.equals(descriptor)) { 648 | return insn; 649 | } 650 | } 651 | return null; 652 | } 653 | 654 | /** 655 | * Finds the first field call in the given method matching the given opcode, owner, name and descriptor after the 656 | * given instruction. 657 | * 658 | * @param method the method to search in 659 | * @param opcode the opcode of field call to search for 660 | * @param owner the method call's owner to search for 661 | * @param name the method call's name 662 | * @param descriptor the method call's descriptor 663 | * @param startInsn the instruction after which to start searching (inclusive) 664 | * @return the found method call node, {@code null} if none matched after the given index 665 | */ 666 | public static @Nullable FieldInsnNode findFirstFieldCallAfter(MethodNode method, int opcode, String owner, String name, String descriptor, AbstractInsnNode startInsn) { 667 | return findFirstFieldCallAfter(method, opcode, owner, name, descriptor, method.instructions.indexOf(startInsn)); 668 | } 669 | 670 | /** 671 | * Finds the first field call in the given method matching the given opcode, owner, name and descriptor before the 672 | * given index in reverse search. 673 | * 674 | * @param method the method to search in 675 | * @param opcode the opcode of field call to search for 676 | * @param owner the method call's owner to search for 677 | * @param name the method call's name 678 | * @param descriptor the method call's descriptor 679 | * @param startIndex the index at which to start searching (inclusive) 680 | * @return the found method call node or {@code null} if none matched before the given startIndex 681 | */ 682 | public static @Nullable FieldInsnNode findFirstFieldCallBefore(MethodNode method, int opcode, String owner, String name, String descriptor, int startIndex) { 683 | for (int i = Math.min(method.instructions.size() - 1, startIndex); i >= 0; i--) { 684 | if (method.instructions.get(i) instanceof FieldInsnNode insn 685 | && insn.getOpcode() == opcode 686 | && insn.owner.equals(owner) 687 | && insn.name.equals(name) 688 | && insn.desc.equals(descriptor)) { 689 | return insn; 690 | } 691 | } 692 | return null; 693 | } 694 | 695 | /** 696 | * Finds the first field call in the given method matching the given opcode, owner, name and descriptor before the 697 | * given instruction in reverse search. 698 | * 699 | * @param method the method to search in 700 | * @param opcode the opcode of field call to search for 701 | * @param owner the method call's owner to search for 702 | * @param name the method call's name 703 | * @param descriptor the method call's descriptor 704 | * @param startInsn the instruction at which to start searching (inclusive) 705 | * @return the found method call node or {@code null} if none matched before the given startIndex 706 | */ 707 | public static @Nullable FieldInsnNode findFirstFieldCallBefore(MethodNode method, int opcode, String owner, String name, String descriptor, AbstractInsnNode startInsn) { 708 | return findFirstFieldCallBefore(method, opcode, owner, name, descriptor, method.instructions.indexOf(startInsn)); 709 | } 710 | 711 | 712 | /* CREATING AND FINDING METHODS */ 713 | 714 | /** 715 | * Creates a new empty {@link MethodNode}. 716 | * 717 | * @return The created method node 718 | * 719 | * @see MethodNode#MethodNode(int) 720 | */ 721 | public static MethodNode getMethodNode() { 722 | var method = new MethodNode(Opcodes.ASM9); 723 | 724 | // ASM usually creates an empty list for null exceptions on the other constructors 725 | // let's do this as well, just to make sure we don't run into problems later. 726 | method.exceptions = new ArrayList<>(); 727 | 728 | return method; 729 | } 730 | 731 | /** 732 | * Creates a new empty {@link MethodNode} with the given access codes, name and descriptor. 733 | * 734 | * @param access The access codes 735 | * @param name The method name 736 | * @param descriptor The method descriptor 737 | * @return The created method node 738 | * 739 | * @see MethodNode#MethodNode(int, int, String, String, String, String[]) 740 | */ 741 | public static MethodNode getMethodNode(int access, String name, String descriptor) { 742 | return new MethodNode(Opcodes.ASM9, access, name, descriptor, null, null); 743 | } 744 | 745 | /** 746 | * Creates a new empty {@link MethodNode} with the given access codes, name, descriptor, and signature. 747 | * 748 | * @param access The access codes 749 | * @param name The method name 750 | * @param descriptor The method descriptor 751 | * @param signature The method signature 752 | * @return The created method node 753 | * 754 | * @see MethodNode#MethodNode(int, int, String, String, String, String[]) 755 | */ 756 | public static MethodNode getMethodNode(int access, String name, String descriptor, @Nullable String signature) { 757 | return new MethodNode(Opcodes.ASM9, access, name, descriptor, signature, null); 758 | } 759 | 760 | /** 761 | * Creates a new empty {@link MethodNode} with the given access codes, name, descriptor, signature, and exceptions. 762 | * 763 | * @param access The access codes 764 | * @param name The method name 765 | * @param descriptor The method descriptor 766 | * @param signature The method signature 767 | * @param exceptions The internal names of the method's exceptions 768 | * @return The created method node 769 | * 770 | * @see MethodNode#MethodNode(int, int, String, String, String, String[]) 771 | */ 772 | public static MethodNode getMethodNode(int access, String name, String descriptor, @Nullable String signature, @Nullable String[] exceptions) { 773 | return new MethodNode(Opcodes.ASM9, access, name, descriptor, signature, exceptions); 774 | } 775 | 776 | /** 777 | * Finds the first method node from the given class node that matches the given name and descriptor. 778 | * 779 | * @param clazz The class node to search 780 | * @param name The name of the desired method 781 | * @param desc The descriptor of the desired method 782 | * @return The found method node or {@code null} if none matched 783 | */ 784 | public static @Nullable MethodNode findMethodNode(ClassNode clazz, String name, String desc) { 785 | return findMethodNode(clazz, name, desc, null, false); 786 | } 787 | 788 | /** 789 | * Finds the first method node from the given class node that matches the given name, descriptor, and signature. 790 | * 791 | * @param clazz The class node to search 792 | * @param name The name of the desired method 793 | * @param desc The descriptor of the desired method 794 | * @param signature The signature of the desired method 795 | * @return The found method node or {@code null} if none matched 796 | * 797 | * @apiNote This method will attempt to match the signature of the method, even if it is {@code null}. It may be 798 | * useful for that use case in particular. If you have no need to match the signature, consider using 799 | * {@link #findMethodNode(ClassNode, String, String)} 800 | */ 801 | public static @Nullable MethodNode findMethodNode(ClassNode clazz, String name, String desc, @Nullable String signature) { 802 | return findMethodNode(clazz, name, desc, signature, true); 803 | } 804 | 805 | private static @Nullable MethodNode findMethodNode(ClassNode clazz, String name, String desc, @Nullable String signature, boolean checkSignature) { 806 | for (MethodNode method : clazz.methods) { 807 | // we have to use Objects.equals here in case the found method has null attributes 808 | if (Objects.equals(method.name, name) && Objects.equals(method.desc, desc) && (!checkSignature || Objects.equals(method.signature, signature))) { 809 | return method; 810 | } 811 | } 812 | 813 | return null; 814 | } 815 | 816 | 817 | /* CREATING AND FINDING FIELDS */ 818 | 819 | /** 820 | * Creates a new empty {@link FieldNode} with the given access codes, name, and descriptor. 821 | * 822 | * @param access The access codes 823 | * @param name The field name 824 | * @param descriptor The field descriptor 825 | * @return The created field node 826 | * 827 | * @see FieldNode#FieldNode(int, int, String, String, String, Object) 828 | */ 829 | public static FieldNode getFieldNode(int access, String name, String descriptor) { 830 | return new FieldNode(Opcodes.ASM9, access, name, descriptor, null, null); 831 | } 832 | 833 | /** 834 | * Creates a new empty {@link FieldNode} with the given access codes, name, descriptor, and signature. 835 | * 836 | * @param access The access codes 837 | * @param name The field name 838 | * @param descriptor The field descriptor 839 | * @param signature The field signature 840 | * @return The created field node 841 | * 842 | * @see FieldNode#FieldNode(int, int, String, String, String, Object) 843 | */ 844 | public static FieldNode getFieldNode(int access, String name, String descriptor, @Nullable String signature) { 845 | return new FieldNode(Opcodes.ASM9, access, name, descriptor, signature, null); 846 | } 847 | 848 | /** 849 | * Creates a new empty {@link FieldNode} with the given access codes, name, descriptor, signature, and initial 850 | * object value. 851 | * 852 | * @param access The access codes 853 | * @param name The field name 854 | * @param descriptor The field descriptor 855 | * @param signature The field signature 856 | * @param value The initial value of the field 857 | * @return The created field node 858 | * 859 | * @see FieldNode#FieldNode(int, int, String, String, String, Object) 860 | */ 861 | public static FieldNode getFieldNode(int access, String name, String descriptor, @Nullable String signature, String value) { 862 | return new FieldNode(Opcodes.ASM9, access, name, descriptor, signature, value); 863 | } 864 | 865 | /** 866 | * Creates a new empty {@link FieldNode} with the given access codes, name, descriptor, signature, and initial 867 | * number value. 868 | * 869 | * @param access The access codes 870 | * @param name The field name 871 | * @param descriptor The field descriptor 872 | * @param signature The field signature 873 | * @param value The initial value of the field 874 | * @param valueType The number type of the initial value 875 | * @return The created field node 876 | * 877 | * @see FieldNode#FieldNode(int, int, String, String, String, Object) 878 | */ 879 | public static FieldNode getFieldNode(int access, String name, String descriptor, @Nullable String signature, Number value, NumberType valueType) { 880 | return new FieldNode(Opcodes.ASM9, access, name, descriptor, signature, castNumber(value, valueType)); 881 | } 882 | 883 | /** 884 | * Finds the first field node from the given class node that matches the given name and descriptor. 885 | * 886 | * @param clazz The class node to search 887 | * @param name The name of the desired field 888 | * @param desc The descriptor of the desired field 889 | * @return The found field node or {@code null} if none matched 890 | */ 891 | public static @Nullable FieldNode findFieldNode(ClassNode clazz, String name, String desc) { 892 | return findFieldNode(clazz, name, desc, null, false); 893 | } 894 | 895 | /** 896 | * Finds the first field node from the given class node that matches the given name, descriptor, and signature. 897 | * 898 | * @param clazz The class node to search 899 | * @param name The name of the desired field 900 | * @param desc The descriptor of the desired field 901 | * @param signature The signature of the desired field 902 | * @return The found field node or {@code null} if none matched 903 | * 904 | * @apiNote This method will attempt to match the signature of the field, even if it is {@code null}. It may be 905 | * useful for that use case in particular. If you have no need to match the signature, consider using 906 | * {@link #findFieldNode(ClassNode, String, String)} 907 | */ 908 | public static @Nullable FieldNode findFieldNode(ClassNode clazz, String name, String desc, @Nullable String signature) { 909 | return findFieldNode(clazz, name, desc, signature, true); 910 | } 911 | 912 | private static @Nullable FieldNode findFieldNode(ClassNode clazz, String name, String desc, @Nullable String signature, boolean checkSignature) { 913 | for (FieldNode field : clazz.fields) { 914 | // we have to use Objects.equals here in case the found field has null attributes 915 | if (Objects.equals(field.name, name) && Objects.equals(field.desc, desc) && (!checkSignature || Objects.equals(field.signature, signature))) { 916 | return field; 917 | } 918 | } 919 | 920 | return null; 921 | } 922 | 923 | 924 | /* BUILDING METHOD CALLS */ 925 | 926 | /** 927 | * Signifies the method invocation type. Mirrors "INVOKE-" opcodes from ASM. 928 | */ 929 | public enum MethodType { 930 | VIRTUAL(Opcodes.INVOKEVIRTUAL), 931 | SPECIAL(Opcodes.INVOKESPECIAL), 932 | STATIC(Opcodes.INVOKESTATIC), 933 | INTERFACE(Opcodes.INVOKEINTERFACE), 934 | DYNAMIC(Opcodes.INVOKEDYNAMIC); 935 | 936 | private final int opcode; 937 | 938 | MethodType(int opcode) { 939 | this.opcode = opcode; 940 | } 941 | 942 | /** 943 | * Gets the opcode of the method type. 944 | * 945 | * @return The opcode 946 | */ 947 | public int toOpcode() { 948 | return this.opcode; 949 | } 950 | } 951 | 952 | /** 953 | * Builds a new {@link MethodInsnNode} with the given parameters. The opcode of the method call is determined by the 954 | * given {@link MethodType}. 955 | * 956 | * @param type The type of method call 957 | * @param ownerName The method owner (class) 958 | * @param methodName The method name 959 | * @param methodDescriptor The method descriptor 960 | * @return The built method call node 961 | */ 962 | public static MethodInsnNode buildMethodCall(final MethodType type, final String ownerName, final String methodName, final String methodDescriptor) { 963 | return new MethodInsnNode(type.toOpcode(), ownerName, methodName, methodDescriptor, type == MethodType.INTERFACE); 964 | } 965 | 966 | /** 967 | * Builds a new {@link MethodInsnNode} with the given parameters. The opcode of the method call is determined by the 968 | * given {@link MethodType}. 969 | * 970 | * @param ownerName The method owner (class) 971 | * @param methodName The method name 972 | * @param methodDescriptor The method descriptor 973 | * @param type The type of method call 974 | * @return The built method call node 975 | * 976 | * @deprecated Use {@link #buildMethodCall(MethodType, String, String, String)} 977 | */ 978 | @Deprecated(forRemoval = true, since = "6.0") // when we major update, prefer the method type as first parameter 979 | public static MethodInsnNode buildMethodCall(final String ownerName, final String methodName, final String methodDescriptor, final MethodType type) { 980 | return new MethodInsnNode(type.toOpcode(), ownerName, methodName, methodDescriptor, type == MethodType.INTERFACE); 981 | } 982 | 983 | 984 | /* BUILDING FIELD CALLS */ 985 | 986 | public static FieldInsnNode buildFieldCall(final int opcode, final String owner, final String name, final String desc) { 987 | return new FieldInsnNode(opcode, owner, name, desc); 988 | } 989 | 990 | 991 | /* LDC AND NUMBER TYPE HELPERS */ 992 | 993 | /** 994 | * Signifies the type of number constant for a {@link NumberType}. 995 | */ 996 | public enum NumberType { 997 | INTEGER(Number::intValue), 998 | FLOAT(Number::floatValue), 999 | LONG(Number::longValue), 1000 | DOUBLE(Number::doubleValue); 1001 | 1002 | private final Function mapper; 1003 | 1004 | NumberType(Function mapper) { 1005 | this.mapper = mapper; 1006 | } 1007 | } 1008 | 1009 | /** 1010 | * Casts a given number to a given specific {@link NumberType}. This helps alleviate the problems that comes with 1011 | * JavaScript's ambiguous number system. 1012 | *

1013 | * The result is returned as an {@link Object} so it can be used as a value in various instructions that require 1014 | * values, such as {@link FieldNode} and {@link LdcInsnNode}. 1015 | * 1016 | * @param value The number to cast 1017 | * @param type The type of number to cast to 1018 | * @return The casted number 1019 | */ 1020 | public static Object castNumber(final Number value, final NumberType type) { 1021 | return type.mapper.apply(value); 1022 | } 1023 | 1024 | /** 1025 | * Builds a new {@link LdcInsnNode} with the given number value and {@link NumberType}. 1026 | * 1027 | * @param value The number value 1028 | * @param type The type of the number 1029 | * @return The built LDC node 1030 | */ 1031 | public static LdcInsnNode buildNumberLdcInsnNode(final Number value, final NumberType type) { 1032 | return new LdcInsnNode(castNumber(value, type)); 1033 | } 1034 | 1035 | 1036 | /* SPECIALIZED TRANSFORMATION */ 1037 | 1038 | /** 1039 | * Rewrites accesses to a specific field in the given class to a method-call. 1040 | *

1041 | * The field specified by fieldName must be private and non-static. The method-call the field-access is redirected 1042 | * to does not take any parameters and returns an object of the same type as the field. If no methodName is passed, 1043 | * any method matching the described signature will be used as callable method. 1044 | * 1045 | * @param classNode the class to rewrite the accesses in 1046 | * @param fieldName the field accesses should be redirected to 1047 | * @param methodName the name of the method to redirect accesses through, or {@code null} if any method with 1048 | * matching signature should be applicable 1049 | * @apiNote This method was written as a special use case for Forge. It is not recommended to use this method 1050 | * unless you know what you are doing. 1051 | */ 1052 | public static void redirectFieldToMethod(final ClassNode classNode, final String fieldName, @Nullable final String methodName) { 1053 | MethodNode foundMethod = null; 1054 | FieldNode foundField = null; 1055 | for (FieldNode fieldNode : classNode.fields) { 1056 | if (Objects.equals(fieldNode.name, fieldName)) { 1057 | if (foundField == null) { 1058 | foundField = fieldNode; 1059 | } else { 1060 | throw new IllegalStateException("Found multiple fields with name " + fieldName); 1061 | } 1062 | } 1063 | } 1064 | 1065 | if (foundField == null) { 1066 | throw new IllegalStateException("No field with name " + fieldName + " found"); 1067 | } 1068 | if (!Modifier.isPrivate(foundField.access) || Modifier.isStatic(foundField.access)) { 1069 | throw new IllegalStateException("Field " + fieldName + " is not private and an instance field"); 1070 | } 1071 | 1072 | final String methodSignature = "()" + foundField.desc; 1073 | 1074 | for (MethodNode methodNode : classNode.methods) { 1075 | if (Objects.equals(methodNode.desc, methodSignature)) { 1076 | if (foundMethod == null && Objects.equals(methodNode.name, methodName)) { 1077 | foundMethod = methodNode; 1078 | } else if (foundMethod == null && methodName == null) { 1079 | foundMethod = methodNode; 1080 | } else if (foundMethod != null && (methodName == null || Objects.equals(methodNode.name, methodName))) { 1081 | throw new IllegalStateException("Found duplicate method with signature " + methodSignature); 1082 | } 1083 | } 1084 | } 1085 | 1086 | if (foundMethod == null) { 1087 | throw new IllegalStateException("Unable to find method " + methodSignature); 1088 | } 1089 | 1090 | for (MethodNode methodNode : classNode.methods) { 1091 | // skip the found getter method 1092 | if (methodNode == foundMethod) continue; 1093 | 1094 | if (Objects.equals(methodNode.desc, methodSignature)) continue; 1095 | 1096 | final ListIterator iterator = methodNode.instructions.iterator(); 1097 | while (iterator.hasNext()) { 1098 | AbstractInsnNode insnNode = iterator.next(); 1099 | if (insnNode.getOpcode() == Opcodes.GETFIELD) { 1100 | FieldInsnNode fieldInsnNode = (FieldInsnNode) insnNode; 1101 | if (Objects.equals(fieldInsnNode.name, fieldName)) { 1102 | iterator.remove(); 1103 | MethodInsnNode replace = new MethodInsnNode(Opcodes.INVOKEVIRTUAL, classNode.name, foundMethod.name, foundMethod.desc, false); 1104 | iterator.add(replace); 1105 | } 1106 | } 1107 | } 1108 | } 1109 | } 1110 | 1111 | 1112 | /* SRG NAME REMAPPING */ 1113 | 1114 | /** 1115 | * Maps a method from the given SRG name to the mapped name at deobfuscated runtime. 1116 | * 1117 | * @param name The SRG name of the method 1118 | * @return The mapped name of the method 1119 | * 1120 | * @apiNote As of Minecraft 1.20.4, Forge no longer uses SRG names in production. While the mapping system will 1121 | * still work for sake of backwards-compatibility, you should not be using this method if you are on 1.20.4 or 1122 | * later. 1123 | */ 1124 | public static String mapMethod(String name) { 1125 | return map(name, INameMappingService.Domain.METHOD); 1126 | } 1127 | 1128 | /** 1129 | * Maps a field from the given SRG name to the mapped name at deobfuscated runtime. 1130 | * 1131 | * @param name The SRG name of the field 1132 | * @return The mapped name of the field 1133 | * 1134 | * @apiNote As of Minecraft 1.20.4, Forge no longer uses SRG names in production. While the mapping system will 1135 | * still work for sake of backwards-compatibility, you should not be using this method if you are on 1.20.4 or 1136 | * later. 1137 | */ 1138 | public static String mapField(String name) { 1139 | return map(name, INameMappingService.Domain.FIELD); 1140 | } 1141 | 1142 | private static String map(String name, INameMappingService.Domain domain) { 1143 | return Optional.ofNullable(Launcher.INSTANCE). 1144 | map(Launcher::environment). 1145 | flatMap(env -> env.findNameMapping("srg")). 1146 | map(f -> f.apply(domain, name)).orElse(name); 1147 | } 1148 | 1149 | 1150 | /* ADDITIONAL DATA */ 1151 | 1152 | /** 1153 | * Checks if the given JVM property (or if the property prepended with {@code "coremod."}) is {@code true}. 1154 | * 1155 | * @param propertyName the property to check 1156 | * @return true if the property is {@code true} 1157 | */ 1158 | public static boolean getSystemPropertyFlag(final String propertyName) { 1159 | // TODO: Remove all backwards-compatible logic in 6.0 1160 | return Boolean.getBoolean(propertyName) // actually checks the flag 1161 | || Boolean.getBoolean("coremod." + propertyName) // the original intended purpose 1162 | || Boolean.getBoolean(System.getProperty("coremod." + propertyName, "TRUE")); // the bugged logic for backwards-compatibility 1163 | } 1164 | 1165 | /** 1166 | * Loads a JavaScript file by file name. Useful for reusing code across multiple files. 1167 | * 1168 | * @param file The file name to load 1169 | * @return {@code true} if the file load was successful. The file will only be loaded in the 1170 | * {@code initializeCoreMod()} or any of the transformer functions returned by it. 1171 | * 1172 | * @throws ScriptException If the script engine encounters an error, usually due to a syntax error in the script 1173 | * @throws IOException If an I/O error occurs while reading the file, usually due to a corrupt or missing file 1174 | */ 1175 | public static boolean loadFile(String file) throws ScriptException, IOException { 1176 | return CoreModTracker.loadFileByName(file); 1177 | } 1178 | 1179 | /** 1180 | * Loads JSON data from a file by file name. 1181 | * 1182 | * @param file The file name to load 1183 | * @return The loaded JSON data if successful, or {@code null} if not. The data will only be loaded in the 1184 | * {@code initializeCoreMod()} or any of the transformer functions returned by it. 1185 | * 1186 | * @throws ScriptException If the parsed JSON data is malformed 1187 | * @throws IOException If an I/O error occurs while reading the file, usually due to a corrupt or missing file 1188 | */ 1189 | @Nullable 1190 | public static Object loadData(String file) throws ScriptException, IOException { 1191 | return CoreModTracker.loadDataByName(file); 1192 | } 1193 | 1194 | 1195 | /* LOGGING AND DEBUGGING */ 1196 | 1197 | /** 1198 | * Logs the given message at the given level. The message can contain formatting arguments. Uses a 1199 | * {@link org.apache.logging.log4j.Logger}. 1200 | * 1201 | * @param level The log level 1202 | * @param message The message 1203 | * @param args Any formatting arguments 1204 | * @see CoreModTracker#log(String, String, Object[]) 1205 | */ 1206 | public static void log(String level, String message, Object... args) { 1207 | CoreModTracker.log(level, message, args); 1208 | } 1209 | 1210 | /** 1211 | * Converts a {@link ClassNode} to a string representation. Useful for evaluating changes after transformation. 1212 | * 1213 | * @param node The class node to convert 1214 | * @return The string representation of the class node 1215 | */ 1216 | public static String classNodeToString(ClassNode node) { 1217 | Textifier text = new Textifier(); 1218 | node.accept(new TraceClassVisitor(null, text, null)); 1219 | return toString(text); 1220 | } 1221 | 1222 | /** 1223 | * Converts a {@link MethodNode} to a string representation. Useful for evaluating changes after transformation. 1224 | * 1225 | * @param node The method node to convert 1226 | * @return The string representation of the method node 1227 | */ 1228 | public static String methodNodeToString(MethodNode node) { 1229 | Textifier text = new Textifier(); 1230 | node.accept(new TraceMethodVisitor(text)); 1231 | return toString(text); 1232 | } 1233 | 1234 | /** 1235 | * Converts a {@link FieldNode} to a string representation. Useful for evaluating changes after transformation. 1236 | * 1237 | * @param node The field node to convert 1238 | * @return The string representation of the field node 1239 | */ 1240 | public static String fieldNodeToString(FieldNode node) { 1241 | Textifier text = new Textifier(); 1242 | node.accept(new TraceClassVisitor(null, text, null)); 1243 | return toString(text); 1244 | } 1245 | 1246 | /** 1247 | * Converts an {@link InsnList} to a string representation, displaying each instruction in the list similar to 1248 | * {@link #insnToString(AbstractInsnNode)}. 1249 | * 1250 | * @param list The list to convert 1251 | * @return The string representation of the instruction list 1252 | */ 1253 | public static String insnListToString(InsnList list) { 1254 | Textifier text = new Textifier(); 1255 | list.accept(new TraceMethodVisitor(text)); 1256 | return toString(text); 1257 | } 1258 | 1259 | /** 1260 | * Converts an {@link AbstractInsnNode} to a string representation. 1261 | * 1262 | * @param insn The instruction to convert 1263 | * @return The string representation of the instruction 1264 | */ 1265 | public static String insnToString(AbstractInsnNode insn) { 1266 | Textifier text = new Textifier(); 1267 | insn.accept(new TraceMethodVisitor(text)); 1268 | return toString(text); 1269 | } 1270 | 1271 | /** 1272 | * Gets the LDC constant's class name as a string. Useful for debugging existing LDC instructions. 1273 | * 1274 | * @param insn The LDC instruction 1275 | * @return The class name of the LDC constant 1276 | */ 1277 | public static String ldcInsnClassToString(LdcInsnNode insn) { 1278 | return insn.cst.getClass().toString(); 1279 | } 1280 | 1281 | private static String toString(Textifier text) { 1282 | StringWriter sw = new StringWriter(); 1283 | PrintWriter pw = new PrintWriter(sw); 1284 | text.print(pw); 1285 | pw.flush(); 1286 | return sw.toString(); 1287 | } 1288 | 1289 | 1290 | /* MISCELLANEOUS */ 1291 | 1292 | // private because this is really only used to clamp indexes 1293 | private static int clamp(int value, int min, int max) { 1294 | return Math.max(min, Math.min(max, value)); 1295 | } 1296 | } 1297 | -------------------------------------------------------------------------------- /src/main/java/net/minecraftforge/coremod/transformer/CoreModBaseTransformer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Forge Development LLC 3 | * SPDX-License-Identifier: LGPL-2.1-only 4 | */ 5 | package net.minecraftforge.coremod.transformer; 6 | 7 | import cpw.mods.modlauncher.api.ITransformer; 8 | import cpw.mods.modlauncher.api.ITransformerVotingContext; 9 | import cpw.mods.modlauncher.api.TransformerVoteResult; 10 | import net.minecraftforge.coremod.CoreMod; 11 | import net.minecraftforge.coremod.CoreModTracker; 12 | import org.apache.logging.log4j.LogManager; 13 | import org.apache.logging.log4j.Logger; 14 | import org.apache.logging.log4j.Marker; 15 | import org.apache.logging.log4j.MarkerManager; 16 | 17 | import java.util.Set; 18 | import java.util.function.Function; 19 | 20 | /** 21 | * The base-level transformer for CoreMods. 22 | * 23 | * @param The type of node to transform 24 | */ 25 | public abstract class CoreModBaseTransformer implements ITransformer { 26 | static final Logger LOGGER = LogManager.getLogger(); 27 | static final Marker COREMOD = MarkerManager.getMarker("COREMOD"); 28 | final CoreMod coreMod; 29 | final Set targets; 30 | final Function function; 31 | final String coreName; 32 | 33 | /** 34 | * Creates a new base-level transformer with the given targets and transformer function. 35 | * 36 | * @param coreMod The CoreMod that this transformer belongs to 37 | * @param coreName The name of the CoreMod 38 | * @param targets The targets to apply this transformer to 39 | * @param function The transformer function 40 | */ 41 | public CoreModBaseTransformer(CoreMod coreMod, final String coreName, final Set targets, final Function function) { 42 | this.coreMod = coreMod; 43 | this.coreName = coreName; 44 | this.targets = targets; 45 | this.function = function; 46 | } 47 | 48 | /** 49 | * Transforms the given input node using the transformer function. 50 | * 51 | * @param input The ASM input node to transform 52 | * @param context The voting context for ModLauncher 53 | * @return The transformed node 54 | */ 55 | @Override 56 | public T transform(T input, ITransformerVotingContext context) { 57 | CoreModTracker.setCoreMod(coreMod); 58 | T result = input; 59 | try { 60 | result = runCoremod(result); 61 | } catch (Exception e) { 62 | LOGGER.error(COREMOD, "Error occurred applying transform of coremod {} function {}", this.coreMod.getPath(), this.coreName, e); 63 | // TODO CRASH THE FUCKING GAME HERE 64 | } finally { 65 | CoreModTracker.clearCoreMod(); 66 | } 67 | return result; 68 | } 69 | 70 | abstract T runCoremod(T input); 71 | 72 | /** 73 | * The transformer vote that this CoreMod should use as a result of transformation. 74 | * 75 | * @param context The context of the vote 76 | * @return The desired transformer vote 77 | */ 78 | @Override 79 | public TransformerVoteResult castVote(ITransformerVotingContext context) { 80 | return TransformerVoteResult.YES; 81 | } 82 | 83 | /** 84 | * Gets the desired transformer targets of this CoreMod. 85 | * 86 | * @return The targets for transformation 87 | * 88 | * @apiNote The result of this method does not usually indicate that it will be used directly in 89 | * @link #runCoremod(Object)}. 90 | */ 91 | @Override 92 | public Set targets() { 93 | return targets; 94 | } 95 | 96 | /** 97 | * Gets the identification labels of this transformer to be used in ModLauncher. 98 | * 99 | * @return The identification labels 100 | */ 101 | @Override 102 | public String[] labels() { 103 | return new String[] {coreMod.getFile().getOwnerId(), coreName}; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/net/minecraftforge/coremod/transformer/CoreModClassTransformer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Forge Development LLC 3 | * SPDX-License-Identifier: LGPL-2.1-only 4 | */ 5 | package net.minecraftforge.coremod.transformer; 6 | 7 | import cpw.mods.modlauncher.api.ITransformer; 8 | import net.minecraftforge.coremod.CoreMod; 9 | import org.objectweb.asm.tree.ClassNode; 10 | 11 | import java.util.Set; 12 | import java.util.function.Function; 13 | 14 | public class CoreModClassTransformer extends CoreModBaseTransformer implements ITransformer { 15 | public CoreModClassTransformer(CoreMod coreMod, String coreName, Set targets, Function function) { 16 | super(coreMod, coreName, targets, function); 17 | } 18 | 19 | @Override 20 | ClassNode runCoremod(ClassNode input) { 21 | LOGGER.debug(COREMOD, "Transforming {}", input.name); 22 | return function.apply(input); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/net/minecraftforge/coremod/transformer/CoreModFieldTransformer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Forge Development LLC 3 | * SPDX-License-Identifier: LGPL-2.1-only 4 | */ 5 | package net.minecraftforge.coremod.transformer; 6 | 7 | import cpw.mods.modlauncher.api.ITransformer; 8 | import net.minecraftforge.coremod.CoreMod; 9 | import org.objectweb.asm.tree.FieldNode; 10 | 11 | import java.util.Set; 12 | import java.util.function.Function; 13 | 14 | public class CoreModFieldTransformer extends CoreModBaseTransformer implements ITransformer { 15 | public CoreModFieldTransformer(CoreMod coreMod, String coreName, Set targets, Function function) { 16 | super(coreMod, coreName, targets, function); 17 | } 18 | 19 | @Override 20 | FieldNode runCoremod(FieldNode input) { 21 | LOGGER.debug(COREMOD, "Transforming {} with desc {}", input.name, input.desc); 22 | return function.apply(input); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/net/minecraftforge/coremod/transformer/CoreModMethodTransformer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Forge Development LLC 3 | * SPDX-License-Identifier: LGPL-2.1-only 4 | */ 5 | package net.minecraftforge.coremod.transformer; 6 | 7 | import cpw.mods.modlauncher.api.ITransformer; 8 | import net.minecraftforge.coremod.CoreMod; 9 | import org.objectweb.asm.tree.MethodNode; 10 | 11 | import java.util.Set; 12 | import java.util.function.Function; 13 | 14 | public class CoreModMethodTransformer extends CoreModBaseTransformer implements ITransformer { 15 | public CoreModMethodTransformer(CoreMod coreMod, String coreName, Set targets, Function function) { 16 | super(coreMod, coreName, targets, function); 17 | } 18 | 19 | @Override 20 | MethodNode runCoremod(MethodNode input) { 21 | LOGGER.debug(COREMOD, "Transforming {} with desc {}", input.name, input.desc); 22 | return function.apply(input); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/net.minecraftforge.forgespi.coremod.ICoreModProvider: -------------------------------------------------------------------------------- 1 | net.minecraftforge.coremod.CoreModProvider --------------------------------------------------------------------------------