diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1c59d9d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2023 Linnea Gräf +# +# SPDX-License-Identifier: CC0-1.0 + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = tab +insert_final_newline = true +max_line_length = 120 + + +[*.kt] +ij_kotlin_name_count_to_use_star_import = 99999 +ij_kotlin_name_count_to_use_star_import_for_members = 99999 +ij_kotlin_imports_layout = *, |, kotlinx.**, kotlin.**, net.minecraft.**, moe.nea.firmament.**, |, $* +ij_kotlin_packages_to_use_import_on_demand = false diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..344d93e --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2023 Linnea Gräf +# +# SPDX-License-Identifier: CC0-1.0 + +repos: + - repo: https://github.com/fsfe/reuse-tool + rev: v4.0.3 + hooks: + - id: reuse + - repo: local + hooks: + - id: ordered_translations + name: "Check translation ordering" + language: script + files: translations/.* + entry: ./check-translation-ordering.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f9815e5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,52 @@ + +# Contributing to Firmament + +Contributions are tentatively welcomed. The structure of the mod is probably not really transparent to newcomers, but if +you are interested, feel free to tackle any [issues](https://github.com/nea89o/Firmament/issues/) or create your own +features. + +## Community + +If you need any help contributing feel free to join the [discord]. This is where you can raise more casual issues. Note +that using the discord is not mandatory for contributing. If you don't want to join the discord, feel free to ask +questions in issues or to otherwise contact me. + +## State of Firmament + +Many of the foundations of Firmament are not yet fix. If you find things confusing or think some of the fundamental +building blocks of Firmament should be changed, feel free to raise an issue. More than likely there is no deeper reason +for something confusing, aside from this being a bit of a sloppy project. + +## Development workflow + +Firmament is a fabric mod so the development workflow is quite similar to any other fabric mod. To build you will need a +Java capable IDE (strongly recommend IntelliJ here), as well as Java 21. To contribute back to Firmament you will need +to create a [fork](https://github.com/nea89o/Firmament/fork). This is your own copy of Firmament which you can change. +You can load that fork using IntelliJs "New" -> "Project from version control" import option. You might need to +authenticate your GitHub account to IntelliJ during this process or at a later point in time. + +### JBR + +By default, Firmament tries to launch with hotswapping enabled in the devenv. This will fail if your JVM does not +support hotswapping. In that case you can either remove the hotswapping arguments, or you can use [JBR](jbr). You can +manually select JBR in your run configuration, after it has been generated by gradle. + +``` +-XX:+AllowEnhancedClassRedefinition -XX:HotswapAgent=external -javaagent:SOMEPATH +``` + +### Features + +Typically, for each new set of features you will create a branch and then create a pull request back to the main +Firmament repo. Note that Firmament makes use of commit names to generate a changelog. As such the first line of each +commit should be something that can be interpreted by an end user. If you want to make an internal change you can use +the `[no changelog]` tag inside your commit message body to hide a commit from the changelog. Try to make one commit for +each feature. Don't worry if you have any problems with your git history, your pull request will history will be +rewritten to be fixed (but it would help me if you can keep your commit history clean). + +[discord]: https://discord.gg/64pFP94AWA +[jbr]: https://github.com/JetBrains/JetBrainsRuntime/releases/tag/jbr-release-21.0.5b631.16 diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt new file mode 100644 index 0000000..137069b --- /dev/null +++ b/LICENSES/Apache-2.0.txt @@ -0,0 +1,73 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSES/CC-BY-4.0.txt b/LICENSES/CC-BY-4.0.txt new file mode 100644 index 0000000..13ca539 --- /dev/null +++ b/LICENSES/CC-BY-4.0.txt @@ -0,0 +1,156 @@ +Creative Commons Attribution 4.0 International + + Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. + +Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. More considerations for licensors. + +Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public. + +Creative Commons Attribution 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. + +Section 1 – Definitions. + + a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. + + d. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. + + g. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights under this Public License. + + i. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. + + j. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. + + k. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. + +Section 2 – Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: + + A. reproduce and Share the Licensed Material, in whole or in part; and + + B. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. + + 3. Term. The term of this Public License is specified in Section 6(a). + + 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. + + 5. Downstream recipients. + + A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. + + B. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. + + 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). + +b. Other rights. + + 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this Public License. + + 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. + +Section 3 – License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified form), You must: + + A. retain the following if it is supplied by the Licensor with the Licensed Material: + + i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of warranties; + + v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; + + B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and + + C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. + + 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License. + +Section 4 – Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; + + b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. +For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. + +Section 5 – Disclaimer of Warranties and Limitation of Liability. + + a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You. + + b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You. + + c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. + +Section 6 – Term and Termination. + + a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or + + 2. upon express reinstatement by the Licensor. + + c. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. + + d. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. + + e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. + +Section 7 – Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. + +Section 8 – Interpretation. + + a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. + + c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. + + d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. + +Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. + +Creative Commons may be contacted at creativecommons.org. diff --git a/LICENSES/CC0-1.0.txt b/LICENSES/CC0-1.0.txt new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/LICENSES/CC0-1.0.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/LICENSES/GPL-3.0-or-later.txt b/LICENSES/GPL-3.0-or-later.txt new file mode 100644 index 0000000..f6cdd22 --- /dev/null +++ b/LICENSES/GPL-3.0-or-later.txt @@ -0,0 +1,232 @@ +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +Preamble + +The GNU General Public License is a free, copyleft license for software and other kinds of works. + +The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. + +To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. + +Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. + +The precise terms and conditions for copying, distribution and modification follow. + +TERMS AND CONDITIONS + +0. Definitions. + +“This License” refers to version 3 of the GNU General Public License. + +“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. + +“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations. + +To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work. + +A “covered work” means either the unmodified Program or a work based on the Program. + +To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. + +To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. + +1. Source Code. +The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work. + +A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. + +The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. + +The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +2. Basic Permissions. +All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. +No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. + +4. Conveying Verbatim Copies. +You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. +You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”. + + c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. + +6. Conveying Non-Source Forms. +You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: + + a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. + + d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. + +A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. + +“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). + +The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. + +7. Additional Terms. +“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or authors of the material; or + + e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. + +8. Termination. +You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. + +9. Acceptance Not Required for Having Copies. +You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. +Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. + +An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. + +11. Patents. +A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”. + +A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. + +In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. + +A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. +If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. + +13. Use with the GNU Affero General Public License. +Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. + +14. Revised Versions of this License. +The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. + +Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. +If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”. + +You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . + +The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt new file mode 100644 index 0000000..2071b23 --- /dev/null +++ b/LICENSES/MIT.txt @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/REUSE.toml b/REUSE.toml new file mode 100644 index 0000000..9a6cf0f --- /dev/null +++ b/REUSE.toml @@ -0,0 +1,53 @@ +#SPDX-FileCopyrightText: 2024 Linnea Gräf +# +#SPDX-License-Identifier: CC0-1.0 + +version = 1 +SPDX-PackageName = "Firmament" +SPDX-PackageSupplier = "Linnea Gräf " +SPDX-PackageDownloadLocation = "https://github.com/nea89o/Firmament" + +[[annotations]] +path = ["**/*.kt", "**/*.java"] +SPDX-License-Identifier = "GPL-3.0-or-later" +SPDX-FileCopyrightText = ["Linnea Gräf ", "Firmament Contributors"] + +[[annotations]] +path = ["src/main/resources/**/*.png", "src/main/resources/**/*.mcmeta"] +SPDX-License-Identifier = "CC-BY-4.0" +SPDX-FileCopyrightText = ["Linnea Gräf ", "Firmament Contributors"] + +[[annotations]] +path = ["src/main/resources/assets/firmament/shaders/**/*"] +SPDX-License-Identifier = "GPL-3.0-or-later" +SPDX-FileCopyrightText = ["Linnea Gräf ", "Firmament Contributors"] + +[[annotations]] +path = "translations/*.json" +SPDX-License-Identifier = "CC0-1.0" +SPDX-FileCopyrightText = ["Linnea Gräf ", "Firmament Contributors"] + +[[annotations]] +path = ["src/main/resources/assets/firmament/gui/**/*.xml"] +SPDX-License-Identifier = "CC0-1.0" +SPDX-FileCopyrightText = ["Linnea Gräf ", "Firmament Contributors"] + +[[annotations]] +path = "**/*.gradle.kts" +SPDX-License-Identifier = "CC0-1.0" +SPDX-FileCopyrightText = ["Linnea Gräf ", "Firmament Contributors"] + +[[annotations]] +path = ["**/META-INF/services/*"] +SPDX-License-Identifier = "CC0-1.0" +SPDX-FileCopyrightText = ["Linnea Gräf ", "Firmament Contributors"] + +[[annotations]] +path = ["src/test/resources/testdata/**/*.snbt"] +SPDX-License-Identifier = "CC-BY-4.0" +SPDX-FileCopyrightText = ["Linnea Gräf ", "Firmament Contributors"] + +[[annotations]] +path = ["src/main/resources/legacy_data/*.json"] +SPDX-License-Identifier = "MIT" +SPDX-FileCopyrightText = ["PrismarineJS Minecraft Data"] diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts new file mode 100644 index 0000000..431a04e --- /dev/null +++ b/build-logic/build.gradle.kts @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2023 Linnea Gräf +// +// SPDX-License-Identifier: CC0-1.0 + +plugins { + `kotlin-dsl` + kotlin("jvm") version "2.0.21" +} +repositories { + mavenCentral() + gradlePluginPortal() + maven { + name = "jitpack" + url = uri("https://jitpack.io") + } +} +dependencies { + implementation("com.github.romangraef:neaslicenseextractificator:1.1.0") + api("com.gradleup.shadow:shadow-gradle-plugin:9.0.0-rc1") + implementation("net.fabricmc:access-widener:2.1.0") + implementation("com.google.code.gson:gson:2.10.1") +} diff --git a/build-logic/gradle/wrapper b/build-logic/gradle/wrapper new file mode 100644 index 0000000..3232fe4 --- /dev/null +++ b/build-logic/gradle/wrapper @@ -0,0 +1 @@ +../../gradle/wrapper \ No newline at end of file diff --git a/build-logic/gradlew b/build-logic/gradlew new file mode 100644 index 0000000..faf9300 --- /dev/null +++ b/build-logic/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/build-logic/gradlew.bat b/build-logic/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/build-logic/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts new file mode 100644 index 0000000..0108b7a --- /dev/null +++ b/build-logic/settings.gradle.kts @@ -0,0 +1,8 @@ +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} +rootProject.name = "firm-build-logic" diff --git a/build-logic/src/main/kotlin/EnvFile.kt b/build-logic/src/main/kotlin/EnvFile.kt new file mode 100644 index 0000000..ceec763 --- /dev/null +++ b/build-logic/src/main/kotlin/EnvFile.kt @@ -0,0 +1,13 @@ + +import java.io.File + +fun parseEnvFile(file: File): Map { + if (!file.exists()) return mapOf() + val map = mutableMapOf() + for (line in file.readText().lines()) { + if (line.isEmpty() || line.startsWith("#")) continue + val parts = line.split("=", limit = 2) + map[parts[0]] = parts.getOrNull(1) ?: "" + } + return map +} diff --git a/build-logic/src/main/kotlin/FabricModTransform.kt b/build-logic/src/main/kotlin/FabricModTransform.kt new file mode 100644 index 0000000..53affbe --- /dev/null +++ b/build-logic/src/main/kotlin/FabricModTransform.kt @@ -0,0 +1,80 @@ +import com.github.jengelman.gradle.plugins.shadow.transformers.ResourceTransformer +import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext +import com.google.gson.Gson +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import java.io.Serializable +import net.fabricmc.accesswidener.AccessWidenerReader +import net.fabricmc.accesswidener.AccessWidenerWriter +import org.apache.tools.zip.ZipEntry +import org.apache.tools.zip.ZipOutputStream +import org.gradle.api.file.FileTreeElement +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal + +open class FabricModTransform : ResourceTransformer { + + enum class AccessWidenerInclusion : Serializable { + ALL, + NONE, + } + + @get:Input + var mergeAccessWideners: AccessWidenerInclusion = AccessWidenerInclusion.ALL + + @get:Internal + internal var mergedFmj: JsonObject? = null + + @get:Internal + internal val foundAccessWideners = AccessWidenerWriter() + + @get:Internal + internal var foundAnyAccessWidener = false + + override fun canTransformResource(element: FileTreeElement): Boolean { + if (mergeAccessWideners == AccessWidenerInclusion.ALL && element.name.endsWith(".accesswidener")) + return true + return element.path == "fabric.mod.json" + } + + override fun transform(context: TransformerContext) { + if (context.path.endsWith(".accesswidener")) { + foundAnyAccessWidener = true + // TODO: allow filtering for only those mentioned in a fabric.mod.json, potentially + context.inputStream.use { stream -> + AccessWidenerReader(foundAccessWideners).read(stream.bufferedReader()) + } + return + } + // TODO: mixins.json relocations + val fmj = context.inputStream.use { stream -> + Gson().fromJson(stream.bufferedReader(), JsonObject::class.java) + } + val mergedFmj = this.mergedFmj + println("${fmj["id"]} is first? ${mergedFmj == null}") + if (mergedFmj == null) { + this.mergedFmj = fmj + } else { + // TODO: merge stuff + } + } + + override fun hasTransformedResource(): Boolean { + return mergedFmj != null + } + + override fun modifyOutputStream(os: ZipOutputStream, preserveFileTimestamps: Boolean) { + val mergedFmj = mergedFmj!! + if (foundAnyAccessWidener) { + val awFile = mergedFmj["accessWidener"] + require(awFile is JsonPrimitive && awFile.isString) + os.putNextEntry(ZipEntry(awFile.asString)) + os.write(foundAccessWideners.write()) + os.closeEntry() + } + os.putNextEntry(ZipEntry("fabric.mod.json")) + os.write(mergedFmj.toString().toByteArray()) + os.closeEntry() + } +} diff --git a/build-logic/src/main/kotlin/InnerJarsUnpacker.kt b/build-logic/src/main/kotlin/InnerJarsUnpacker.kt new file mode 100644 index 0000000..de06467 --- /dev/null +++ b/build-logic/src/main/kotlin/InnerJarsUnpacker.kt @@ -0,0 +1,70 @@ +import com.google.gson.Gson +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import java.io.File +import java.util.zip.ZipInputStream +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.OutputFiles +import org.gradle.api.tasks.TaskAction +import kotlin.io.path.createDirectories +import kotlin.io.path.outputStream + +abstract class InnerJarsUnpacker : DefaultTask() { + @get:InputFiles + abstract val inputJars: ConfigurableFileCollection + + @get:OutputDirectory + abstract val outputDir: DirectoryProperty + + private fun getFabricModJson(inputFile: File): JsonObject { + inputFile.inputStream().use { + val zis = ZipInputStream(it) + while (true) { + val entry = zis.nextEntry ?: error("Failed to find fabric.mod.json") + if (entry.name == "fabric.mod.json") { + return Gson().fromJson(zis.reader(), JsonObject::class.java) + } + } + } + } + + @TaskAction + fun unpack() { + inputJars.forEach { inputFile -> + val fabricModObject = getFabricModJson(inputFile) + val jars = fabricModObject["jars"] as? JsonArray ?: error("No jars to unpack in $inputFile") + val jarPaths = jars.map { + ((it as? JsonObject)?.get("file") as? JsonPrimitive)?.asString + ?: error("Invalid Jar $it in $inputFile") + } + extractJars(inputFile, jarPaths) + } + } + + private fun extractJars(inputFile: File, jarPaths: List) { + val outputFile = outputDir.get().asFile.toPath() + val jarPathSet = jarPaths.toMutableSet() + inputFile.inputStream().use { + val zis = ZipInputStream(it) + while (true) { + val entry = zis.nextEntry ?: break + if (jarPathSet.remove(entry.name)) { + val resolvedPath = outputFile.resolve(entry.name) + resolvedPath.parent.createDirectories() + resolvedPath.outputStream().use { os -> + zis.copyTo(os) + } + } + } + } + if (jarPathSet.isNotEmpty()) { + error("Could not extract all jars: $jarPathSet") + } + } +} diff --git a/build-logic/src/main/kotlin/RepoDownload.kt b/build-logic/src/main/kotlin/RepoDownload.kt new file mode 100644 index 0000000..42a09b3 --- /dev/null +++ b/build-logic/src/main/kotlin/RepoDownload.kt @@ -0,0 +1,41 @@ +import java.net.URI +import java.util.zip.ZipInputStream +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction + +abstract class RepoDownload : DefaultTask() { + @get:Input + abstract val hash: Property + + @get:OutputDirectory + abstract val outputDirectory: DirectoryProperty + + init { + outputDirectory.convention(project.layout.buildDirectory.dir("extracted-test-repo")) + } + + @TaskAction + fun performDownload() { + val outputDir = outputDirectory.asFile.get().absoluteFile + outputDir.mkdirs() + URI("https://github.com/notEnoughUpdates/notEnoughUpdates-rEPO/archive/${hash.get()}.zip").toURL().openStream() + .let(::ZipInputStream) + .use { zipInput -> + while (true) { + val entry = zipInput.nextEntry ?: break + val destination = outputDir.resolve( + entry.name.substringAfter('/')).absoluteFile + require(outputDir in generateSequence(destination) { it.parentFile }) + if (entry.isDirectory) continue + destination.parentFile.mkdirs() + destination.outputStream().use { output -> + zipInput.copyTo(output) + } + } + } + } +} diff --git a/build-logic/src/main/kotlin/firmament.base.gradle.kts b/build-logic/src/main/kotlin/firmament.base.gradle.kts new file mode 100644 index 0000000..8c512a4 --- /dev/null +++ b/build-logic/src/main/kotlin/firmament.base.gradle.kts @@ -0,0 +1 @@ +group = "moe.nea.firmament" diff --git a/build-logic/src/main/kotlin/firmament.common.gradle.kts b/build-logic/src/main/kotlin/firmament.common.gradle.kts new file mode 100644 index 0000000..a359b3d --- /dev/null +++ b/build-logic/src/main/kotlin/firmament.common.gradle.kts @@ -0,0 +1,2 @@ +apply(plugin = "firmament.base") +apply(plugin = "firmament.repositories") diff --git a/build-logic/src/main/kotlin/firmament.license-management.gradle.kts b/build-logic/src/main/kotlin/firmament.license-management.gradle.kts new file mode 100644 index 0000000..0a2626b --- /dev/null +++ b/build-logic/src/main/kotlin/firmament.license-management.gradle.kts @@ -0,0 +1,5 @@ +apply(plugin = "moe.nea.licenseextractificator") + +configure { + addExtraLicenseMatchers() +} diff --git a/build-logic/src/main/kotlin/firmament.repositories.gradle.kts b/build-logic/src/main/kotlin/firmament.repositories.gradle.kts new file mode 100644 index 0000000..07a5709 --- /dev/null +++ b/build-logic/src/main/kotlin/firmament.repositories.gradle.kts @@ -0,0 +1,46 @@ +repositories { + mavenCentral() + maven("https://maven.terraformersmc.com/releases/") + maven("https://maven.shedaniel.me") + maven("https://maven.fabricmc.net") + maven("https://pkgs.dev.azure.com/djtheredstoner/DevAuth/_packaging/public/maven/v1") + maven("https://api.modrinth.com/maven") { + content { + includeGroup("maven.modrinth") + } + } + maven("https://repo.sleeping.town") { + content { + includeGroup("com.unascribed") + } + } + ivy("https://github.com/HotswapProjects/HotswapAgent/releases/download") { + patternLayout { + artifact("[revision]/[artifact]-[revision].[ext]") + } + content { + includeGroup("virtual.github.hotswapagent") + } + metadataSources { + artifact() + } + } + maven("https://server.bbkr.space/artifactory/libs-release") + maven("https://repo.nea.moe/releases") + maven("https://maven.notenoughupdates.org/releases") + maven("https://repo.nea.moe/mirror") + maven("https://jitpack.io/") { + content { + includeGroupByRegex("(com|io)\\.github\\..+") + excludeModule("io.github.cottonmc", "LibGui") + } + } + maven("https://repo.hypixel.net/repository/Hypixel/") + maven("https://maven.azureaaron.net/snapshots") + maven("https://maven.azureaaron.net/releases") + maven("https://www.cursemaven.com") + maven("https://maven.isxander.dev/releases") { + name = "Xander Maven" + } + mavenLocal() +} diff --git a/build-logic/src/main/kotlin/licenseinfo.kt b/build-logic/src/main/kotlin/licenseinfo.kt new file mode 100644 index 0000000..50e4593 --- /dev/null +++ b/build-logic/src/main/kotlin/licenseinfo.kt @@ -0,0 +1,144 @@ +// SPDX-FileCopyrightText: 2023 Linnea Gräf +// +// SPDX-License-Identifier: CC0-1.0 + +import moe.nea.licenseextractificator.LicenseExtension + +fun LicenseExtension.addExtraLicenseMatchers() { + solo { + name = "Firmament" + description = "A Hypixel SkyBlock mod" + developer("Linnea Gräf") { + webPresence = "https://nea.moe/" + } + spdxLicense.`GPL-3-0-or-later`() + webPresence = "https://git.nea.moe/nea/Firmament/" + } + match { + if (group == "net.minecraft") useLicense { + name = "Minecraft" + description = "Minecraft - The critically acclaimed video game" + license("All Rights Reserved", "https://www.minecraft.net/en-us/eula") + developer("Mojang") { + webPresence = "https://mojang.com" + } + webPresence = "https://www.minecraft.net/en-us" + } + if (module == "architectury") useLicense { + name = "Architectury API" + description = "An intermediary api aimed at easing development of multiplatform mods." + spdxLicense.`LGPL-3-0-or-later`() + developer("Architectury") { + webPresence = "https://docs.architectury.dev/" + } + webPresence = "https://github.com/architectury/architectury-api" + } + if (module.startsWith("RoughlyEnoughItems")) useLicense { + name = module + description = "Your recipe viewer mod for 1.13+." + spdxLicense.MIT() + developer("Shedaniel") { + webPresence = "https://shedaniel.me/" + } + webPresence = "https://github.com/shedaniel/RoughlyEnoughItems" + } + if (module == "cloth-config") useLicense { + name = "Cloth Config" + description = "Client sided configuration API" + spdxLicense.`LGPL-3-0-or-later`() + developer("Shedaniel") { + webPresence = "https://shedaniel.me/" + } + webPresence = "https://github.com/shedaniel/cloth-config" + } + if (module == "basic-math") useLicense { + name = "Cloth BasicMath" + description = "Basic Math Operations" + spdxLicense.Unlicense() + developer("Shedaniel") { + webPresence = "https://shedaniel.me/" + } + webPresence = "https://github.com/shedaniel/cloth-basic-math" + } + if (module == "fabric-language-kotlin") useLicense { + name = "Fabric Language Kotlin" + description = "Kotlin Language Support for Fabric mods" + webPresence = "https://github.com/FabricMC/fabric-language-kotlin" + spdxLicense.`Apache-2-0`() + developer("FabricMC") { + webPresence = "https://fabricmc.net/" + } + } + if (group == "com.mojang") useLicense { + name = module + description = "Mojang library packaged by Minecraft" + } + } + module("net.fabricmc", "yarn") { + name = "Yarn" + description = "Libre Minecraft mappings, free to use for everyone. No exceptions." + spdxLicense.`CC0-1-0`() + developer("FabricMC") { + webPresence = "https://fabricmc.net/" + } + webPresence = "https://github.com/FabricMC/yarn/" + } + module("com.mojang", "datafixerupper") { + name = "DataFixerUpper" + description = + "A set of utilities designed for incremental building, merging and optimization of data transformations." + spdxLicense.MIT() + developer("Mojang") { + webPresence = "https://mojang.com" + } + webPresence = "https://github.com/Mojang/DataFixerUpper" + } + module("com.mojang", "brigadier") { + name = "Brigadier" + description = "Brigadier is a command parser & dispatcher, designed and developed for Minecraft: Java Edition." + spdxLicense.MIT() + developer("Mojang") { + webPresence = "https://mojang.com" + } + webPresence = "https://github.com/Mojang/brigadier" + } + module("net.fabricmc", "tiny-remapper") { + name = "Tiny Remapper" + description = "Tiny JAR remapping tool" + spdxLicense.`LGPL-3-0-or-later`() + webPresence = "https://github.com/FabricMC/tiny-remapper" + developer("FabricMC") { + webPresence = "https://fabricmc.net/" + } + } + module("net.fabricmc", "sponge-mixin") { + name = "Mixin" + description = "Mixin is a trait/mixin framework for Java using ASM" + spdxLicense.MIT() + webPresence = "https://github.com/FabricMC/mixin" + developer("FabricMC") { + webPresence = "https://fabricmc.net/" + } + developer("SpongePowered") { + webPresence = "https://spongepowered.org/" + } + } + module("net.fabricmc", "tiny-mappings-parser") { + name = "Tiny Mappings Parser" + webPresence = "https://github.com/fabricMC/tiny-mappings-parser" + description = "Library for parsing .tiny mapping files" + developer("FabricMC") { + webPresence = "https://fabricmc.net/" + } + spdxLicense.`Apache-2-0`() + } + module("net.fabricmc", "fabric-loader") { + name = "Fabric Loader" + description = " Fabric's mostly-version-independent mod loader." + spdxLicense.`Apache-2-0`() + developer("FabricMC") { + webPresence = "https://fabricmc.net/" + } + webPresence = "https://github.com/FabricMC/fabric-loader/" + } +} diff --git a/build-logic/src/main/kotlin/lookupversion.kt b/build-logic/src/main/kotlin/lookupversion.kt new file mode 100644 index 0000000..8a7c2de --- /dev/null +++ b/build-logic/src/main/kotlin/lookupversion.kt @@ -0,0 +1,25 @@ +fun execString(vararg args: String): String { + val pb = ProcessBuilder(*args) + .redirectOutput(ProcessBuilder.Redirect.PIPE) + .start() + pb.waitFor() + return pb.inputStream.readAllBytes().decodeToString().trim() +} + +private val tag = "([0-9.]+)(?:\\+[^-]*)?".toRegex() +private val tagOffset = "([0-9.]+)(?:\\+.*)?-([0-9]+)-(.+)".toRegex() + +inline fun Regex.useMatcher(string: String, block: (MatchResult) -> T): T? { + return matchEntire(string)?.let(block) +} + +fun getGitTagInfo(mcVersion: String): String { + val str = execString("git", "describe", "--tags", "HEAD") + tag.useMatcher(str) { + return it.groupValues[1] + "+mc$mcVersion" + } + tagOffset.useMatcher(str) { + return it.groupValues[1] + "-dev+mc$mcVersion+" + it.groupValues[3] + } + return "nogitversion+mc$mcVersion" +} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..c8bdfdb --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,521 @@ +/* + * SPDX-FileCopyrightText: 2023 Linnea Gräf + * SPDX-FileCopyrightText: 2024 Linnea Gräf + * + * SPDX-License-Identifier: CC0-1.0 + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import com.google.common.hash.Hashing +import com.google.devtools.ksp.gradle.KspAATask +import com.google.gson.Gson +import com.google.gson.JsonObject +import moe.nea.licenseextractificator.LicenseDiscoveryTask +import moe.nea.mcautotranslations.gradle.CollectTranslations +import net.fabricmc.loom.LoomGradleExtension +import org.apache.tools.ant.taskdefs.condition.Os +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import java.nio.charset.StandardCharsets +import java.util.* + +plugins { + java + `maven-publish` + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.kotlin.plugin.serialization) + alias(libs.plugins.kotlin.plugin.powerassert) + alias(libs.plugins.kotlin.plugin.ksp) + // alias(libs.plugins.loom) + alias(libs.plugins.shadow) apply false + // TODO: use arch loom once they update to 1.8 + id("fabric-loom") version "1.10.1" + id("firmament.common") + id("firmament.license-management") + alias(libs.plugins.mcAutoTranslations) +} + +version = getGitTagInfo(libs.versions.minecraft.get()) + +java { + withSourcesJar() + toolchain { + languageVersion.set(JavaLanguageVersion.of(21)) + } +} + +loom { + mixin.useLegacyMixinAp.set(false) +} + +tasks.withType(KotlinCompile::class) { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_21) + } +} + +kotlin { + sourceSets.all { + languageSettings { + enableLanguageFeature("BreakContinueInInlineLambdas") + } + } +} +fun String.capitalizeN() = replaceFirstChar { it.uppercaseChar() } +// Usually a normal sync takes care of this, but in CI everything needs to run in one shot, so we need to improvise. +val unpackAllJars by tasks.registering +fun innerJarsOf(name: String, dependency: Dependency): Provider { + val task = tasks.create("unpackInnerJarsFor${name.capitalizeN()}", InnerJarsUnpacker::class) { + this.inputJars.setFrom(files(configurations.detachedConfiguration(dependency))) + this.outputDir.set(layout.buildDirectory.dir("unpackedJars/$name").also { + it.get().asFile.mkdirs() + }) + } + unpackAllJars { dependsOn(task) } + return project.provider { + project.files(task).asFileTree + } +} + +val collectTranslations by tasks.registering(CollectTranslations::class) { + this.baseTranslations.from(file("translations/en_us.json")) + this.baseTranslations.from(file("translations/extra.json")) + this.classes.from(sourceSets.main.get().kotlin.classesDirectory) +} + +val shadowJar = tasks.register("shadowJar", ShadowJar::class) +val mergedSourceSetsJar = tasks.register("mergedSourceSetsJar", ShadowJar::class) + +val compatSourceSets: MutableSet = mutableSetOf() +fun createIsolatedSourceSet(name: String, path: String = "compat/$name", isEnabled: Boolean = true): SourceSet { + val ss = sourceSets.create(name) { + this.java.setSrcDirs(listOf(layout.projectDirectory.dir("src/$path/java"))) + this.kotlin.setSrcDirs(listOf(layout.projectDirectory.dir("src/$path/java"))) + } + val mainSS = sourceSets.main.get() + val upperName = ss.name.capitalizeN() + afterEvaluate { + tasks.named("ksp${upperName}Kotlin", KspAATask::class) { + this.commandLineArgumentProviders.add { // TODO: update https://github.com/google/ksp/issues/2075 + listOf("firmament.sourceset=${ss.name}") + } + } + tasks.named("compile${upperName}Kotlin", KotlinCompile::class) { + this.enabled = isEnabled + } + tasks.named("compile${upperName}Java", JavaCompile::class) { + this.enabled = isEnabled + } + } + compatSourceSets.add(ss) + loom.createRemapConfigurations(ss) + if (!isEnabled) { + ss.output.files.forEach { it.deleteRecursively() } + return ss + } + configurations { + (ss.implementationConfigurationName) { + extendsFrom(getByName(mainSS.compileClasspathConfigurationName)) + } + (ss.annotationProcessorConfigurationName) { + extendsFrom(getByName(mainSS.annotationProcessorConfigurationName)) + } + (mainSS.runtimeOnlyConfigurationName) { + if (isEnabled) + extendsFrom(getByName(ss.runtimeClasspathConfigurationName)) + } + ("ksp$upperName") { + extendsFrom(ksp.get()) + } + } + dependencies { + if (isEnabled) + runtimeOnly(ss.output) + (ss.implementationConfigurationName)(project.files(tasks.compileKotlin.map { it.destinationDirectory })) + (ss.implementationConfigurationName)(project.files(tasks.compileJava.map { it.destinationDirectory })) + } + mergedSourceSetsJar.configure { + from(ss.output) + } + // TODO: figure out why inheritances are not being respected by tiny kotlin names + tasks.remapJar { + classpath.from(configurations.getByName(ss.compileClasspathConfigurationName)) + } + collectTranslations { + this.classes.from(ss.kotlin.classesDirectory) + } + return ss +} + +val SourceSet.modImplementationConfigurationName + get() = + loom.remapConfigurations.find { + it.targetConfigurationName.get() == this.implementationConfigurationName + }!!.sourceConfiguration +val SourceSet.modRuntimeOnlyConfigurationName + get() = + loom.remapConfigurations.find { + it.targetConfigurationName.get() == this.runtimeOnlyConfigurationName + }!!.sourceConfiguration + +val shadowMe by configurations.creating { + exclude(group = "org.jetbrains.kotlin") + exclude(group = "org.jetbrains.kotlinx") + exclude(group = "org.jetbrains") + exclude(module = "gson") + exclude(group = "org.slf4j") +} +val transInclude by configurations.creating { + exclude(group = "com.mojang") + exclude(group = "org.jetbrains.kotlin") + exclude(group = "org.jetbrains.kotlinx") + isTransitive = true +} + +val hotswap by configurations.creating { + isVisible = false +} + +val nonModImplentation by configurations.creating { + configurations.implementation.get().extendsFrom(this) +} +val testAgent by configurations.creating { + isVisible = false +} + + +val configuredSourceSet = createIsolatedSourceSet( + "configured", + isEnabled = false +) // Wait for update (also low prio, because configured sucks) +val sodiumSourceSet = createIsolatedSourceSet("sodium", isEnabled = false) +val citResewnSourceSet = createIsolatedSourceSet("citresewn", isEnabled = false) // TODO: Wait for update +val yaclSourceSet = createIsolatedSourceSet("yacl") +val explosiveEnhancementSourceSet = + createIsolatedSourceSet("explosiveEnhancement", isEnabled = false) // TODO: wait for their port +val wildfireGenderSourceSet = createIsolatedSourceSet("wildfireGender") +val jadeSourceSet = createIsolatedSourceSet("jade") +val modmenuSourceSet = createIsolatedSourceSet("modmenu") +val reiSourceSet = createIsolatedSourceSet("rei") +val moulconfigSourceSet = createIsolatedSourceSet("moulconfig") +val customTexturesSourceSet = createIsolatedSourceSet("texturePacks", "texturePacks") + +dependencies { + // Minecraft dependencies + "minecraft"(libs.minecraft) + "mappings"("net.fabricmc:yarn:${libs.versions.yarn.get()}:v2") + + // Hotswap Dependency + hotswap(libs.hotswap) + + // Fabric dependencies + modImplementation(libs.fabric.loader) + modImplementation(libs.fabric.kotlin) + modImplementation(libs.moulconfig) + modImplementation(libs.manninghamMills) + modImplementation(libs.basicMath) + include(libs.basicMath) + (modmenuSourceSet.modImplementationConfigurationName)(libs.modmenu) + (explosiveEnhancementSourceSet.modImplementationConfigurationName)(libs.explosiveenhancement) + modImplementation(libs.hypixelmodapi) + include(libs.hypixelmodapi.fabric) + compileOnly(projects.javaplugin) + annotationProcessor(projects.javaplugin) + nonModImplentation("com.google.auto.service:auto-service-annotations:1.1.1") + ksp("dev.zacsweers.autoservice:auto-service-ksp:1.2.0") + include(libs.manninghamMills) + shadowMe(libs.moulconfig) + + annotationProcessor(libs.mixinextras) + nonModImplentation(libs.mixinextras) + include(libs.mixinextras) + + nonModImplentation(libs.nealisp) + shadowMe(libs.nealisp) + + modCompileOnly(libs.fabric.api) + modRuntimeOnly(libs.fabric.api.deprecated) + modCompileOnly(libs.jarvis.api) + include(libs.jarvis.fabric) + + (wildfireGenderSourceSet.modImplementationConfigurationName)(libs.femalegender) + (wildfireGenderSourceSet.implementationConfigurationName)(customTexturesSourceSet.output) + (configuredSourceSet.modImplementationConfigurationName)(libs.configured) + (sodiumSourceSet.modImplementationConfigurationName)(libs.sodium) + (jadeSourceSet.modImplementationConfigurationName)(libs.jade) + + (citResewnSourceSet.modImplementationConfigurationName)( + innerJarsOf("citresewn", dependencies.create(libs.citresewn.get())) + ) + (citResewnSourceSet.modImplementationConfigurationName)(libs.citresewn) + (yaclSourceSet.modImplementationConfigurationName)(libs.yacl) + + // Actual dependencies + (reiSourceSet.modImplementationConfigurationName)(libs.rei.api) + (reiSourceSet.modRuntimeOnlyConfigurationName)(libs.rei.fabric) + nonModImplentation(libs.repoparser) + shadowMe(libs.repoparser) + fun ktor(mod: String) = "io.ktor:ktor-$mod-jvm:${libs.versions.ktor.get()}" + // TODO: get rid of ktor. lowkey ballooning file size and like not neccessary at all for what i am doing.0 + transInclude(nonModImplentation(ktor("client-core"))!!) + transInclude(nonModImplentation(ktor("client-java"))!!) + transInclude(nonModImplentation(ktor("serialization-kotlinx-json"))!!) + transInclude(nonModImplentation(ktor("client-content-negotiation"))!!) + transInclude(nonModImplentation(ktor("client-encoding"))!!) + transInclude(nonModImplentation(ktor("client-logging"))!!) + + // Dev environment preinstalled mods + modLocalRuntime(libs.bundles.runtime.required) + modLocalRuntime(libs.bundles.runtime.optional) + modLocalRuntime(libs.jarvis.fabric) + modLocalRuntime(libs.modmenu) + + transInclude.resolvedConfiguration.resolvedArtifacts.forEach { + include(it.moduleVersion.id.toString()) + } + + + testImplementation("net.fabricmc:fabric-loader-junit:${libs.versions.fabric.loader.get()}") + testAgent(files(tasks.getByPath(":testagent:jar"))) + + implementation(projects.symbols) + ksp(projects.symbols) +} + +loom { + clientOnlyMinecraftJar() + accessWidenerPath.set(project.file("src/main/resources/firmament.accesswidener")) + runs { + removeIf { it.name != "client" } + configureEach { + property("fabric.log.level", "info") + property("firmament.debug", "true") + property( + "firmament.classroots", + compatSourceSets.joinToString(File.pathSeparator) { + File(it.output.classesDirs.asPath).absolutePath + }) + property("mixin.debug.export", "true") + property("mixin.debug", "true") + + parseEnvFile(file(".env")).forEach { (t, u) -> + environmentVariable(t, u) + } + parseEnvFile(file(".properties")).forEach { (t, u) -> + property(t, u) + } + } + named("client") { + property("devauth.enabled", "true") + vmArg("-ea") +// vmArg("-XX:+AllowEnhancedClassRedefinition") +// vmArg("-XX:HotswapAgent=external") +// vmArg("-javaagent:${hotswap.resolve().single().absolutePath}") + } + } +} + +mcAutoTranslations { + translationFunction.set("moe.nea.firmament.util.tr") + translationFunctionResolved.set("moe.nea.firmament.util.trResolved") +} + +val downloadTestRepo by tasks.registering(RepoDownload::class) { + this.hash.set(project.property("firmament.compiletimerepohash") as String) +} + +val updateTestRepo by tasks.registering { + outputs.upToDateWhen { false } + doLast { + val propertiesFile = rootProject.file("gradle.properties") + val json = + Gson().fromJson( + uri("https://api.github.com/repos/NotEnoughUpdates/NotEnoughUpdates-REPO/branches/master") + .toURL().readText(), JsonObject::class.java + ) + val latestSha = json["commit"].asJsonObject["sha"].asString + var text = propertiesFile.readText() + text = text.replace( + "firmament\\.compiletimerepohash=[^\n]*".toRegex(), + "firmament.compiletimerepohash=$latestSha" + ) + propertiesFile.writeText(text) + } +} + + +tasks.test { + val wd = file("build/testWorkDir") + workingDir(wd) + dependsOn(downloadTestRepo) + dependsOn(testAgent) + doFirst { + wd.mkdirs() + wd.resolve("config").deleteRecursively() + systemProperty( + "firmament.testrepo", + downloadTestRepo.flatMap { it.outputDirectory.asFile }.map { it.absolutePath }.get() + ) + jvmArgs("-javaagent:${testAgent.singleFile.absolutePath}") + } + systemProperty("jdk.attach.allowAttachSelf", "true") + jvmArgs("-XX:+EnableDynamicAgentLoading") + systemProperties( + "kotest.framework.classpath.scanning.config.disable" to true, + "kotest.framework.config.fqn" to "moe.nea.firmament.test.testutil.KotestPlugin", + ) + useJUnitPlatform() +} + + +tasks.withType { + this.sourceCompatibility = "21" + this.targetCompatibility = "21" + options.encoding = "UTF-8" + val module = "ALL-UNNAMED" + options.forkOptions.jvmArgs!!.addAll( + listOf( + "--add-exports=jdk.compiler/com.sun.tools.javac.util=$module", + "--add-exports=jdk.compiler/com.sun.tools.javac.comp=$module", + "--add-exports=jdk.compiler/com.sun.tools.javac.tree=$module", + "--add-exports=jdk.compiler/com.sun.tools.javac.api=$module", + "--add-exports=jdk.compiler/com.sun.tools.javac.code=$module", + ) + ) + options.isFork = true + afterEvaluate { + options.compilerArgs.add("-Xplugin:IntermediaryNameReplacement mappingFile=${LoomGradleExtension.get(project).mappingsFile.absolutePath} sourceNs=named") + } +} + +tasks.jar { + destinationDirectory.set(layout.buildDirectory.dir("badjars")) + archiveClassifier.set("slim") +} +mergedSourceSetsJar.configure { + from(zipTree(tasks.jar.flatMap { it.archiveFile })) + destinationDirectory.set(layout.buildDirectory.dir("badjars")) + archiveClassifier.set("merged-source-sets") + mergeServiceFiles() +} +shadowJar.configure { + from(zipTree(tasks.remapJar.flatMap { it.archiveFile })) + configurations = listOf(shadowMe) + archiveClassifier.set("") + relocate("io.github.moulberry.repo", "moe.nea.firmament.deps.repo") + relocate("io.github.notenoughupdates.moulconfig", "moe.nea.firmament.deps.moulconfig") + mergeServiceFiles() + transform() +} + +tasks.remapJar { +// injectAccessWidener.set(true) + inputFile.set(mergedSourceSetsJar.flatMap { it.archiveFile }) + dependsOn(mergedSourceSetsJar) + destinationDirectory.set(layout.buildDirectory.dir("badjars")) + archiveClassifier.set("remapped") +} + +tasks.assemble { dependsOn(shadowJar) } + + +tasks.processResources { + val replacements = listOf( + "version" to project.version.toString(), + "minecraft_version" to libs.versions.minecraft.get(), + "fabric_kotlin_version" to libs.versions.fabric.kotlin.get(), + "fabric_api_version" to libs.versions.fabric.api.get(), + "rei_version" to libs.versions.rei.get() + ) + replacements.forEach { (key, value) -> inputs.property(key, value) } + filesMatching("**/fabric.mod.json") { + expand(*replacements.toTypedArray()) + } + exclude("**/*.license") + from(tasks.scanLicenses) + from(collectTranslations) { + into("assets/firmament/lang") + } +} + +tasks.scanLicenses { + scanConfiguration(nonModImplentation) + scanConfiguration(configurations.modCompileClasspath.get()) + compatSourceSets.forEach { + scanConfiguration(it.modImplementationConfigurationName.get()) + } + outputFile.set(layout.buildDirectory.file("LICENSES-FIRMAMENT.json")) + licenseFormatter.set(moe.nea.licenseextractificator.JsonLicenseFormatter()) +} +tasks.register("printAllLicenses", LicenseDiscoveryTask::class.java, licensing).configure { + outputFile.set(layout.buildDirectory.file("LICENSES-FIRMAMENT.txt")) + licenseFormatter.set(moe.nea.licenseextractificator.TextLicenseFormatter()) + compatSourceSets.forEach { + scanConfiguration(it.modImplementationConfigurationName.get()) + } + scanConfiguration(nonModImplentation) + scanConfiguration(configurations.modCompileClasspath.get()) + doLast { + println(outputFile.get().asFile.readText()) + } + outputs.upToDateWhen { false } +} +fun patchRenderDoc( + javaLauncher: JavaLauncher, +): JavaLauncher { + val wrappedJavaExecutable = javaLauncher.executablePath.asFile.absolutePath + require("\"" !in wrappedJavaExecutable) + val hashBytes = Hashing.sha256().hashString(wrappedJavaExecutable, StandardCharsets.UTF_8) + val hash = Base64.getUrlEncoder().encodeToString(hashBytes.asBytes()) + .replace("=", "") + val wrapperJavaRoot = rootProject.layout.buildDirectory + .dir("binaries/renderdoc-wrapped-java/$hash/") + .get() + val isWindows = Os.isFamily(Os.FAMILY_WINDOWS) + val wrapperJavaExe = + if (isWindows) wrapperJavaRoot.file("java.cmd") + else wrapperJavaRoot.file("java") + return object : JavaLauncher { + override fun getMetadata(): JavaInstallationMetadata { + return object : JavaInstallationMetadata by javaLauncher.metadata { + override fun isCurrentJvm(): Boolean { + return false + } + } + } + + override fun getExecutablePath(): RegularFile { + val fileF = wrapperJavaExe.asFile + if (!fileF.exists()) { + fileF.parentFile.mkdirs() + if (isWindows) { + fileF.writeText( + """ + setlocal enableextensions + start "" renderdoccmd.exe capture --opt-hook-children --wait-for-exit --working-dir . "$wrappedJavaExecutable" %* + endlocal + """.trimIndent() + ) + } else { + fileF.writeText( + """ + #!/usr/bin/env bash + exec renderdoccmd capture --opt-hook-children --wait-for-exit --working-dir . "$wrappedJavaExecutable" "$@" + """.trimIndent() + ) + fileF.setExecutable(true) + } + } + return wrapperJavaExe + } + } +} +tasks.runClient { + javaLauncher.set(javaToolchains.launcherFor(java.toolchain).map { patchRenderDoc(it) }) +} + +tasks.withType().configureEach { + isPreserveFileTimestamps = false + isReproducibleFileOrder = true +} diff --git a/check-translation-ordering.sh b/check-translation-ordering.sh new file mode 100644 index 0000000..ebecb7c --- /dev/null +++ b/check-translation-ordering.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2023 Linnea Gräf +# +# SPDX-License-Identifier: GPL-3.0-or-later + +set -euo pipefail +jq -S --tab < translations/en_us.json | diff translations/en_us.json - diff --git a/docs/How to release.md b/docs/How to release.md new file mode 100644 index 0000000..d0d51d4 --- /dev/null +++ b/docs/How to release.md @@ -0,0 +1,19 @@ + + +# How to create a release + +There is a release script to automate some of these actions. + +- Bump the version on gradle.properties +- Create a tag with that same version (without `v` prefix, please) +- Create a changelog based on + `git log --pretty='- %s' --grep '[no changelog]' --invert-grep --fixed-strings oldversion..newversion | tac`, while + filtering out commits that should not be in the changelog. +- Upload to [GitHub](https://github.com/romangraef/Firmament/releases/new) +- Upload to [Modrinth](https://modrinth.com/mod/firmament/versions) +- Send a message in [Discord](https://discord.com/channels/1088154030628417616/1108565050693783683) +- Send a message in [the thread](https://hypixel.net/threads/firmament-a-skyblock-mod-for-1-20-1-fabric.5446366/) diff --git a/docs/firmament_logo.webp b/docs/firmament_logo.webp new file mode 100644 index 0000000..d70327a Binary files /dev/null and b/docs/firmament_logo.webp differ diff --git a/docs/firmament_logo.webp.license b/docs/firmament_logo.webp.license new file mode 100644 index 0000000..8b77b1b --- /dev/null +++ b/docs/firmament_logo.webp.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2025 ic22487 + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/docs/firmament_logo_256.webp b/docs/firmament_logo_256.webp new file mode 100644 index 0000000..2aba841 Binary files /dev/null and b/docs/firmament_logo_256.webp differ diff --git a/docs/firmament_logo_256.webp.license b/docs/firmament_logo_256.webp.license new file mode 100644 index 0000000..8b77b1b --- /dev/null +++ b/docs/firmament_logo_256.webp.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2025 ic22487 + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/docs/firmament_logo_256_nobg.webp b/docs/firmament_logo_256_nobg.webp new file mode 100644 index 0000000..c557fca Binary files /dev/null and b/docs/firmament_logo_256_nobg.webp differ diff --git a/docs/firmament_logo_256_nobg.webp.license b/docs/firmament_logo_256_nobg.webp.license new file mode 100644 index 0000000..8b77b1b --- /dev/null +++ b/docs/firmament_logo_256_nobg.webp.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2025 ic22487 + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/docs/firmament_logo_256_trans.webp b/docs/firmament_logo_256_trans.webp new file mode 100644 index 0000000..6e05b83 Binary files /dev/null and b/docs/firmament_logo_256_trans.webp differ diff --git a/docs/firmament_logo_256_trans.webp.license b/docs/firmament_logo_256_trans.webp.license new file mode 100644 index 0000000..8b77b1b --- /dev/null +++ b/docs/firmament_logo_256_trans.webp.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2025 ic22487 + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/docs/firmament_logo_nobg.webp b/docs/firmament_logo_nobg.webp new file mode 100644 index 0000000..9b76f3c Binary files /dev/null and b/docs/firmament_logo_nobg.webp differ diff --git a/docs/firmament_logo_nobg.webp.license b/docs/firmament_logo_nobg.webp.license new file mode 100644 index 0000000..8b77b1b --- /dev/null +++ b/docs/firmament_logo_nobg.webp.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2025 ic22487 + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/docs/firmament_logo_trans.webp b/docs/firmament_logo_trans.webp new file mode 100644 index 0000000..73a15f7 Binary files /dev/null and b/docs/firmament_logo_trans.webp differ diff --git a/docs/firmament_logo_trans.webp.license b/docs/firmament_logo_trans.webp.license new file mode 100644 index 0000000..8b77b1b --- /dev/null +++ b/docs/firmament_logo_trans.webp.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2025 ic22487 + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/docs/release_script.sh b/docs/release_script.sh new file mode 100644 index 0000000..8d87a09 --- /dev/null +++ b/docs/release_script.sh @@ -0,0 +1,225 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2023 Linnea Gräf +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# ARG_OPTIONAL_BOOLEAN([no-check],[n],[Skip checking preconditions, such as a clean git working directory]) +# ARG_OPTIONAL_BOOLEAN([no-test],[t],[Skip running gradle tests.]) +# ARG_OPTIONAL_BOOLEAN([dry],[d],[Dry run]) +# ARG_HELP([Script to help creating releases]) +# ARGBASH_GO() +# needed because of Argbash --> m4_ignore([ +### START OF CODE GENERATED BY Argbash v2.10.0 one line above ### +# Argbash is a bash code generator used to get arguments parsing right. +# Argbash is FREE SOFTWARE, see https://argbash.io for more info + + +die() +{ + local _ret="${2:-1}" + test "${_PRINT_HELP:-no}" = yes && print_help >&2 + echo "$1" >&2 + exit "${_ret}" +} + + +begins_with_short_option() +{ + local first_option all_short_options='ntdh' + first_option="${1:0:1}" + test "$all_short_options" = "${all_short_options/$first_option/}" && return 1 || return 0 +} + +# THE DEFAULTS INITIALIZATION - OPTIONALS +_arg_no_check="off" +_arg_no_test="off" +_arg_dry="off" + + +print_help() +{ + printf '%s\n' "Script to help creating releases" + printf 'Usage: %s [-n|--(no-)no-check] [-t|--(no-)no-test] [-d|--(no-)dry] [-h|--help]\n' "$0" + printf '\t%s\n' "-n, --no-check, --no-no-check: Skip checking preconditions, such as a clean git working directory (off by default)" + printf '\t%s\n' "-t, --no-test, --no-no-test: Skip running gradle tests. (off by default)" + printf '\t%s\n' "-d, --dry, --no-dry: Dry run (off by default)" + printf '\t%s\n' "-h, --help: Prints help" +} + + +parse_commandline() +{ + while test $# -gt 0 + do + _key="$1" + case "$_key" in + -n|--no-no-check|--no-check) + _arg_no_check="on" + test "${1:0:5}" = "--no-" && _arg_no_check="off" + ;; + -n*) + _arg_no_check="on" + _next="${_key##-n}" + if test -n "$_next" -a "$_next" != "$_key" + then + { begins_with_short_option "$_next" && shift && set -- "-n" "-${_next}" "$@"; } || die "The short option '$_key' can't be decomposed to ${_key:0:2} and -${_key:2}, because ${_key:0:2} doesn't accept value and '-${_key:2:1}' doesn't correspond to a short option." + fi + ;; + -t|--no-no-test|--no-test) + _arg_no_test="on" + test "${1:0:5}" = "--no-" && _arg_no_test="off" + ;; + -t*) + _arg_no_test="on" + _next="${_key##-t}" + if test -n "$_next" -a "$_next" != "$_key" + then + { begins_with_short_option "$_next" && shift && set -- "-t" "-${_next}" "$@"; } || die "The short option '$_key' can't be decomposed to ${_key:0:2} and -${_key:2}, because ${_key:0:2} doesn't accept value and '-${_key:2:1}' doesn't correspond to a short option." + fi + ;; + -d|--no-dry|--dry) + _arg_dry="on" + test "${1:0:5}" = "--no-" && _arg_dry="off" + ;; + -d*) + _arg_dry="on" + _next="${_key##-d}" + if test -n "$_next" -a "$_next" != "$_key" + then + { begins_with_short_option "$_next" && shift && set -- "-d" "-${_next}" "$@"; } || die "The short option '$_key' can't be decomposed to ${_key:0:2} and -${_key:2}, because ${_key:0:2} doesn't accept value and '-${_key:2:1}' doesn't correspond to a short option." + fi + ;; + -h|--help) + print_help + exit 0 + ;; + -h*) + print_help + exit 0 + ;; + *) + _PRINT_HELP=yes die "FATAL ERROR: Got an unexpected argument '$1'" 1 + ;; + esac + shift + done +} + +parse_commandline "$@" + +# OTHER STUFF GENERATED BY Argbash + +### END OF CODE GENERATED BY Argbash (sortof) ### ]) +# [ <-- needed because of Argbash +set -euo pipefail + +REMOTE=origin + +basedir="$(dirname "$(dirname "$(realpath "$0")")")" +echo "Found base directory at $basedir" + +if ! [ -d "$basedir/.git" ]; then + echo Could not find git directory. + exit 1 +fi + +if ! "${JAVA_HOME}"/bin/java -version 2>&1 | grep 'version "21.'>/dev/null && [ "$_arg_no_check" == off ]; then + echo Wrong java version + exit 1 +fi + +if [ -n "$(git status --porcelain)" ] && [ "$_arg_no_check" == off ]; then + echo Unclean git working environment + exit 1 +fi + +current_branch="$(git rev-parse --abbrev-ref HEAD)" + +if ! [[ "$current_branch" = "master" ]] && ! [[ "$current_branch" = mc-* ]]; then + echo "Not on branch master or a mc- branch." + exit 1 +fi + +git fetch --tags "$REMOTE" + +git tag --list --sort=v:refname + +oldversion="$(git describe --tags --abbrev=0|tr -d '\n')" + +echo "Choosing old version as $oldversion" + +# TODO: auto choose next version based on a command line flag: --minor --hotfix --major as well as minecraft info from libs.versions.toml +echo -n "Choosing next version as: " +read newversion + +if ! [[ "$newversion" = *+mc* ]] && [ "$_arg_no_check" == off ]; then + echo "Illegal next version $newversion. Please use a.b.c+mcx.y.z" + exit 1 +fi + +echo "Confirming new version as $newversion" + +if [ "$_arg_dry" == off ]; then + echo Committing release commit + git commit --allow-empty -m 'Prepare release '"$newversion"' + +[no changelog]' + echo Tagging release commit + git tag "$newversion" +fi +mkdir -p "$basedir/.gradle" +releasenotes="$basedir/.gradle/releasenotes.md" + +comparetag="$( +if [ "$_arg_dry" == off ]; then + echo "$newversion" +else + echo "HEAD" +fi)" + +echo Building release notes +echo "**Full Changelog**: " > "$releasenotes" +echo >> "$releasenotes" +git log --pretty='- %s' --grep '[no changelog]' --invert-grep --fixed-strings "$oldversion..$comparetag" | tac >> "$releasenotes" +echo >> "$releasenotes" + +echo Check Release notes: +echo ---------------------------------------------- +cat "$releasenotes" +echo ---------------------------------------------- +echo Press Enter to resume +read + +echo Building JAR +"$basedir"/gradlew --stop +if [ "$_arg_no_test" == off ]; then + echo Building and testing + "$basedir"/gradlew clean build +else + echo Building without testing + "$basedir"/gradlew clean assemble +fi + +echo Release notes: +echo ---------------------------------------------- +cat "$releasenotes" +echo ---------------------------------------------- + +if [ "$_arg_dry" == off ]; then + echo Pushing to github + git push "$REMOTE" "HEAD" "$newversion" + if command -v gh; then + echo Creating github release + (set -x; gh release create -t "Firmament $newversion" "$newversion" -F "$releasenotes" "$basedir/build/libs/Firmament-$newversion.jar") + else + echo Could not find github command utility. Opening github releases + xdg-open "https://github.com/nea89o/firmament/releases/new" + fi +fi + +echo Opening modrinth releases +xdg-open "https://modrinth.com/mod/firmament/versions" + +echo "Don't forget to upload a discord release as well:" + +# ] <-- needed because of Argbash diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..2385103 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2023 Linnea Gräf +# +# SPDX-License-Identifier: CC0-1.0 +# suppress inspection "UnusedProperty" for whole file +org.gradle.jvmargs=-Xmx4096M + +loom.platform=fabric + +archives_base_name=Firmament +maven_group=moe.nea.firmament + +firmament.compiletimerepohash=a6116d945491d7c57c93d43f51250f93d62d8434 + +# TODO: remove after https://github.com/google/ksp/issues/2072 +ksp.incremental=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..d4ff2c4 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,168 @@ +# SPDX-FileCopyrightText: 2023 Linnea Gräf +# +# SPDX-License-Identifier: CC0-1.0 + +[versions] +minecraft = "1.21.5" + +# Update from https://kotlinlang.org/ +kotlin = "2.1.20" +# Update from https://github.com/google/ksp/releases +kotlin_ksp = "2.1.20-2.0.0" + +# Update from https://linkie.shedaniel.me/dependencies?loader=fabric +fabric_loader = "0.16.13" +fabric_api = "0.119.9+1.21.5" +yarn = "1.21.5+build.1" +modmenu = "14.0.0-rc.2" +architectury = "16.0.3" +# Update from https://maven.architectury.dev/me/shedaniel/RoughlyEnoughItems-fabric/ (but is typically late) +rei = "19.0.805" + +# Update from https://maven.fabricmc.net/net/fabricmc/fabric-language-kotlin/ +fabric_kotlin = "1.13.2+kotlin.2.1.20" + +# Update from https://maven.architectury.dev/dev/architectury/loom/dev.architectury.loom.gradle.plugin/ +loom = "1.7.414" # TODO: port back to architectury (and) 1.9.424 + +# Update from https://modrinth.com/mod/qolify/versions?l=fabric +qolify = "1.6.0-1.21.1" + +# Update from https://modrinth.com/mod/sodium/versions?l=fabric +sodium = "mc1.21.5-0.6.13-fabric" + +# Update from https://modrinth.com/mod/freecam/versions?l=fabric +freecammod = "1.3.3+mc1.21.5" + +# Update from https://modrinth.com/mod/no-chat-reports/versions?l=fabric +ncr = "Fabric-1.21.5-v2.12.0" + +# Update from https://modrinth.com/mod/female-gender/versions?l=fabric +femalegender = "4.3.4+1.21.5" + +shadow = "8.3.8" + +# Update from https://modrinth.com/mod/explosive-enhancement/versions?l=fabric +explosiveenhancement = "1.2.3-1.21.0" + +# Update from https://modrinth.com/mod/not-enough-animations/versions?l=fabric +notenoughanimations = "prj4BdjU" + +# Update from https://modrinth.com/mod/cit-resewn/versions?l=fabric +citresewn = "1.2.0+1.21" + +# Update from https://modrinth.com/mod/jade/versions?l=fabric +jade = "18.1.0+fabric" + +devauth = "1.2.1" + +# Update from https://ktor.io/docs/ +ktor = "3.1.2" + +# Update from https://repo.nea.moe/#/releases/moe/nea/neurepoparser +neurepoparser = "1.8.0" + +# Update from https://github.com/HotswapProjects/HotswapAgent/releases +# TODO: bump to 2.0.1 +hotswap_agent = "1.4.2-SNAPSHOT" + +# Update from https://github.com/LlamaLad7/MixinExtras/tags +mixinextras = "0.4.1" + +jarvis = "1.1.4" +nealisp = "1.1.0" + +# Update from https://github.com/NotEnoughUpdates/MoulConfig/tags +moulconfig = "4.0.1-beta" + +# Update from https://repo.nea.moe/#/releases/moe/nea/mc-auto-translations/moe.nea.mc-auto-translations.gradle.plugin +mcAutoTranslations = "0.3.0" + +# Update from https://www.curseforge.com/minecraft/mc-mods/configured/files/all?page=1&pageSize=20 +configured = "6023970" + +# Update from https://modrinth.com/mod/hypixel-mod-api/versions?l=fabric +hypixelmodapi = "1.0.1" +hypixelmodapi_fabric = "1.0.1+build.1+mc1.21" + +# Update from https://github.com/shedaniel/fabric-asm or https://maven.shedaniel.me/me/shedaniel/mm/ +manninghamMills = "2.4.1" + +# Update from https://docs.isxander.dev/yet-another-config-lib/installing-yacl +# Nvm, they just don't update docs: https://modrinth.com/mod/yacl/versions?l=fabric +yacl = "3.6.6+1.21.5-fabric" + +# Update from https://maven.shedaniel.me/me/shedaniel/cloth/basic-math/ +basicMath = "0.6.1" + +# Update from https://mvnrepository.com/artifact/net.lenni0451.classtransform/core +classtransform = "1.14.1" + +# Update from https://mvnrepository.com/artifact/org.ow2.asm/asm/ +asm = "9.8" + +[libraries] +minecraft = { module = "com.mojang:minecraft", version.ref = "minecraft" } +fabric_loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric_loader" } +fabric_api = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fabric_api" } +fabric_api_deprecated = { module = "net.fabricmc.fabric-api:fabric-api-deprecated", version.ref = "fabric_api" } +fabric_kotlin = { module = "net.fabricmc:fabric-language-kotlin", version.ref = "fabric_kotlin" } +architectury = { module = "dev.architectury:architectury", version.ref = "architectury" } +rei_api = { module = "me.shedaniel:RoughlyEnoughItems-api", version.ref = "rei" } +moulconfig = { module = "org.notenoughupdates.moulconfig:modern-1.21.5", version.ref = "moulconfig" } +repoparser = { module = "moe.nea:neurepoparser", version.ref = "neurepoparser" } +mixinextras = { module = "io.github.llamalad7:mixinextras-fabric", version.ref = "mixinextras" } +jarvis_api = { module = "moe.nea.jarvis:jarvis-api", version.ref = "jarvis" } +jarvis_fabric = { module = "moe.nea.jarvis:jarvis-fabric", version.ref = "jarvis" } +nealisp = { module = "moe.nea:nealisp", version.ref = "nealisp" } +explosiveenhancement = { module = "maven.modrinth:explosive-enhancement", version.ref = "explosiveenhancement" } +manninghamMills = { module = "me.shedaniel:mm", version.ref = "manninghamMills" } +aaronhmapi = { module = "net.azureaaron:hm-api", version = "1.0.0+1.21" } +hypixelmodapi = { module = "net.hypixel:mod-api", version.ref = "hypixelmodapi" } +hypixelmodapi_fabric = { module = "maven.modrinth:hypixel-mod-api", version.ref = "hypixelmodapi_fabric" } +configured = { module = "curse.maven:configured-457570", version.ref = "configured" } +# Runtime: +notenoughanimations = { module = "maven.modrinth:not-enough-animations", version.ref = "notenoughanimations" } +hotswap = { module = "virtual.github.hotswapagent:hotswap-agent", version.ref = "hotswap_agent" } +architectury_fabric = { module = "dev.architectury:architectury-fabric", version.ref = "architectury" } +rei_fabric = { module = "me.shedaniel:RoughlyEnoughItems-fabric", version.ref = "rei" } +devauth = { module = "me.djtheredstoner:DevAuth-fabric", version.ref = "devauth" } +modmenu = { module = "maven.modrinth:modmenu", version.ref = "modmenu" } +qolify = { module = "maven.modrinth:qolify", version.ref = "qolify" } +ncr = { module = "maven.modrinth:no-chat-reports", version.ref = "ncr" } +sodium = { module = "maven.modrinth:sodium", version.ref = "sodium" } +freecammod = { module = "maven.modrinth:freecam", version.ref = "freecammod" } +citresewn = { module = "maven.modrinth:cit-resewn", version.ref = "citresewn" } +femalegender = { module = "maven.modrinth:female-gender", version.ref = "femalegender" } +jade = { module = "maven.modrinth:jade", version.ref = "jade" } +yacl = { module = "dev.isxander:yet-another-config-lib", version.ref = "yacl" } +basicMath = { module = "me.shedaniel.cloth:basic-math", version.ref = "basicMath" } +shadow = { module = "com.gradleup.shadow:shadow-gradle-plugin", version.ref = "shadow" } +classTransform-mixinsTranslator = { module = "net.lenni0451.classtransform:mixinstranslator", version.ref = "classtransform" } +classTransform-core = { module = "net.lenni0451.classtransform:core", version.ref = "classtransform" } + +asm = { module = "org.ow2.asm:asm", version.ref = "asm" } + +[bundles] +runtime_required = [ + # "rei_fabric", + # "notenoughanimations", + "hypixelmodapi_fabric", +] +runtime_optional = [ + "devauth", + # "freecammod", + # "sodium", + # "qolify", + # "ncr", + # "citresewn", +] + +[plugins] +kotlin_jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin_plugin_serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +kotlin_plugin_powerassert = { id = "org.jetbrains.kotlin.plugin.power-assert", version.ref = "kotlin" } +kotlin_plugin_ksp = { id = "com.google.devtools.ksp", version.ref = "kotlin_ksp" } +shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } +loom = { id = "dev.architectury.loom", version.ref = "loom" } +mcAutoTranslations = { id = "moe.nea.mc-auto-translations", version.ref = "mcAutoTranslations" } diff --git a/gradle/wrapper/gradle-wrapper.jar.license b/gradle/wrapper/gradle-wrapper.jar.license new file mode 100644 index 0000000..9d85393 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.jar.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2023 Gradle, Inc. + +SPDX-License-Identifier: Apache-2.0 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..4cdd0fb --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2023 Gradle, Inc. +# +# SPDX-License-Identifier: Apache-2.0 + +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..bc899af --- /dev/null +++ b/gradlew @@ -0,0 +1,223 @@ +#!/bin/sh + +# Copyright © 2015-2021 the original authors. +# SPDX-FileCopyrightText: 2023 Gradle, Inc. +# +# SPDX-License-Identifier: Apache-2.0 + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..05b887c --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,93 @@ +REM SPDX-FileCopyrightText: 2023 Gradle, Inc. +REM +REM SPDX-License-Identifier: Apache-2.0 + +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/javaplugin/build.gradle.kts b/javaplugin/build.gradle.kts new file mode 100644 index 0000000..dc461bc --- /dev/null +++ b/javaplugin/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + java + idea + id("firmament.common") +} +dependencies { + implementation("net.fabricmc:stitch:0.6.2") +} +val compilerModules = listOf("util", "comp", "tree", "api", "code") + .map { "jdk.compiler/com.sun.tools.javac.$it" } + +tasks.withType(JavaCompile::class) { + val module = "ALL-UNNAMED" + options.compilerArgs.addAll( + compilerModules.map { "--add-exports=$it=$module" } + ) +} diff --git a/javaplugin/src/main/java/moe/nea/firmament/javaplugin/InitReplacer.java b/javaplugin/src/main/java/moe/nea/firmament/javaplugin/InitReplacer.java new file mode 100644 index 0000000..7ba3b44 --- /dev/null +++ b/javaplugin/src/main/java/moe/nea/firmament/javaplugin/InitReplacer.java @@ -0,0 +1,81 @@ +package moe.nea.firmament.javaplugin; + +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.TreeScanner; +import com.sun.tools.javac.code.Symbol; +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.tree.TreeMaker; +import com.sun.tools.javac.util.List; +import com.sun.tools.javac.util.Names; + +public class InitReplacer extends TreeScanner { + private final MappingTree mappingTree; + private final TreeMaker treeMaker; + private final Names names; + private final IntermediaryNameResolutionTask plugin; + private Symbol.ClassSymbol classTree; + private CompilationUnitTree compilationUnitTree; + + public InitReplacer(MappingTree mappingTree, IntermediaryNameResolutionTask plugin) { + this.mappingTree = mappingTree; + this.treeMaker = plugin.treeMaker; + this.names = plugin.names; + this.plugin = plugin; + } + + @Override + public Void visitClass(ClassTree node, Void unused) { + this.classTree = plugin.utils.getSymbol(node); + return super.visitClass(node, unused); + } + + @Override + public Void visitCompilationUnit(CompilationUnitTree node, Void unused) { + this.compilationUnitTree = node; + return super.visitCompilationUnit(node, unused); + } + + @Override + public Void visitVariable(VariableTree node, Void unused) { + var annotation = node + .getModifiers().getAnnotations() + .stream() + .filter(it -> it.getAnnotationType().toString().equals("IntermediaryName")) // Crazy type-safety! + .findAny(); + if (annotation.isEmpty()) + return super.visitVariable(node, unused); + var jcAnnotation = (JCTree.JCAnnotation) annotation.get(); + var jcNode = (JCTree.JCVariableDecl) node; + if (node.getInitializer() != null) { + plugin.utils.reportError( + compilationUnitTree.getSourceFile(), + jcNode.getInitializer(), + "Providing an initializer for a variable is illegal for @IntermediaryName annotated fields" + ); + return super.visitVariable(node, unused); + } + var target = plugin.utils.getAnnotationValue(jcAnnotation, "value"); + var targetClass = plugin.utils.resolveClassLiteralExpression(target).tsym.flatName().toString(); + var intermediaryClass = mappingTree.resolveClassToIntermediary(targetClass); + if (intermediaryClass == null){ + plugin.utils.reportError( + compilationUnitTree.getSourceFile(), + jcNode.init, + "Unknown class name " + targetClass + ); + return super.visitVariable(node, unused); + } + var remapper = treeMaker.Select(treeMaker.This(classTree.type), names.fromString("remapper")); + var remappingCall = treeMaker.Apply( + List.nil(), + treeMaker.Select(remapper, names.fromString("mapClassName")), + List.of(treeMaker.Literal("intermediary"), + treeMaker.Literal(intermediaryClass))); + jcNode.init = remappingCall; + jcNode.mods.annotations = List.filter(jcNode.mods.annotations, jcAnnotation); + return super.visitVariable(node, unused); + } + +} diff --git a/javaplugin/src/main/java/moe/nea/firmament/javaplugin/IntermediaryMethodReplacer.java b/javaplugin/src/main/java/moe/nea/firmament/javaplugin/IntermediaryMethodReplacer.java new file mode 100644 index 0000000..cb87b20 --- /dev/null +++ b/javaplugin/src/main/java/moe/nea/firmament/javaplugin/IntermediaryMethodReplacer.java @@ -0,0 +1,77 @@ +package moe.nea.firmament.javaplugin; + +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.util.TreeScanner; +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.util.List; + +import javax.tools.JavaFileObject; + +public class IntermediaryMethodReplacer extends TreeScanner { + private final MappingTree mappings; + private final IntermediaryNameResolutionTask plugin; + private JavaFileObject sourceFile; + private CompilationUnitTree compilationUnit; + + public IntermediaryMethodReplacer(MappingTree mappings, IntermediaryNameResolutionTask plugin) { + this.mappings = mappings; + this.plugin = plugin; + } + + + @Override + public Void visitCompilationUnit(CompilationUnitTree node, Void unused) { + sourceFile = node.getSourceFile(); + compilationUnit = node; + return super.visitCompilationUnit(node, unused); + } + + public void replaceMethodName(JCTree.JCMethodInvocation node) { + var select = node.getMethodSelect(); + if (!(select instanceof JCTree.JCFieldAccess fieldAccess)) return; + if (!fieldAccess.name.contentEquals("methodName")) return; + if (!(node.args.head instanceof JCTree.JCMemberReference methodReference)) { + plugin.utils.reportError(sourceFile, node, "Please provide a Class::method reference directly (and nothing else)"); + return; + } + var clearName = methodReference.name.toString(); + var classRef = methodReference.expr; + var type = plugin.utils.resolveClassName(classRef, compilationUnit); + var intermediaryName = mappings.resolveMethodToIntermediary( + type.tsym.flatName().toString(), + clearName + ); + fieldAccess.name = plugin.names.fromString("id"); + node.args = List.of(plugin.treeMaker.Literal(intermediaryName)); + } + + public void replaceClassName(JCTree.JCMethodInvocation node) { + var select = node.getMethodSelect(); + if (!(select instanceof JCTree.JCFieldAccess fieldAccess)) return; + if (!fieldAccess.name.contentEquals("className")) return; + if (node.getTypeArguments().size() != 1) { + plugin.utils.reportError(sourceFile, node, "You need to explicitly provide the class you want the intermediary name for"); + return; + } + var head = node.typeargs.head; + var resolved = plugin.utils.resolveClassName(head, compilationUnit); + var sourceName = resolved.tsym.flatName().toString(); + var mappedName = mappings.resolveClassToIntermediary(sourceName); + if (mappedName == null) { + plugin.utils.reportError(sourceFile, node, "Unknown class name " + sourceName); + return; + } + fieldAccess.name = plugin.names.fromString("id"); + node.typeargs = List.nil(); + node.args = List.of(plugin.treeMaker.Literal(mappedName)); + } + + @Override + public Void visitMethodInvocation(MethodInvocationTree node, Void unused) { + replaceClassName((JCTree.JCMethodInvocation) node); + replaceMethodName((JCTree.JCMethodInvocation) node); + return super.visitMethodInvocation(node, unused); + } +} diff --git a/javaplugin/src/main/java/moe/nea/firmament/javaplugin/IntermediaryNameResolutionPlugin.java b/javaplugin/src/main/java/moe/nea/firmament/javaplugin/IntermediaryNameResolutionPlugin.java new file mode 100644 index 0000000..ba6a0c5 --- /dev/null +++ b/javaplugin/src/main/java/moe/nea/firmament/javaplugin/IntermediaryNameResolutionPlugin.java @@ -0,0 +1,25 @@ +package moe.nea.firmament.javaplugin; + +import com.sun.source.util.JavacTask; +import com.sun.source.util.Plugin; + +import java.util.HashMap; +import java.util.Map; + +public class IntermediaryNameResolutionPlugin implements Plugin { + + @Override + public String getName() { + return "IntermediaryNameReplacement"; + } + + @Override + public void init(JavacTask task, String... args) { + Map argMap = new HashMap<>(); + for (String arg : args) { + String[] parts = arg.split("=", 2); + argMap.put(parts[0], parts.length == 2 ? parts[1] : "true"); + } + task.addTaskListener(new IntermediaryNameResolutionTask(this, task, argMap)); + } +} diff --git a/javaplugin/src/main/java/moe/nea/firmament/javaplugin/IntermediaryNameResolutionTask.java b/javaplugin/src/main/java/moe/nea/firmament/javaplugin/IntermediaryNameResolutionTask.java new file mode 100644 index 0000000..86a5598 --- /dev/null +++ b/javaplugin/src/main/java/moe/nea/firmament/javaplugin/IntermediaryNameResolutionTask.java @@ -0,0 +1,44 @@ +package moe.nea.firmament.javaplugin; + +import com.sun.source.util.JavacTask; +import com.sun.source.util.TaskEvent; +import com.sun.source.util.TaskListener; +import com.sun.tools.javac.api.BasicJavacTask; +import com.sun.tools.javac.tree.TreeMaker; +import com.sun.tools.javac.util.Names; +import net.fabricmc.stitch.commands.tinyv2.TinyV2Reader; + +import java.io.File; +import java.io.IOException; +import java.util.Map; + +public class IntermediaryNameResolutionTask implements TaskListener { + TreeMaker treeMaker; + Names names; + MappingTree mappings; + Utils utils; + + public IntermediaryNameResolutionTask(IntermediaryNameResolutionPlugin intermediaryNameResolutionPlugin, JavacTask task, Map argMap) { + var context = ((BasicJavacTask) task).getContext(); + var mappingFile = new File(argMap.get("mappingFile")); + System.err.println("Loading mappings from " + mappingFile); + try { + var tinyV2File = TinyV2Reader.read(mappingFile.toPath()); + mappings = new MappingTree(tinyV2File, argMap.get("sourceNs"), argMap.getOrDefault("targetNs", "intermediary")); + } catch (IOException e) { + throw new RuntimeException(e); + } + treeMaker = TreeMaker.instance(context); + names = Names.instance(context); + utils = Utils.instance(context); + } + + @Override + public void finished(TaskEvent e) { + if (e.getKind() != TaskEvent.Kind.ENTER) return; + if (e.getCompilationUnit() == null || e.getSourceFile() == null) return; + e.getCompilationUnit().accept(new InitReplacer(mappings, this), null); + e.getCompilationUnit().accept(new IntermediaryMethodReplacer(mappings, this), null); + } + +} diff --git a/javaplugin/src/main/java/moe/nea/firmament/javaplugin/MappingTree.java b/javaplugin/src/main/java/moe/nea/firmament/javaplugin/MappingTree.java new file mode 100644 index 0000000..eef5f9a --- /dev/null +++ b/javaplugin/src/main/java/moe/nea/firmament/javaplugin/MappingTree.java @@ -0,0 +1,51 @@ +package moe.nea.firmament.javaplugin; + +import net.fabricmc.stitch.commands.tinyv2.TinyClass; +import net.fabricmc.stitch.commands.tinyv2.TinyFile; +import net.fabricmc.stitch.commands.tinyv2.TinyMethod; + +import java.util.Map; +import java.util.stream.Collectors; + +public class MappingTree { + + private final Map classLookup; + private final int targetIndex; + private final int sourceIndex; + + public MappingTree(TinyFile tinyV2File, String sourceNamespace, String targetNamespace) { + sourceIndex = tinyV2File.getHeader().getNamespaces().indexOf(sourceNamespace); + if (sourceIndex < 0) + throw new RuntimeException("Could not find source namespace " + sourceNamespace + " in mappings file."); + this.classLookup = tinyV2File + .getClassEntries() + .stream() + .collect(Collectors.toMap(it -> it.getClassNames().get(sourceIndex), it -> it)); + targetIndex = tinyV2File.getHeader().getNamespaces().indexOf(targetNamespace); + if (targetIndex < 0) + throw new RuntimeException("Could not find target namespace " + targetNamespace + " in mappings file."); + } + + public String resolveMethodToIntermediary(String className, String methodName) { + var classData = classLookup.get(className.replace(".", "/")); + TinyMethod candidate = null; + for (TinyMethod method : classData.getMethods()) { + if (method.getMethodNames().get(sourceIndex).equals(methodName)) { + if (candidate != null) { + throw new RuntimeException("Found two candidates for method " + className + "." + methodName); + } + candidate = method; + } + } + return candidate.getMethodNames().get(targetIndex); + } + + public String resolveClassToIntermediary(String className) { + var cls = classLookup.get(className.replace(".", "/")); + if (cls == null) { + return null; + } + return cls.getClassNames().get(targetIndex) + .replace("/", "."); + } +} diff --git a/javaplugin/src/main/java/moe/nea/firmament/javaplugin/Utils.java b/javaplugin/src/main/java/moe/nea/firmament/javaplugin/Utils.java new file mode 100644 index 0000000..4c8806d --- /dev/null +++ b/javaplugin/src/main/java/moe/nea/firmament/javaplugin/Utils.java @@ -0,0 +1,121 @@ +package moe.nea.firmament.javaplugin; + +import com.sun.source.tree.AnnotationTree; +import com.sun.source.tree.AssignmentTree; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.MemberSelectTree; +import com.sun.source.tree.Tree; +import com.sun.tools.javac.comp.Attr; +import com.sun.tools.javac.code.Symbol; +import com.sun.tools.javac.code.Type; +import com.sun.tools.javac.code.Types; +import com.sun.tools.javac.comp.AttrContext; +import com.sun.tools.javac.comp.Enter; +import com.sun.tools.javac.comp.Env; +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.util.Context; +import com.sun.tools.javac.util.JCDiagnostic; +import com.sun.tools.javac.util.JavacMessages; +import com.sun.tools.javac.util.Log; + +import javax.tools.JavaFileObject; +import java.util.ListResourceBundle; + +public class Utils { + private static final Context.Key KEY = new Context.Key<>(); + private final Log log; + private final JCDiagnostic.Factory diagnostics; + private final Types types; + private final Attr attr; + private final Enter enter; + + private Utils(Context context) { + context.put(KEY, this); + JavacMessages.instance(context).add(l -> new ListResourceBundle() { + + @Override + protected Object[][] getContents() { + return new Object[][]{ + new Object[]{"compiler.err.firmament.generic", "{0}"} + }; + } + }); + log = Log.instance(context); + diagnostics = JCDiagnostic.Factory.instance(context); + types = Types.instance(context); + attr = Attr.instance(context); + enter = Enter.instance(context); + } + + public static Utils instance(Context context) { + var utils = context.get(KEY); + if (utils == null) { + utils = new Utils(context); + } + return utils; + } + + public Type resolveClassName(ExpressionTree expression) { + var tree = (JCTree) expression; + return tree.type; + } + + public Type resolveClassName(ExpressionTree tree, CompilationUnitTree unit) { + return resolveClassName(tree, enter.getTopLevelEnv((JCTree.JCCompilationUnit) unit)); + } + + public Type resolveClassName(ExpressionTree tree, Env env) { + var t = resolveClassName(tree); + if (t != null) return t; + return attr.attribType((JCTree) tree, env); + } + + public Symbol getSymbol(IdentifierTree tree) { + return ((JCTree.JCIdent) tree).sym; + } + + public Symbol.ClassSymbol getSymbol(ClassTree tree) { + return ((JCTree.JCClassDecl) tree).sym; + } + + public ExpressionTree getAnnotationValue( + AnnotationTree tree, + String name) { + // TODO: strip parenthesis + for (var argument : tree.getArguments()) { + var assignment = (AssignmentTree) argument; + if (((IdentifierTree) assignment.getVariable()).getName().toString().equals(name)) + return assignment.getExpression(); + } + return null; + } + + public Type.ClassType resolveClassLiteralExpression(ExpressionTree tree) { + if (!(tree instanceof MemberSelectTree select)) + throw new RuntimeException("Cannot resolve non field access class literal: " + tree); + if (!select.getIdentifier().toString().equals("class")) + throw new RuntimeException("Class literal " + select + "accessed non .class attribute"); + + return (Type.ClassType) resolveClassName(select.getExpression()); + } + + public void reportError( + JavaFileObject file, + Tree node, + String message + ) { + var originalSource = log.useSource(file); + var error = diagnostics.error( + JCDiagnostic.DiagnosticFlag.API, + log.currentSource(), + node == null ? null : ((JCTree) node).pos(), + "firmament.generic", + message + ); + log.report(error); + log.useSource(originalSource); + } +} diff --git a/javaplugin/src/main/resources/META-INF/services/com.sun.source.util.Plugin b/javaplugin/src/main/resources/META-INF/services/com.sun.source.util.Plugin new file mode 100644 index 0000000..a9e5dbe --- /dev/null +++ b/javaplugin/src/main/resources/META-INF/services/com.sun.source.util.Plugin @@ -0,0 +1 @@ +moe.nea.firmament.javaplugin.IntermediaryNameResolutionPlugin diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..7b298b6 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2023 Linnea Gräf + * SPDX-FileCopyrightText: 2024 Linnea Gräf + * + * SPDX-License-Identifier: CC0-1.0 + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +pluginManagement { + repositories { + mavenLocal() + maven { + name = "fabricmc" + url = uri("https://maven.fabricmc.net/") + } + maven { + name = "architectury" + url = uri("https://maven.architectury.dev/") + } + maven { + name = "forgemc" + url = uri("https://maven.minecraftforge.net/") + } + maven { + name = "jitpack" + url = uri("https://jitpack.io") + } + maven { + url = uri("https://repo.nea.moe/releases") + } + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = "Firmament" + +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") +include("symbols") +include("javaplugin") +include("testagent") +includeBuild("build-logic") diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..ef882e8 --- /dev/null +++ b/shell.nix @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: 2023 Linnea Gräf +# +# SPDX-License-Identifier: GPL-3.0-or-later +{pkgs ? import {}}: +pkgs.mkShell { + buildInputs = with pkgs; [ + bash + gh + git + xdg-utils + reuse + pre-commit + glfw + jdk21 + libGL + wayland + flite + jack2 + openal + pulseaudio + pipewire + glibc + ]; + shellHook = '' + export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${pkgs.glfw}/lib" + export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${pkgs.pipewire}/lib" + export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${pkgs.pulseaudio}/lib" + export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${pkgs.jack2}/lib" + export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${pkgs.openal}/lib" + export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${pkgs.libGL}/lib" + export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${pkgs.wayland}/lib" + export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${pkgs.flite}/lib" + export JAVA_HOME=${pkgs.jdk21} + apply() { + echo "LD_LIBRARY_PATH=$LD_LIBRARY_PATH" >.env + echo org.lwjgl.glfw.libname=${pkgs.glfw}/lib/libglfw.so >.properties + echo "jna.library.path=$LD_LIBRARY_PATH" >>.properties + echo "java.library.path=$LD_LIBRARY_PATH" >>.properties + } + + ''; +} diff --git a/src/compat/citresewn/java/ConditionDirectAccessHelper.kt b/src/compat/citresewn/java/ConditionDirectAccessHelper.kt new file mode 100644 index 0000000..af97a40 --- /dev/null +++ b/src/compat/citresewn/java/ConditionDirectAccessHelper.kt @@ -0,0 +1,59 @@ +package moe.nea.firmament.compat.citresewn + +import java.lang.invoke.MethodHandles +import java.util.function.BiPredicate +import java.util.function.Function +import shcm.shsupercm.fabric.citresewn.defaults.cit.conditions.ConditionNBT + +object ConditionNBTMixin { + class Helper { + // TODO: make lambdametafactory work by way of modifying the actual modifiers + + val stringMatcherType = ConditionNBT::class.java.getDeclaredField("matchString").type + + val accessMatcher = run { + val matchStringF = ConditionNBT::class.java.getDeclaredField("matchString"); + matchStringF.isAccessible = true + val l = MethodHandles.privateLookupIn(ConditionNBT::class.java, MethodHandles.lookup()) +// val mt = MethodType.methodType(stringMatcherType, ConditionNBT::class.java) +// val callsite = LambdaMetafactory.metafactory( +// l, "apply", +// MethodType.methodType(Function::class.java), +// MethodType.methodType(java.lang.Object::class.java, java.lang.Object::class.java), +// l.unreflectGetter(matchStringF), +// mt +// ) + val getter = l.unreflectGetter(matchStringF) + Function { getter.invoke(it) as StringMatcher } + } + val directCaller = run { + val matchM = stringMatcherType.getDeclaredMethod("matches", String::class.java); + matchM.isAccessible = true + val l = MethodHandles.privateLookupIn(ConditionNBT::class.java, MethodHandles.lookup()) +// val mt = MethodType.methodType(java.lang.Boolean.TYPE, stringMatcherType, String::class.java) +// val callsite = LambdaMetafactory.metafactory( +// l, "test", +// MethodType.methodType(BiPredicate::class.java), +// mt, +// l.unreflect(matchM), +// mt +// ) + val func = l.unreflect(matchM) + BiPredicate { a, b -> func.invoke(a, b) as Boolean } + } + + fun test(condition: ConditionNBT, text: String): Boolean { + return directCaller.test(accessMatcher.apply(condition), text) as Boolean + } + } + + val helper = Helper() + + @JvmStatic + fun invokeDirectConditionNBTStringMatch( + nbt: ConditionNBT, + text: String, + ): Boolean { + return helper.test(nbt, text) + } +} diff --git a/src/compat/citresewn/java/moe/nea/firmament/mixins/compat/citresewn/MixinConditionComponents.java b/src/compat/citresewn/java/moe/nea/firmament/mixins/compat/citresewn/MixinConditionComponents.java new file mode 100644 index 0000000..0743d40 --- /dev/null +++ b/src/compat/citresewn/java/moe/nea/firmament/mixins/compat/citresewn/MixinConditionComponents.java @@ -0,0 +1,123 @@ +package moe.nea.firmament.mixins.compat.citresewn; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import com.llamalad7.mixinextras.sugar.ref.LocalRef; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.DynamicOps; +import moe.nea.firmament.compat.citresewn.ConditionNBTMixin; +import moe.nea.firmament.features.texturepack.CustomSkyBlockTextures; +import net.minecraft.component.ComponentType; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.NbtComponent; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import shcm.shsupercm.fabric.citresewn.CITResewn; +import shcm.shsupercm.fabric.citresewn.cit.CITContext; +import shcm.shsupercm.fabric.citresewn.defaults.cit.conditions.ConditionComponents; +import shcm.shsupercm.fabric.citresewn.defaults.cit.conditions.ConditionNBT; +import shcm.shsupercm.fabric.citresewn.pack.format.PropertyGroup; +import shcm.shsupercm.fabric.citresewn.pack.format.PropertyKey; +import shcm.shsupercm.fabric.citresewn.pack.format.PropertyValue; + +// People are complaining but this really is not my place to fix things + +@Mixin(ConditionComponents.class) +@Pseudo +public class MixinConditionComponents { + @Shadow + private ComponentType componentType; + + @Shadow(remap = false) + private ConditionNBT fallbackNBTCheck; + @Unique + private String[] pathCheck; + @Unique + private int loreInt = -1; + + @Inject(method = "load", at = @At("HEAD"), remap = false) + public void addExtraAttributeSupport(PropertyKey key, PropertyValue value, PropertyGroup properties, CallbackInfo ci, + @Local(argsOnly = true) LocalRef keyRef, + @Local(argsOnly = true) LocalRef valueRef) { + if (!CustomSkyBlockTextures.TConfig.INSTANCE.getEnableLegacyCIT()) return; + if (!"nbt".equals(key.path())) return; + if (!value.keyMetadata().startsWith("ExtraAttributes.")) return; + keyRef.set(new PropertyKey(key.namespace(), "component")); + valueRef.set(new PropertyValue( + "minecraft:custom_data" + value.keyMetadata().substring("ExtraAttributes".length()), + value.value(), + value.separator(), + value.position(), + value.propertiesIdentifier(), + value.packName() + )); + CITResewn.logWarnLoading(properties.messageWithDescriptorOf("NBT condition is not supported since 1.21. THIS IS A FIRMAMENT SPECIAL FEATURE ALLOWING YOU TO CONTINUE TO USE nbt.ExtraAttributes.* PROPERTIES FOR A LIMITED TIME! UPDATE YOUR .PROPERTIES FILES OR SWITCH TO FIRMAMENT CIT (https://github.com/FirmamentMC/CitToFirm)", + value.position())); + } + + @Inject(method = "load", + at = @At(value = "INVOKE", remap = false, target = "Lshcm/shsupercm/fabric/citresewn/defaults/cit/conditions/ConditionNBT;loadNbtCondition(Lshcm/shsupercm/fabric/citresewn/pack/format/PropertyValue;Lshcm/shsupercm/fabric/citresewn/pack/format/PropertyGroup;[Ljava/lang/String;Ljava/lang/String;)V"), + remap = false) + private void onLoadSavePath(PropertyKey key, PropertyValue value, PropertyGroup properties, CallbackInfo ci, + @Local String[] path) { + this.pathCheck = path; + this.loreInt = -1; + } + + @Unique + private boolean matchStringDirect(String directString, CITContext context) { + return ConditionNBTMixin.invokeDirectConditionNBTStringMatch(fallbackNBTCheck, directString); + } + + @WrapOperation(method = "test", at = @At(value = "INVOKE", target = "Lcom/mojang/serialization/Codec;encodeStart(Lcom/mojang/serialization/DynamicOps;Ljava/lang/Object;)Lcom/mojang/serialization/DataResult;"), remap = false) + DataResult fastPathUnsafeNbtComponent( + Codec instance, + DynamicOps dynamicOps, + Object o, + Operation original) { + if (o instanceof NbtComponent nbtComponent) { + return DataResult.success(nbtComponent.getNbt()); + } + return original.call(instance, dynamicOps, o); + } + + @Inject(method = "test", at = @At("HEAD"), cancellable = true, remap = false) + void fastPathDisplayName(CITContext context, CallbackInfoReturnable cir) { + if (this.componentType == DataComponentTypes.CUSTOM_NAME && pathCheck.length == 0) { + var displayName = context.stack.getComponents().get(DataComponentTypes.CUSTOM_NAME); + if (displayName != null) { + cir.setReturnValue(matchStringDirect((displayName.getString()), context)); + } + } + if (this.componentType == DataComponentTypes.LORE && pathCheck.length == 1) { + var lore = context.stack.getComponents().get(DataComponentTypes.LORE); + if (lore != null) { + var loreLines = lore.lines(); + if (pathCheck[0].equals("*")) { + for (var loreLine : loreLines) { + if (matchStringDirect((loreLine.getString()), context)) { + cir.setReturnValue(true); + return; + } + } + cir.setReturnValue(false); + } else { + if (loreInt < 0) + loreInt = Integer.parseInt(pathCheck[0]); + cir.setReturnValue(0 <= loreInt && loreInt < loreLines.size() && + matchStringDirect((loreLines.get(loreInt).getString()), context)); + } + } + } + } + + +} diff --git a/src/compat/configured/java/BaseConfigNode.kt b/src/compat/configured/java/BaseConfigNode.kt new file mode 100644 index 0000000..afe0375 --- /dev/null +++ b/src/compat/configured/java/BaseConfigNode.kt @@ -0,0 +1,40 @@ +package moe.nea.firmament.compat.configured + +import com.mrcrayfish.configured.api.IConfigEntry +import com.mrcrayfish.configured.api.IConfigValue +import net.minecraft.text.Text +import moe.nea.firmament.gui.config.AllConfigsGui +import moe.nea.firmament.gui.config.ManagedConfig + +object BaseConfigNode : IConfigEntry { + override fun getChildren(): List { + return ManagedConfig.allManagedConfigs.getAll().map { + ConfigNode(it) // TODO: fix add categories here + } + } + + override fun isRoot(): Boolean { + return true + } + + override fun isLeaf(): Boolean { + return false + } + + override fun getValue(): IConfigValue<*>? { + return null + } + + override fun getEntryName(): String { + return "Firmament" + } + + override fun getTooltip(): Text? { + return null + } + + override fun getTranslationKey(): String? { + return null + } + +} diff --git a/src/compat/configured/java/ConfigCategory.kt b/src/compat/configured/java/ConfigCategory.kt new file mode 100644 index 0000000..4e33b8b --- /dev/null +++ b/src/compat/configured/java/ConfigCategory.kt @@ -0,0 +1,48 @@ +package moe.nea.firmament.compat.configured + +import com.mrcrayfish.configured.api.ConfigType +import com.mrcrayfish.configured.api.IConfigEntry +import com.mrcrayfish.configured.api.IModConfig +import com.mrcrayfish.configured.util.ConfigHelper +import java.nio.file.Path +import java.util.function.Consumer +import moe.nea.firmament.Firmament +import moe.nea.firmament.gui.config.ManagedConfig + +class ConfigCategory( + val category: ManagedConfig +) : BaseConfig() { + + override fun getRoot(): IConfigEntry { + return ConfigNode(category) + } + + override fun getTranslationKey(): String? { + return category.translationKey + } +} + +abstract class BaseConfig : IModConfig { + override fun update(p0: IConfigEntry) { + ConfigHelper.getChangedValues(p0).forEach { + it as ConfigValue + it.saveValue() + } + } + + override fun getType(): ConfigType { + return ConfigType.CLIENT + } + + override fun getFileName(): String { + return "" + } + + override fun getModId(): String { + return Firmament.MOD_ID + } + + override fun loadWorldConfig(p0: Path?, p1: Consumer?) { + } + +} diff --git a/src/compat/configured/java/ConfigNode.kt b/src/compat/configured/java/ConfigNode.kt new file mode 100644 index 0000000..16e54a6 --- /dev/null +++ b/src/compat/configured/java/ConfigNode.kt @@ -0,0 +1,39 @@ +package moe.nea.firmament.compat.configured + +import com.mrcrayfish.configured.api.IConfigEntry +import com.mrcrayfish.configured.api.IConfigValue +import net.minecraft.text.Text +import moe.nea.firmament.gui.config.ManagedConfig + +class ConfigNode(val config: ManagedConfig) : IConfigEntry { + override fun getChildren(): List { + return config.allOptions.map { + ConfigValueNode(it.value) + } + } + + override fun isRoot(): Boolean { + return false + } + + override fun isLeaf(): Boolean { + return false + } + + override fun getValue(): IConfigValue<*>? { + return null + } + + override fun getEntryName(): String { + return config.translationKey + } + + override fun getTooltip(): Text? { + return null + } + + override fun getTranslationKey(): String { + return config.translationKey + } + +} diff --git a/src/compat/configured/java/ConfigValue.kt b/src/compat/configured/java/ConfigValue.kt new file mode 100644 index 0000000..e16c51c --- /dev/null +++ b/src/compat/configured/java/ConfigValue.kt @@ -0,0 +1,72 @@ +package moe.nea.firmament.compat.configured + +import com.mrcrayfish.configured.api.IConfigValue +import net.minecraft.text.Text +import moe.nea.firmament.gui.config.ManagedOption + +class ConfigValue(val option: ManagedOption) : IConfigValue { + var value = option.get() + var initialValue = option.get() + + override fun get(): T { + return value + } + + override fun set(p0: T) { + this.value = p0 + } + + override fun getDefault(): T { + return option.default() + } + + override fun isDefault(): Boolean { + // TODO: should this be an option in handlers? + return option == option.default() + } + + override fun isChanged(): Boolean { + return value != initialValue + } + + override fun restore() { + this.value = option.default() + } + + override fun getComment(): Text? { + return null + } + + override fun getTranslationKey(): String? { + return option.rawLabelText + } + + override fun getValidationHint(): Text? { + return null + } + + override fun getName(): String { + return "" + } + + override fun cleanCache() { + + } + + override fun requiresWorldRestart(): Boolean { + return false + } + + override fun requiresGameRestart(): Boolean { + return false + } + + override fun isValid(p0: T): Boolean { + // TODO: should this be validated? + return true + } + + fun saveValue() { + option.set(value) + } +} diff --git a/src/compat/configured/java/ConfigValueNode.kt b/src/compat/configured/java/ConfigValueNode.kt new file mode 100644 index 0000000..df59739 --- /dev/null +++ b/src/compat/configured/java/ConfigValueNode.kt @@ -0,0 +1,37 @@ +package moe.nea.firmament.compat.configured + +import com.mrcrayfish.configured.api.IConfigEntry +import com.mrcrayfish.configured.api.IConfigValue +import net.minecraft.text.Text +import moe.nea.firmament.gui.config.ManagedOption + +class ConfigValueNode(val option: ManagedOption<*>) : IConfigEntry { + override fun getChildren(): List { + return listOf() + } + + override fun isRoot(): Boolean { + return false + } + + override fun isLeaf(): Boolean { + return true + } + + val value = ConfigValue(option) + override fun getValue(): IConfigValue<*>? { + return value + } + + override fun getEntryName(): String { + return option.propertyName + } + + override fun getTooltip(): Text? { + return null + } + + override fun getTranslationKey(): String? { + return option.rawLabelText + } +} diff --git a/src/compat/configured/java/ConfiguredCompat.kt b/src/compat/configured/java/ConfiguredCompat.kt new file mode 100644 index 0000000..8e8b022 --- /dev/null +++ b/src/compat/configured/java/ConfiguredCompat.kt @@ -0,0 +1,30 @@ +package moe.nea.firmament.compat.configured + +import com.mrcrayfish.configured.api.IConfigEntry +import com.mrcrayfish.configured.api.IModConfig +import com.mrcrayfish.configured.api.IModConfigProvider +import com.mrcrayfish.configured.api.ModContext +import moe.nea.firmament.Firmament +import moe.nea.firmament.gui.config.AllConfigsGui +import moe.nea.firmament.gui.config.ManagedConfig + +/** + * Registered in `fabric.mod.json` at `custom.configured.providers` + */ +class ConfiguredCompat : IModConfigProvider { + override fun getConfigurationsForMod(modContext: ModContext): Set { + if (modContext.modId != Firmament.MOD_ID) return emptySet() + return buildSet { + add(object : BaseConfig() { + override fun getRoot(): IConfigEntry { + return BaseConfigNode + } + + override fun getTranslationKey(): String? { + return "firmament.config.all-configs" + } + }) + ManagedConfig.allManagedConfigs.getAll().mapTo(this) { ConfigCategory(it) } + } + } +} diff --git a/src/compat/configured/java/ConfiguredConfigScreenProvider.kt b/src/compat/configured/java/ConfiguredConfigScreenProvider.kt new file mode 100644 index 0000000..c0095bf --- /dev/null +++ b/src/compat/configured/java/ConfiguredConfigScreenProvider.kt @@ -0,0 +1,22 @@ +package moe.nea.firmament.compat.configured + +import com.google.auto.service.AutoService +import com.mrcrayfish.configured.integration.CatalogueConfigFactory +import net.fabricmc.loader.api.FabricLoader +import net.minecraft.client.gui.screen.Screen +import moe.nea.firmament.Firmament +import moe.nea.firmament.gui.config.FirmamentConfigScreenProvider + +@AutoService(FirmamentConfigScreenProvider::class) +class ConfiguredConfigScreenProvider : FirmamentConfigScreenProvider { + override val key: String + get() = "configured" + override val isEnabled: Boolean + get() = FabricLoader.getInstance().isModLoaded("configured") + + override fun open(parent: Screen?): Screen { + return CatalogueConfigFactory.createConfigScreen( + parent, + FabricLoader.getInstance().getModContainer(Firmament.MOD_ID).get()) + } +} diff --git a/src/compat/explosiveEnhancement/java/ExplosiveEnhancementSpawner.kt b/src/compat/explosiveEnhancement/java/ExplosiveEnhancementSpawner.kt new file mode 100644 index 0000000..caf16a5 --- /dev/null +++ b/src/compat/explosiveEnhancement/java/ExplosiveEnhancementSpawner.kt @@ -0,0 +1,17 @@ +package moe.nea.firmament.compat.explosiveenhancement + +import com.google.auto.service.AutoService +import net.superkat.explosiveenhancement.api.ExplosiveApi +import net.minecraft.util.math.Vec3d +import moe.nea.firmament.features.fixes.CompatibliltyFeatures +import moe.nea.firmament.features.fixes.CompatibliltyFeatures.TConfig +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.compatloader.CompatLoader + +@AutoService(CompatibliltyFeatures.ExplosiveApiWrapper::class) +@CompatLoader.RequireMod("explosiveenhancement") +class ExplosiveEnhancementSpawner : CompatibliltyFeatures.ExplosiveApiWrapper { + override fun spawnParticle(vec3d: Vec3d, power: Float) { + ExplosiveApi.spawnParticles(MC.world, vec3d.x, vec3d.y, vec3d.z, TConfig.explosionSize / 10F) + } +} diff --git a/src/compat/jade/java/moe/nea/firmament/compat/jade/Compat.kt b/src/compat/jade/java/moe/nea/firmament/compat/jade/Compat.kt new file mode 100644 index 0000000..d1cfef4 --- /dev/null +++ b/src/compat/jade/java/moe/nea/firmament/compat/jade/Compat.kt @@ -0,0 +1,12 @@ +package moe.nea.firmament.compat.jade + +import net.fabricmc.loader.api.FabricLoader +import moe.nea.firmament.util.compatloader.CompatMeta +import moe.nea.firmament.util.compatloader.ICompatMeta + +@CompatMeta +object Compat : ICompatMeta { + override fun shouldLoad(): Boolean { + return FabricLoader.getInstance().isModLoaded("jade") + } +} diff --git a/src/compat/jade/java/moe/nea/firmament/compat/jade/CustomFakeBlockProvider.kt b/src/compat/jade/java/moe/nea/firmament/compat/jade/CustomFakeBlockProvider.kt new file mode 100644 index 0000000..53e3255 --- /dev/null +++ b/src/compat/jade/java/moe/nea/firmament/compat/jade/CustomFakeBlockProvider.kt @@ -0,0 +1,41 @@ +package moe.nea.firmament.compat.jade + +import snownee.jade.api.Accessor +import snownee.jade.api.BlockAccessor +import snownee.jade.api.IWailaClientRegistration +import snownee.jade.api.callback.JadeRayTraceCallback +import net.minecraft.util.hit.HitResult +import moe.nea.firmament.repo.MiningRepoData +import moe.nea.firmament.util.mc.FirmamentDataComponentTypes + +class CustomFakeBlockProvider(val registration: IWailaClientRegistration) : JadeRayTraceCallback { + + override fun onRayTrace( + hitResult: HitResult, + accessor: Accessor<*>?, + originalAccessor: Accessor<*>? + ): Accessor<*>? { + if (!JadeIntegration.TConfig.blockDetection) return accessor + if (accessor !is BlockAccessor) return accessor + val customBlock = JadeIntegration.customBlocks[accessor.block] + if (customBlock == null) return accessor + return registration.blockAccessor() + .from(accessor) + .fakeBlock(customBlock.getDisplayItem(accessor.block)) + .build() + } + + companion object { + @JvmStatic + fun hasCustomBlock(accessor: BlockAccessor): Boolean { + return getCustomBlock(accessor) != null + } + + @JvmStatic + fun getCustomBlock(accessor: BlockAccessor): MiningRepoData.CustomMiningBlock? { + if (!accessor.isFakeBlock) return null + val item = accessor.fakeBlock + return item.get(FirmamentDataComponentTypes.CUSTOM_MINING_BLOCK_DATA) + } + } +} diff --git a/src/compat/jade/java/moe/nea/firmament/compat/jade/CustomMiningHardnessProvider.kt b/src/compat/jade/java/moe/nea/firmament/compat/jade/CustomMiningHardnessProvider.kt new file mode 100644 index 0000000..29fecd2 --- /dev/null +++ b/src/compat/jade/java/moe/nea/firmament/compat/jade/CustomMiningHardnessProvider.kt @@ -0,0 +1,97 @@ +package moe.nea.firmament.compat.jade + +import snownee.jade.api.BlockAccessor +import snownee.jade.api.IBlockComponentProvider +import snownee.jade.api.ITooltip +import snownee.jade.api.config.IPluginConfig +import kotlin.time.DurationUnit +import net.minecraft.block.BlockState +import net.minecraft.util.Identifier +import net.minecraft.util.math.BlockPos +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.TickEvent +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.tr + +object CustomMiningHardnessProvider : IBlockComponentProvider { + + override fun appendTooltip( + tooltip: ITooltip, + block: BlockAccessor, + config: IPluginConfig? + ) { + val customBlock = CustomFakeBlockProvider.getCustomBlock(block) ?: return + if (customBlock.breakingPower > 0) + tooltip.add(tr("firmament.jade.breaking_power", "Required Breaking Power: ${customBlock.breakingPower}")) + } + + override fun getUid(): Identifier = + Firmament.identifier("custom_mining_hardness") + + data class BreakingInfo( + val blockPos: BlockPos, val stage: Int, + val state: BlockState?, + val ts: TimeMark = TimeMark.now() + ) + + var previousBreakingInfo: BreakingInfo? = null + var currentBreakingInfo: BreakingInfo? = null + + @Subscribe + fun clearInfoOnStopBreaking(event: TickEvent) { + val isBreakingBlock = MC.interactionManager?.isBreakingBlock ?: false + if (!isBreakingBlock) { + previousBreakingInfo = null + currentBreakingInfo = null + } + } + + @JvmStatic + fun setBreakingInfo(blockPos: BlockPos, stage: Int) { + previousBreakingInfo = currentBreakingInfo + val state = MC.world?.getBlockState(blockPos) + if (previousBreakingInfo?.let { it.state != state || it.blockPos != blockPos } ?: false) + previousBreakingInfo == null + currentBreakingInfo = BreakingInfo(blockPos.toImmutable(), stage, state) + // For some reason hypixel initially sends a stage 10 packet, and then fixes it up with a stage 0 packet. + // Ignore the stage 10 packet if we dont have any previous packets for this block. + // This could in theory still have issues if someone perfectly stops breaking a block the tick it finishes and then does not break another block until it respawns, but i deem that to be too much of an edge case. + if (stage == 10 && previousBreakingInfo == null) { + previousBreakingInfo = null + currentBreakingInfo = null + } + } + + @JvmStatic + fun replaceBreakProgress(original: Float): Float { + if (!JadeIntegration.TConfig.miningProgress) return original + if (!isOnMiningIsland()) return original + val pos = MC.interactionManager?.currentBreakingPos ?: return original + val info = currentBreakingInfo + if (info?.blockPos != pos || info.state != MC.world?.getBlockState(pos)) { + currentBreakingInfo = null + previousBreakingInfo = null + return original + } + // TODO: improve this interpolation to work across all stages, to alleviate some of the jittery bar. + // Maybe introduce a proper mining API that tracks the actual progress with some sort of FSM + val interpolatedStage = previousBreakingInfo?.let { prev -> + val timeBetweenTicks = (info.ts - prev.ts).toDouble(DurationUnit.SECONDS) + val stagesPerUpdate = (info.stage - prev.stage).toDouble() + if (stagesPerUpdate < 1) return@let null + val stagesPerSecond = stagesPerUpdate / timeBetweenTicks + info.stage + (info.ts.passedTime().toDouble(DurationUnit.SECONDS) * stagesPerSecond) + .coerceAtMost(stagesPerUpdate) + }?.toFloat() + val stage = interpolatedStage ?: info.stage.toFloat() + return stage / 10F + } + + @JvmStatic + fun replaceBlockBreakSpeed(original: Float): Float { + if (isOnMiningIsland()) return 0F + return original + } +} diff --git a/src/compat/jade/java/moe/nea/firmament/compat/jade/DrillToolProvider.kt b/src/compat/jade/java/moe/nea/firmament/compat/jade/DrillToolProvider.kt new file mode 100644 index 0000000..10bff1b --- /dev/null +++ b/src/compat/jade/java/moe/nea/firmament/compat/jade/DrillToolProvider.kt @@ -0,0 +1,77 @@ +package moe.nea.firmament.compat.jade + +import java.util.Optional +import java.util.function.UnaryOperator +import snownee.jade.api.BlockAccessor +import snownee.jade.api.IBlockComponentProvider +import snownee.jade.api.ITooltip +import snownee.jade.api.JadeIds +import snownee.jade.api.config.IPluginConfig +import snownee.jade.api.theme.IThemeHelper +import snownee.jade.api.ui.IElement +import snownee.jade.api.ui.IElementHelper +import snownee.jade.impl.ui.ItemStackElement +import snownee.jade.impl.ui.TextElement +import kotlin.jvm.optionals.getOrDefault +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import net.minecraft.util.math.Vec2f +import moe.nea.firmament.Firmament +import moe.nea.firmament.repo.ExpensiveItemCacheApi +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.util.MC + +class DrillToolProvider : IBlockComponentProvider { + @OptIn(ExpensiveItemCacheApi::class) + override fun appendTooltip( + tooltip: ITooltip, + accessor: BlockAccessor, + p2: IPluginConfig? + ) { + val customBlock = CustomFakeBlockProvider.getCustomBlock(accessor) ?: return + val tool = RepoManager.miningData.getToolsThatCanBreak(customBlock.breakingPower).firstOrNull() + ?.asImmutableItemStack() ?: return + tooltip.replace(JadeIds.MC_HARVEST_TOOL, UnaryOperator { elements -> + elements.map { inner -> + val lastItemIndex = inner.indexOfLast { it is ItemStackElement } + if (lastItemIndex < 0) return@map inner + val innerMut = inner.toMutableList() + val harvestIndicator = innerMut.indexOfLast { + it is TextElement && it.cachedSize == Vec2f.ZERO && it.text.visit { + if (it.isEmpty()) Optional.empty() else Optional.of(true) + }.getOrDefault(false) + } + val canHarvest = SBItemStack(MC.stackInHand).breakingPower >= customBlock.breakingPower + val lastItem = innerMut[lastItemIndex] as ItemStackElement + if (harvestIndicator < 0) { + innerMut.add(lastItemIndex + 1, canHarvestIndicator(canHarvest, lastItem.alignment)) + } else { + innerMut.set(harvestIndicator, canHarvestIndicator(canHarvest, lastItem.alignment)) + } + innerMut.set(lastItemIndex, IElementHelper.get() + .item(tool, 0.75f) + .translate(lastItem.translation) + .size(lastItem.size) + .message(null) + .align(lastItem.alignment)) + innerMut.subList(0, lastItemIndex - 1).removeIf { it is ItemStackElement } + innerMut + } + }) + } + + fun canHarvestIndicator(canHarvest: Boolean, align: IElement.Align): IElement { + val t = IThemeHelper.get() + val text = if (canHarvest) t.success(CHECK) else t.danger(X) + return IElementHelper.get().text(text) + .scale(0.75F).zOffset(800).size(Vec2f.ZERO).translate(Vec2f(-3F, 3.25F)).align(align) + } + + private val CHECK: Text = Text.literal("✔") + private val X: Text = Text.literal("✕") + + override fun getUid(): Identifier { + return Firmament.identifier("toolprovider") + } +} diff --git a/src/compat/jade/java/moe/nea/firmament/compat/jade/FirmamentJadePlugin.kt b/src/compat/jade/java/moe/nea/firmament/compat/jade/FirmamentJadePlugin.kt new file mode 100644 index 0000000..51e2453 --- /dev/null +++ b/src/compat/jade/java/moe/nea/firmament/compat/jade/FirmamentJadePlugin.kt @@ -0,0 +1,21 @@ +package moe.nea.firmament.compat.jade + +import snownee.jade.api.IWailaClientRegistration +import snownee.jade.api.IWailaCommonRegistration +import snownee.jade.api.IWailaPlugin +import snownee.jade.api.WailaPlugin +import net.minecraft.block.Block +import moe.nea.firmament.Firmament + +@WailaPlugin +class FirmamentJadePlugin : IWailaPlugin { + override fun register(registration: IWailaCommonRegistration) { + Firmament.logger.debug("Registering Jade integration...") + } + + override fun registerClient(registration: IWailaClientRegistration) { + registration.registerBlockComponent(CustomMiningHardnessProvider, Block::class.java) + registration.registerBlockComponent(DrillToolProvider(), Block::class.java) + registration.addRayTraceCallback(CustomFakeBlockProvider(registration)) + } +} diff --git a/src/compat/jade/java/moe/nea/firmament/compat/jade/JadeIntegration.kt b/src/compat/jade/java/moe/nea/firmament/compat/jade/JadeIntegration.kt new file mode 100644 index 0000000..d411c26 --- /dev/null +++ b/src/compat/jade/java/moe/nea/firmament/compat/jade/JadeIntegration.kt @@ -0,0 +1,50 @@ +package moe.nea.firmament.compat.jade + +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.SkyblockServerUpdateEvent +import moe.nea.firmament.repo.MiningRepoData +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.util.ErrorUtil +import net.minecraft.block.Block +import moe.nea.firmament.events.ReloadRegistrationEvent +import moe.nea.firmament.gui.config.ManagedConfig + +object JadeIntegration { + object TConfig : ManagedConfig("jade-integration", Category.INTEGRATIONS) { + val miningProgress by toggle("progress") { true } + val blockDetection by toggle("blocks") { true } + } + + var customBlocks: Map = mapOf() + + fun refreshBlockInfo() { + if (!isOnMiningIsland()) { + customBlocks = mapOf() + return + } + val blocks = RepoManager.miningData.customMiningBlocks + .flatMap { customBlock -> + // TODO: add a lifted helper method for this + customBlock.blocks189.filter { it.isCurrentlyActive } + .mapNotNull { it.block } + .map { customBlock to it } + } + .groupBy { it.second } + customBlocks = blocks.mapNotNull { (block, customBlocks) -> + val singleMatch = + ErrorUtil.notNullOr(customBlocks.singleOrNull()?.first, + "Two custom blocks both want to supply custom mining behaviour for $block.") { return@mapNotNull null } + block to singleMatch + }.toMap() + } + + @Subscribe + fun onRepoReload(event: ReloadRegistrationEvent) { + event.repo.registerReloadListener { refreshBlockInfo() } + } + + @Subscribe + fun onWorldSwap(event: SkyblockServerUpdateEvent) { + refreshBlockInfo() + } +} diff --git a/src/compat/jade/java/moe/nea/firmament/compat/jade/utils.kt b/src/compat/jade/java/moe/nea/firmament/compat/jade/utils.kt new file mode 100644 index 0000000..364dc02 --- /dev/null +++ b/src/compat/jade/java/moe/nea/firmament/compat/jade/utils.kt @@ -0,0 +1,6 @@ +package moe.nea.firmament.compat.jade + +import moe.nea.firmament.util.SBData + +fun isOnMiningIsland(): Boolean = + SBData.skyblockLocation?.hasCustomMining ?: false diff --git a/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/ElementAccessor.java b/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/ElementAccessor.java new file mode 100644 index 0000000..1b58e3c --- /dev/null +++ b/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/ElementAccessor.java @@ -0,0 +1,12 @@ +package moe.nea.firmament.mixins.compat.jade; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import snownee.jade.api.ui.Element; +import snownee.jade.api.ui.IElement; + +@Mixin(Element.class) +public interface ElementAccessor { + @Accessor("align") + IElement.Align getAlign_firmament(); +} diff --git a/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/EnforceToolDisplayForCustomBlocksInHarvestToolProvider.java b/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/EnforceToolDisplayForCustomBlocksInHarvestToolProvider.java new file mode 100644 index 0000000..3677d01 --- /dev/null +++ b/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/EnforceToolDisplayForCustomBlocksInHarvestToolProvider.java @@ -0,0 +1,33 @@ +package moe.nea.firmament.mixins.compat.jade; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.sugar.Local; +import moe.nea.firmament.compat.jade.CustomFakeBlockProvider; +import net.minecraft.block.Blocks; +import net.minecraft.item.ItemStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import snownee.jade.addon.harvest.HarvestToolProvider; +import snownee.jade.api.BlockAccessor; + +import java.util.List; + +@Mixin(HarvestToolProvider.class) +public class EnforceToolDisplayForCustomBlocksInHarvestToolProvider { + @ModifyExpressionValue(method = "getText", at = @At(value = "INVOKE", target = "Lnet/minecraft/block/BlockState;isToolRequired()Z")) + private boolean overwriteRequiresTool(boolean original, @Local(argsOnly = true) BlockAccessor accessor) { + if (CustomFakeBlockProvider.hasCustomBlock(accessor)) + return true; + return original; + } + + private static final List REPLACEABLE_TOOL = List.of(new ItemStack(Blocks.ENCHANTING_TABLE)); + + @ModifyExpressionValue(method = "getText", at = @At(value = "INVOKE", target = "Lcom/google/common/cache/Cache;get(Ljava/lang/Object;Ljava/util/concurrent/Callable;)Ljava/lang/Object;")) + private Object overwriteAvailableTools(Object original, @Local(argsOnly = true) BlockAccessor accessor) { + var orig = (List) original; + if (orig.isEmpty() && CustomFakeBlockProvider.hasCustomBlock(accessor)) + return REPLACEABLE_TOOL; + return orig; + } +} diff --git a/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/OnUpdateBreakProgress.java b/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/OnUpdateBreakProgress.java new file mode 100644 index 0000000..7d71ae8 --- /dev/null +++ b/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/OnUpdateBreakProgress.java @@ -0,0 +1,22 @@ +package moe.nea.firmament.mixins.compat.jade; + +import moe.nea.firmament.compat.jade.CustomMiningHardnessProvider; +import moe.nea.firmament.util.MC; +import net.minecraft.client.render.WorldRenderer; +import net.minecraft.util.math.BlockPos; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Objects; + +@Mixin(WorldRenderer.class) +public class OnUpdateBreakProgress { + @Inject(method = "setBlockBreakingInfo", at = @At("HEAD")) + private void replaceBreakProgress(int entityId, BlockPos pos, int stage, CallbackInfo ci) { + if (entityId == 0 && null != MC.INSTANCE.getInteractionManager() && Objects.equals(MC.INSTANCE.getInteractionManager().currentBreakingPos, pos)) { + CustomMiningHardnessProvider.setBreakingInfo(pos, stage); + } + } +} diff --git a/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/PatchBreakingBarSpeedJade.java b/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/PatchBreakingBarSpeedJade.java new file mode 100644 index 0000000..203f7e4 --- /dev/null +++ b/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/PatchBreakingBarSpeedJade.java @@ -0,0 +1,25 @@ +package moe.nea.firmament.mixins.compat.jade; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import moe.nea.firmament.compat.jade.CustomMiningHardnessProvider; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import snownee.jade.JadeClient; + +@Mixin(JadeClient.class) +public class PatchBreakingBarSpeedJade { + @ModifyExpressionValue( + method = "drawBreakingProgress", + at = @At(value = "FIELD", target = "Lnet/minecraft/client/network/ClientPlayerInteractionManager;currentBreakingProgress:F", opcode = Opcodes.GETFIELD) + ) + private static float replaceBlockBreakingProgress(float original) { + return CustomMiningHardnessProvider.replaceBreakProgress(original); + } + + @ModifyExpressionValue(method = "drawBreakingProgress", + at = @At(value = "INVOKE", target = "Lnet/minecraft/block/BlockState;calcBlockBreakingDelta(Lnet/minecraft/entity/player/PlayerEntity;Lnet/minecraft/world/BlockView;Lnet/minecraft/util/math/BlockPos;)F")) + private static float replacePlayerSpecificBreakingProgress(float original) { + return CustomMiningHardnessProvider.replaceBlockBreakSpeed(original); + } +} diff --git a/src/compat/modmenu/java/moe/nea/firmament/compat/modmenu/FirmamentModMenuPlugin.kt b/src/compat/modmenu/java/moe/nea/firmament/compat/modmenu/FirmamentModMenuPlugin.kt new file mode 100644 index 0000000..ff58c20 --- /dev/null +++ b/src/compat/modmenu/java/moe/nea/firmament/compat/modmenu/FirmamentModMenuPlugin.kt @@ -0,0 +1,11 @@ +package moe.nea.firmament.compat.modmenu + +import com.terraformersmc.modmenu.api.ConfigScreenFactory +import com.terraformersmc.modmenu.api.ModMenuApi +import moe.nea.firmament.gui.config.AllConfigsGui + +class FirmamentModMenuPlugin : ModMenuApi { + override fun getModConfigScreenFactory(): ConfigScreenFactory<*> { + return ConfigScreenFactory { AllConfigsGui.makeScreen(parent = it) } + } +} diff --git a/src/compat/moulconfig/java/MCConfigEditorIntegration.kt b/src/compat/moulconfig/java/MCConfigEditorIntegration.kt new file mode 100644 index 0000000..874e58d --- /dev/null +++ b/src/compat/moulconfig/java/MCConfigEditorIntegration.kt @@ -0,0 +1,438 @@ +package moe.nea.firmament.compat.moulconfig + +import com.google.auto.service.AutoService +import io.github.notenoughupdates.moulconfig.ChromaColour +import io.github.notenoughupdates.moulconfig.Config +import io.github.notenoughupdates.moulconfig.DescriptionRendereringBehaviour +import io.github.notenoughupdates.moulconfig.Social +import io.github.notenoughupdates.moulconfig.common.IMinecraft +import io.github.notenoughupdates.moulconfig.common.MyResourceLocation +import io.github.notenoughupdates.moulconfig.gui.GuiComponent +import io.github.notenoughupdates.moulconfig.gui.GuiElementWrapper +import io.github.notenoughupdates.moulconfig.gui.GuiOptionEditor +import io.github.notenoughupdates.moulconfig.gui.HorizontalAlign +import io.github.notenoughupdates.moulconfig.gui.MoulConfigEditor +import io.github.notenoughupdates.moulconfig.gui.VerticalAlign +import io.github.notenoughupdates.moulconfig.gui.component.AlignComponent +import io.github.notenoughupdates.moulconfig.gui.component.RowComponent +import io.github.notenoughupdates.moulconfig.gui.component.SliderComponent +import io.github.notenoughupdates.moulconfig.gui.component.TextComponent +import io.github.notenoughupdates.moulconfig.gui.editors.ComponentEditor +import io.github.notenoughupdates.moulconfig.gui.editors.GuiOptionEditorAccordion +import io.github.notenoughupdates.moulconfig.gui.editors.GuiOptionEditorBoolean +import io.github.notenoughupdates.moulconfig.gui.editors.GuiOptionEditorButton +import io.github.notenoughupdates.moulconfig.gui.editors.GuiOptionEditorColour +import io.github.notenoughupdates.moulconfig.gui.editors.GuiOptionEditorDropdown +import io.github.notenoughupdates.moulconfig.gui.editors.GuiOptionEditorText +import io.github.notenoughupdates.moulconfig.observer.GetSetter +import io.github.notenoughupdates.moulconfig.processor.ProcessedCategory +import io.github.notenoughupdates.moulconfig.processor.ProcessedOption +import java.lang.reflect.Type +import java.net.URI +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import kotlin.time.DurationUnit +import net.minecraft.client.gui.screen.Screen +import net.minecraft.util.Identifier +import net.minecraft.util.StringIdentifiable +import net.minecraft.util.Util +import moe.nea.firmament.Firmament +import moe.nea.firmament.gui.config.AllConfigsGui +import moe.nea.firmament.gui.config.BooleanHandler +import moe.nea.firmament.gui.config.ChoiceHandler +import moe.nea.firmament.gui.config.ClickHandler +import moe.nea.firmament.gui.config.ColourHandler +import moe.nea.firmament.gui.config.DurationHandler +import moe.nea.firmament.gui.config.FirmamentConfigScreenProvider +import moe.nea.firmament.gui.config.HudMeta +import moe.nea.firmament.gui.config.HudMetaHandler +import moe.nea.firmament.gui.config.IntegerHandler +import moe.nea.firmament.gui.config.KeyBindingHandler +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.gui.config.ManagedOption +import moe.nea.firmament.gui.config.StringHandler +import moe.nea.firmament.gui.toMoulConfig +import moe.nea.firmament.keybindings.SavedKeyBinding +import moe.nea.firmament.util.ErrorUtil +import moe.nea.firmament.util.FirmFormatters +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.MoulConfigUtils.xmap + +@AutoService(FirmamentConfigScreenProvider::class) +class MCConfigEditorIntegration : FirmamentConfigScreenProvider { + override val key: String + get() = "moulconfig" + + val handlers: MutableMap>, ((ManagedConfig.OptionHandler<*>, ManagedOption<*>, accordionId: Int, configObject: Config) -> ProcessedEditableOptionFirm<*>)> = + mutableMapOf() + + fun > register( + handlerClass: Class, + transform: (H, ManagedOption, accordionId: Int, configObject: Config) -> ProcessedEditableOptionFirm + ) { + handlers[handlerClass] = + transform as ((ManagedConfig.OptionHandler<*>, ManagedOption<*>, accordionId: Int, configObject: Config) -> ProcessedEditableOptionFirm<*>) + } + + fun getHandler( + option: ManagedOption, + accordionId: Int, + configObject: Config + ): ProcessedEditableOptionFirm<*> { + val transform = handlers[option.handler.javaClass] + ?: error("Could not transform ${option.handler}") // TODO: replace with soft error and an error config element + return transform.invoke(option.handler, option, accordionId, configObject) as ProcessedEditableOptionFirm + } + + class CustomSliderEditor( + option: ProcessedOption, + setter: GetSetter, + fromT: (T) -> Float, + toT: (Float) -> T, + minValue: T, maxValue: T, + minStep: Float, + formatter: (T) -> String, + ) : ComponentEditor(option) { + override fun getDelegate(): GuiComponent { + return delegateI + } + + val mappedSetter = setter.xmap(fromT, toT) + + private val delegateI by lazy { + wrapComponent( + RowComponent( + AlignComponent( + TextComponent( + IMinecraft.instance.defaultFontRenderer, + { formatter(setter.get()) }, + 25, + TextComponent.TextAlignment.CENTER, false, false + ), + GetSetter.constant(HorizontalAlign.CENTER), + GetSetter.constant(VerticalAlign.CENTER) + ), + SliderComponent( + mappedSetter, + fromT(minValue), + fromT(maxValue), + minStep, + 40 + ) + ) + ) + } + } + + fun helpRegisterChoice() where T : Enum, T : StringIdentifiable { + register(ChoiceHandler::class.java as Class>) { handler, option, categoryAccordionId, configObject -> + object : ProcessedEditableOptionFirm(option, categoryAccordionId, configObject) { + override fun createEditor(): GuiOptionEditor { + return GuiOptionEditorDropdown( + this, + handler.universe.map { handler.renderer.getName(option, it).string }.toTypedArray() + ) + } + + override fun toT(any: Any?): T? { + return handler.universe[any as Int] + } + + override fun getType(): Type { + return Int::class.java + } + + override fun fromT(t: T): Any { + return t.ordinal + } + } + } + } + + init { + helpRegisterChoice() + register(BooleanHandler::class.java) { handler, option, categoryAccordionId, configObject -> + object : ProcessedEditableOptionFirm(option, categoryAccordionId, configObject) { + override fun createEditor(): GuiOptionEditor { + return GuiOptionEditorBoolean(this, -1, configObject) + } + + override fun toT(any: Any?): Boolean? { + return any as Boolean + } + + override fun getType(): Type { + return Boolean::class.java + } + + override fun fromT(t: Boolean): Any { + return t + } + } + } + register(StringHandler::class.java) { handler, option, categoryAccordionId, configObject -> + object : ProcessedEditableOptionFirm(option, categoryAccordionId, configObject) { + override fun createEditor(): GuiOptionEditor { + return GuiOptionEditorText(this) + } + + override fun getType(): Type { + return String::class.java + } + + override fun fromT(t: String): Any { + return t + } + + override fun toT(any: Any?): String? { + return any as String + } + } + } + register(ColourHandler::class.java) { handler, option, accordionId, configObject -> + object : ProcessedEditableOptionFirm(option, accordionId, configObject) { + override fun fromT(t: ChromaColour): Any { + return t + } + + override fun toT(any: Any?): ChromaColour? { + return any as ChromaColour? + } + + override fun createEditor(): GuiOptionEditor { + return GuiOptionEditorColour(this) + } + + override fun getType(): Type? { + return ChromaColour::class.java + } + } + + } + register(ClickHandler::class.java) { handler, option, categoryAccordionId, configObject -> + object : ProcessedEditableOptionFirm(option, categoryAccordionId, configObject) { + override fun createEditor(): GuiOptionEditor { + return GuiOptionEditorButton(this, -1, "Click", configObject) + } + + override fun toT(any: Any?): Unit? { + return null + } + + override fun fromT(t: Unit): Any { + return Runnable { handler.runnable() } + } + + override fun getType(): Type { + return Runnable::class.java + } + } + } + register(HudMetaHandler::class.java) { handler, option, categoryAccordionId, configObject -> + object : ProcessedEditableOptionFirm(option, categoryAccordionId, configObject) { + override fun createEditor(): GuiOptionEditor { + return GuiOptionEditorButton(this, -1, "Edit HUD", configObject) + } + + override fun fromT(t: HudMeta): Any { + return Runnable { + handler.openEditor(option, MC.screen!!) + } + } + + + override fun getType(): Type { + return Runnable::class.java + } + + override fun toT(any: Any?): HudMeta? = null + } + } + register(DurationHandler::class.java) { handler, option, categoryAccordionId, configObject -> + object : ProcessedEditableOptionFirm(option, categoryAccordionId, configObject) { + override fun createEditor(): GuiOptionEditor { + return CustomSliderEditor( + this, + option, + { it.toDouble(DurationUnit.SECONDS).toFloat() }, + { it.toDouble().seconds }, + handler.min, + handler.max, + 0.1F, + FirmFormatters::formatTimespan + ) + } + + override fun toT(any: Any?): Duration? = null + override fun fromT(t: Duration): Any { + ErrorUtil.softError("Getting on a slider component") + return Unit + } + + override fun getType(): Type { + return Nothing::class.java + } + + } + } + register(IntegerHandler::class.java) { handler, option, categoryAccordionId, configObject -> + object : ProcessedEditableOptionFirm(option, categoryAccordionId, configObject) { + override fun createEditor(): GuiOptionEditor { + return CustomSliderEditor( + this, + option, + { it.toFloat() }, + { it.toInt() }, + handler.min, + handler.max, + 1F, + Integer::toString + ) + } + + override fun toT(any: Any?): Int? = null + override fun fromT(t: Int): Any { + ErrorUtil.softError("Getting on a slider component") + return Unit + } + + override fun getType(): Type { + return Nothing::class.java + } + } + } + register(KeyBindingHandler::class.java) { handler, option, categoryAccordionId, configObject -> + object : ProcessedEditableOptionFirm(option, categoryAccordionId, configObject) { + override fun createEditor(): GuiOptionEditor { + return object : ComponentEditor(this) { + val button = wrapComponent(handler.createButtonComponent(option)) + override fun getDelegate(): GuiComponent { + return button + } + } + } + + override fun toT(any: Any?): SavedKeyBinding? { + return null + } + + override fun getType(): Type { + return Nothing::class.java + } + + override fun fromT(t: SavedKeyBinding): Any { + ErrorUtil.softError("Cannot get a keybinding editor") + return Unit + } + } + } + } + + val configObject = object : Config() { + override fun saveNow() { + ManagedConfig.allManagedConfigs.getAll().forEach { it.save() } + } + + override fun shouldAutoFocusSearchbar(): Boolean { + return true + } + + override fun getTitle(): String { + return "Firmament ${Firmament.version.friendlyString}" + } + + @Deprecated("Deprecated in java") + override fun executeRunnable(runnableId: Int) { + if (runnableId >= 0) + ErrorUtil.softError("Executed runnable $runnableId") + } + + override fun getDescriptionBehaviour(option: ProcessedOption?): DescriptionRendereringBehaviour { + return DescriptionRendereringBehaviour.EXPAND_PANEL + } + + fun mkSocial(name: String, identifier: Identifier, link: String) = object : Social() { + override fun onClick() { + Util.getOperatingSystem().open(URI(link)) + } + + override fun getTooltip(): List { + return listOf(name) + } + + override fun getIcon(): MyResourceLocation { + return identifier.toMoulConfig() + } + } + + private val socials = listOf( + mkSocial( + "Discord", Firmament.identifier("textures/socials/discord.png"), + Firmament.modContainer.metadata.contact.get("discord").get() + ), + mkSocial( + "Source Code", Firmament.identifier("textures/socials/git.png"), + Firmament.modContainer.metadata.contact.get("sources").get() + ), + mkSocial( + "Modrinth", Firmament.identifier("textures/socials/modrinth.png"), + Firmament.modContainer.metadata.contact.get("modrinth").get() + ), + ) + + override fun getSocials(): List { + return socials + } + } + val categories = ManagedConfig.Category.entries.map { + val options = mutableListOf() + var nextAccordionId = 720 + it.configs.forEach { config -> + val categoryAccordionId = nextAccordionId++ + options.add(object : ProcessedOptionFirm(-1, configObject) { + override fun getDebugDeclarationLocation(): String { + return "FirmamentConfig:${config.name}" + } + + override fun getName(): String { + return config.labelText.string + } + + override fun getDescription(): String { + return "Missing description" + } + + override fun get(): Any { + return Unit + } + + override fun getType(): Type { + return Unit.javaClass + } + + override fun set(value: Any?): Boolean { + return false + } + + override fun createEditor(): GuiOptionEditor { + return GuiOptionEditorAccordion(this, categoryAccordionId) + } + }) + config.allOptions.forEach { (key, option) -> + val processedOption = getHandler(option, categoryAccordionId, configObject) + options.add(processedOption) + } + } + + return@map ProcessedCategoryFirm(it, options) + } + + override fun open(search: String?, parent: Screen?): Screen { + val editor = MoulConfigEditor(ProcessedCategory.collect(categories), configObject) + if (search != null) + editor.search(search) + editor.setWide(AllConfigsGui.ConfigConfig.enableWideMC) + return GuiElementWrapper(editor) // TODO : add parent support + } + +} diff --git a/src/compat/moulconfig/java/ProcessedCategoryFirm.kt b/src/compat/moulconfig/java/ProcessedCategoryFirm.kt new file mode 100644 index 0000000..5313441 --- /dev/null +++ b/src/compat/moulconfig/java/ProcessedCategoryFirm.kt @@ -0,0 +1,47 @@ +package moe.nea.firmament.compat.moulconfig + +import io.github.notenoughupdates.moulconfig.gui.editors.GuiOptionEditorAccordion +import io.github.notenoughupdates.moulconfig.processor.ProcessedCategory +import io.github.notenoughupdates.moulconfig.processor.ProcessedOption +import moe.nea.firmament.gui.config.ManagedConfig + +class ProcessedCategoryFirm( + val category: ManagedConfig.Category, + private val options: List +) : ProcessedCategory { + val accordions = options.filter { it.editor is GuiOptionEditorAccordion } + .associateBy { (it.editor as GuiOptionEditorAccordion).accordionId } + init { + for (option in options) { + option.category = this + } + } + + override fun getDebugDeclarationLocation(): String? { + return "FirmamentCategory.${category.name}" + } + + override fun getDisplayName(): String { + return category.labelText.string + } + + override fun getDescription(): String { + return category.description.string + } + + override fun getIdentifier(): String { + return category.name + } + + override fun getParentCategoryId(): String? { + return null + } + + override fun getOptions(): List { + return options + } + + override fun getAccordionAnchors(): Map { + return accordions + } +} diff --git a/src/compat/moulconfig/java/ProcessedEditableOptionFirm.kt b/src/compat/moulconfig/java/ProcessedEditableOptionFirm.kt new file mode 100644 index 0000000..f0e9aa4 --- /dev/null +++ b/src/compat/moulconfig/java/ProcessedEditableOptionFirm.kt @@ -0,0 +1,44 @@ +package moe.nea.firmament.compat.moulconfig + +import io.github.notenoughupdates.moulconfig.Config +import moe.nea.firmament.gui.config.ManagedOption +import moe.nea.firmament.util.ErrorUtil + +abstract class ProcessedEditableOptionFirm( + val managedOption: ManagedOption, + categoryAccordionId: Int, + configObject: Config, +) : ProcessedOptionFirm(categoryAccordionId, configObject) { + val managedConfig = managedOption.element + override fun getDebugDeclarationLocation(): String { + return "FirmamentOption:${managedConfig.name}:${managedOption.propertyName}" + } + + override fun getName(): String { + return managedOption.labelText.string + } + + override fun getDescription(): String { + return managedOption.labelDescription.string + } + + abstract fun fromT(t: T): Any + abstract fun toT(any: Any?): T? + + final override fun get(): Any { + return fromT(managedOption.value) + } + + final override fun set(p0: Any?): Boolean { + managedOption.value = toT(p0) ?: run { + ErrorUtil.softError("Failed to set value p0 in $this") + return false + } + managedConfig.save() + return true + } + + override fun explicitNotifyChange() { + managedConfig.save() + } +} diff --git a/src/compat/moulconfig/java/ProcessedOptionFirm.kt b/src/compat/moulconfig/java/ProcessedOptionFirm.kt new file mode 100644 index 0000000..6936048 --- /dev/null +++ b/src/compat/moulconfig/java/ProcessedOptionFirm.kt @@ -0,0 +1,42 @@ +package moe.nea.firmament.compat.moulconfig + +import io.github.notenoughupdates.moulconfig.Config +import io.github.notenoughupdates.moulconfig.annotations.SearchTag +import io.github.notenoughupdates.moulconfig.gui.GuiOptionEditor +import io.github.notenoughupdates.moulconfig.processor.ProcessedCategory +import io.github.notenoughupdates.moulconfig.processor.ProcessedOption + +abstract class ProcessedOptionFirm( + private val accordionId: Int, + private val config: Config +) : ProcessedOption { + override fun getPath(): String? { + return "nonsense" + } + lateinit var category: ProcessedCategoryFirm + override fun getAccordionId(): Int { + return accordionId + } + + protected abstract fun createEditor(): GuiOptionEditor + val editorInstance by lazy { createEditor() } + + override fun getSearchTags(): Array { + return emptyArray() + } + + override fun getEditor(): GuiOptionEditor { + return editorInstance + } + + override fun getCategory(): ProcessedCategory { + return category + } + + override fun getConfig(): Config { + return config + } + + override fun explicitNotifyChange() { + } +} diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/Compat.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/Compat.kt new file mode 100644 index 0000000..9ab4d22 --- /dev/null +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/Compat.kt @@ -0,0 +1,12 @@ +package moe.nea.firmament.compat.rei + +import net.fabricmc.loader.api.FabricLoader +import moe.nea.firmament.util.compatloader.CompatMeta +import moe.nea.firmament.util.compatloader.ICompatMeta + +@CompatMeta +object Compat : ICompatMeta { + override fun shouldLoad(): Boolean { + return FabricLoader.getInstance().isModLoaded("roughlyenoughitems") + } +} diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/EntityWidget.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/EntityWidget.kt new file mode 100644 index 0000000..1097654 --- /dev/null +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/EntityWidget.kt @@ -0,0 +1,54 @@ +package moe.nea.firmament.compat.rei + +import me.shedaniel.math.Dimension +import me.shedaniel.math.FloatingDimension +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import me.shedaniel.rei.api.client.gui.widgets.WidgetWithBounds +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.gui.Element +import net.minecraft.entity.LivingEntity +import moe.nea.firmament.gui.entity.EntityRenderer +import moe.nea.firmament.util.ErrorUtil + + +class EntityWidget( + val entity: LivingEntity?, + val point: Point, + val size: FloatingDimension = FloatingDimension(defaultSize) +) : WidgetWithBounds() { + override fun children(): List { + return emptyList() + } + + var hasErrored = false + + override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + try { + if (!hasErrored) { + EntityRenderer.renderEntity( + entity!!, + context, + point.x, point.y, + size.width, size.height, + mouseX.toDouble(), + mouseY.toDouble()) + } + } catch (ex: Exception) { + ErrorUtil.softError("Failed to render constructed entity: $entity", ex) + hasErrored = true + } finally { + } + if (hasErrored) { + context.fill(point.x, point.y, point.x + size.width.toInt(), point.y + size.height.toInt(), 0xFFAA2222.toInt()) + } + } + + companion object { + val defaultSize = Dimension(50, 80) + } + + override fun getBounds(): Rectangle { + return Rectangle(point, size) + } +} diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiCommonPlugin.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiCommonPlugin.kt new file mode 100644 index 0000000..71e867a --- /dev/null +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiCommonPlugin.kt @@ -0,0 +1,12 @@ +package moe.nea.firmament.compat.rei + +import me.shedaniel.rei.api.common.entry.type.EntryTypeRegistry +import me.shedaniel.rei.api.common.plugins.REICommonPlugin +import moe.nea.firmament.repo.RepoManager + +class FirmamentReiCommonPlugin : REICommonPlugin { + override fun registerEntryTypes(registry: EntryTypeRegistry) { + if (!RepoManager.shouldLoadREI()) return + registry.register(FirmamentReiPlugin.SKYBLOCK_ITEM_TYPE_ID, SBItemEntryDefinition) + } +} diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiPlugin.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiPlugin.kt new file mode 100644 index 0000000..3a494b9 --- /dev/null +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiPlugin.kt @@ -0,0 +1,172 @@ +package moe.nea.firmament.compat.rei + +import io.github.moulberry.repo.data.NEUCraftingRecipe +import me.shedaniel.rei.api.client.plugins.REIClientPlugin +import me.shedaniel.rei.api.client.registry.category.CategoryRegistry +import me.shedaniel.rei.api.client.registry.display.DisplayRegistry +import me.shedaniel.rei.api.client.registry.entry.CollapsibleEntryRegistry +import me.shedaniel.rei.api.client.registry.entry.EntryRegistry +import me.shedaniel.rei.api.client.registry.screen.ExclusionZones +import me.shedaniel.rei.api.client.registry.screen.OverlayDecider +import me.shedaniel.rei.api.client.registry.screen.ScreenRegistry +import me.shedaniel.rei.api.client.registry.transfer.TransferHandler +import me.shedaniel.rei.api.client.registry.transfer.TransferHandlerRegistry +import me.shedaniel.rei.api.common.entry.EntryStack +import me.shedaniel.rei.api.common.entry.type.VanillaEntryTypes +import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.gui.screen.ingame.GenericContainerScreen +import net.minecraft.client.gui.screen.ingame.HandledScreen +import net.minecraft.item.ItemStack +import net.minecraft.text.Text +import net.minecraft.util.ActionResult +import net.minecraft.util.Identifier +import moe.nea.firmament.compat.rei.recipes.GenericREIRecipeCategory +import moe.nea.firmament.compat.rei.recipes.SBKatRecipe +import moe.nea.firmament.compat.rei.recipes.SBMobDropRecipe +import moe.nea.firmament.compat.rei.recipes.SBRecipe +import moe.nea.firmament.compat.rei.recipes.SBReforgeRecipe +import moe.nea.firmament.compat.rei.recipes.SBShopRecipe +import moe.nea.firmament.events.HandledScreenPushREIEvent +import moe.nea.firmament.features.inventory.CraftingOverlay +import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlayScreen +import moe.nea.firmament.repo.ExpensiveItemCacheApi +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.repo.recipes.SBCraftingRecipeRenderer +import moe.nea.firmament.repo.recipes.SBEssenceUpgradeRecipeRenderer +import moe.nea.firmament.repo.recipes.SBForgeRecipeRenderer +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.guessRecipeId +import moe.nea.firmament.util.skyblockId +import moe.nea.firmament.util.unformattedString + + +class FirmamentReiPlugin : REIClientPlugin { + + companion object { + @ExpensiveItemCacheApi + fun EntryStack.asItemEntry(): EntryStack { + return EntryStack.of(VanillaEntryTypes.ITEM, value.asImmutableItemStack()) + } + + val SKYBLOCK_ITEM_TYPE_ID = Identifier.of("firmament", "skyblockitems") + } + + @OptIn(ExpensiveItemCacheApi::class) + override fun registerTransferHandlers(registry: TransferHandlerRegistry) { + if (!RepoManager.shouldLoadREI()) return + registry.register(TransferHandler { context -> + val screen = context.containerScreen + val display = context.display + if (display !is SBRecipe) return@TransferHandler TransferHandler.Result.createNotApplicable() + val recipe = display.neuRecipe + if (recipe !is NEUCraftingRecipe) return@TransferHandler TransferHandler.Result.createNotApplicable() + val neuItem = RepoManager.getNEUItem(SkyblockId(recipe.output.itemId)) + ?: error("Could not find neu item ${recipe.output.itemId} which is used in a recipe output") + val useSuperCraft = context.isStackedCrafting || RepoManager.Config.alwaysSuperCraft + if (neuItem.isVanilla && useSuperCraft) return@TransferHandler TransferHandler.Result.createFailed( + Text.translatable( + "firmament.recipe.novanilla" + ) + ) + var shouldReturn = true + if (context.isActuallyCrafting && !useSuperCraft) { + val craftingScreen = (screen as? GenericContainerScreen) + ?.takeIf { it.title?.unformattedString == CraftingOverlay.CRAFTING_SCREEN_NAME } + if (craftingScreen == null) { + MC.sendCommand("craft") + shouldReturn = false + } + CraftingOverlay.setOverlay(craftingScreen, recipe) + } + if (context.isActuallyCrafting && useSuperCraft) { + shouldReturn = false + MC.sendCommand("viewrecipe ${neuItem.guessRecipeId()}") + } + return@TransferHandler TransferHandler.Result.createSuccessful().blocksFurtherHandling(shouldReturn) + }) + } + + + val generics = listOf>( + // Order matters: The order in here is the order in which they show up in REI + GenericREIRecipeCategory(SBCraftingRecipeRenderer), + GenericREIRecipeCategory(SBForgeRecipeRenderer), + GenericREIRecipeCategory(SBEssenceUpgradeRecipeRenderer), + ) + + override fun registerCategories(registry: CategoryRegistry) { + if (!RepoManager.shouldLoadREI()) return + + registry.add(generics) + registry.add(SBMobDropRecipe.Category) + registry.add(SBKatRecipe.Category) + registry.add(SBReforgeRecipe.Category) + registry.add(SBShopRecipe.Category) + } + + override fun registerExclusionZones(zones: ExclusionZones) { + zones.register(HandledScreen::class.java) { HandledScreenPushREIEvent.publish(HandledScreenPushREIEvent(it)).rectangles } + zones.register(StorageOverlayScreen::class.java) { it.getBounds() } + } + + override fun registerDisplays(registry: DisplayRegistry) { + if (!RepoManager.shouldLoadREI()) return + + generics.forEach { + it.registerDynamicGenerator(registry) + } + registry.registerDisplayGenerator( + SBReforgeRecipe.catIdentifier, + SBReforgeRecipe.DynamicGenerator + ) + registry.registerDisplayGenerator( + SBMobDropRecipe.Category.categoryIdentifier, + SkyblockMobDropRecipeDynamicGenerator + ) + registry.registerDisplayGenerator( + SBShopRecipe.Category.categoryIdentifier, + SkyblockShopRecipeDynamicGenerator + ) + registry.registerDisplayGenerator( + SBKatRecipe.Category.categoryIdentifier, + SkyblockKatRecipeDynamicGenerator + ) + } + + override fun registerCollapsibleEntries(registry: CollapsibleEntryRegistry) { + if (!RepoManager.shouldLoadREI()) return + + if (!RepoManager.Config.disableItemGroups) + RepoManager.neuRepo.constants.parents.parents + .forEach { (parent, children) -> + registry.group( + SkyblockId(parent).identifier, + Text.literal(RepoManager.getNEUItem(SkyblockId(parent))?.displayName ?: parent), + (children + parent).map { SBItemEntryDefinition.getEntry(SkyblockId(it)) }) + } + } + + override fun registerScreens(registry: ScreenRegistry) { + registry.registerDecider(object : OverlayDecider { + override fun isHandingScreen(screen: Class?): Boolean { + return screen == StorageOverlayScreen::class.java + } + + override fun shouldScreenBeOverlaid(screen: R): ActionResult { + return ActionResult.SUCCESS + } + }) + registry.registerFocusedStack(SkyblockItemIdFocusedStackProvider) + } + + override fun registerEntries(registry: EntryRegistry) { + if (!RepoManager.shouldLoadREI()) return + + registry.removeEntryIf { true } + RepoManager.neuRepo.items?.items?.values?.forEach { neuItem -> + registry.addEntry(SBItemEntryDefinition.getEntry(neuItem.skyblockId)) + } + } +} diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/HoveredItemStackProvider.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/HoveredItemStackProvider.kt new file mode 100644 index 0000000..b917c3e --- /dev/null +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/HoveredItemStackProvider.kt @@ -0,0 +1,38 @@ +package moe.nea.firmament.compat.rei + +import com.google.auto.service.AutoService +import me.shedaniel.math.impl.PointHelper +import me.shedaniel.rei.api.client.REIRuntime +import me.shedaniel.rei.api.client.gui.widgets.Slot +import me.shedaniel.rei.api.client.registry.screen.ScreenRegistry +import net.minecraft.client.gui.Element +import net.minecraft.client.gui.ParentElement +import net.minecraft.client.gui.screen.ingame.HandledScreen +import net.minecraft.item.ItemStack +import moe.nea.firmament.util.HoveredItemStackProvider +import moe.nea.firmament.util.compatloader.CompatLoader + +@AutoService(HoveredItemStackProvider::class) +@CompatLoader.RequireMod("roughlyenoughitems") +class ScreenRegistryHoveredItemStackProvider : HoveredItemStackProvider { + override fun provideHoveredItemStack(screen: HandledScreen<*>): ItemStack? { + val entryStack = ScreenRegistry.getInstance().getFocusedStack(screen, PointHelper.ofMouse()) + ?: return null + return entryStack.value as? ItemStack ?: entryStack.cheatsAs().value + } +} + +@AutoService(HoveredItemStackProvider::class) +@CompatLoader.RequireMod("roughlyenoughitems") +class OverlayHoveredItemStackProvider : HoveredItemStackProvider { + override fun provideHoveredItemStack(screen: HandledScreen<*>): ItemStack? { + var baseElement: Element? = REIRuntime.getInstance().overlay.orElse(null) + val mx = PointHelper.getMouseFloatingX() + val my = PointHelper.getMouseFloatingY() + while (true) { + if (baseElement is Slot) return baseElement.currentEntry.cheatsAs().value + if (baseElement !is ParentElement) return null + baseElement = baseElement.hoveredElement(mx, my).orElse(null) + } + } +} diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/NEUItemEntryRenderer.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/NEUItemEntryRenderer.kt new file mode 100644 index 0000000..5e4eee3 --- /dev/null +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/NEUItemEntryRenderer.kt @@ -0,0 +1,123 @@ +/* + * SPDX-FileCopyrightText: 2018-2023 shedaniel + * SPDX-FileCopyrightText: 2023 Linnea Gräf + * SPDX-FileCopyrightText: 2024 Linnea Gräf + * + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-License-Identifier: MIT + */ + +package moe.nea.firmament.compat.rei + +import me.shedaniel.math.Rectangle +import me.shedaniel.rei.api.client.entry.renderer.EntryRenderer +import me.shedaniel.rei.api.client.gui.widgets.Tooltip +import me.shedaniel.rei.api.client.gui.widgets.TooltipContext +import me.shedaniel.rei.api.common.entry.EntryStack +import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback +import net.minecraft.client.MinecraftClient +import net.minecraft.client.gui.DrawContext +import net.minecraft.item.ItemStack +import net.minecraft.item.Items +import net.minecraft.item.tooltip.TooltipType +import net.minecraft.text.Text +import moe.nea.firmament.events.ItemTooltipEvent +import moe.nea.firmament.repo.ExpensiveItemCacheApi +import moe.nea.firmament.repo.ItemCache +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.util.ErrorUtil +import moe.nea.firmament.util.FirmFormatters +import moe.nea.firmament.util.darkGrey +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.loreAccordingToNbt + +// TODO: make this re implement BatchedEntryRenderer, if possible (likely not, due to no-alloc rendering) +// Also it is probably not even that much faster now, with render layers. +object NEUItemEntryRenderer : EntryRenderer { + @OptIn(ExpensiveItemCacheApi::class) + override fun render( + entry: EntryStack, + context: DrawContext, + bounds: Rectangle, + mouseX: Int, + mouseY: Int, + delta: Float + ) { + val neuItem = entry.value.neuItem + val itemToRender = if(!RepoManager.Config.perfectRenders.rendersPerfectVisuals() && !entry.value.isWarm() && neuItem != null) { + ItemCache.recacheSoon(neuItem) + ItemStack(Items.PAINTING) + } else { + entry.value.asImmutableItemStack() + } + + context.matrices.push() + context.matrices.translate(bounds.centerX.toFloat(), bounds.centerY.toFloat(), 0F) + context.matrices.scale(bounds.width.toFloat() / 16F, bounds.height.toFloat() / 16F, 1f) + context.drawItemWithoutEntity(itemToRender, -8, -8) + context.drawStackOverlay( + minecraft.textRenderer, itemToRender, -8, -8, + if (entry.value.getStackSize() > 1000) FirmFormatters.shortFormat( + entry.value.getStackSize() + .toDouble() + ) + else null + ) + context.matrices.pop() + } + + val minecraft = MinecraftClient.getInstance() + var canUseVanillaTooltipEvents = true + + @OptIn(ExpensiveItemCacheApi::class) + override fun getTooltip(entry: EntryStack, tooltipContext: TooltipContext): Tooltip? { + if (!entry.value.isWarm() && !RepoManager.Config.perfectRenders.rendersPerfectText()) { + val neuItem = entry.value.neuItem + if (neuItem != null) { + val lore = mutableListOf() + lore.add(Text.literal(neuItem.displayName)) + neuItem.lore.mapTo(mutableListOf()) { Text.literal(it) } + return Tooltip.create(lore) + } + } + + val stack = entry.value.asImmutableItemStack() + + val lore = mutableListOf(stack.displayNameAccordingToNbt) + lore.addAll(stack.loreAccordingToNbt) + if (canUseVanillaTooltipEvents) { + try { + ItemTooltipCallback.EVENT.invoker().getTooltip( + stack, tooltipContext.vanillaContext(), TooltipType.BASIC, lore + ) + } catch (ex: Exception) { + canUseVanillaTooltipEvents = false + ErrorUtil.softError("Failed to use vanilla tooltips", ex) + } + } else { + ItemTooltipEvent.publish( + ItemTooltipEvent( + stack, + tooltipContext.vanillaContext(), + TooltipType.BASIC, + lore + ) + ) + } + if (entry.value.getStackSize() > 1000 && lore.isNotEmpty()) + lore.add(1, Text.literal("${entry.value.getStackSize()}x").darkGrey()) + // TODO: tags aren't sent as early now so some tooltip components that use tags will crash the game +// stack.getTooltip( +// Item.TooltipContext.create( +// tooltipContext.vanillaContext().registryLookup +// ?: MC.defaultRegistries +// ), +// MC.player, +// TooltipType.BASIC +// ) + return Tooltip.create(lore) + } + + +} diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/NEUItemEntrySerializer.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/NEUItemEntrySerializer.kt new file mode 100644 index 0000000..724d193 --- /dev/null +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/NEUItemEntrySerializer.kt @@ -0,0 +1,17 @@ +package moe.nea.firmament.compat.rei + +import com.mojang.serialization.Codec +import me.shedaniel.rei.api.common.entry.EntrySerializer +import net.minecraft.network.RegistryByteBuf +import net.minecraft.network.codec.PacketCodec +import moe.nea.firmament.repo.SBItemStack + +object NEUItemEntrySerializer : EntrySerializer { + override fun codec(): Codec { + return SBItemStack.CODEC + } + + override fun streamCodec(): PacketCodec { + return SBItemStack.PACKET_CODEC.cast() + } +} diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/REIRecipeLayouter.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/REIRecipeLayouter.kt new file mode 100644 index 0000000..8e39f28 --- /dev/null +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/REIRecipeLayouter.kt @@ -0,0 +1,62 @@ +package moe.nea.firmament.compat.rei + +import io.github.notenoughupdates.moulconfig.gui.GuiComponent +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import me.shedaniel.rei.api.client.gui.widgets.Widget +import me.shedaniel.rei.api.client.gui.widgets.Widgets +import net.minecraft.text.Text +import moe.nea.firmament.compat.rei.recipes.wrapWidget +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.repo.recipes.RecipeLayouter + +class REIRecipeLayouter : RecipeLayouter { + val container: MutableList = mutableListOf() + fun add(t: T): T = t.also(container::add) + + override fun createItemSlot( + x: Int, + y: Int, + content: SBItemStack?, + slotKind: RecipeLayouter.SlotKind + ) { + val slot = Widgets.createSlot(Point(x, y)) + if (content != null) + slot.entry(SBItemEntryDefinition.getEntry(content)) + when (slotKind) { + RecipeLayouter.SlotKind.SMALL_INPUT -> slot.markInput() + RecipeLayouter.SlotKind.SMALL_OUTPUT -> slot.markOutput() + RecipeLayouter.SlotKind.BIG_OUTPUT -> { + slot.markOutput().disableBackground() + add(Widgets.createResultSlotBackground(Point(x, y))) + } + } + add(slot) + } + + override fun createTooltip(rectangle: Rectangle, label: Text) { + add(Widgets.createTooltip(rectangle, label)) + } + + override fun createLabel(x: Int, y: Int, text: Text) { + add(Widgets.createLabel(Point(x, y), text)) + } + + override fun createArrow(x: Int, y: Int) = + add(Widgets.createArrow(Point(x, y))).bounds + + override fun createMoulConfig( + x: Int, + y: Int, + w: Int, + h: Int, + component: GuiComponent + ) { + add(wrapWidget(Rectangle(Point(x, y), Dimension(w, h)), component)) + } + + override fun createFire(ingredientsCenter: Point, animationTicks: Int) { + add(Widgets.createBurningFire(ingredientsCenter).animationDurationTicks(animationTicks.toDouble())) + } +} diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/SBItemEntryDefinition.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/SBItemEntryDefinition.kt new file mode 100644 index 0000000..740eeeb --- /dev/null +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/SBItemEntryDefinition.kt @@ -0,0 +1,97 @@ +package moe.nea.firmament.compat.rei + +import io.github.moulberry.repo.data.NEUIngredient +import java.util.stream.Stream +import me.shedaniel.rei.api.client.entry.renderer.EntryRenderer +import me.shedaniel.rei.api.common.entry.EntrySerializer +import me.shedaniel.rei.api.common.entry.EntryStack +import me.shedaniel.rei.api.common.entry.comparison.ComparisonContext +import me.shedaniel.rei.api.common.entry.type.EntryDefinition +import me.shedaniel.rei.api.common.entry.type.EntryType +import me.shedaniel.rei.api.common.entry.type.VanillaEntryTypes +import net.minecraft.item.ItemConvertible +import net.minecraft.item.ItemStack +import net.minecraft.registry.tag.TagKey +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import moe.nea.firmament.compat.rei.FirmamentReiPlugin.Companion.asItemEntry +import moe.nea.firmament.repo.ExpensiveItemCacheApi +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.util.SkyblockId + +object SBItemEntryDefinition : EntryDefinition { + override fun equals(o1: SBItemStack, o2: SBItemStack, context: ComparisonContext): Boolean { + return o1.skyblockId == o2.skyblockId && o1.getStackSize() == o2.getStackSize() + } + + @OptIn(ExpensiveItemCacheApi::class) + override fun cheatsAs(entry: EntryStack?, value: SBItemStack): ItemStack { + return value.asCopiedItemStack() + } + + override fun getValueType(): Class = SBItemStack::class.java + override fun getType(): EntryType = EntryType.deferred(FirmamentReiPlugin.SKYBLOCK_ITEM_TYPE_ID) + + override fun getRenderer(): EntryRenderer = NEUItemEntryRenderer + + override fun getSerializer(): EntrySerializer { + return NEUItemEntrySerializer + } + + override fun getTagsFor(entry: EntryStack?, value: SBItemStack?): Stream>? { + return Stream.empty() + } + + @OptIn(ExpensiveItemCacheApi::class) + override fun asFormattedText(entry: EntryStack, value: SBItemStack): Text { + val neuItem = entry.value.neuItem + return if (!RepoManager.Config.perfectRenders.rendersPerfectText() || entry.value.isWarm() || neuItem == null) { + VanillaEntryTypes.ITEM.definition.asFormattedText(entry.asItemEntry(), value.asImmutableItemStack()) + } else { + Text.literal(neuItem.displayName) + } + } + + override fun hash(entry: EntryStack, value: SBItemStack, context: ComparisonContext): Long { + // Repo items are immutable, and get replaced entirely when loaded from disk + return value.skyblockId.hashCode() * 31L + } + + override fun wildcard(entry: EntryStack?, value: SBItemStack): SBItemStack { + return value.copy( + stackSize = 1, petData = RepoManager.getPotentialStubPetData(value.skyblockId), + stars = 0, extraLore = listOf(), reforge = null + ) + } + + override fun normalize(entry: EntryStack?, value: SBItemStack): SBItemStack { + return wildcard(entry, value) + } + + override fun copy(entry: EntryStack?, value: SBItemStack): SBItemStack { + return value + } + + override fun isEmpty(entry: EntryStack?, value: SBItemStack): Boolean { + return value.getStackSize() == 0 + } + + override fun getIdentifier(entry: EntryStack?, value: SBItemStack): Identifier { + return value.skyblockId.identifier + } + + fun getEntry(sbItemStack: SBItemStack): EntryStack = + EntryStack.of(this, sbItemStack) + + fun getEntry(skyblockId: SkyblockId, count: Int = 1): EntryStack = + getEntry(SBItemStack(skyblockId, count)) + + fun getEntry(ingredient: NEUIngredient): EntryStack = + getEntry(SkyblockId(ingredient.itemId), count = ingredient.amount.toInt()) + + fun getPassthrough(item: ItemConvertible) = getEntry(SBItemStack.passthrough(ItemStack(item.asItem()))) + + fun getEntry(stack: ItemStack): EntryStack = + getEntry(SBItemStack(stack)) +} diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockCraftingRecipeDynamicGenerator.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockCraftingRecipeDynamicGenerator.kt new file mode 100644 index 0000000..900ebab --- /dev/null +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockCraftingRecipeDynamicGenerator.kt @@ -0,0 +1,56 @@ +package moe.nea.firmament.compat.rei + +import io.github.moulberry.repo.data.NEUForgeRecipe +import io.github.moulberry.repo.data.NEUKatUpgradeRecipe +import io.github.moulberry.repo.data.NEUMobDropRecipe +import io.github.moulberry.repo.data.NEUNpcShopRecipe +import io.github.moulberry.repo.data.NEURecipe +import java.util.Optional +import me.shedaniel.rei.api.client.registry.display.DynamicDisplayGenerator +import me.shedaniel.rei.api.client.view.ViewSearchBuilder +import me.shedaniel.rei.api.common.display.Display +import me.shedaniel.rei.api.common.entry.EntryStack +import moe.nea.firmament.compat.rei.recipes.SBKatRecipe +import moe.nea.firmament.compat.rei.recipes.SBMobDropRecipe +import moe.nea.firmament.compat.rei.recipes.SBShopRecipe +import moe.nea.firmament.repo.EssenceRecipeProvider +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.repo.SBItemStack + + +val SkyblockMobDropRecipeDynamicGenerator = + neuDisplayGenerator { SBMobDropRecipe(it) } +val SkyblockShopRecipeDynamicGenerator = + neuDisplayGenerator { SBShopRecipe(it) } +val SkyblockKatRecipeDynamicGenerator = + neuDisplayGenerator { SBKatRecipe(it) } + +inline fun neuDisplayGenerator(crossinline mapper: (T) -> D) = + neuDisplayGeneratorWithItem { _, it -> mapper(it) } + +inline fun neuDisplayGeneratorWithItem(crossinline mapper: (SBItemStack, T) -> D) = + neuDisplayGeneratorWithItem(T::class.java, mapper) +inline fun neuDisplayGeneratorWithItem( + filter: Class, + crossinline mapper: (SBItemStack, T) -> D) = + object : DynamicDisplayGenerator { + override fun getRecipeFor(entry: EntryStack<*>): Optional> { + if (entry.type != SBItemEntryDefinition.type) return Optional.empty() + val item = entry.castValue() + val recipes = RepoManager.getRecipesFor(item.skyblockId) + val craftingRecipes = recipes.filterIsInstance(filter) + return Optional.of(craftingRecipes.map { mapper(item, it) }) + } + + override fun generate(builder: ViewSearchBuilder): Optional> { + return Optional.empty() // TODO: allows searching without blocking getRecipeFor + } + + override fun getUsageFor(entry: EntryStack<*>): Optional> { + if (entry.type != SBItemEntryDefinition.type) return Optional.empty() + val item = entry.castValue() + val recipes = RepoManager.getUsagesFor(item.skyblockId) + val craftingRecipes = recipes.filterIsInstance(filter) + return Optional.of(craftingRecipes.map { mapper(item, it) }) + } + } diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockItemIdFocusedStackProvider.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockItemIdFocusedStackProvider.kt new file mode 100644 index 0000000..9ccfab4 --- /dev/null +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockItemIdFocusedStackProvider.kt @@ -0,0 +1,23 @@ + + +package moe.nea.firmament.compat.rei + +import dev.architectury.event.CompoundEventResult +import me.shedaniel.math.Point +import me.shedaniel.rei.api.client.registry.screen.FocusedStackProvider +import me.shedaniel.rei.api.common.entry.EntryStack +import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.gui.screen.ingame.HandledScreen +import moe.nea.firmament.mixins.accessor.AccessorHandledScreen + +object SkyblockItemIdFocusedStackProvider : FocusedStackProvider { + override fun provide(screen: Screen?, mouse: Point?): CompoundEventResult> { + if (screen !is HandledScreen<*>) return CompoundEventResult.pass() + screen as AccessorHandledScreen + val focusedSlot = screen.focusedSlot_Firmament ?: return CompoundEventResult.pass() + val item = focusedSlot.stack ?: return CompoundEventResult.pass() + return CompoundEventResult.interruptTrue(SBItemEntryDefinition.getEntry(item)) + } + + override fun getPriority(): Double = 1_000_000.0 +} diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/math.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/math.kt new file mode 100644 index 0000000..f4808c7 --- /dev/null +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/math.kt @@ -0,0 +1,8 @@ +package moe.nea.firmament.compat.rei + +import me.shedaniel.math.Point + +operator fun Point.plus(other: Point): Point = Point( + this.x + other.x, + this.y + other.y, +) diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/GenericREIRecipeCategory.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/GenericREIRecipeCategory.kt new file mode 100644 index 0000000..15cb818 --- /dev/null +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/GenericREIRecipeCategory.kt @@ -0,0 +1,67 @@ +package moe.nea.firmament.compat.rei.recipes + +import io.github.moulberry.repo.data.NEURecipe +import me.shedaniel.math.Rectangle +import me.shedaniel.rei.api.client.gui.Renderer +import me.shedaniel.rei.api.client.gui.widgets.Widget +import me.shedaniel.rei.api.client.gui.widgets.Widgets +import me.shedaniel.rei.api.client.registry.display.DisplayCategory +import me.shedaniel.rei.api.client.registry.display.DisplayRegistry +import me.shedaniel.rei.api.common.category.CategoryIdentifier +import me.shedaniel.rei.api.common.util.EntryStacks +import net.minecraft.text.Text +import moe.nea.firmament.compat.rei.REIRecipeLayouter +import moe.nea.firmament.compat.rei.neuDisplayGeneratorWithItem +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.repo.recipes.GenericRecipeRenderer + +class GenericREIRecipeCategory( + val renderer: GenericRecipeRenderer, +) : DisplayCategory> { + private val dynamicGenerator = + neuDisplayGeneratorWithItem, T>(renderer.typ) { item, recipe -> + GenericRecipe( + recipe, + item, + categoryIdentifier + ) + } + + private val categoryIdentifier = CategoryIdentifier.of>(renderer.identifier) + override fun getCategoryIdentifier(): CategoryIdentifier> { + return categoryIdentifier + } + + override fun getDisplayHeight(): Int { + return renderer.displayHeight + } + + override fun getTitle(): Text? { + return renderer.title + } + + override fun getIcon(): Renderer? { + return EntryStacks.of(renderer.icon) + } + + override fun setupDisplay(display: GenericRecipe, bounds: Rectangle): List { + val layouter = REIRecipeLayouter() + layouter.container.add(Widgets.createRecipeBase(bounds)) + renderer.render(display.neuRecipe, bounds, layouter, display.sourceItem) + return layouter.container + } + + fun registerDynamicGenerator(registry: DisplayRegistry) { + registry.registerDisplayGenerator(categoryIdentifier, dynamicGenerator) + } +} + +class GenericRecipe( + override val neuRecipe: T, + val sourceItem: SBItemStack?, + val id: CategoryIdentifier> +) : SBRecipe() { + override fun getCategoryIdentifier(): CategoryIdentifier<*>? { + return id + } +} diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBKatRecipe.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBKatRecipe.kt new file mode 100644 index 0000000..cce1465 --- /dev/null +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBKatRecipe.kt @@ -0,0 +1,222 @@ +package moe.nea.firmament.compat.rei.recipes + +import io.github.moulberry.repo.data.NEUKatUpgradeRecipe +import io.github.notenoughupdates.moulconfig.common.IMinecraft +import io.github.notenoughupdates.moulconfig.gui.GuiComponent +import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext +import io.github.notenoughupdates.moulconfig.gui.MouseEvent +import io.github.notenoughupdates.moulconfig.gui.component.SliderComponent +import io.github.notenoughupdates.moulconfig.observer.GetSetter +import io.github.notenoughupdates.moulconfig.observer.Property +import io.github.notenoughupdates.moulconfig.platform.ModernRenderContext +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import me.shedaniel.rei.api.client.gui.Renderer +import me.shedaniel.rei.api.client.gui.widgets.Widget +import me.shedaniel.rei.api.client.gui.widgets.WidgetWithBounds +import me.shedaniel.rei.api.client.gui.widgets.Widgets +import me.shedaniel.rei.api.client.registry.display.DisplayCategory +import me.shedaniel.rei.api.common.category.CategoryIdentifier +import me.shedaniel.rei.api.common.util.EntryStacks +import kotlin.time.Duration.Companion.seconds +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.gui.Element +import net.minecraft.item.Items +import net.minecraft.text.Text +import moe.nea.firmament.Firmament +import moe.nea.firmament.compat.rei.SBItemEntryDefinition +import moe.nea.firmament.repo.PetData +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.util.FirmFormatters +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SkyblockId + +class SBKatRecipe(override val neuRecipe: NEUKatUpgradeRecipe) : SBRecipe() { + override fun getCategoryIdentifier(): CategoryIdentifier<*> = Category.categoryIdentifier + + object Category : DisplayCategory { + override fun getCategoryIdentifier(): CategoryIdentifier = + CategoryIdentifier.of(Firmament.MOD_ID, "kat_recipe") + + override fun getTitle(): Text = Text.literal("Kat Pet Upgrade") + override fun getDisplayHeight(): Int { + return 100 + } + + override fun getIcon(): Renderer = SBItemEntryDefinition.getPassthrough(Items.BONE) + override fun setupDisplay(display: SBKatRecipe, bounds: Rectangle): List { + return buildList { + val arrowWidth = 24 + val recipe = display.neuRecipe + val levelValue = Property.upgrade(GetSetter.floating(0F)) + val slider = SliderComponent(levelValue, 1F, 100F, 1f, 100) + val outputStack = SBItemStack(SkyblockId(recipe.output.itemId)) + val inputStack = SBItemStack(SkyblockId(recipe.input.itemId)) + val inputLevelLabelCenter = Point(bounds.minX + 30 - 18 + 5 + 8, bounds.minY + 25) + val inputLevelLabel = Widgets.createLabel( + inputLevelLabelCenter, + Text.literal("")).centered() + val outputLevelLabelCenter = Point(bounds.maxX - 30 + 8, bounds.minY + 25) + val outputLevelLabel = Widgets.createLabel( + outputLevelLabelCenter, + Text.literal("")).centered() + val coinStack = SBItemStack(SkyblockId.COINS, recipe.coins.toInt()) + levelValue.whenChanged { oldValue, newValue -> + if (oldValue.toInt() == newValue.toInt()) return@whenChanged + val oldInput = inputStack.getPetData() ?: return@whenChanged + val newInput = PetData.forLevel(oldInput.petId, oldInput.rarity, newValue.toInt()) + inputStack.setPetData(newInput) + val oldOutput = outputStack.getPetData() ?: return@whenChanged + val newOutput = PetData(oldOutput.rarity, oldOutput.petId, newInput.exp) + outputStack.setPetData(newOutput) + inputLevelLabel.message = Text.literal(newInput.levelData.currentLevel.toString()) + inputLevelLabel.bounds.location = Point( + inputLevelLabelCenter.x - MC.font.getWidth(inputLevelLabel.message) / 2, + inputLevelLabelCenter.y) + outputLevelLabel.message = Text.literal(newOutput.levelData.currentLevel.toString()) + outputLevelLabel.bounds.location = Point( + outputLevelLabelCenter.x - MC.font.getWidth(outputLevelLabel.message) / 2, + outputLevelLabelCenter.y) + coinStack.setStackSize((recipe.coins * (1 - 0.3 * newValue / 100)).toInt()) + } + levelValue.set(1F) + add(Widgets.createRecipeBase(bounds)) + add(wrapWidget(Rectangle(bounds.centerX - slider.width / 2, + bounds.maxY - 30, + slider.width, + slider.height), + slider)) + add(Widgets.withTooltip( + Widgets.createArrow(Point(bounds.centerX - arrowWidth / 2, bounds.minY + 40)), + Text.literal("Upgrade time: " + FirmFormatters.formatTimespan(recipe.seconds.seconds)))) + + add(Widgets.createResultSlotBackground(Point(bounds.maxX - 30, bounds.minY + 40))) + add(inputLevelLabel) + add(outputLevelLabel) + add(Widgets.createSlot(Point(bounds.maxX - 30, bounds.minY + 40)).markOutput().disableBackground() + .entry(SBItemEntryDefinition.getEntry(outputStack))) + add(Widgets.createSlot(Point(bounds.minX + 30 - 18 + 5, bounds.minY + 40)).markInput() + .entry(SBItemEntryDefinition.getEntry(inputStack))) + + val allInputs = recipe.items.map { SBItemEntryDefinition.getEntry(it) } + + listOf(SBItemEntryDefinition.getEntry(coinStack)) + for ((index, item) in allInputs.withIndex()) { + add(Widgets.createSlot( + Point(bounds.centerX + index * 20 - allInputs.size * 18 / 2 - (allInputs.size - 1) * 2 / 2, + bounds.minY + 20)) + .markInput() + .entry(item)) + } + } + } + } +} + +fun wrapWidget(bounds: Rectangle, component: GuiComponent): Widget { + return object : WidgetWithBounds() { + override fun getBounds(): Rectangle { + return bounds + } + + override fun children(): List { + return listOf() + } + + override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + context.matrices.push() + context.matrices.translate(bounds.minX.toFloat(), bounds.minY.toFloat(), 0F) + component.render( + GuiImmediateContext( + ModernRenderContext(context), + bounds.minX, bounds.minY, + bounds.width, bounds.height, + mouseX - bounds.minX, mouseY - bounds.minY, + mouseX, mouseY, + mouseX.toFloat(), mouseY.toFloat() + )) + context.matrices.pop() + } + + override fun mouseMoved(mouseX: Double, mouseY: Double) { + val mouseXInt = mouseX.toInt() + val mouseYInt = mouseY.toInt() + component.mouseEvent(MouseEvent.Move(0F, 0F), + GuiImmediateContext( + IMinecraft.instance.provideTopLevelRenderContext(), + bounds.minX, bounds.minY, + bounds.width, bounds.height, + mouseXInt - bounds.minX, mouseYInt - bounds.minY, + mouseXInt, mouseYInt, + mouseX.toFloat(), mouseY.toFloat() + )) + } + + override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { + val mouseXInt = mouseX.toInt() + val mouseYInt = mouseY.toInt() + return component.mouseEvent(MouseEvent.Click(button, true), + GuiImmediateContext( + IMinecraft.instance.provideTopLevelRenderContext(), + bounds.minX, bounds.minY, + bounds.width, bounds.height, + mouseXInt - bounds.minX, mouseYInt - bounds.minY, + mouseXInt, mouseYInt, + mouseX.toFloat(), mouseY.toFloat() + )) + } + + override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { + val mouseXInt = mouseX.toInt() + val mouseYInt = mouseY.toInt() + return component.mouseEvent(MouseEvent.Click(button, false), + GuiImmediateContext( + IMinecraft.instance.provideTopLevelRenderContext(), + bounds.minX, bounds.minY, + bounds.width, bounds.height, + mouseXInt - bounds.minX, mouseYInt - bounds.minY, + mouseXInt, mouseYInt, + mouseX.toFloat(), mouseY.toFloat() + )) + } + + override fun mouseDragged( + mouseX: Double, + mouseY: Double, + button: Int, + deltaX: Double, + deltaY: Double + ): Boolean { + val mouseXInt = mouseX.toInt() + val mouseYInt = mouseY.toInt() + return component.mouseEvent(MouseEvent.Move(deltaX.toFloat(), deltaY.toFloat()), + GuiImmediateContext( + IMinecraft.instance.provideTopLevelRenderContext(), + bounds.minX, bounds.minY, + bounds.width, bounds.height, + mouseXInt - bounds.minX, mouseYInt - bounds.minY, + mouseXInt, mouseYInt, + mouseX.toFloat(), mouseY.toFloat() + )) + + } + + override fun mouseScrolled( + mouseX: Double, + mouseY: Double, + horizontalAmount: Double, + verticalAmount: Double + ): Boolean { + val mouseXInt = mouseX.toInt() + val mouseYInt = mouseY.toInt() + return component.mouseEvent(MouseEvent.Scroll(verticalAmount.toFloat()), + GuiImmediateContext( + IMinecraft.instance.provideTopLevelRenderContext(), + bounds.minX, bounds.minY, + bounds.width, bounds.height, + mouseXInt - bounds.minX, mouseYInt - bounds.minY, + mouseXInt, mouseYInt, + mouseX.toFloat(), mouseY.toFloat() + )) + } + } +} diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBMobDropRecipe.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBMobDropRecipe.kt new file mode 100644 index 0000000..b595c23 --- /dev/null +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBMobDropRecipe.kt @@ -0,0 +1,105 @@ +package moe.nea.firmament.compat.rei.recipes + +import io.github.moulberry.repo.data.NEUMobDropRecipe +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import me.shedaniel.rei.api.client.gui.Renderer +import me.shedaniel.rei.api.client.gui.widgets.Widget +import me.shedaniel.rei.api.client.gui.widgets.Widgets +import me.shedaniel.rei.api.client.registry.display.DisplayCategory +import me.shedaniel.rei.api.common.category.CategoryIdentifier +import me.shedaniel.rei.api.common.util.EntryStacks +import net.minecraft.item.Items +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import moe.nea.firmament.Firmament +import moe.nea.firmament.compat.rei.EntityWidget +import moe.nea.firmament.compat.rei.SBItemEntryDefinition +import moe.nea.firmament.gui.entity.EntityRenderer + +class SBMobDropRecipe(override val neuRecipe: NEUMobDropRecipe) : SBRecipe() { + override fun getCategoryIdentifier(): CategoryIdentifier<*> = Category.categoryIdentifier + + object Category : DisplayCategory { + override fun getCategoryIdentifier(): CategoryIdentifier = + CategoryIdentifier.of(Firmament.MOD_ID, "mob_drop_recipe") + + override fun getTitle(): Text = Text.literal("Mob Drops") + override fun getDisplayHeight(): Int { + return 100 + } + + override fun getIcon(): Renderer = SBItemEntryDefinition.getPassthrough(Items.DIAMOND_SWORD) + override fun setupDisplay(display: SBMobDropRecipe, bounds: Rectangle): List { + return buildList { + add(Widgets.createRecipeBase(bounds)) + val source = display.neuRecipe.render + val entity = if (source.startsWith("@")) { + EntityRenderer.constructEntity(Identifier.of(source.substring(1))) + } else { + EntityRenderer.applyModifiers(source, listOf()) + } + val level = display.neuRecipe.level + val fullMobName = + if (level > 0) Text.translatable("firmament.recipe.mobs.name", level, display.neuRecipe.name) + else Text.translatable("firmament.recipe.mobs.name.nolevel", display.neuRecipe.name) + val tt = mutableListOf() + tt.add((fullMobName)) + tt.add(Text.literal("")) + if (display.neuRecipe.coins > 0) { + tt.add(Text.stringifiedTranslatable("firmament.recipe.mobs.coins", display.neuRecipe.coins)) + } + if (display.neuRecipe.combatExperience > 0) { + tt.add( + Text.stringifiedTranslatable( + "firmament.recipe.mobs.combat", + display.neuRecipe.combatExperience + ) + ) + } + if (display.neuRecipe.enchantingExperience > 0) { + tt.add( + Text.stringifiedTranslatable( + "firmament.recipe.mobs.exp", + display.neuRecipe.enchantingExperience + ) + ) + } + if (display.neuRecipe.extra != null) + display.neuRecipe.extra.mapTo(tt) { Text.literal(it) } + if (tt.size == 2) + tt.removeAt(1) + add( + Widgets.withTooltip( + EntityWidget(entity, Point(bounds.minX + 5, bounds.minY + 15)), + tt + ) + ) + add( + Widgets.createLabel(Point(bounds.minX + 15, bounds.minY + 5), Text.literal(display.neuRecipe.name)) + .leftAligned() + ) + var x = bounds.minX + 60 + var y = bounds.minY + 20 + for (drop in display.neuRecipe.drops) { + val lore = drop.extra.mapTo(mutableListOf()) { Text.literal(it) } + if (drop.chance != null) { + lore += listOf(Text.translatable("firmament.recipe.mobs.drops", drop.chance)) + } + val item = SBItemEntryDefinition.getEntry(drop.dropItem) + .value.copy(extraLore = lore) + add( + Widgets.createSlot(Point(x, y)).markOutput() + .entries(listOf(SBItemEntryDefinition.getEntry(item))) + ) + x += 18 + if (x > bounds.maxX - 30) { + x = bounds.minX + 60 + y += 18 + } + } + } + } + } + +} diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBRecipe.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBRecipe.kt new file mode 100644 index 0000000..de7779f --- /dev/null +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBRecipe.kt @@ -0,0 +1,42 @@ +package moe.nea.firmament.compat.rei.recipes + +import io.github.moulberry.repo.data.NEUIngredient +import io.github.moulberry.repo.data.NEURecipe +import java.util.Optional +import me.shedaniel.rei.api.common.display.Display +import me.shedaniel.rei.api.common.display.DisplaySerializer +import me.shedaniel.rei.api.common.entry.EntryIngredient +import net.minecraft.util.Identifier +import moe.nea.firmament.compat.rei.SBItemEntryDefinition +import moe.nea.firmament.util.SkyblockId + +abstract class SBRecipe : Display { + override fun getDisplayLocation(): Optional { + // In theory, we could return a location for the neuRecipe here. (Something along the lines of neurepo:items/item_id.json/0 for the 0th recipe in the items/item_id.json recipes array). + return Optional.empty() + } + + override fun getSerializer(): DisplaySerializer? { + // While returning null here is discouraged, we are fine to do so, since this recipe will never travel through the network + return null + } + + abstract val neuRecipe: NEURecipe + override fun getInputEntries(): List { + return neuRecipe.allInputs + .filter { it.itemId != NEUIngredient.NEU_SENTINEL_EMPTY } + .map { + val entryStack = SBItemEntryDefinition.getEntry(SkyblockId(it.itemId)) + EntryIngredient.of(entryStack) + } + } + + override fun getOutputEntries(): List { + return neuRecipe.allOutputs + .filter { it.itemId != NEUIngredient.NEU_SENTINEL_EMPTY } + .map { + val entryStack = SBItemEntryDefinition.getEntry(SkyblockId(it.itemId)) + EntryIngredient.of(entryStack) + } + } +} diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt new file mode 100644 index 0000000..fca3edf --- /dev/null +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt @@ -0,0 +1,217 @@ +@file:OptIn(ExpensiveItemCacheApi::class) + +package moe.nea.firmament.compat.rei.recipes + +import java.util.Optional +import me.shedaniel.math.Dimension +import me.shedaniel.math.FloatingDimension +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import me.shedaniel.rei.api.client.gui.Renderer +import me.shedaniel.rei.api.client.gui.widgets.Label +import me.shedaniel.rei.api.client.gui.widgets.Widget +import me.shedaniel.rei.api.client.gui.widgets.Widgets +import me.shedaniel.rei.api.client.registry.display.DisplayCategory +import me.shedaniel.rei.api.client.registry.display.DynamicDisplayGenerator +import me.shedaniel.rei.api.client.view.ViewSearchBuilder +import me.shedaniel.rei.api.common.category.CategoryIdentifier +import me.shedaniel.rei.api.common.display.Display +import me.shedaniel.rei.api.common.display.DisplaySerializer +import me.shedaniel.rei.api.common.entry.EntryIngredient +import me.shedaniel.rei.api.common.entry.EntryStack +import net.minecraft.entity.EntityType +import net.minecraft.entity.SpawnReason +import net.minecraft.registry.entry.RegistryEntry +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import net.minecraft.village.VillagerProfession +import moe.nea.firmament.Firmament +import moe.nea.firmament.compat.rei.EntityWidget +import moe.nea.firmament.compat.rei.SBItemEntryDefinition +import moe.nea.firmament.gui.entity.EntityRenderer +import moe.nea.firmament.repo.ExpensiveItemCacheApi +import moe.nea.firmament.repo.Reforge +import moe.nea.firmament.repo.ReforgeStore +import moe.nea.firmament.repo.RepoItemTypeCache +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.util.AprilFoolsUtil +import moe.nea.firmament.util.FirmFormatters +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.gold +import moe.nea.firmament.util.grey +import moe.nea.firmament.util.skyblock.ItemType +import moe.nea.firmament.util.skyblock.Rarity +import moe.nea.firmament.util.skyblock.SkyBlockItems +import moe.nea.firmament.util.skyblockId +import moe.nea.firmament.util.tr + +class SBReforgeRecipe( + val reforge: Reforge, + val limitToItem: SBItemStack?, +) : Display { + companion object { + val catIdentifier = CategoryIdentifier.of(Firmament.MOD_ID, "reforge_recipe") + } + + object Category : DisplayCategory { + override fun getCategoryIdentifier(): CategoryIdentifier { + return catIdentifier + } + + override fun getTitle(): Text { + return tr("firmament.recipecategory.reforge", "Reforge") + } + + override fun getIcon(): Renderer { + return SBItemEntryDefinition.getEntry(SkyBlockItems.REFORGE_ANVIL) + } + + override fun setupDisplay(display: SBReforgeRecipe, bounds: Rectangle): MutableList { + val list = mutableListOf() + list.add(Widgets.createRecipeBase(bounds)) + val inputSlot = Widgets.createSlot(Point(bounds.minX + 10, bounds.centerY - 9)) + .markInput().entries(display.inputItems) + list.add(inputSlot) + if (display.reforgeStone != null) { + list.add(Widgets.createSlot(Point(bounds.minX + 10 + 24, bounds.centerY - 9 - 10)) + .markInput().entry(display.reforgeStone)) + list.add(Widgets.withTooltip( + Widgets.withTranslate(Widgets.wrapRenderer( + Rectangle(Point(bounds.minX + 10 + 24, bounds.centerY - 9 + 10), Dimension(16, 16)), + SBItemEntryDefinition.getEntry(SkyBlockItems.REFORGE_ANVIL)), 0.0, 0.0, 150.0), + Rarity.entries.mapNotNull { rarity -> + display.reforge.reforgeCosts?.get(rarity)?.let { rarity to it } + }.map { (rarity, cost) -> + Text.literal("") + .append(rarity.text) + .append(": ") + .append(Text.literal("${FirmFormatters.formatCommas(cost, 0)} Coins").gold()) + } + )) + } else { + val size = if (AprilFoolsUtil.isAprilFoolsDay) 1.2 else 0.6 + val dimension = + FloatingDimension(EntityWidget.defaultSize.width * size, EntityWidget.defaultSize.height * size) + list.add(Widgets.withTooltip( + EntityWidget( + EntityType.VILLAGER.create(EntityRenderer.fakeWorld, SpawnReason.COMMAND) + ?.also { it.villagerData = it.villagerData.withProfession(MC.currentOrDefaultRegistries.getEntryOrThrow(VillagerProfession.WEAPONSMITH)) }, + Point(bounds.minX + 10 + 24 + 8 - dimension.width / 2, bounds.centerY - dimension.height / 2), + dimension + ), + tr("firmament.recipecategory.reforge.basic", + "This is a basic reforge, available at the Blacksmith.").grey() + )) + } + list.add(Widgets.createSlot(Point(bounds.minX + 10 + 24 + 24, bounds.centerY - 9)) + .markInput().entries(display.outputItems)) + val statToLineMappings = mutableListOf>() + for ((i, statId) in display.reforge.statUniverse.withIndex()) { + val label = Widgets.createLabel( + Point(bounds.minX + 10 + 24 + 24 + 20, bounds.minY + 8 + i * 11), + SBItemStack.Companion.StatLine(SBItemStack.statIdToName(statId), null).reconstitute(7)) + .horizontalAlignment(Label.LEFT_ALIGNED) + statToLineMappings.add(statId to label) + list.add(label) + } + fun updateStatLines() { + val entry = inputSlot.currentEntry?.castValue() ?: return + val stats = display.reforge.reforgeStats?.get(entry.rarity) ?: mapOf() + for ((stat, label) in statToLineMappings) { + label.message = + SBItemStack.Companion.StatLine( + SBItemStack.statIdToName(stat), null, + valueNum = stats[stat] + ).reconstitute(7) + } + } + updateStatLines() + inputSlot.withEntriesListener { updateStatLines() } + return list + } + } + + object DynamicGenerator : DynamicDisplayGenerator { + fun getRecipesForSBItemStack(item: SBItemStack): Optional> { + val reforgeRecipes = mutableListOf() + for (reforge in ReforgeStore.findEligibleForInternalName(item.skyblockId)) { + reforgeRecipes.add(SBReforgeRecipe(reforge, item)) + } + for (reforge in ReforgeStore.findEligibleForItem(item.itemType ?: ItemType.NIL)) { + reforgeRecipes.add(SBReforgeRecipe(reforge, item)) + } + if (reforgeRecipes.isEmpty()) return Optional.empty() + return Optional.of(reforgeRecipes) + } + + override fun getRecipeFor(entry: EntryStack<*>): Optional> { + if (entry.type != SBItemEntryDefinition.type) return Optional.empty() + val item = entry.castValue() + return getRecipesForSBItemStack(item) + } + + override fun getUsageFor(entry: EntryStack<*>): Optional> { + if (entry.type != SBItemEntryDefinition.type) return Optional.empty() + val item = entry.castValue() + ReforgeStore.byReforgeStone[item.skyblockId]?.let { stoneReforge -> + return Optional.of(listOf(SBReforgeRecipe(stoneReforge, null))) + } + return getRecipesForSBItemStack(item) + } + + override fun generate(builder: ViewSearchBuilder): Optional> { + // TODO: check builder.recipesFor and such and optionally return all reforge recipes + return Optional.empty() + } + } + + private val inputItems = run { + if (limitToItem != null) return@run listOf(SBItemEntryDefinition.getEntry(limitToItem)) + val eligibleItems = reforge.eligibleItems.flatMap { + when (it) { + is Reforge.ReforgeEligibilityFilter.AllowsInternalName -> + listOfNotNull(RepoManager.getNEUItem(it.internalName)) + + is Reforge.ReforgeEligibilityFilter.AllowsItemType -> + ReforgeStore.resolveItemType(it.itemType) + .flatMapTo(mutableSetOf()) { + (RepoItemTypeCache.byItemType[it] ?: listOf()) + + (RepoItemTypeCache.byItemType[it.dungeonVariant] ?: listOf()) + }.toList() + + is Reforge.ReforgeEligibilityFilter.AllowsVanillaItemType -> { + listOf() // TODO: add filter support for this and potentially rework this to search for the declared item type in repo, instead of remapped item type + } + } + } + eligibleItems.map { SBItemEntryDefinition.getEntry(it.skyblockId) } + } + private val outputItems = + inputItems.map { SBItemEntryDefinition.getEntry(it.value.copy(reforge = reforge.reforgeId)) } + private val reforgeStone = reforge.reforgeStone?.let(SBItemEntryDefinition::getEntry) + private val inputEntries = + listOf(EntryIngredient.of(inputItems)) + listOfNotNull(reforgeStone?.let(EntryIngredient::of)) + private val outputEntries = listOf(EntryIngredient.of(outputItems)) + + override fun getInputEntries(): List { + return inputEntries + } + + override fun getOutputEntries(): List { + return outputEntries + } + + override fun getCategoryIdentifier(): CategoryIdentifier<*> { + return catIdentifier + } + + override fun getDisplayLocation(): Optional { + return Optional.empty() + } + + override fun getSerializer(): DisplaySerializer? { + return null + } +} diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBShopRecipe.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBShopRecipe.kt new file mode 100644 index 0000000..a252802 --- /dev/null +++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBShopRecipe.kt @@ -0,0 +1,61 @@ +package moe.nea.firmament.compat.rei.recipes + +import io.github.moulberry.repo.data.NEUNpcShopRecipe +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import me.shedaniel.rei.api.client.gui.Renderer +import me.shedaniel.rei.api.client.gui.widgets.Widget +import me.shedaniel.rei.api.client.gui.widgets.Widgets +import me.shedaniel.rei.api.client.registry.display.DisplayCategory +import me.shedaniel.rei.api.common.category.CategoryIdentifier +import me.shedaniel.rei.api.common.entry.EntryIngredient +import net.minecraft.item.Items +import net.minecraft.text.Text +import moe.nea.firmament.Firmament +import moe.nea.firmament.compat.rei.SBItemEntryDefinition +import moe.nea.firmament.util.skyblockId + +class SBShopRecipe(override val neuRecipe: NEUNpcShopRecipe) : SBRecipe() { + override fun getCategoryIdentifier(): CategoryIdentifier<*> = Category.catIdentifier + val merchant = SBItemEntryDefinition.getEntry(neuRecipe.isSoldBy.skyblockId) + override fun getInputEntries(): List { + return listOf(EntryIngredient.of(merchant)) + super.getInputEntries() + } + + object Category : DisplayCategory { + val catIdentifier = CategoryIdentifier.of(Firmament.MOD_ID, "npc_shopping") + override fun getCategoryIdentifier(): CategoryIdentifier = catIdentifier + + override fun getTitle(): Text = Text.literal("SkyBlock NPC Shopping") + + override fun getIcon(): Renderer = SBItemEntryDefinition.getPassthrough(Items.EMERALD) + override fun setupDisplay(display: SBShopRecipe, bounds: Rectangle): List { + val point = Point(bounds.centerX, bounds.centerY) + return buildList { + add(Widgets.createRecipeBase(bounds)) + add(Widgets.createSlot(Point(point.x - 2 - 18 / 2, point.y - 18 - 6)) + .unmarkInputOrOutput() + .entry(display.merchant) + .disableBackground()) + add(Widgets.createArrow(Point(point.x - 2 - 24 / 2, point.y - 6))) + val cost = display.neuRecipe.cost + for ((i, item) in cost.withIndex()) { + add(Widgets.createSlot(Point( + point.x - 14 - 18, + point.y + i * 18 - 18 * cost.size / 2)) + .entry(SBItemEntryDefinition.getEntry(item)) + .markInput()) + // TODO: fix frame clipping + } + add(Widgets.createResultSlotBackground(Point(point.x + 18, point.y - 18 / 2))) + add( + Widgets.createSlot(Point(point.x + 18, point.y - 18 / 2)) + .entry(SBItemEntryDefinition.getEntry(display.neuRecipe.result)) + .disableBackground().markOutput() + ) + } + } + + } + +} diff --git a/src/compat/rei/java/moe/nea/firmament/mixins/compat/HideREIRecipeWarning.java b/src/compat/rei/java/moe/nea/firmament/mixins/compat/HideREIRecipeWarning.java new file mode 100644 index 0000000..14eeaf2 --- /dev/null +++ b/src/compat/rei/java/moe/nea/firmament/mixins/compat/HideREIRecipeWarning.java @@ -0,0 +1,20 @@ +package moe.nea.firmament.mixins.compat; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(targets = "me.shedaniel.rei.impl.client.gui.hints.ImportantWarningsWidget") +@Pseudo +public class HideREIRecipeWarning { + @Shadow + private boolean visible; + + @Inject(method = "", at = @At("TAIL")) + private void onCreateImportantWidget(CallbackInfo ci) { + visible = false; + } +} diff --git a/src/compat/sodium/java/SodiumChunkReloader.kt b/src/compat/sodium/java/SodiumChunkReloader.kt new file mode 100644 index 0000000..0256b88 --- /dev/null +++ b/src/compat/sodium/java/SodiumChunkReloader.kt @@ -0,0 +1,12 @@ +package moe.nea.firmament.compat.sodium + +import net.caffeinemc.mods.sodium.client.render.SodiumWorldRenderer +import moe.nea.firmament.mixins.accessor.sodium.AccessorSodiumWorldRenderer + +class SodiumChunkReloader : Runnable { + override fun run() { + (SodiumWorldRenderer.instanceNullable() as? AccessorSodiumWorldRenderer) + ?.renderSectionManager_firmament + ?.markGraphDirty() + } +} diff --git a/src/compat/sodium/java/moe/nea/firmament/mixins/accessor/sodium/AccessorSodiumWorldRenderer.java b/src/compat/sodium/java/moe/nea/firmament/mixins/accessor/sodium/AccessorSodiumWorldRenderer.java new file mode 100644 index 0000000..f75874d --- /dev/null +++ b/src/compat/sodium/java/moe/nea/firmament/mixins/accessor/sodium/AccessorSodiumWorldRenderer.java @@ -0,0 +1,14 @@ +package moe.nea.firmament.mixins.accessor.sodium; + +import net.caffeinemc.mods.sodium.client.render.SodiumWorldRenderer; +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSectionManager; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(SodiumWorldRenderer.class) +@Pseudo +public interface AccessorSodiumWorldRenderer { + @Accessor(value = "renderSectionManager", remap = false) + RenderSectionManager getRenderSectionManager_firmament(); +} diff --git a/src/compat/sodium/java/moe/nea/firmament/mixins/custommodels/PatchBlockModelInSodiumChunkGenerator.java b/src/compat/sodium/java/moe/nea/firmament/mixins/custommodels/PatchBlockModelInSodiumChunkGenerator.java new file mode 100644 index 0000000..fe87310 --- /dev/null +++ b/src/compat/sodium/java/moe/nea/firmament/mixins/custommodels/PatchBlockModelInSodiumChunkGenerator.java @@ -0,0 +1,29 @@ +package moe.nea.firmament.mixins.custommodels; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import moe.nea.firmament.features.texturepack.CustomBlockTextures; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderMeshingTask; +import net.minecraft.block.BlockState; +import net.minecraft.client.render.block.BlockModels; +import net.minecraft.client.render.model.BakedModel; +import net.minecraft.util.math.BlockPos; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(ChunkBuilderMeshingTask.class) +public class PatchBlockModelInSodiumChunkGenerator { + @WrapOperation( + method = "execute(Lnet/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildContext;Lnet/caffeinemc/mods/sodium/client/util/task/CancellationToken;)Lnet/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildOutput;", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/block/BlockModels;getModel(Lnet/minecraft/block/BlockState;)Lnet/minecraft/client/render/model/BakedModel;")) + private BakedModel replaceBlockModel(BlockModels instance, BlockState state, Operation original, + @Local(name = "blockPos") BlockPos.Mutable pos) { + var replacement = CustomBlockTextures.getReplacementModel(state, pos); + if (replacement != null) return replacement; + CustomBlockTextures.enterFallbackCall(); + var fallback = original.call(instance, state); + CustomBlockTextures.exitFallbackCall(); + return fallback; + } +} diff --git a/src/compat/wildfireGender/java/moe/nea/firmament/compat/gender/Compat.kt b/src/compat/wildfireGender/java/moe/nea/firmament/compat/gender/Compat.kt new file mode 100644 index 0000000..347dd5d --- /dev/null +++ b/src/compat/wildfireGender/java/moe/nea/firmament/compat/gender/Compat.kt @@ -0,0 +1,13 @@ +package moe.nea.firmament.compat.gender + +import net.fabricmc.loader.api.FabricLoader +import moe.nea.firmament.util.compatloader.CompatMeta +import moe.nea.firmament.util.compatloader.ICompatMeta + +@CompatMeta +object Compat : ICompatMeta { + override fun shouldLoad(): Boolean { + return FabricLoader.getInstance().isModLoaded("wildfire_gender") + } + +} diff --git a/src/compat/wildfireGender/java/moe/nea/firmament/mixins/compat/wildfiregender/PatchArmorTexturesInGenderMod.java b/src/compat/wildfireGender/java/moe/nea/firmament/mixins/compat/wildfiregender/PatchArmorTexturesInGenderMod.java new file mode 100644 index 0000000..c3e8950 --- /dev/null +++ b/src/compat/wildfireGender/java/moe/nea/firmament/mixins/compat/wildfiregender/PatchArmorTexturesInGenderMod.java @@ -0,0 +1,23 @@ +package moe.nea.firmament.mixins.compat.wildfiregender; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.sugar.Local; +import com.wildfire.render.GenderArmorLayer; +import moe.nea.firmament.features.texturepack.CustomGlobalArmorOverrides; +import net.minecraft.component.type.EquippableComponent; +import net.minecraft.entity.EquipmentSlot; +import net.minecraft.item.ItemStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(GenderArmorLayer.class) +@Pseudo +public class PatchArmorTexturesInGenderMod { + @ModifyExpressionValue(method = "render(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/client/render/entity/state/BipedEntityRenderState;FF)V", + at = @At(value = "INVOKE", target = "Lnet/minecraft/item/ItemStack;get(Lnet/minecraft/component/ComponentType;)Ljava/lang/Object;")) + private Object replaceArmorMaterial(Object original, @Local ItemStack chestplate) { + var overrides = CustomGlobalArmorOverrides.overrideArmor(chestplate, EquipmentSlot.CHEST); + return overrides.orElse((EquippableComponent) original); + } +} diff --git a/src/compat/yacl/java/KeybindingBuilder.kt b/src/compat/yacl/java/KeybindingBuilder.kt new file mode 100644 index 0000000..322ddff --- /dev/null +++ b/src/compat/yacl/java/KeybindingBuilder.kt @@ -0,0 +1,16 @@ +package moe.nea.firmament.compat.yacl + +import dev.isxander.yacl3.api.Controller +import dev.isxander.yacl3.api.Option +import dev.isxander.yacl3.api.controller.ControllerBuilder +import moe.nea.firmament.gui.config.ManagedOption +import moe.nea.firmament.keybindings.SavedKeyBinding + +class KeybindingBuilder( + val option: Option, + val managedOption: ManagedOption +) : ControllerBuilder { + override fun build(): Controller { + return KeybindingController(option, managedOption) + } +} diff --git a/src/compat/yacl/java/KeybindingController.kt b/src/compat/yacl/java/KeybindingController.kt new file mode 100644 index 0000000..204d521 --- /dev/null +++ b/src/compat/yacl/java/KeybindingController.kt @@ -0,0 +1,83 @@ +package moe.nea.firmament.compat.yacl + +import dev.isxander.yacl3.api.Controller +import dev.isxander.yacl3.api.Option +import dev.isxander.yacl3.api.utils.Dimension +import dev.isxander.yacl3.gui.AbstractWidget +import dev.isxander.yacl3.gui.YACLScreen +import dev.isxander.yacl3.gui.controllers.ControllerWidget +import net.minecraft.text.Text +import moe.nea.firmament.gui.config.KeyBindingHandler +import moe.nea.firmament.gui.config.KeyBindingStateManager +import moe.nea.firmament.gui.config.ManagedOption +import moe.nea.firmament.keybindings.SavedKeyBinding + +class KeybindingController( + val option: Option, + val managedOption: ManagedOption, +) : Controller { + val handler = managedOption.handler as KeyBindingHandler + override fun option(): Option { + return option + } + + override fun formatValue(): Text { + return option.pendingValue().format() + } + + override fun provideWidget(screen: YACLScreen, widgetDimension: Dimension): AbstractWidget { + lateinit var button: ControllerWidget + val sm = KeyBindingStateManager( + { option.pendingValue() }, + { option.requestSet(it) }, + { screen.focused = null }, + { screen.focused = button }, + ) + button = KeybindingWidget(sm, this, screen, widgetDimension) + option.addListener { t, u -> + sm.updateLabel() + } + sm.updateLabel() + return button + } +} + +class KeybindingWidget( + val sm: KeyBindingStateManager, + controller: KeybindingController, + screen: YACLScreen, + dimension: Dimension +) : ControllerWidget(controller, screen, dimension) { + override fun getHoveredControlWidth(): Int { + return 130 + } + + override fun getValueText(): Text { + return sm.label + } + + override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + return sm.keyboardEvent(keyCode, true) + } + + override fun keyReleased(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + return sm.keyboardEvent(keyCode, false) + } + + override fun unfocus() { + sm.onLostFocus() + } + + override fun setFocused(focused: Boolean) { + super.setFocused(focused) + if (!focused) sm.onLostFocus() + } + + override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { + if (button == 0 && isHovered) { + sm.onClick() + return true + } + return super.mouseClicked(mouseX, mouseY, button) + } +} diff --git a/src/compat/yacl/java/YaclIntegration.kt b/src/compat/yacl/java/YaclIntegration.kt new file mode 100644 index 0000000..285d60c --- /dev/null +++ b/src/compat/yacl/java/YaclIntegration.kt @@ -0,0 +1,227 @@ +package moe.nea.firmament.compat.yacl + +import com.google.auto.service.AutoService +import dev.isxander.yacl3.api.Binding +import dev.isxander.yacl3.api.ButtonOption +import dev.isxander.yacl3.api.ConfigCategory +import dev.isxander.yacl3.api.LabelOption +import dev.isxander.yacl3.api.Option +import dev.isxander.yacl3.api.OptionDescription +import dev.isxander.yacl3.api.OptionGroup +import dev.isxander.yacl3.api.YetAnotherConfigLib +import dev.isxander.yacl3.api.controller.ColorControllerBuilder +import dev.isxander.yacl3.api.controller.ControllerBuilder +import dev.isxander.yacl3.api.controller.DoubleSliderControllerBuilder +import dev.isxander.yacl3.api.controller.EnumControllerBuilder +import dev.isxander.yacl3.api.controller.IntegerSliderControllerBuilder +import dev.isxander.yacl3.api.controller.StringControllerBuilder +import dev.isxander.yacl3.api.controller.TickBoxControllerBuilder +import dev.isxander.yacl3.api.controller.ValueFormatter +import dev.isxander.yacl3.gui.YACLScreen +import dev.isxander.yacl3.gui.tab.ListHolderWidget +import io.github.notenoughupdates.moulconfig.ChromaColour +import java.awt.Color +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import kotlin.time.DurationUnit +import net.minecraft.client.gui.Element +import net.minecraft.client.gui.screen.Screen +import net.minecraft.text.Text +import moe.nea.firmament.gui.config.BooleanHandler +import moe.nea.firmament.gui.config.ChoiceHandler +import moe.nea.firmament.gui.config.ClickHandler +import moe.nea.firmament.gui.config.ColourHandler +import moe.nea.firmament.gui.config.DurationHandler +import moe.nea.firmament.gui.config.EnumRenderer +import moe.nea.firmament.gui.config.FirmamentConfigScreenProvider +import moe.nea.firmament.gui.config.HudMeta +import moe.nea.firmament.gui.config.HudMetaHandler +import moe.nea.firmament.gui.config.IntegerHandler +import moe.nea.firmament.gui.config.KeyBindingHandler +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.gui.config.ManagedOption +import moe.nea.firmament.gui.config.StringHandler +import moe.nea.firmament.keybindings.SavedKeyBinding +import moe.nea.firmament.util.FirmFormatters +import moe.nea.firmament.util.getRGBAWithoutAnimation +import moe.nea.firmament.util.toChromaWithoutAnimation + + +@AutoService(FirmamentConfigScreenProvider::class) +class YaclIntegration : FirmamentConfigScreenProvider { + fun buildCategories() = + ManagedConfig.Category.entries + .map(::buildCategory) + + private fun buildCategory(category: ManagedConfig.Category): ConfigCategory { + return ConfigCategory.createBuilder() + .name(category.labelText) + .also { categoryB -> + category.configs.forEach { + categoryB.group( + OptionGroup.createBuilder() + .name(it.labelText) + .options(buildOptions(it.sortedOptions)) + .build() + ) + } + } + .build() + } + + fun buildOptions(options: List>): Collection> = + options.flatMap { buildOption(it) } + + private fun buildOption(managedOption: ManagedOption): Collection> { + val handler = managedOption.handler + val binding = Binding.generic( + managedOption.default(), + managedOption::value, + { managedOption.value = it; managedOption.element.save() }) + + fun createDefaultBinding(function: (Option) -> ControllerBuilder): Option.Builder { + return Option.createBuilder() + .name(managedOption.labelText) + .description(OptionDescription.of(managedOption.labelDescription)) + .binding(binding as Binding) + .controller { function(it) } + } + + fun Option.single() = listOf(this) + fun ButtonOption.Builder.single() = build().single() + fun Option.Builder.single() = build().single() + when (handler) { + is ClickHandler -> return ButtonOption.createBuilder() + .name(managedOption.labelText) + .action { t, u -> + handler.runnable() + } + .single() + + is HudMetaHandler -> return ButtonOption.createBuilder() + .name(managedOption.labelText) + .action { t, u -> + handler.openEditor(managedOption as ManagedOption, t) + } + .single() + + is ChoiceHandler<*> -> return createDefaultBinding { + createChoiceBinding(handler as ChoiceHandler<*>, managedOption as ManagedOption<*>, it as Option<*>) + }.single() + + is ColourHandler -> { + managedOption as ManagedOption + val colorBinding = + Binding.generic( + managedOption.default().getRGBAWithoutAnimation(), + { managedOption.value.getRGBAWithoutAnimation() }, + { + managedOption.value = + it.toChromaWithoutAnimation(managedOption.value.timeForFullRotationInMillis) + managedOption.element.save() + }) + val speedBinding = + Binding.generic( + managedOption.default().timeForFullRotationInMillis, + { managedOption.value.timeForFullRotationInMillis }, + { + managedOption.value = managedOption.value.copy(timeForFullRotationInMillis = it) + managedOption.element.save() + } + ) + + return listOf( + Option.createBuilder() + .name(managedOption.labelText) + .binding(colorBinding) + .controller { + ColorControllerBuilder.create(it) + .allowAlpha(true) + } + .build(), + Option.createBuilder() + .name(managedOption.labelText) + .binding(speedBinding) + .controller { IntegerSliderControllerBuilder.create(it).range(0, 60_000).step(10) } + .build(), + ) + } + + is BooleanHandler -> return createDefaultBinding(TickBoxControllerBuilder::create).single() + is StringHandler -> return createDefaultBinding(StringControllerBuilder::create).single() + is IntegerHandler -> return createDefaultBinding { + IntegerSliderControllerBuilder.create(it).range(handler.min, handler.max).step(1) + }.single() + + is DurationHandler -> return Option.createBuilder() + .name(managedOption.labelText) + .binding((binding as Binding).xmap({ it.toDouble(DurationUnit.SECONDS) }, { it.seconds })) + .controller { + DoubleSliderControllerBuilder.create(it) + .formatValue { Text.literal(FirmFormatters.formatTimespan(it.seconds)) } + .step(0.1) + .range(handler.min.toDouble(DurationUnit.SECONDS), handler.max.toDouble(DurationUnit.SECONDS)) + } + .single() + + is KeyBindingHandler -> return createDefaultBinding { + KeybindingBuilder(it, managedOption as ManagedOption) + }.single() + + else -> return listOf(LabelOption.create(Text.literal("This option is currently unhandled for this config menu. Please report this as a bug."))) + } + } + + private enum class Sacrifice {} + + private fun createChoiceBinding( + handler: ChoiceHandler<*>, + managedOption: ManagedOption<*>, + option: Option<*> + ): ControllerBuilder { + val b = EnumControllerBuilder.create(option as Option) + b.enumClass(handler.enumClass as Class) + /** + * This is a function with E to avoid realizing the Sacrifice outside of a `X` wrapper. + */ + fun > makeValueFormatter(): ValueFormatter { + return ValueFormatter { + (handler.renderer as EnumRenderer).getName(managedOption as ManagedOption, it) + } + } + b.formatValue(makeValueFormatter()) + return b as ControllerBuilder + } + + + fun buildConfig(): YetAnotherConfigLib { + return YetAnotherConfigLib.createBuilder() + .title(Text.literal("Firmament")) + .categories(buildCategories()) + .build() + } + + override val key: String + get() = "yacl" + + override fun open(search: String?, parent: Screen?): Screen { + return object : YACLScreen(buildConfig(), parent) { + override fun setFocused(focused: Element?) { + if (this.focused is KeybindingWidget && + focused is ListHolderWidget<*> + ) { + return + } + super.setFocused(focused) + } + + override fun shouldCloseOnEsc(): Boolean { + if (focused is KeybindingWidget) { + return false + } + return super.shouldCloseOnEsc() + } + } + } + +} diff --git a/src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java b/src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java new file mode 100644 index 0000000..a9db7f9 --- /dev/null +++ b/src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java @@ -0,0 +1,187 @@ +package moe.nea.firmament.init; + + +import moe.nea.firmament.util.ErrorUtil; +import moe.nea.firmament.util.compatloader.ICompatMeta; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +public class AutoDiscoveryPlugin { + public static List getDefaultAllMixinClassesFQNs() { + var defaultName = "moe.nea.firmament.mixins"; + var plugin = new AutoDiscoveryPlugin(); + plugin.setMixinPackage(defaultName); + var mixins = plugin.getMixins(); + return mixins.stream().map(it -> defaultName + "." + it).toList(); + } + + private static final List mixinPlugins = new ArrayList<>(); + + public static List getMixinPlugins() { + return mixinPlugins; + } + + private String mixinPackage; + + public void setMixinPackage(String mixinPackage) { + this.mixinPackage = mixinPackage; + mixinPlugins.add(this); + } + + /** + * Resolves the base class root for a given class URL. This resolves either the JAR root, or the class file root. + * In either case the return value of this + the class name will resolve back to the original class url, or to other + * class urls for other classes. + */ + public URL getBaseUrlForClassUrl(URL classUrl) { + String string = classUrl.toString(); + if (classUrl.getProtocol().equals("jar")) { + try { + return new URL(string.substring(4).split("!")[0]); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + if (string.endsWith(".class")) { + try { + return new URL(string.replace("\\", "/") + .replace(getClass().getCanonicalName() + .replace(".", "/") + ".class", "")); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + return classUrl; + } + + /** + * Get the package that contains all the mixins. This value is set using {@link #setMixinPackage}. + */ + public String getMixinPackage() { + return mixinPackage; + } + + /** + * Get the path inside the class root to the mixin package + */ + public String getMixinBaseDir() { + return mixinPackage.replace(".", "/"); + } + + /** + * A list of all discovered mixins. + */ + private List mixins = null; + + /** + * Try to add mixin class ot the mixins based on the filepath inside of the class root. + * Removes the {@code .class} file suffix, as well as the base mixin package. + *

This method cannot be called after mixin initialization.

+ * + * @param className the name or path of a class to be registered as a mixin. + */ + public void tryAddMixinClass(String className) { + if (!className.endsWith(".class")) return; + String norm = (className.substring(0, className.length() - ".class".length())) + .replace("\\", "/") + .replace("/", "."); + if (norm.startsWith(getMixinPackage() + ".") && !norm.endsWith(".") && ICompatMeta.Companion.shouldLoad(norm)) { + mixins.add(norm.substring(getMixinPackage().length() + 1)); + } + } + + private void tryDiscoverFromContentFile(URL url) { + Path file; + try { + file = Paths.get(getBaseUrlForClassUrl(url).toURI()); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + System.out.println("Base directory found at " + file); + if (!Files.exists(file)) { + System.out.println("Skipping non-existing mixin root: " + file); + return; + } + if (Files.isDirectory(file)) { + walkDir(file); + } else { + walkJar(file); + } + System.out.println("Found mixins: " + mixins); + + } + + /** + * Search through the JAR or class directory to find mixins contained in {@link #getMixinPackage()} + */ + public List getMixins() { + if (mixins != null) return mixins; + try { + System.out.println("Trying to discover mixins"); + mixins = new ArrayList<>(); + URL classUrl = getClass().getProtectionDomain().getCodeSource().getLocation(); + System.out.println("Found classes at " + classUrl); + tryDiscoverFromContentFile(classUrl); + var classRoots = System.getProperty("firmament.classroots"); + if (classRoots != null && !classRoots.isBlank()) { + System.out.println("Found firmament class roots: " + classRoots); + for (String s : classRoots.split(File.pathSeparator)) { + if (s.isBlank()) { + continue; + } + tryDiscoverFromContentFile(new File(s).toURI().toURL()); + } + } + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); + } + return mixins; + } + + /** + * Search through directory for mixin classes based on {@link #getMixinBaseDir}. + * + * @param classRoot The root directory in which classes are stored for the default package. + */ + private void walkDir(Path classRoot) { + System.out.println("Trying to find mixins from directory"); + var path = classRoot.resolve(getMixinBaseDir()); + if (!Files.exists(path)) return; + try (Stream classes = Files.walk(path)) { + classes.map(it -> classRoot.relativize(it).toString()) + .forEach(this::tryAddMixinClass); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Read through a JAR file, trying to find all mixins inside. + */ + private void walkJar(Path file) { + System.out.println("Trying to find mixins from jar file"); + try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(file))) { + ZipEntry next; + while ((next = zis.getNextEntry()) != null) { + tryAddMixinClass(next.getName()); + zis.closeEntry(); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/main/java/moe/nea/firmament/init/ClientPlayerRiser.java b/src/main/java/moe/nea/firmament/init/ClientPlayerRiser.java new file mode 100644 index 0000000..d60e3e7 --- /dev/null +++ b/src/main/java/moe/nea/firmament/init/ClientPlayerRiser.java @@ -0,0 +1,75 @@ +package moe.nea.firmament.init; + +import me.shedaniel.mm.api.ClassTinkerers; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.VarInsnNode; + +import java.lang.reflect.Modifier; +import java.util.Objects; + +public class ClientPlayerRiser extends RiserUtils { + @IntermediaryName(net.minecraft.entity.player.PlayerEntity.class) + String PlayerEntity; + @IntermediaryName(net.minecraft.world.World.class) + String World; + String GameProfile = "com.mojang.authlib.GameProfile"; + @IntermediaryName(net.minecraft.util.math.BlockPos.class) + String BlockPos; + @IntermediaryName(net.minecraft.client.network.AbstractClientPlayerEntity.class) + String AbstractClientPlayerEntity; + String GuiPlayer = "moe.nea.firmament.gui.entity.GuiPlayer"; + // World world, BlockPos pos, float yaw, GameProfile gameProfile + Type constructorDescriptor = Type.getMethodType(Type.VOID_TYPE, getTypeForClassName(World), getTypeForClassName(BlockPos), Type.FLOAT_TYPE, getTypeForClassName(GameProfile)); + + + private void mapClassNode(ClassNode classNode, Type superClass) { + for (MethodNode method : classNode.methods) { + if (Objects.equals(method.name, "") && Type.getMethodType(method.desc).equals(constructorDescriptor)) { + modifyConstructor(method, superClass); + return; + } + } + var node = new MethodNode(Opcodes.ASM9, "", constructorDescriptor.getDescriptor(), null, null); + classNode.methods.add(node); + modifyConstructor(node, superClass); + } + + + private void modifyConstructor(MethodNode method, Type superClass) { + method.access = (method.access | Modifier.PUBLIC) & ~Modifier.PRIVATE & ~Modifier.PROTECTED; + if (method.instructions.size() != 0) return; // Some other mod has already made a constructor here + + // World world, BlockPos pos, float yaw, GameProfile gameProfile + // ALOAD this + method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); + + // ALOAD World + method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 1)); + + // ALOAD BlockPos + method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 2)); + + // ALOAD yaw + method.instructions.add(new VarInsnNode(Opcodes.FLOAD, 3)); + + // ALOAD gameProfile + method.instructions.add(new VarInsnNode(Opcodes.ALOAD, 4)); + + // Call super + method.instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, superClass.getInternalName(), "", constructorDescriptor.getDescriptor(), false)); + + // Return + method.instructions.add(new InsnNode(Opcodes.RETURN)); + } + + @Override + public void addTinkerers() { + ClassTinkerers.addTransformation(AbstractClientPlayerEntity, it -> mapClassNode(it, getTypeForClassName(PlayerEntity)), true); + ClassTinkerers.addTransformation(GuiPlayer, it -> mapClassNode(it, getTypeForClassName(AbstractClientPlayerEntity)), true); + } +} diff --git a/src/main/java/moe/nea/firmament/init/EarlyRiser.java b/src/main/java/moe/nea/firmament/init/EarlyRiser.java new file mode 100644 index 0000000..5441255 --- /dev/null +++ b/src/main/java/moe/nea/firmament/init/EarlyRiser.java @@ -0,0 +1,12 @@ + +package moe.nea.firmament.init; + +public class EarlyRiser implements Runnable { + @Override + public void run() { + new ClientPlayerRiser().addTinkerers(); + new HandledScreenRiser().addTinkerers(); + new SectionBuilderRiser().addTinkerers(); +// TODO: new ItemColorsSodiumRiser().addTinkerers(); + } +} diff --git a/src/main/java/moe/nea/firmament/init/HandledScreenRiser.java b/src/main/java/moe/nea/firmament/init/HandledScreenRiser.java new file mode 100644 index 0000000..f7db18c --- /dev/null +++ b/src/main/java/moe/nea/firmament/init/HandledScreenRiser.java @@ -0,0 +1,151 @@ + +package moe.nea.firmament.init; + +import me.shedaniel.mm.api.ClassTinkerers; +import net.minecraft.client.gui.Element; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.JumpInsnNode; +import org.objectweb.asm.tree.LabelNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.VarInsnNode; + +import java.lang.reflect.Modifier; +import java.util.function.Consumer; + +public class HandledScreenRiser extends RiserUtils { + @IntermediaryName(net.minecraft.client.gui.screen.Screen.class) + String Screen; + @IntermediaryName(net.minecraft.client.gui.screen.ingame.HandledScreen.class) + String HandledScreen; + Type mouseScrolledDesc = Type.getMethodType(Type.BOOLEAN_TYPE, Type.DOUBLE_TYPE, Type.DOUBLE_TYPE, Type.DOUBLE_TYPE, Type.DOUBLE_TYPE); + String mouseScrolled = remapper.mapMethodName("intermediary", "net.minecraft.class_364", "method_25401", + mouseScrolledDesc.getDescriptor()); + // boolean keyReleased(int keyCode, int scanCode, int modifiers) + Type keyReleasedDesc = Type.getMethodType(Type.BOOLEAN_TYPE, Type.INT_TYPE, Type.INT_TYPE, Type.INT_TYPE); + String keyReleased = remapper.mapMethodName("intermediary", Intermediary.className(), + Intermediary.methodName(Element::keyReleased), + keyReleasedDesc.getDescriptor()); + // public boolean charTyped(char chr, int modifiers) + Type charTypedDesc = Type.getMethodType(Type.BOOLEAN_TYPE, Type.CHAR_TYPE, Type.INT_TYPE); + String charTyped = remapper.mapMethodName("intermediary", Intermediary.className(), + Intermediary.methodName(Element::charTyped), + charTypedDesc.getDescriptor()); + + + @Override + public void addTinkerers() { + ClassTinkerers.addTransformation(HandledScreen, this::addMouseScroll, true); + ClassTinkerers.addTransformation(HandledScreen, this::addKeyReleased, true); + ClassTinkerers.addTransformation(HandledScreen, this::addCharTyped, true); + } + + /** + * Insert a handler that roughly inserts the following code at the beginning of the instruction list: + *
+	 * if (insertInvoke(insertLoads)) return true
+	 * 
+ * + * @param node The method node to prepend the instructions to + * @param insertLoads insert all the loads, including the {@code this} parameter + * @param insertInvoke insert the invokevirtual/invokestatic call + */ + void insertTrueHandler(MethodNode node, + Consumer insertLoads, + Consumer insertInvoke) { + + var insns = new InsnList(); + insertLoads.accept(insns); + insertInvoke.accept(insns); + // Create jump target (but not insert it yet) + var jumpIfFalse = new LabelNode(); + // IFEQ (if returned boolean == 0), jump to jumpIfFalse + insns.add(new JumpInsnNode(Opcodes.IFEQ, jumpIfFalse)); + // LDC 1 (as int, which is what booleans are at runtime) + insns.add(new LdcInsnNode(1)); + // IRETURN return int on stack (booleans are int at runtime) + insns.add(new InsnNode(Opcodes.IRETURN)); + insns.add(jumpIfFalse); + node.instructions.insert(insns); + } + + void addKeyReleased(ClassNode classNode) { + addSuperInjector( + classNode, keyReleased, keyReleasedDesc, "keyReleased_firmament", + insns -> { + // ALOAD 0, load this + insns.add(new VarInsnNode(Opcodes.ALOAD, 0)); + // ILOAD 1-3, load args + insns.add(new VarInsnNode(Opcodes.ILOAD, 1)); + insns.add(new VarInsnNode(Opcodes.ILOAD, 2)); + insns.add(new VarInsnNode(Opcodes.ILOAD, 3)); + }); + } + + void addCharTyped(ClassNode classNode) { + addSuperInjector( + classNode, charTyped, charTypedDesc, "charTyped_firmament", + insns -> { + // ALOAD 0, load this + insns.add(new VarInsnNode(Opcodes.ALOAD, 0)); + // ILOAD 1-2, load args. chars = ints + insns.add(new VarInsnNode(Opcodes.ILOAD, 1)); + insns.add(new VarInsnNode(Opcodes.ILOAD, 2)); + }); + } + + void addSuperInjector( + ClassNode classNode, + String name, + Type desc, + String firmamentName, + Consumer loadArgs + ) { + var keyReleasedNode = findMethod(classNode, name, desc); + if (keyReleasedNode == null) { + keyReleasedNode = new MethodNode( + Modifier.PUBLIC, + name, + desc.getDescriptor(), + null, + new String[0] + ); + var insns = keyReleasedNode.instructions; + loadArgs.accept(insns); + // INVOKESPECIAL call super method + insns.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, getTypeForClassName(Screen).getInternalName(), + name, desc.getDescriptor())); + // IRETURN return int on stack (booleans are int at runtime) + insns.add(new InsnNode(Opcodes.IRETURN)); + classNode.methods.add(keyReleasedNode); + } + insertTrueHandler(keyReleasedNode, loadArgs, insns -> { + // INVOKEVIRTUAL call custom handler + insns.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, + getTypeForClassName(HandledScreen).getInternalName(), + firmamentName, + desc.getDescriptor())); + }); + + } + + void addMouseScroll(ClassNode classNode) { + addSuperInjector( + classNode, mouseScrolled, mouseScrolledDesc, "mouseScrolled_firmament", + insns -> { + // ALOAD 0, load this + insns.add(new VarInsnNode(Opcodes.ALOAD, 0)); + // DLOAD 1-4, load the 4 argument doubles. Note that since doubles are two entries wide we skip 2 each time. + insns.add(new VarInsnNode(Opcodes.DLOAD, 1)); + insns.add(new VarInsnNode(Opcodes.DLOAD, 3)); + insns.add(new VarInsnNode(Opcodes.DLOAD, 5)); + insns.add(new VarInsnNode(Opcodes.DLOAD, 7)); + }); + } + +} diff --git a/src/main/java/moe/nea/firmament/init/Intermediary.java b/src/main/java/moe/nea/firmament/init/Intermediary.java new file mode 100644 index 0000000..61494d7 --- /dev/null +++ b/src/main/java/moe/nea/firmament/init/Intermediary.java @@ -0,0 +1,63 @@ +package moe.nea.firmament.init; + +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.MappingResolver; +import org.objectweb.asm.Type; + +import java.util.List; + +public class Intermediary { + private static final MappingResolver RESOLVER = FabricLoader.getInstance().getMappingResolver(); + + static String methodName(Object object) { + throw new AssertionError("Cannot be called at runtime"); + } + + static String className() { + throw new AssertionError("Cannot be called at runtime"); + } + + static String id(String source) { + return source; + } + +// public record Class( +// Type intermediaryClass +// ) { +// public Class(String intermediaryClass) { +// this(Type.getObjectType(intermediaryClass.replace('.', '/'))); +// } +// +// public String getMappedName() { +// return RESOLVER.mapClassName("intermediary", intermediaryClass.getInternalName() +// .replace('/', '.')); +// } +// } +// +// public record Method( +// Type intermediaryClassName, +// String intermediaryMethodName, +// Type intermediaryReturnType, +// List intermediaryArgumentTypes +// ) { +// public Method( +// String intermediaryClassName, +// String intermediaryMethodName, +// String intermediaryReturnType, +// String... intermediaryArgumentTypes +// ) { +// this(intermediaryClassName, intermediaryMethodName, intermediaryReturnType, List.of(intermediaryArgumentTypes)); +// } +// +// public String getMappedMethodName() { +// return RESOLVER.mapMethodName("intermediary", +// intermediaryClassName.getInternalName().replace('/', '.')); +// } +// +// public Type getIntermediaryDescriptor() { +// return Type.getMethodType(intermediaryReturnType, intermediaryArgumentTypes.toArray(Type[]::new)); +// } +// +// +// } +} diff --git a/src/main/java/moe/nea/firmament/init/IntermediaryName.java b/src/main/java/moe/nea/firmament/init/IntermediaryName.java new file mode 100644 index 0000000..a22ad0f --- /dev/null +++ b/src/main/java/moe/nea/firmament/init/IntermediaryName.java @@ -0,0 +1,21 @@ +package moe.nea.firmament.init; + +import net.fabricmc.loader.api.MappingResolver; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Injects the intermediary name of the given field into this field by replacing its initializer with a call to + * {@link MappingResolver#mapClassName(String, String)} + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.FIELD) +public @interface IntermediaryName { + // String method() default ""; +// +// String field() default ""; + Class value(); +} diff --git a/src/main/java/moe/nea/firmament/init/MixinPlugin.java b/src/main/java/moe/nea/firmament/init/MixinPlugin.java new file mode 100644 index 0000000..d48139b --- /dev/null +++ b/src/main/java/moe/nea/firmament/init/MixinPlugin.java @@ -0,0 +1,76 @@ + + +package moe.nea.firmament.init; + +import com.llamalad7.mixinextras.MixinExtrasBootstrap; +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class MixinPlugin implements IMixinConfigPlugin { + + AutoDiscoveryPlugin autoDiscoveryPlugin = new AutoDiscoveryPlugin(); + public static List instances = new ArrayList<>(); + public String mixinPackage; + + @Override + public void onLoad(String mixinPackage) { + MixinExtrasBootstrap.init(); + instances.add(this); + this.mixinPackage = mixinPackage; + autoDiscoveryPlugin.setMixinPackage(mixinPackage); + } + + @Override + public String getRefMapperConfig() { + return null; + } + + @Override + public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { + if (!Boolean.getBoolean("firmament.debug") && mixinClassName.contains("devenv.")) { + return false; + } + return true; + } + + @Override + public void acceptTargets(Set myTargets, Set otherTargets) { + + } + + @Override + public List getMixins() { + return autoDiscoveryPlugin.getMixins().stream().filter(it -> this.shouldApplyMixin(null, it)) + .toList(); + } + + @Override + public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + + } + + public Set getAppliedFullPathMixins() { + return new HashSet<>(appliedMixins); + } + + public Set getExpectedFullPathMixins() { + return getMixins() + .stream() + .map(it -> mixinPackage + "." + it) + .collect(Collectors.toSet()); + } + + public List appliedMixins = new ArrayList<>(); + + @Override + public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + appliedMixins.add(mixinClassName); + } +} diff --git a/src/main/java/moe/nea/firmament/init/RiserUtils.java b/src/main/java/moe/nea/firmament/init/RiserUtils.java new file mode 100644 index 0000000..c1c8fd1 --- /dev/null +++ b/src/main/java/moe/nea/firmament/init/RiserUtils.java @@ -0,0 +1,27 @@ + +package moe.nea.firmament.init; + +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.MappingResolver; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodNode; + +public abstract class RiserUtils { + protected Type getTypeForClassName(String className) { + return Type.getObjectType(className.replace('.', '/')); + } + + protected MappingResolver remapper = FabricLoader.getInstance().getMappingResolver(); + + public abstract void addTinkerers(); + + protected MethodNode findMethod(ClassNode classNode, String name, Type desc) { + for (MethodNode method : classNode.methods) { + if (method.name.equals(name) && desc.getDescriptor().equals(method.desc)) + return method; + } + return null; + } + +} diff --git a/src/main/java/moe/nea/firmament/init/SectionBuilderRiser.java b/src/main/java/moe/nea/firmament/init/SectionBuilderRiser.java new file mode 100644 index 0000000..8b65946 --- /dev/null +++ b/src/main/java/moe/nea/firmament/init/SectionBuilderRiser.java @@ -0,0 +1,118 @@ +package moe.nea.firmament.init; + +import me.shedaniel.mm.api.ClassTinkerers; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.block.BlockState; +import net.minecraft.client.render.block.BlockRenderManager; +import net.minecraft.client.render.chunk.SectionBuilder; +import net.minecraft.client.render.model.BlockStateModel; +import net.minecraft.util.math.BlockPos; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.LocalVariableNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.VarInsnNode; + +public class SectionBuilderRiser extends RiserUtils { + + @IntermediaryName(SectionBuilder.class) + String SectionBuilder; + @IntermediaryName(BlockPos.class) + String BlockPos; + @IntermediaryName(BlockRenderManager.class) + String BlockRenderManager; + @IntermediaryName(BlockState.class) + String BlockState; + @IntermediaryName(BlockStateModel.class) + String BlockStateModel; + String CustomBlockTextures = "moe.nea.firmament.features.texturepack.CustomBlockTextures"; + + Type getModelDesc = Type.getMethodType( + getTypeForClassName(BlockRenderManager), + getTypeForClassName(BlockState) + ); + String getModel = remapper.mapMethodName( + "intermediary", + Intermediary.className(), + Intermediary.methodName(net.minecraft.client.render.block.BlockRenderManager::getModel), + Type.getMethodDescriptor( + getTypeForClassName(Intermediary.className()), + getTypeForClassName(Intermediary.className()) + ) + ); + + @Override + public void addTinkerers() { + if (FabricLoader.getInstance().isModLoaded("fabric-renderer-indigo")) + ClassTinkerers.addTransformation(SectionBuilder, this::handle, true); + } + + private void handle(ClassNode classNode) { + System.out.println("AVAST! "+ getModel); + for (MethodNode method : classNode.methods) { + if ((method.name.endsWith("$fabric-renderer-indigo$hookBuildRenderBlock") + || method.name.endsWith("$fabric-renderer-indigo$hookChunkBuildTessellate")) && + method.name.startsWith("redirect$")) { + handleIndigo(method); + return; + } + } + System.err.println("Could not inject indigo rendering hook. Is a custom renderer installed (e.g. sodium)?"); + } + + private void handleIndigo(MethodNode method) { + LocalVariableNode blockPosVar = null, blockStateVar = null; + for (LocalVariableNode localVariable : method.localVariables) { + if (Type.getType(localVariable.desc).equals(getTypeForClassName(BlockPos))) { + blockPosVar = localVariable; + } + if (Type.getType(localVariable.desc).equals(getTypeForClassName(BlockState))) { + blockStateVar = localVariable; + } + } + if (blockPosVar == null || blockStateVar == null) { + System.err.println("Firmament could inject into indigo: missing either block pos or blockstate"); + return; + } + for (AbstractInsnNode instruction : method.instructions) { + if (instruction.getOpcode() != Opcodes.INVOKEVIRTUAL) continue; + var methodInsn = (MethodInsnNode) instruction; + if (!(methodInsn.name.equals(getModel) && Type.getObjectType(methodInsn.owner).equals(getTypeForClassName(BlockRenderManager)))) + continue; + method.instructions.insertBefore( + methodInsn, + new MethodInsnNode( + Opcodes.INVOKESTATIC, + getTypeForClassName(CustomBlockTextures).getInternalName(), + "enterFallbackCall", + Type.getMethodDescriptor(Type.VOID_TYPE) + )); + + var insnList = new InsnList(); + insnList.add(new MethodInsnNode( + Opcodes.INVOKESTATIC, + getTypeForClassName(CustomBlockTextures).getInternalName(), + "exitFallbackCall", + Type.getMethodDescriptor(Type.VOID_TYPE) + )); + insnList.add(new VarInsnNode(Opcodes.ALOAD, blockPosVar.index)); + insnList.add(new VarInsnNode(Opcodes.ALOAD, blockStateVar.index)); + insnList.add(new MethodInsnNode( + Opcodes.INVOKESTATIC, + getTypeForClassName(CustomBlockTextures).getInternalName(), + "patchIndigo", + Type.getMethodDescriptor( + getTypeForClassName(BlockStateModel), + getTypeForClassName(BlockStateModel), + getTypeForClassName(BlockPos), + getTypeForClassName(BlockState)), + false + )); + method.instructions.insert(methodInsn, insnList); + } + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/AlwaysDisplayFirmamentClientCommandErrors.java b/src/main/java/moe/nea/firmament/mixins/AlwaysDisplayFirmamentClientCommandErrors.java new file mode 100644 index 0000000..59769c6 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/AlwaysDisplayFirmamentClientCommandErrors.java @@ -0,0 +1,18 @@ +package moe.nea.firmament.mixins; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.sugar.Local; +import net.fabricmc.fabric.impl.command.client.ClientCommandInternals; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(ClientCommandInternals.class) +public class AlwaysDisplayFirmamentClientCommandErrors { + @ModifyExpressionValue(method = "executeCommand", at = @At(value = "INVOKE", target = "Lnet/fabricmc/fabric/impl/command/client/ClientCommandInternals;isIgnoredException(Lcom/mojang/brigadier/exceptions/CommandExceptionType;)Z")) + private static boolean markFirmamentExceptionsAsNotIgnores(boolean original, @Local(argsOnly = true) String command) { + if (command.startsWith("firm ") || command.equals("firm") || command.startsWith("firmament ") || command.equals("firmament")) { + return false; + } + return original; + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/AppendRepoAsResourcePack.java b/src/main/java/moe/nea/firmament/mixins/AppendRepoAsResourcePack.java new file mode 100644 index 0000000..d8e35d7 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/AppendRepoAsResourcePack.java @@ -0,0 +1,34 @@ + +package moe.nea.firmament.mixins; + +import com.llamalad7.mixinextras.sugar.Local; +import moe.nea.firmament.repo.RepoModResourcePack; +import net.fabricmc.fabric.api.resource.ModResourcePack; +import net.fabricmc.fabric.impl.resource.loader.ModResourcePackSorter; +import net.fabricmc.fabric.impl.resource.loader.ModResourcePackUtil; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.resource.ResourceType; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.List; + +@Mixin(ModResourcePackUtil.class) +public class AppendRepoAsResourcePack { + @Inject( + method = "getModResourcePacks", + at = @At(value = "INVOKE", target = "Lnet/fabricmc/fabric/impl/resource/loader/ModResourcePackSorter;getPacks()Ljava/util/List;"), + require = 0 + ) + private static void onAppendModResourcePack( + FabricLoader fabricLoader, ResourceType type, @Nullable String subPath, CallbackInfoReturnable> cir, + @Local ModResourcePackSorter sorter + ) { + RepoModResourcePack.Companion.append(sorter); + } + +} diff --git a/src/main/java/moe/nea/firmament/mixins/BandAidResourcePackPatch.java b/src/main/java/moe/nea/firmament/mixins/BandAidResourcePackPatch.java new file mode 100644 index 0000000..d898c44 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/BandAidResourcePackPatch.java @@ -0,0 +1,25 @@ + +package moe.nea.firmament.mixins; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import com.llamalad7.mixinextras.sugar.Local; +import moe.nea.firmament.repo.RepoModResourcePack; +import net.minecraft.resource.ReloadableResourceManagerImpl; +import net.minecraft.resource.Resource; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +import java.util.Optional; + +@Mixin(ReloadableResourceManagerImpl.class) +public class BandAidResourcePackPatch { + + @ModifyReturnValue( + method = "getResource", + at = @At("RETURN") + ) + private Optional injectOurCustomResourcesInCaseExistingMethodsFailed(Optional original, @Local Identifier identifier) { + return original.or(() -> RepoModResourcePack.Companion.createResourceDirectly(identifier)); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/CaseInsensitiveCommandMapPatch.java b/src/main/java/moe/nea/firmament/mixins/CaseInsensitiveCommandMapPatch.java new file mode 100644 index 0000000..3bbf2ff --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/CaseInsensitiveCommandMapPatch.java @@ -0,0 +1,25 @@ + + +package moe.nea.firmament.mixins; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.mojang.brigadier.tree.CommandNode; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +import java.util.Locale; +import java.util.Map; + +@Mixin(value = CommandNode.class, remap = false) +public class CaseInsensitiveCommandMapPatch { + @WrapOperation(method = "getRelevantNodes", at = @At(value = "INVOKE", target = "Ljava/util/Map;get(Ljava/lang/Object;)Ljava/lang/Object;"), remap = false) + public Object modify(Map map, Object text, Operation op) { + var original = op.call(map, text); + if (original == null) { + return map.get(((String) text).toLowerCase(Locale.ROOT)); + } + return original; + } + +} diff --git a/src/main/java/moe/nea/firmament/mixins/ChatPeekingPatch.java b/src/main/java/moe/nea/firmament/mixins/ChatPeekingPatch.java new file mode 100644 index 0000000..9f6fb4d --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/ChatPeekingPatch.java @@ -0,0 +1,25 @@ + + +package moe.nea.firmament.mixins; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import moe.nea.firmament.features.fixes.Fixes; +import net.minecraft.client.gui.hud.ChatHud; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyVariable; + +@Mixin(ChatHud.class) +public class ChatPeekingPatch { + + @ModifyVariable(method = "render", at = @At(value = "HEAD"), index = 5, argsOnly = true) + public boolean onGetChatHud(boolean old) { + return old || Fixes.INSTANCE.shouldPeekChat(); + } + + @ModifyExpressionValue(method = "getHeight()I", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/hud/ChatHud;isChatFocused()Z")) + public boolean onGetChatHudHeight(boolean old) { + return old || Fixes.INSTANCE.shouldPeekChat(); + } + +} diff --git a/src/main/java/moe/nea/firmament/mixins/CopyChatPatch.java b/src/main/java/moe/nea/firmament/mixins/CopyChatPatch.java new file mode 100644 index 0000000..6996818 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/CopyChatPatch.java @@ -0,0 +1,44 @@ +package moe.nea.firmament.mixins; + +import moe.nea.firmament.features.chat.CopyChat; +import moe.nea.firmament.mixins.accessor.AccessorChatHud; +import moe.nea.firmament.util.ClipboardUtils; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.hud.ChatHud; +import net.minecraft.client.gui.hud.ChatHudLine; +import net.minecraft.client.gui.screen.ChatScreen; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.math.MathHelper; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import java.util.List; + +@Mixin(ChatScreen.class) +public class CopyChatPatch { + @Inject(method = "mouseClicked", at = @At("HEAD"), cancellable = true) + private void onRightClick(double mouseX, double mouseY, int button, CallbackInfoReturnable cir) throws NoSuchFieldException, IllegalAccessException { + if (button != 1 || !CopyChat.TConfig.INSTANCE.getCopyChat()) return; + MinecraftClient client = MinecraftClient.getInstance(); + ChatHud chatHud = client.inGameHud.getChatHud(); + int lineIndex = getChatLineIndex(chatHud, mouseY); + if (lineIndex < 0) return; + List visible = ((AccessorChatHud) chatHud).getVisibleMessages_firmament(); + if (lineIndex >= visible.size()) return; + ChatHudLine.Visible line = visible.get(lineIndex); + String text = CopyChat.INSTANCE.orderedTextToString(line.content()); + ClipboardUtils.INSTANCE.setTextContent(text); + chatHud.addMessage(Text.literal("Copied: ").append(text).formatted(Formatting.GRAY)); + cir.setReturnValue(true); + cir.cancel(); + } + + @Unique + private int getChatLineIndex(ChatHud chatHud, double mouseY) { + double chatLineY = ((AccessorChatHud) chatHud).toChatLineY_firmament(mouseY); + return MathHelper.floor(chatLineY + ((AccessorChatHud) chatHud).getScrolledLines_firmament()); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/CustomDurabilityBarPatch.java b/src/main/java/moe/nea/firmament/mixins/CustomDurabilityBarPatch.java new file mode 100644 index 0000000..fde3580 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/CustomDurabilityBarPatch.java @@ -0,0 +1,53 @@ + +package moe.nea.firmament.mixins; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalRef; +import moe.nea.firmament.util.DurabilityBarEvent; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.item.ItemStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(DrawContext.class) +public class CustomDurabilityBarPatch { + @WrapOperation( + method = "drawItemBar", + at = @At(value = "INVOKE", target = "Lnet/minecraft/item/ItemStack;isItemBarVisible()Z") + ) + private boolean onIsItemBarVisible( + ItemStack instance, Operation original, + @Share("barOverride") LocalRef barOverride + ) { + if (original.call(instance)) + return true; + DurabilityBarEvent event = new DurabilityBarEvent(instance); + DurabilityBarEvent.Companion.publish(event); + barOverride.set(event.getBarOverride()); + return barOverride.get() != null; + } + + @WrapOperation(method = "drawItemBar", + at = @At(value = "INVOKE", target = "Lnet/minecraft/item/ItemStack;getItemBarStep()I")) + private int overrideItemStep( + ItemStack instance, Operation original, + @Share("barOverride") LocalRef barOverride + ) { + if (barOverride.get() != null) + return Math.round(barOverride.get().getPercentage() * 13); + return original.call(instance); + } + + @WrapOperation(method = "drawItemBar", + at = @At(value = "INVOKE", target = "Lnet/minecraft/item/ItemStack;getItemBarColor()I")) + private int overrideItemColor( + ItemStack instance, Operation original, + @Share("barOverride") LocalRef barOverride + ) { + if (barOverride.get() != null) + return barOverride.get().getColor().getColor(); + return original.call(instance); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/DFUEntityIdFixPatch.java b/src/main/java/moe/nea/firmament/mixins/DFUEntityIdFixPatch.java new file mode 100644 index 0000000..717d404 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/DFUEntityIdFixPatch.java @@ -0,0 +1,35 @@ + + +package moe.nea.firmament.mixins; + +import com.mojang.datafixers.DataFix; +import com.mojang.datafixers.TypeRewriteRule; +import com.mojang.datafixers.schemas.Schema; +import com.mojang.datafixers.util.Pair; +import net.minecraft.datafixer.TypeReferences; +import net.minecraft.datafixer.fix.EntityIdFix; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.Map; + +// TODO: rework this +@Mixin(EntityIdFix.class) +public abstract class DFUEntityIdFixPatch extends DataFix { + @Shadow + @Final + private static Map RENAMED_ENTITIES; + + public DFUEntityIdFixPatch(Schema outputSchema, boolean changesType) { + super(outputSchema, changesType); + } + + @Inject(method = "makeRule", at = @At("RETURN"), cancellable = true) + public void onMakeRule(CallbackInfoReturnable cir) { + cir.setReturnValue(TypeRewriteRule.seq(fixTypeEverywhere("EntityIdFix", getInputSchema().findChoiceType(TypeReferences.ENTITY), getOutputSchema().findChoiceType(TypeReferences.ENTITY), dynamicOps -> pair -> ((Pair) pair).mapFirst(string -> RENAMED_ENTITIES.getOrDefault(string, (String) string))), convertUnchecked("Fix Type", getInputSchema().getType(TypeReferences.ITEM_STACK), getOutputSchema().getType(TypeReferences.ITEM_STACK)))); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/DisableHurtCam.java b/src/main/java/moe/nea/firmament/mixins/DisableHurtCam.java new file mode 100644 index 0000000..ed7a2d4 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/DisableHurtCam.java @@ -0,0 +1,18 @@ +package moe.nea.firmament.mixins; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import moe.nea.firmament.features.fixes.Fixes; +import net.minecraft.client.render.GameRenderer; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(GameRenderer.class) +public class DisableHurtCam { + @ModifyExpressionValue(method = "tiltViewWhenHurt", at = @At(value = "FIELD", target = "Lnet/minecraft/entity/LivingEntity;hurtTime:I", opcode = Opcodes.GETFIELD)) + private int replaceHurtTime(int original) { + if (Fixes.TConfig.INSTANCE.getNoHurtCam()) + return 0; + return original; + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/DispatchMouseInputEventsPatch.java b/src/main/java/moe/nea/firmament/mixins/DispatchMouseInputEventsPatch.java new file mode 100644 index 0000000..f1b07bb --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/DispatchMouseInputEventsPatch.java @@ -0,0 +1,17 @@ +package moe.nea.firmament.mixins; + +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import moe.nea.firmament.events.WorldMouseMoveEvent; +import net.minecraft.client.Mouse; +import net.minecraft.client.network.ClientPlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(Mouse.class) +public class DispatchMouseInputEventsPatch { + @WrapWithCondition(method = "updateMouse", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayerEntity;changeLookDirection(DD)V")) + public boolean onRotatePlayer(ClientPlayerEntity instance, double deltaX, double deltaY) { + var event = WorldMouseMoveEvent.Companion.publish(new WorldMouseMoveEvent(deltaX, deltaY)); + return !event.getCancelled(); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/EarlyResourceReloadPatch.java b/src/main/java/moe/nea/firmament/mixins/EarlyResourceReloadPatch.java new file mode 100644 index 0000000..e98faf6 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/EarlyResourceReloadPatch.java @@ -0,0 +1,25 @@ + +package moe.nea.firmament.mixins; + +import moe.nea.firmament.events.EarlyResourceReloadEvent; +import net.minecraft.resource.ReloadableResourceManagerImpl; +import net.minecraft.resource.ResourceManager; +import net.minecraft.resource.ResourcePack; +import net.minecraft.resource.ResourceReload; +import net.minecraft.util.Unit; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +@Mixin(ReloadableResourceManagerImpl.class) +public abstract class EarlyResourceReloadPatch implements ResourceManager { + @Inject(method = "reload", at = @At(value = "INVOKE", target = "Lnet/minecraft/resource/SimpleResourceReload;start(Lnet/minecraft/resource/ResourceManager;Ljava/util/List;Ljava/util/concurrent/Executor;Ljava/util/concurrent/Executor;Ljava/util/concurrent/CompletableFuture;Z)Lnet/minecraft/resource/ResourceReload;", shift = At.Shift.BEFORE)) + public void onResourceReload(Executor prepareExecutor, Executor applyExecutor, CompletableFuture initialStage, List packs, CallbackInfoReturnable cir) { + EarlyResourceReloadEvent.Companion.publish(new EarlyResourceReloadEvent(this, prepareExecutor)); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/EntityDespawnPatch.java b/src/main/java/moe/nea/firmament/mixins/EntityDespawnPatch.java new file mode 100644 index 0000000..22bebec --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/EntityDespawnPatch.java @@ -0,0 +1,20 @@ + +package moe.nea.firmament.mixins; + +import com.llamalad7.mixinextras.sugar.Local; +import moe.nea.firmament.events.EntityDespawnEvent; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.Entity; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ClientWorld.class) +public class EntityDespawnPatch { + @Inject(method = "removeEntity", at = @At(value = "TAIL")) + private void onRemoved(int entityId, Entity.RemovalReason removalReason, CallbackInfo ci, @Local @Nullable Entity entity) { + EntityDespawnEvent.Companion.publish(new EntityDespawnEvent(entity, entityId, removalReason)); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/EntityInteractEventPatch.java b/src/main/java/moe/nea/firmament/mixins/EntityInteractEventPatch.java new file mode 100644 index 0000000..8ade59b --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/EntityInteractEventPatch.java @@ -0,0 +1,34 @@ + +package moe.nea.firmament.mixins; + +import moe.nea.firmament.events.EntityInteractionEvent; +import net.minecraft.client.network.ClientPlayerInteractionManager; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.EntityHitResult; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(ClientPlayerInteractionManager.class) +public class EntityInteractEventPatch { + @Inject(method = "attackEntity", at = @At("HEAD")) + private void onAttack(PlayerEntity player, Entity target, CallbackInfo ci) { + EntityInteractionEvent.Companion.publish(new EntityInteractionEvent(EntityInteractionEvent.InteractionKind.ATTACK, target, Hand.MAIN_HAND)); + } + + @Inject(method = "interactEntity", at = @At("HEAD")) + private void onInteract(PlayerEntity player, Entity entity, Hand hand, CallbackInfoReturnable cir) { + EntityInteractionEvent.Companion.publish(new EntityInteractionEvent(EntityInteractionEvent.InteractionKind.INTERACT, entity, hand)); + } + + @Inject(method = "interactEntityAtLocation", at = @At("HEAD")) + private void onInteractAtLocation(PlayerEntity player, Entity entity, EntityHitResult hitResult, Hand hand, CallbackInfoReturnable cir) { + EntityInteractionEvent.Companion.publish(new EntityInteractionEvent(EntityInteractionEvent.InteractionKind.INTERACT_AT_LOCATION, entity, hand)); + } + +} diff --git a/src/main/java/moe/nea/firmament/mixins/EntityUpdateEventListener.java b/src/main/java/moe/nea/firmament/mixins/EntityUpdateEventListener.java new file mode 100644 index 0000000..d956da9 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/EntityUpdateEventListener.java @@ -0,0 +1,48 @@ + +package moe.nea.firmament.mixins; + +import com.llamalad7.mixinextras.sugar.Local; +import moe.nea.firmament.events.EntityUpdateEvent; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientCommonNetworkHandler; +import net.minecraft.client.network.ClientConnectionState; +import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.Entity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.network.ClientConnection; +import net.minecraft.network.packet.s2c.play.EntityAttributesS2CPacket; +import net.minecraft.network.packet.s2c.play.EntityEquipmentUpdateS2CPacket; +import net.minecraft.network.packet.s2c.play.EntityTrackerUpdateS2CPacket; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ClientPlayNetworkHandler.class) +public abstract class EntityUpdateEventListener extends ClientCommonNetworkHandler { + + @Shadow + private ClientWorld world; + + protected EntityUpdateEventListener(MinecraftClient client, ClientConnection connection, ClientConnectionState connectionState) { + super(client, connection, connectionState); + } + + @Inject(method = "onEntityEquipmentUpdate", at = @At(value = "INVOKE", target = "Ljava/util/List;forEach(Ljava/util/function/Consumer;)V", shift = At.Shift.AFTER)) + private void onEquipmentUpdate(EntityEquipmentUpdateS2CPacket packet, CallbackInfo ci, @Local LivingEntity entity) { + EntityUpdateEvent.Companion.publish(new EntityUpdateEvent.EquipmentUpdate(entity, packet.getEquipmentList())); + } + + @Inject(method = "onEntityAttributes", at = @At("TAIL")) + private void onAttributeUpdate(EntityAttributesS2CPacket packet, CallbackInfo ci) { + EntityUpdateEvent.Companion.publish(new EntityUpdateEvent.AttributeUpdate( + (LivingEntity) world.getEntityById(packet.getEntityId()), packet.getEntries())); + } + + @Inject(method = "onEntityTrackerUpdate", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/data/DataTracker;writeUpdatedEntries(Ljava/util/List;)V", shift = At.Shift.AFTER)) + private void onEntityTracker(EntityTrackerUpdateS2CPacket packet, CallbackInfo ci, @Local Entity entity) { + EntityUpdateEvent.Companion.publish(new EntityUpdateEvent.TrackedDataUpdate(entity, packet.trackedValues())); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/FirmKeybindsInVanillaControlsPatch.java b/src/main/java/moe/nea/firmament/mixins/FirmKeybindsInVanillaControlsPatch.java new file mode 100644 index 0000000..699d5b7 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/FirmKeybindsInVanillaControlsPatch.java @@ -0,0 +1,58 @@ + + +package moe.nea.firmament.mixins; + +import moe.nea.firmament.gui.config.KeyBindingHandler; +import moe.nea.firmament.gui.config.ManagedConfig; +import moe.nea.firmament.keybindings.FirmamentKeyBindings; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.option.ControlsListWidget; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.option.KeyBinding; +import net.minecraft.text.Text; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyArg; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ControlsListWidget.KeyBindingEntry.class) +public class FirmKeybindsInVanillaControlsPatch { + + @Mutable + @Shadow + @Final + private ButtonWidget editButton; + + @Shadow + @Final + private KeyBinding binding; + + @Shadow + @Final + private ButtonWidget resetButton; + + @ModifyArg(method = "", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/widget/ButtonWidget;builder(Lnet/minecraft/text/Text;Lnet/minecraft/client/gui/widget/ButtonWidget$PressAction;)Lnet/minecraft/client/gui/widget/ButtonWidget$Builder;")) + public ButtonWidget.PressAction onInit(ButtonWidget.PressAction action) { + var config = FirmamentKeyBindings.INSTANCE.getKeyBindings().get(binding); + if (config == null) return action; + return button -> { + ((KeyBindingHandler) config.getHandler()) + .getManagedConfig() + .showConfigEditor(MinecraftClient.getInstance().currentScreen); + }; + } + + @Inject(method = "update", at = @At("HEAD"), cancellable = true) + public void onUpdate(CallbackInfo ci) { + var config = FirmamentKeyBindings.INSTANCE.getKeyBindings().get(binding); + if (config == null) return; + resetButton.active = false; + editButton.setMessage(Text.translatable("firmament.keybinding.external", config.getValue().format())); + ci.cancel(); + } + +} diff --git a/src/main/java/moe/nea/firmament/mixins/HideStatusEffectsPatch.java b/src/main/java/moe/nea/firmament/mixins/HideStatusEffectsPatch.java new file mode 100644 index 0000000..c5af8b6 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/HideStatusEffectsPatch.java @@ -0,0 +1,29 @@ +package moe.nea.firmament.mixins; + +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import moe.nea.firmament.features.fixes.Fixes; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.ingame.InventoryScreen; +import net.minecraft.client.gui.screen.ingame.StatusEffectsDisplay; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(InventoryScreen.class) +public abstract class HideStatusEffectsPatch { + @Shadow + public abstract boolean shouldHideStatusEffectHud(); + + @Inject(method = "shouldHideStatusEffectHud", at = @At("HEAD"), cancellable = true) + private void hideStatusEffects(CallbackInfoReturnable cir) { + cir.setReturnValue(!Fixes.TConfig.INSTANCE.getHidePotionEffects()); + } + + @WrapWithCondition(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/ingame/StatusEffectsDisplay;drawStatusEffects(Lnet/minecraft/client/gui/DrawContext;IIF)V")) + private boolean conditionalRenderStatuses(StatusEffectsDisplay instance, DrawContext context, int mouseX, int mouseY, float tickDelta) { + return shouldHideStatusEffectHud() || !Fixes.TConfig.INSTANCE.getHidePotionEffects(); + } + +} diff --git a/src/main/java/moe/nea/firmament/mixins/HudRenderEventsPatch.java b/src/main/java/moe/nea/firmament/mixins/HudRenderEventsPatch.java new file mode 100644 index 0000000..49e86fb --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/HudRenderEventsPatch.java @@ -0,0 +1,36 @@ + + +package moe.nea.firmament.mixins; + +import moe.nea.firmament.events.HotbarItemRenderEvent; +import moe.nea.firmament.events.HudRenderEvent; +import moe.nea.firmament.features.fixes.Fixes; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.hud.InGameHud; +import net.minecraft.client.render.RenderTickCounter; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(InGameHud.class) +public class HudRenderEventsPatch { + @Inject(method = "renderSleepOverlay", at = @At(value = "HEAD")) + public void renderCallBack(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) { + HudRenderEvent.Companion.publish(new HudRenderEvent(context, tickCounter)); + } + + @Inject(method = "renderHotbarItem", at = @At("HEAD")) + public void onRenderHotbarItem(DrawContext context, int x, int y, RenderTickCounter tickCounter, PlayerEntity player, ItemStack stack, int seed, CallbackInfo ci) { + if (stack != null && !stack.isEmpty()) + HotbarItemRenderEvent.Companion.publish(new HotbarItemRenderEvent(stack, context, x, y, tickCounter)); + } + + @Inject(method = "renderStatusEffectOverlay", at = @At("HEAD"), cancellable = true) + public void hideStatusEffects(CallbackInfo ci) { + if (Fixes.TConfig.INSTANCE.getHidePotionEffectsHud()) ci.cancel(); + } + +} diff --git a/src/main/java/moe/nea/firmament/mixins/IncomingPacketListenerPatches.java b/src/main/java/moe/nea/firmament/mixins/IncomingPacketListenerPatches.java new file mode 100644 index 0000000..a7c3875 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/IncomingPacketListenerPatches.java @@ -0,0 +1,41 @@ + + +package moe.nea.firmament.mixins; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.mojang.brigadier.CommandDispatcher; +import moe.nea.firmament.events.MaskCommands; +import moe.nea.firmament.events.ParticleSpawnEvent; +import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.network.packet.s2c.play.ParticleS2CPacket; +import net.minecraft.util.math.Vec3d; +import org.joml.Vector3f; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ClientPlayNetworkHandler.class) +public abstract class IncomingPacketListenerPatches { + + @ModifyExpressionValue(method = "onCommandTree", at = @At(value = "NEW", target = "(Lcom/mojang/brigadier/tree/RootCommandNode;)Lcom/mojang/brigadier/CommandDispatcher;", remap = false)) + public CommandDispatcher onOnCommandTree(CommandDispatcher dispatcher) { + MaskCommands.Companion.publish(new MaskCommands(dispatcher)); + return dispatcher; + } + + @Inject(method = "onParticle", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/NetworkThreadUtils;forceMainThread(Lnet/minecraft/network/packet/Packet;Lnet/minecraft/network/listener/PacketListener;Lnet/minecraft/util/thread/ThreadExecutor;)V", shift = At.Shift.AFTER), cancellable = true) + public void onParticleSpawn(ParticleS2CPacket packet, CallbackInfo ci) { + var event = new ParticleSpawnEvent( + packet.getParameters(), + new Vec3d(packet.getX(), packet.getY(), packet.getZ()), + new Vector3f(packet.getOffsetX(), packet.getOffsetY(), packet.getOffsetZ()), + packet.isImportant(), + packet.getCount(), + packet.getSpeed() + ); + ParticleSpawnEvent.Companion.publish(event); + if (event.getCancelled()) + ci.cancel(); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/KeyPressInWorldEventPatch.java b/src/main/java/moe/nea/firmament/mixins/KeyPressInWorldEventPatch.java new file mode 100644 index 0000000..d2b3f91 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/KeyPressInWorldEventPatch.java @@ -0,0 +1,20 @@ + + +package moe.nea.firmament.mixins; + +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import moe.nea.firmament.events.WorldKeyboardEvent; +import net.minecraft.client.Keyboard; +import net.minecraft.client.util.InputUtil; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(Keyboard.class) +public class KeyPressInWorldEventPatch { + + @WrapWithCondition(method = "onKey", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/option/KeyBinding;onKeyPressed(Lnet/minecraft/client/util/InputUtil$Key;)V")) + public boolean onKeyBoardInWorld(InputUtil.Key key, long window, int _key, int scancode, int action, int modifiers) { + var event = WorldKeyboardEvent.Companion.publish(new WorldKeyboardEvent(_key, scancode, modifiers)); + return !event.getCancelled(); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/MainWindowFirstLoadPatch.java b/src/main/java/moe/nea/firmament/mixins/MainWindowFirstLoadPatch.java new file mode 100644 index 0000000..0a90b35 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/MainWindowFirstLoadPatch.java @@ -0,0 +1,31 @@ +package moe.nea.firmament.mixins; + +import moe.nea.firmament.Firmament; +import moe.nea.firmament.events.DebugInstantiateEvent; +import net.minecraft.client.gui.LogoDrawer; +import net.minecraft.client.gui.screen.TitleScreen; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(TitleScreen.class) +public class MainWindowFirstLoadPatch { + @Unique + private static boolean hasInited = false; + + @Inject(method = "(ZLnet/minecraft/client/gui/LogoDrawer;)V", at = @At("RETURN")) + private void onCreate(boolean doBackgroundFade, LogoDrawer logoDrawer, CallbackInfo ci) { + if (!hasInited) { + try { + DebugInstantiateEvent.Companion.publish(new DebugInstantiateEvent()); + } catch (Throwable t) { + Firmament.INSTANCE.getLogger().error("Failed to instantiate debug instances", t); + System.exit(1); + throw t; + } + } + hasInited = true; + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/MinecraftInitLevelListener.java b/src/main/java/moe/nea/firmament/mixins/MinecraftInitLevelListener.java new file mode 100644 index 0000000..1673987 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/MinecraftInitLevelListener.java @@ -0,0 +1,26 @@ +package moe.nea.firmament.mixins; + +import moe.nea.firmament.util.mc.InitLevel; +import net.minecraft.client.MinecraftClient; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(MinecraftClient.class) +public class MinecraftInitLevelListener { + @Inject(method = "", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderSystem;initBackendSystem()Lnet/minecraft/util/TimeSupplier$Nanoseconds;")) + private void onInitRenderBackend(CallbackInfo ci) { + InitLevel.bump(InitLevel.RENDER_INIT); + } + + @Inject(method = "", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderSystem;initRenderer(JIZLjava/util/function/BiFunction;Z)V")) + private void onInitRender(CallbackInfo ci) { + InitLevel.bump(InitLevel.RENDER); + } + + @Inject(method = "", at = @At(value = "TAIL")) + private void onFinishedLoading(CallbackInfo ci) { + InitLevel.bump(InitLevel.MAIN_MENU); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/MixinHandledScreen.java b/src/main/java/moe/nea/firmament/mixins/MixinHandledScreen.java new file mode 100644 index 0000000..43aec40 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/MixinHandledScreen.java @@ -0,0 +1,102 @@ + + +package moe.nea.firmament.mixins; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import moe.nea.firmament.events.HandledScreenClickEvent; +import moe.nea.firmament.events.HandledScreenForegroundEvent; +import moe.nea.firmament.events.HandledScreenKeyPressedEvent; +import moe.nea.firmament.events.IsSlotProtectedEvent; +import moe.nea.firmament.events.SlotRenderEvents; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.ingame.HandledScreen; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.screen.slot.Slot; +import net.minecraft.screen.slot.SlotActionType; +import net.minecraft.text.Text; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(value = HandledScreen.class, priority = 990) +public abstract class MixinHandledScreen { + + @Shadow + @Final + protected T handler; + + @Shadow + public abstract T getScreenHandler(); + + @Shadow + protected int y; + @Shadow + protected int x; + @Unique + PlayerInventory playerInventory; + + @Inject(method = "", at = @At("TAIL")) + public void savePlayerInventory(ScreenHandler handler, PlayerInventory inventory, Text title, CallbackInfo ci) { + this.playerInventory = inventory; + } + + @Inject(method = "keyPressed", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/ingame/HandledScreen;handleHotbarKeyPressed(II)Z", shift = At.Shift.BEFORE), cancellable = true) + public void onKeyPressed(int keyCode, int scanCode, int modifiers, CallbackInfoReturnable cir) { + if (HandledScreenKeyPressedEvent.Companion.publish(new HandledScreenKeyPressedEvent((HandledScreen) (Object) this, keyCode, scanCode, modifiers)).getCancelled()) { + cir.setReturnValue(true); + } + } + + @Inject(method = "mouseClicked", at = @At("HEAD"), cancellable = true) + public void onMouseClicked(double mouseX, double mouseY, int button, CallbackInfoReturnable cir) { + if (HandledScreenClickEvent.Companion.publish(new HandledScreenClickEvent((HandledScreen) (Object) this, mouseX, mouseY, button)).getCancelled()) { + cir.setReturnValue(true); + } + } + + @Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/ingame/HandledScreen;drawForeground(Lnet/minecraft/client/gui/DrawContext;II)V", shift = At.Shift.AFTER)) + public void onAfterRenderForeground(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) { + context.getMatrices().push(); + context.getMatrices().translate(-x, -y, 0); + HandledScreenForegroundEvent.Companion.publish(new HandledScreenForegroundEvent((HandledScreen) (Object) this, context, mouseX, mouseY, delta)); + context.getMatrices().pop(); + } + + @Inject(method = "onMouseClick(Lnet/minecraft/screen/slot/Slot;IILnet/minecraft/screen/slot/SlotActionType;)V", at = @At("HEAD"), cancellable = true) + public void onMouseClickedSlot(Slot slot, int slotId, int button, SlotActionType actionType, CallbackInfo ci) { + if (slotId == -999 && getScreenHandler() != null && actionType == SlotActionType.PICKUP) { // -999 is code for "clicked outside the main window" + ItemStack cursorStack = getScreenHandler().getCursorStack(); + if (cursorStack != null && IsSlotProtectedEvent.shouldBlockInteraction(slot, SlotActionType.THROW, IsSlotProtectedEvent.MoveOrigin.INVENTORY_MOVE, cursorStack)) { + ci.cancel(); + return; + } + } + if (IsSlotProtectedEvent.shouldBlockInteraction(slot, actionType, IsSlotProtectedEvent.MoveOrigin.INVENTORY_MOVE)) { + ci.cancel(); + return; + } + if (actionType == SlotActionType.SWAP && 0 <= button && button < 9) { + if (IsSlotProtectedEvent.shouldBlockInteraction(new Slot(playerInventory, button, 0, 0), actionType, IsSlotProtectedEvent.MoveOrigin.INVENTORY_MOVE)) { + ci.cancel(); + } + } + } + + + @WrapOperation(method = "drawSlots", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/ingame/HandledScreen;drawSlot(Lnet/minecraft/client/gui/DrawContext;Lnet/minecraft/screen/slot/Slot;)V")) + public void onDrawSlots(HandledScreen instance, DrawContext context, Slot slot, Operation original) { + var before = new SlotRenderEvents.Before(context, slot); + SlotRenderEvents.Before.Companion.publish(before); + original.call(instance, context, slot); + var after = new SlotRenderEvents.After(context, slot); + SlotRenderEvents.After.Companion.publish(after); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/MixinPlayerScreenHandler.java b/src/main/java/moe/nea/firmament/mixins/MixinPlayerScreenHandler.java new file mode 100644 index 0000000..12455f4 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/MixinPlayerScreenHandler.java @@ -0,0 +1,31 @@ +package moe.nea.firmament.mixins; + +import moe.nea.firmament.features.fixes.Fixes; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.screen.PlayerScreenHandler; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(PlayerScreenHandler.class) +public class MixinPlayerScreenHandler { + + @Unique + private static final int OFF_HAND_SLOT = 40; + + @Inject(method = "", at = @At("TAIL")) + private void moveOffHandSlot(PlayerInventory inventory, boolean onServer, PlayerEntity owner, CallbackInfo ci) { + if (Fixes.TConfig.INSTANCE.getHideOffHand()) { + PlayerScreenHandler self = (PlayerScreenHandler) (Object) this; + self.slots.stream() + .filter(slot -> slot.getIndex() == OFF_HAND_SLOT) + .forEach(slot -> { + slot.x = -1000; + slot.y = -1000; + }); + } + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/MixinRecipeBookScreen.java b/src/main/java/moe/nea/firmament/mixins/MixinRecipeBookScreen.java new file mode 100644 index 0000000..2dbe738 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/MixinRecipeBookScreen.java @@ -0,0 +1,16 @@ +package moe.nea.firmament.mixins; + +import moe.nea.firmament.features.fixes.Fixes; +import net.minecraft.client.gui.screen.ingame.RecipeBookScreen; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(value = RecipeBookScreen.class, priority = 999) +public class MixinRecipeBookScreen { + @Inject(method = "addRecipeBook", at = @At("HEAD"), cancellable = true) + public void addRecipeBook(CallbackInfo ci) { + if (Fixes.TConfig.INSTANCE.getHideRecipeBook()) ci.cancel(); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/OutgoingPacketEventPatch.java b/src/main/java/moe/nea/firmament/mixins/OutgoingPacketEventPatch.java new file mode 100644 index 0000000..25505b7 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/OutgoingPacketEventPatch.java @@ -0,0 +1,21 @@ + + +package moe.nea.firmament.mixins; + +import moe.nea.firmament.events.OutgoingPacketEvent; +import net.minecraft.client.network.ClientCommonNetworkHandler; +import net.minecraft.network.packet.Packet; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ClientCommonNetworkHandler.class) +public class OutgoingPacketEventPatch { + @Inject(method = "sendPacket(Lnet/minecraft/network/packet/Packet;)V", at = @At("HEAD"), cancellable = true) + public void onSendPacket(Packet packet, CallbackInfo ci) { + if (OutgoingPacketEvent.Companion.publish(new OutgoingPacketEvent(packet)).getCancelled()) { + ci.cancel(); + } + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/PlayerDropEventPatch.java b/src/main/java/moe/nea/firmament/mixins/PlayerDropEventPatch.java new file mode 100644 index 0000000..f07604e --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/PlayerDropEventPatch.java @@ -0,0 +1,28 @@ + + +package moe.nea.firmament.mixins; + +import moe.nea.firmament.events.IsSlotProtectedEvent; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.screen.slot.Slot; +import net.minecraft.screen.slot.SlotActionType; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(ClientPlayerEntity.class) +public abstract class PlayerDropEventPatch extends PlayerEntity { + public PlayerDropEventPatch() { + super(null, null, 0, null); + } + + @Inject(method = "dropSelectedItem", at = @At("HEAD"), cancellable = true) + public void onDropSelectedItem(boolean entireStack, CallbackInfoReturnable cir) { + Slot fakeSlot = new Slot(getInventory(), getInventory().getSelectedSlot(), 0, 0); + if (IsSlotProtectedEvent.shouldBlockInteraction(fakeSlot, SlotActionType.THROW, IsSlotProtectedEvent.MoveOrigin.DROP_FROM_HOTBAR)) { + cir.setReturnValue(false); + } + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/PropertySignatureIgnorePatchForSession.java b/src/main/java/moe/nea/firmament/mixins/PropertySignatureIgnorePatchForSession.java new file mode 100644 index 0000000..46995ec --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/PropertySignatureIgnorePatchForSession.java @@ -0,0 +1,24 @@ + +package moe.nea.firmament.mixins; + +import com.mojang.authlib.SignatureState; +import com.mojang.authlib.properties.Property; +import com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService; +import moe.nea.firmament.features.fixes.Fixes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(value = YggdrasilMinecraftSessionService.class, remap = false) +public class PropertySignatureIgnorePatchForSession { + @Inject(method = "getPropertySignatureState", at = @At("HEAD"), cancellable = true, remap = false) + public void markEverythingAsSigned(Property property, CallbackInfoReturnable cir) { + // Due to https://github.com/inglettronald/DulkirMod-Fabric/blob/22a3fc514a080fbe31f76f9ba7e85c36d8d0f67f/src/main/java/com/dulkirfabric/mixin/YggdrasilMinecraftSessionServiceMixin.java + // we sadly need to inject here too. Dulkirmod is very eager to early on mark a signature as unsigned + // and we want the opposite + if (Fixes.TConfig.INSTANCE.getFixUnsignedPlayerSkins()) { + cir.setReturnValue(SignatureState.SIGNED); + } + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/RedirectWithoutSubCommands.java b/src/main/java/moe/nea/firmament/mixins/RedirectWithoutSubCommands.java new file mode 100644 index 0000000..2186aee --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/RedirectWithoutSubCommands.java @@ -0,0 +1,42 @@ +package moe.nea.firmament.mixins; + +import com.llamalad7.mixinextras.sugar.Local; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.ParseResults; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.context.CommandContextBuilder; +import com.mojang.brigadier.tree.CommandNode; +import moe.nea.firmament.util.ErrorUtil; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.HashSet; + +@Mixin(CommandDispatcher.class) +public class RedirectWithoutSubCommands { + @Inject( + method = "parseNodes", + at = @At( + value = "INVOKE", + target = "Lcom/mojang/brigadier/context/CommandContextBuilder;withCommand(Lcom/mojang/brigadier/Command;)Lcom/mojang/brigadier/context/CommandContextBuilder;", + shift = At.Shift.AFTER + ) + ) + private void injectCommandForRedirects( + CommandNode node, StringReader originalReader, CommandContextBuilder contextSoFar, CallbackInfoReturnable> cir, + @Local(index = 10) CommandContextBuilder context, + @Local(index = 9) CommandNode child + ) { + var p = child; + var set = new HashSet<>(); + if (context.getCommand() == null && p.getRedirect() != null) { + p = p.getRedirect(); + context.withCommand(p.getCommand()); + if (!set.add(p)) { + ErrorUtil.INSTANCE.softError("Redirect circle detected in " + p); + } + } + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/ResourceReloaderRegistrationPatch.java b/src/main/java/moe/nea/firmament/mixins/ResourceReloaderRegistrationPatch.java new file mode 100644 index 0000000..28fe3d9 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/ResourceReloaderRegistrationPatch.java @@ -0,0 +1,26 @@ + +package moe.nea.firmament.mixins; + +import moe.nea.firmament.events.FinalizeResourceManagerEvent; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.RunArgs; +import net.minecraft.resource.ReloadableResourceManagerImpl; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(MinecraftClient.class) +public class ResourceReloaderRegistrationPatch { + @Shadow + @Final + private ReloadableResourceManagerImpl resourceManager; + + @Inject(method = "", at = @At(value = "INVOKE", target = "Lnet/minecraft/resource/ResourcePackManager;createResourcePacks()Ljava/util/List;", shift = At.Shift.BEFORE)) + private void onBeforeResourcePackCreation(RunArgs args, CallbackInfo ci) { + FinalizeResourceManagerEvent.Companion.publish(new FinalizeResourceManagerEvent(this.resourceManager)); + } +} + diff --git a/src/main/java/moe/nea/firmament/mixins/SaveCursorPositionPatch.java b/src/main/java/moe/nea/firmament/mixins/SaveCursorPositionPatch.java new file mode 100644 index 0000000..fd3adca --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/SaveCursorPositionPatch.java @@ -0,0 +1,40 @@ + + +package moe.nea.firmament.mixins; + +import kotlin.Pair; +import moe.nea.firmament.features.inventory.SaveCursorPosition; +import net.minecraft.client.Mouse; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Mouse.class) +public class SaveCursorPositionPatch { + @Shadow + private double x; + + @Shadow + private double y; + + @Inject(method = "lockCursor", at = @At(value = "FIELD", opcode = Opcodes.PUTFIELD, target = "Lnet/minecraft/client/Mouse;cursorLocked:Z")) + public void onLockCursor(CallbackInfo ci) { + SaveCursorPosition.saveCursorOriginal(x, y); + } + + @Inject(method = "lockCursor", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/util/Window;getHandle()J")) + public void onLockCursorAfter(CallbackInfo ci) { + SaveCursorPosition.saveCursorMiddle(x, y); + } + + @Inject(method = "unlockCursor", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/util/Window;getHandle()J")) + public void onUnlockCursor(CallbackInfo ci) { + Pair cursorPosition = SaveCursorPosition.loadCursor(this.x, this.y); + if (cursorPosition == null) return; + this.x = cursorPosition.getFirst(); + this.y = cursorPosition.getSecond(); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/SaveOriginalCommandTreePacket.java b/src/main/java/moe/nea/firmament/mixins/SaveOriginalCommandTreePacket.java new file mode 100644 index 0000000..2f2f188 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/SaveOriginalCommandTreePacket.java @@ -0,0 +1,17 @@ +package moe.nea.firmament.mixins; + +import moe.nea.firmament.features.chat.QuickCommands; +import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.network.packet.s2c.play.CommandTreeS2CPacket; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ClientPlayNetworkHandler.class) +public class SaveOriginalCommandTreePacket { + @Inject(method = "onCommandTree", at = @At(value = "RETURN")) + private void saveUnmodifiedCommandTree(CommandTreeS2CPacket packet, CallbackInfo ci) { + QuickCommands.INSTANCE.setLastReceivedTreePacket(packet); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/ScreenChangeEventPatch.java b/src/main/java/moe/nea/firmament/mixins/ScreenChangeEventPatch.java new file mode 100644 index 0000000..6d19405 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/ScreenChangeEventPatch.java @@ -0,0 +1,32 @@ + + +package moe.nea.firmament.mixins; + +import com.llamalad7.mixinextras.sugar.Local; +import com.llamalad7.mixinextras.sugar.ref.LocalRef; +import moe.nea.firmament.events.ScreenChangeEvent; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.Screen; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(MinecraftClient.class) +public abstract class ScreenChangeEventPatch { + @Shadow + @Nullable + public Screen currentScreen; + + @Inject(method = "setScreen", at = @At("HEAD"), cancellable = true) + public void onScreenChange(Screen screen, CallbackInfo ci, @Local(argsOnly = true) LocalRef screenLocalRef) { + var event = new ScreenChangeEvent(currentScreen, screen); + if (ScreenChangeEvent.Companion.publish(event).getCancelled()) { + ci.cancel(); + } else if (event.getOverrideScreen() != null) { + screenLocalRef.set(event.getOverrideScreen()); + } + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/SlotClickEventPatch.java b/src/main/java/moe/nea/firmament/mixins/SlotClickEventPatch.java new file mode 100644 index 0000000..21e7899 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/SlotClickEventPatch.java @@ -0,0 +1,41 @@ + +package moe.nea.firmament.mixins; + +import com.llamalad7.mixinextras.sugar.Local; +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalRef; +import moe.nea.firmament.events.SlotClickEvent; +import net.minecraft.client.network.ClientPlayerInteractionManager; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.screen.slot.SlotActionType; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ClientPlayerInteractionManager.class) +public class SlotClickEventPatch { + + @Inject(method = "clickSlot", at = @At(value = "FIELD", target = "Lnet/minecraft/screen/ScreenHandler;slots:Lnet/minecraft/util/collection/DefaultedList;", opcode = Opcodes.GETFIELD)) + private void onSlotClickSaveSlot(int syncId, int slotId, int button, SlotActionType actionType, PlayerEntity player, CallbackInfo ci, @Local ScreenHandler handler, @Share("slotContent") LocalRef slotContent) { + if (0 <= slotId && slotId < handler.slots.size()) { + slotContent.set(handler.getSlot(slotId).getStack().copy()); + } + } + + @Inject(method = "clickSlot", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayNetworkHandler;sendPacket(Lnet/minecraft/network/packet/Packet;)V")) + private void onSlotClick(int syncId, int slotId, int button, SlotActionType actionType, PlayerEntity player, CallbackInfo ci, @Local ScreenHandler handler, @Share("slotContent") LocalRef slotContent) { + if (0 <= slotId && slotId < handler.slots.size()) { + SlotClickEvent.Companion.publish(new SlotClickEvent( + handler.getSlot(slotId), + slotContent.get(), + button, + actionType + )); + } + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/SlotUpdateListener.java b/src/main/java/moe/nea/firmament/mixins/SlotUpdateListener.java new file mode 100644 index 0000000..a4ae931 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/SlotUpdateListener.java @@ -0,0 +1,54 @@ + +package moe.nea.firmament.mixins; + +import moe.nea.firmament.events.ChestInventoryUpdateEvent; +import moe.nea.firmament.events.PlayerInventoryUpdate; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientCommonNetworkHandler; +import net.minecraft.client.network.ClientConnectionState; +import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.network.ClientConnection; +import net.minecraft.network.packet.s2c.play.InventoryS2CPacket; +import net.minecraft.network.packet.s2c.play.ScreenHandlerSlotUpdateS2CPacket; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ClientPlayNetworkHandler.class) +public abstract class SlotUpdateListener extends ClientCommonNetworkHandler { + protected SlotUpdateListener(MinecraftClient client, ClientConnection connection, ClientConnectionState connectionState) { + super(client, connection, connectionState); + } + + @Inject( + method = "onScreenHandlerSlotUpdate", + at = @At(value = "TAIL")) + private void onSingleSlotUpdate( + ScreenHandlerSlotUpdateS2CPacket packet, + CallbackInfo ci) { + var player = this.client.player; + assert player != null; + if (packet.getSyncId() == 0) { + PlayerInventoryUpdate.Companion.publish(new PlayerInventoryUpdate.Single(packet.getSlot(), packet.getStack())); + } else if (packet.getSyncId() == player.currentScreenHandler.syncId) { + ChestInventoryUpdateEvent.Companion.publish( + new ChestInventoryUpdateEvent.Single(packet.getSlot(), packet.getStack()) + ); + } + } + + @Inject(method = "onInventory", + at = @At("TAIL")) + private void onMultiSlotUpdate(InventoryS2CPacket packet, CallbackInfo ci) { + var player = this.client.player; + assert player != null; + if (packet.syncId() == 0) { + PlayerInventoryUpdate.Companion.publish(new PlayerInventoryUpdate.Multi(packet.contents())); + } else if (packet.syncId() == player.currentScreenHandler.syncId) { + ChestInventoryUpdateEvent.Companion.publish( + new ChestInventoryUpdateEvent.Multi(packet.contents()) + ); + } + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/SoundReceiveEventPatch.java b/src/main/java/moe/nea/firmament/mixins/SoundReceiveEventPatch.java new file mode 100644 index 0000000..b8cba80 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/SoundReceiveEventPatch.java @@ -0,0 +1,32 @@ + +package moe.nea.firmament.mixins; + +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import moe.nea.firmament.events.SoundReceiveEvent; +import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.Entity; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.sound.SoundCategory; +import net.minecraft.sound.SoundEvent; +import net.minecraft.util.math.Vec3d; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(ClientPlayNetworkHandler.class) +public class SoundReceiveEventPatch { + @WrapWithCondition(method = "onPlaySound", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/world/ClientWorld;playSound(Lnet/minecraft/entity/Entity;DDDLnet/minecraft/registry/entry/RegistryEntry;Lnet/minecraft/sound/SoundCategory;FFJ)V")) + private boolean postEventWhenSoundIsPlayed(ClientWorld instance, @Nullable Entity source, double x, double y, double z, RegistryEntry sound, SoundCategory category, float volume, float pitch, long seed) { + var event = new SoundReceiveEvent( + sound, + category, + new Vec3d(x,y,z), + pitch, + volume, + seed + ); + SoundReceiveEvent.Companion.publish(event); + return !event.getCancelled(); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/TextureUnpackBase64PadPatch.java b/src/main/java/moe/nea/firmament/mixins/TextureUnpackBase64PadPatch.java new file mode 100644 index 0000000..43ce950 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/TextureUnpackBase64PadPatch.java @@ -0,0 +1,19 @@ + +package moe.nea.firmament.mixins; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService; +import moe.nea.firmament.util.Base64Util; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(value = YggdrasilMinecraftSessionService.class, remap = false) +public class TextureUnpackBase64PadPatch { + @ModifyExpressionValue(method = "unpackTextures", + remap = false, + at = @At(value = "INVOKE", target = "Lcom/mojang/authlib/properties/Property;value()Ljava/lang/String;")) + private String base64PadTexture(String original) { + if (original.length() % 4 == 0) return original; + return Base64Util.INSTANCE.padToValidBase64(original); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/ToggleSprintPatch.java b/src/main/java/moe/nea/firmament/mixins/ToggleSprintPatch.java new file mode 100644 index 0000000..1acbf20 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/ToggleSprintPatch.java @@ -0,0 +1,18 @@ + + +package moe.nea.firmament.mixins; + +import moe.nea.firmament.features.fixes.Fixes; +import net.minecraft.client.option.KeyBinding; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(KeyBinding.class) +public class ToggleSprintPatch { + @Inject(method = "isPressed", at = @At("HEAD"), cancellable = true) + public void onIsPressed(CallbackInfoReturnable cir) { + Fixes.INSTANCE.handleIsPressed((KeyBinding) (Object) this, cir); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/TolerateFirmamentTolerateRegistryOwners.java b/src/main/java/moe/nea/firmament/mixins/TolerateFirmamentTolerateRegistryOwners.java new file mode 100644 index 0000000..ac6f614 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/TolerateFirmamentTolerateRegistryOwners.java @@ -0,0 +1,18 @@ +package moe.nea.firmament.mixins; + +import moe.nea.firmament.util.mc.TolerantRegistriesOps; +import net.minecraft.registry.entry.RegistryEntryOwner; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(RegistryEntryOwner.class) +public interface TolerateFirmamentTolerateRegistryOwners { + @Inject(method = "ownerEquals", at = @At("HEAD"), cancellable = true) + private void equalTolerantRegistryOwners(RegistryEntryOwner other, CallbackInfoReturnable cir) { + if (other instanceof TolerantRegistriesOps.TolerantOwner) { + cir.setReturnValue(true); + } + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/WorldReadyEventPatch.java b/src/main/java/moe/nea/firmament/mixins/WorldReadyEventPatch.java new file mode 100644 index 0000000..d4b8c9e --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/WorldReadyEventPatch.java @@ -0,0 +1,19 @@ + + +package moe.nea.firmament.mixins; + +import moe.nea.firmament.events.WorldReadyEvent; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.DownloadingTerrainScreen; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(MinecraftClient.class) +public class WorldReadyEventPatch { + @Inject(method = "joinWorld", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MinecraftClient;setWorld(Lnet/minecraft/client/world/ClientWorld;)V", shift = At.Shift.AFTER)) + public void onClose(CallbackInfo ci) { + WorldReadyEvent.Companion.publish(new WorldReadyEvent()); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/WorldRenderLastEventPatch.java b/src/main/java/moe/nea/firmament/mixins/WorldRenderLastEventPatch.java new file mode 100644 index 0000000..3ed8c1b --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/WorldRenderLastEventPatch.java @@ -0,0 +1,45 @@ + + +package moe.nea.firmament.mixins; + +import moe.nea.firmament.events.WorldRenderLastEvent; +import net.minecraft.client.render.*; +import net.minecraft.client.util.Handle; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.profiler.Profiler; +import org.joml.Matrix4f; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(WorldRenderer.class) +public abstract class WorldRenderLastEventPatch { + @Shadow + @Final + private BufferBuilderStorage bufferBuilders; + + @Shadow + @Final + private DefaultFramebufferSet framebufferSet; + + @Shadow + protected abstract void checkEmpty(MatrixStack matrices); + + @Inject(method = "method_62214", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/profiler/Profiler;pop()V", shift = At.Shift.AFTER)) + public void onWorldRenderLast(Fog fog, RenderTickCounter renderTickCounter, Camera camera, Profiler profiler, Matrix4f matrix4f, Matrix4f matrix4f2, Handle handle, Handle handle2, boolean bl, Frustum frustum, Handle handle3, Handle handle4, CallbackInfo ci) { + var imm = this.bufferBuilders.getEntityVertexConsumers(); + var stack = new MatrixStack(); + // TODO: pre-cancel this event if F1 is active + var event = new WorldRenderLastEvent( + stack, renderTickCounter, + camera, + imm + ); + WorldRenderLastEvent.Companion.publish(event); + imm.draw(); + checkEmpty(stack); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/YggdrasilSignatureIgnorePatch.java b/src/main/java/moe/nea/firmament/mixins/YggdrasilSignatureIgnorePatch.java new file mode 100644 index 0000000..2f1499c --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/YggdrasilSignatureIgnorePatch.java @@ -0,0 +1,21 @@ + + +package moe.nea.firmament.mixins; + +import com.mojang.authlib.properties.Property; +import com.mojang.authlib.yggdrasil.YggdrasilServicesKeyInfo; +import moe.nea.firmament.features.fixes.Fixes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(value = YggdrasilServicesKeyInfo.class, remap = false) +public class YggdrasilSignatureIgnorePatch { + @Inject(method = "validateProperty", at = @At("HEAD"), cancellable = true, remap = false) + public void validate(Property property, CallbackInfoReturnable cir) { + if (Fixes.TConfig.INSTANCE.getFixUnsignedPlayerSkins()) { + cir.setReturnValue(true); + } + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/accessor/AccessorAbstractClientPlayerEntity.java b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorAbstractClientPlayerEntity.java new file mode 100644 index 0000000..0a10046 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorAbstractClientPlayerEntity.java @@ -0,0 +1,13 @@ + +package moe.nea.firmament.mixins.accessor; + +import net.minecraft.client.network.AbstractClientPlayerEntity; +import net.minecraft.client.network.PlayerListEntry; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(AbstractClientPlayerEntity.class) +public interface AccessorAbstractClientPlayerEntity { + @Accessor("playerListEntry") + void setPlayerListEntry_firmament(PlayerListEntry playerListEntry); +} diff --git a/src/main/java/moe/nea/firmament/mixins/accessor/AccessorChatHud.java b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorChatHud.java new file mode 100644 index 0000000..d164aac --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorChatHud.java @@ -0,0 +1,24 @@ +package moe.nea.firmament.mixins.accessor; + +import net.minecraft.client.gui.hud.ChatHud; +import net.minecraft.client.gui.hud.ChatHudLine; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; + +import java.util.List; + +@Mixin(ChatHud.class) +public interface AccessorChatHud { + @Accessor("messages") + List getMessages_firmament(); + + @Accessor("visibleMessages") + List getVisibleMessages_firmament(); + + @Accessor("scrolledLines") + int getScrolledLines_firmament(); + + @Invoker("toChatLineY") + double toChatLineY_firmament(double y); +} diff --git a/src/main/java/moe/nea/firmament/mixins/accessor/AccessorHandledScreen.java b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorHandledScreen.java new file mode 100644 index 0000000..f55ef4f --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorHandledScreen.java @@ -0,0 +1,39 @@ +package moe.nea.firmament.mixins.accessor; + +import net.minecraft.client.gui.screen.ingame.HandledScreen; +import net.minecraft.screen.slot.Slot; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(HandledScreen.class) +public interface AccessorHandledScreen { + @Accessor("focusedSlot") + @Nullable + Slot getFocusedSlot_Firmament(); + + @Accessor("backgroundWidth") + int getBackgroundWidth_Firmament(); + + @Accessor("backgroundWidth") + void setBackgroundWidth_Firmament(int newBackgroundWidth); + + @Accessor("backgroundHeight") + int getBackgroundHeight_Firmament(); + + @Accessor("backgroundHeight") + void setBackgroundHeight_Firmament(int newBackgroundHeight); + + @Accessor("x") + int getX_Firmament(); + + @Accessor("x") + void setX_Firmament(int newX); + + @Accessor("y") + int getY_Firmament(); + + @Accessor("y") + void setY_Firmament(int newY); + +} diff --git a/src/main/java/moe/nea/firmament/mixins/accessor/AccessorPlayerListHud.java b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorPlayerListHud.java new file mode 100644 index 0000000..81ea0fd --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorPlayerListHud.java @@ -0,0 +1,31 @@ +package moe.nea.firmament.mixins.accessor; + +import net.minecraft.client.gui.hud.PlayerListHud; +import net.minecraft.client.network.PlayerListEntry; +import net.minecraft.text.Text; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; + +import java.util.Comparator; +import java.util.List; + +@Mixin(PlayerListHud.class) +public interface AccessorPlayerListHud { + + @Accessor("ENTRY_ORDERING") + static Comparator getEntryOrdering() { + throw new AssertionError(); + } + + @Invoker("collectPlayerEntries") + List collectPlayerEntries_firmament(); + + @Accessor("footer") + @Nullable Text getFooter_firmament(); + + @Accessor("header") + @Nullable Text getHeader_firmament(); + +} diff --git a/src/main/java/moe/nea/firmament/mixins/accessor/AccessorWorldRenderer.java b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorWorldRenderer.java new file mode 100644 index 0000000..8b25562 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorWorldRenderer.java @@ -0,0 +1,17 @@ +package moe.nea.firmament.mixins.accessor; + +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import net.minecraft.client.render.WorldRenderer; +import net.minecraft.entity.player.BlockBreakingInfo; +import org.jetbrains.annotations.NotNull; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.SortedSet; + +@Mixin(WorldRenderer.class) +public interface AccessorWorldRenderer { + @Accessor("blockBreakingProgressions") + @NotNull + Long2ObjectMap> getBlockBreakingProgressions_firmament(); +} diff --git a/src/main/java/moe/nea/firmament/mixins/customgui/OriginalSlotCoords.java b/src/main/java/moe/nea/firmament/mixins/customgui/OriginalSlotCoords.java new file mode 100644 index 0000000..c705625 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/customgui/OriginalSlotCoords.java @@ -0,0 +1,43 @@ + +package moe.nea.firmament.mixins.customgui; + +import moe.nea.firmament.util.customgui.CoordRememberingSlot; +import net.minecraft.screen.slot.Slot; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(Slot.class) +public class OriginalSlotCoords implements CoordRememberingSlot { + + @Shadow + public int x; + @Shadow + public int y; + @Unique + public int originalX; + @Unique + public int originalY; + + @Override + public void rememberCoords_firmament() { + this.originalX = this.x; + this.originalY = this.y; + } + + @Override + public void restoreCoords_firmament() { + this.x = this.originalX; + this.y = this.originalY; + } + + @Override + public int getOriginalX_firmament() { + return originalX; + } + + @Override + public int getOriginalY_firmament() { + return originalY; + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/customgui/PatchHandledScreen.java b/src/main/java/moe/nea/firmament/mixins/customgui/PatchHandledScreen.java new file mode 100644 index 0000000..6e1090a --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/customgui/PatchHandledScreen.java @@ -0,0 +1,212 @@ + +package moe.nea.firmament.mixins.customgui; + +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import moe.nea.firmament.events.HandledScreenKeyReleasedEvent; +import moe.nea.firmament.util.customgui.CoordRememberingSlot; +import moe.nea.firmament.util.customgui.CustomGui; +import moe.nea.firmament.util.customgui.HasCustomGui; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.ingame.HandledScreen; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.Text; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(HandledScreen.class) +public class PatchHandledScreen extends Screen implements HasCustomGui { + @Shadow + @Final + protected T handler; + @Shadow + protected int x; + @Shadow + protected int y; + @Shadow + protected int backgroundHeight; + @Shadow + protected int backgroundWidth; + @Unique + public CustomGui override; + @Unique + public boolean hasRememberedSlots = false; + @Unique + private int originalBackgroundWidth; + @Unique + private int originalBackgroundHeight; + + protected PatchHandledScreen(Text title) { + super(title); + } + + @Nullable + @Override + public CustomGui getCustomGui_Firmament() { + return override; + } + + @Override + public void setCustomGui_Firmament(@Nullable CustomGui gui) { + if (this.override != null) { + backgroundHeight = originalBackgroundHeight; + backgroundWidth = originalBackgroundWidth; + } + if (gui != null) { + originalBackgroundHeight = backgroundHeight; + originalBackgroundWidth = backgroundWidth; + } + this.override = gui; + } + + public boolean mouseScrolled_firmament(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) { + return override != null && override.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount); + } + + public boolean keyReleased_firmament(int keyCode, int scanCode, int modifiers) { + if (HandledScreenKeyReleasedEvent.Companion.publish(new HandledScreenKeyReleasedEvent((HandledScreen) (Object) this, keyCode, scanCode, modifiers)).getCancelled()) + return true; + return override != null && override.keyReleased(keyCode, scanCode, modifiers); + } + + public boolean charTyped_firmament(char chr, int modifiers) { + return override != null && override.charTyped(chr, modifiers); + } + + @Inject(method = "init", at = @At("TAIL")) + private void onInit(CallbackInfo ci) { + if (override != null) { + override.onInit(); + } + } + + @Inject(method = "drawForeground", at = @At("HEAD"), cancellable = true) + private void onDrawForeground(DrawContext context, int mouseX, int mouseY, CallbackInfo ci) { + if (override != null && !override.shouldDrawForeground()) + ci.cancel(); + } + + + @WrapOperation( + method = "drawSlots", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/gui/screen/ingame/HandledScreen;drawSlot(Lnet/minecraft/client/gui/DrawContext;Lnet/minecraft/screen/slot/Slot;)V")) + private void beforeSlotRender(HandledScreen instance, DrawContext context, Slot slot, Operation original) { + if (override != null) { + override.beforeSlotRender(context, slot); + } + original.call(instance, context, slot); + if (override != null) { + override.afterSlotRender(context, slot); + } + } + + @Inject(method = "isClickOutsideBounds", at = @At("HEAD"), cancellable = true) + public void onIsClickOutsideBounds(double mouseX, double mouseY, int left, int top, int button, CallbackInfoReturnable cir) { + if (override != null) { + cir.setReturnValue(override.isClickOutsideBounds(mouseX, mouseY)); + } + } + + @Inject(method = "isPointWithinBounds", at = @At("HEAD"), cancellable = true) + public void onIsPointWithinBounds(int x, int y, int width, int height, double pointX, double pointY, CallbackInfoReturnable cir) { + if (override != null) { + cir.setReturnValue(override.isPointWithinBounds(x + this.x, y + this.y, width, height, pointX, pointY)); + } + } + + @Inject(method = "isPointOverSlot", at = @At("HEAD"), cancellable = true) + public void onIsPointOverSlot(Slot slot, double pointX, double pointY, CallbackInfoReturnable cir) { + if (override != null) { + cir.setReturnValue(override.isPointOverSlot(slot, this.x, this.y, pointX, pointY)); + } + } + + @Inject(method = "render", at = @At("HEAD")) + public void moveSlots(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) { + if (override != null) { + for (Slot slot : handler.slots) { + if (!hasRememberedSlots) { + ((CoordRememberingSlot) slot).rememberCoords_firmament(); + } + override.moveSlot(slot); + } + hasRememberedSlots = true; + } else { + if (hasRememberedSlots) { + for (Slot slot : handler.slots) { + ((CoordRememberingSlot) slot).restoreCoords_firmament(); + } + hasRememberedSlots = false; + } + } + } + + @Inject(at = @At("HEAD"), method = "close", cancellable = true) + private void onVoluntaryExit(CallbackInfo ci) { + if (override != null) { + if (!override.onVoluntaryExit()) + ci.cancel(); + } + } + + @WrapWithCondition(method = "renderBackground", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/ingame/HandledScreen;drawBackground(Lnet/minecraft/client/gui/DrawContext;FII)V")) + public boolean preventDrawingBackground(HandledScreen instance, DrawContext drawContext, float delta, int mouseX, int mouseY) { + if (override != null) { + override.render(drawContext, delta, mouseX, mouseY); + } + return override == null; + } + + @WrapOperation( + method = "mouseClicked", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/Screen;mouseClicked(DDI)Z")) + public boolean overrideMouseClicks(HandledScreen instance, double mouseX, double mouseY, int button, + Operation original) { + if (override != null) { + if (override.mouseClick(mouseX, mouseY, button)) + return true; + } + return original.call(instance, mouseX, mouseY, button); + } + + @Inject(method = "mouseDragged", at = @At("HEAD"), cancellable = true) + public void overrideMouseDrags(double mouseX, double mouseY, int button, double deltaX, double deltaY, CallbackInfoReturnable cir) { + if (override != null) { + if (override.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)) + cir.setReturnValue(true); + } + } + + @Inject(method = "keyPressed", at = @At("HEAD"), cancellable = true) + private void overrideKeyPressed(int keyCode, int scanCode, int modifiers, CallbackInfoReturnable cir) { + if (override != null) { + if (override.keyPressed(keyCode, scanCode, modifiers)) { + cir.setReturnValue(true); + } + } + } + + + @Inject( + method = "mouseReleased", + at = @At("HEAD"), cancellable = true) + public void overrideMouseReleases(double mouseX, double mouseY, int button, CallbackInfoReturnable cir) { + if (override != null) { + if (override.mouseReleased(mouseX, mouseY, button)) + cir.setReturnValue(true); + } + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/devenv/DisableCommonPacketWarnings.java b/src/main/java/moe/nea/firmament/mixins/devenv/DisableCommonPacketWarnings.java new file mode 100644 index 0000000..a15d825 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/devenv/DisableCommonPacketWarnings.java @@ -0,0 +1,42 @@ + + +package moe.nea.firmament.mixins.devenv; + +import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.util.Identifier; +import org.slf4j.Logger; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Objects; + +@Mixin(ClientPlayNetworkHandler.class) +public class DisableCommonPacketWarnings { + + @Inject(method = "warnOnUnknownPayload", at = @At("HEAD"), cancellable = true) + public void onCustomPacketError(CustomPayload customPayload, CallbackInfo ci) { + if (Objects.equals(customPayload.getId(), Identifier.of("badlion", "mods"))) { + ci.cancel(); + } + } + + @Redirect(method = "onEntityPassengersSet", at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;)V", remap = false)) + public void onUnknownPassenger(Logger instance, String s) { + // Ignore passenger data for unknown entities, since HyPixel just sends a lot of those. + } + + @Redirect(method = "onTeam", at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;[Ljava/lang/Object;)V", remap = false)) + public void onOnTeam(Logger instance, String s, Object[] objects) { + // Ignore data for unknown teams, since HyPixel just sends a lot of invalid team data. + } + + @Redirect(method = "onPlayerList", at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;)V", remap = false)) + public void onOnPlayerList(Logger instance, String s, Object o, Object o2) { + // Ignore invalid player info, since HyPixel just sends a lot of invalid player info + } + +} diff --git a/src/main/java/moe/nea/firmament/mixins/devenv/DisableInvalidFishingHook.java b/src/main/java/moe/nea/firmament/mixins/devenv/DisableInvalidFishingHook.java new file mode 100644 index 0000000..689a757 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/devenv/DisableInvalidFishingHook.java @@ -0,0 +1,17 @@ + + +package moe.nea.firmament.mixins.devenv; + +import net.minecraft.entity.projectile.FishingBobberEntity; +import org.slf4j.Logger; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(FishingBobberEntity.class) +public class DisableInvalidFishingHook { + @Redirect(method = "onSpawnPacket", at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;error(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;)V", remap = false)) + public void onOnSpawnPacket(Logger instance, String s, Object o, Object o1) { + // Don't warn for broken fishing hooks, since HyPixel sends a bunch of those + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/devenv/EarlyInstantiateTranslations.java b/src/main/java/moe/nea/firmament/mixins/devenv/EarlyInstantiateTranslations.java new file mode 100644 index 0000000..ef8c9eb --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/devenv/EarlyInstantiateTranslations.java @@ -0,0 +1,19 @@ +package moe.nea.firmament.mixins.devenv; + +import net.minecraft.text.TranslatableTextContent; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(TranslatableTextContent.class) +public abstract class EarlyInstantiateTranslations { + @Shadow + protected abstract void updateTranslations(); + + @Inject(method = "", at = @At("TAIL")) + private void onInit(String key, String fallback, Object[] args, CallbackInfo ci) { + updateTranslations(); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/devenv/IdentifyCloser.java b/src/main/java/moe/nea/firmament/mixins/devenv/IdentifyCloser.java new file mode 100644 index 0000000..6620b47 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/devenv/IdentifyCloser.java @@ -0,0 +1,16 @@ + +package moe.nea.firmament.mixins.devenv; + +import net.minecraft.client.util.Window; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Window.class) +public class IdentifyCloser { + @Inject(method = "close", at = @At("HEAD")) + public void onClose(CallbackInfo ci) { + Thread.dumpStack(); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/devenv/IdentifyStopperPatch.java b/src/main/java/moe/nea/firmament/mixins/devenv/IdentifyStopperPatch.java new file mode 100644 index 0000000..fac0688 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/devenv/IdentifyStopperPatch.java @@ -0,0 +1,16 @@ + +package moe.nea.firmament.mixins.devenv; + +import net.minecraft.client.MinecraftClient; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(MinecraftClient.class) +public class IdentifyStopperPatch { + @Inject(method = "scheduleStop", at = @At("HEAD")) + private void onStop(CallbackInfo ci) { + Thread.dumpStack(); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/devenv/MixinKeyboard.java b/src/main/java/moe/nea/firmament/mixins/devenv/MixinKeyboard.java new file mode 100644 index 0000000..d7b6cc3 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/devenv/MixinKeyboard.java @@ -0,0 +1,20 @@ + + +package moe.nea.firmament.mixins.devenv; + +import moe.nea.firmament.features.debug.DeveloperFeatures; +import net.minecraft.client.Keyboard; +import net.minecraft.client.MinecraftClient; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import java.util.concurrent.CompletableFuture; + +@Mixin(Keyboard.class) +public class MixinKeyboard { + @Redirect(method = "processF3", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MinecraftClient;reloadResources()Ljava/util/concurrent/CompletableFuture;")) + public CompletableFuture redirectReloadResources(MinecraftClient instance) { + return DeveloperFeatures.hookOnBeforeResourceReload(instance); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/devenv/MixinScoreboard.java b/src/main/java/moe/nea/firmament/mixins/devenv/MixinScoreboard.java new file mode 100644 index 0000000..34a733c --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/devenv/MixinScoreboard.java @@ -0,0 +1,17 @@ + + +package moe.nea.firmament.mixins.devenv; + +import net.minecraft.scoreboard.Scoreboard; +import org.slf4j.Logger; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(Scoreboard.class) +public class MixinScoreboard { + @Redirect(method = "addTeam", at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;Ljava/lang/Object;)V", remap = false)) + public void onExistingteam(Logger instance, String s, Object o) { + // Ignore creations of existing teams + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/devenv/WarnForUnknownCustomPayloadSends.java b/src/main/java/moe/nea/firmament/mixins/devenv/WarnForUnknownCustomPayloadSends.java new file mode 100644 index 0000000..6d44e29 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/devenv/WarnForUnknownCustomPayloadSends.java @@ -0,0 +1,18 @@ + +package moe.nea.firmament.mixins.devenv; + +import moe.nea.firmament.Firmament; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.network.packet.UnknownCustomPayload; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(UnknownCustomPayload.class) +public class WarnForUnknownCustomPayloadSends { + @Inject(method = "method_56493", at = @At("HEAD")) + private static void warn(UnknownCustomPayload value, PacketByteBuf buf, CallbackInfo ci) { + Firmament.INSTANCE.getLogger().warn("Unknown custom payload is being sent: {}", value); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/devenv/WarnOnMissingTranslations.java b/src/main/java/moe/nea/firmament/mixins/devenv/WarnOnMissingTranslations.java new file mode 100644 index 0000000..33840c1 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/devenv/WarnOnMissingTranslations.java @@ -0,0 +1,38 @@ +package moe.nea.firmament.mixins.devenv; + +import moe.nea.firmament.features.debug.DeveloperFeatures; +import moe.nea.firmament.util.MC; +import net.minecraft.client.resource.language.TranslationStorage; +import net.minecraft.text.Text; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.Set; +import java.util.TreeSet; + +@Mixin(TranslationStorage.class) +public abstract class WarnOnMissingTranslations { + @Shadow + public abstract boolean hasTranslation(String key); + + @Unique + private final Set missingTranslations = new TreeSet<>(); + + @Inject(method = "get", at = @At("HEAD")) + private void onGetTranslationKey(String key, String fallback, CallbackInfoReturnable cir) { + warnForMissingTranslation(key); + } + + @Unique + private void warnForMissingTranslation(String key) { + if (!key.contains("firmament")) return; + if (hasTranslation(key)) return; + if (!missingTranslations.add(key)) return; + MC.INSTANCE.sendChat(Text.literal("Missing firmament translation: " + key)); + DeveloperFeatures.hookMissingTranslations(missingTranslations); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/feature/DisableSlotHighlights.java b/src/main/java/moe/nea/firmament/mixins/feature/DisableSlotHighlights.java new file mode 100644 index 0000000..0abed22 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/feature/DisableSlotHighlights.java @@ -0,0 +1,25 @@ +package moe.nea.firmament.mixins.feature; + +import moe.nea.firmament.features.fixes.Fixes; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.slot.Slot; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(Slot.class) +public abstract class DisableSlotHighlights { + @Shadow + public abstract ItemStack getStack(); + + @Inject(method = "canBeHighlighted", at = @At("HEAD"), cancellable = true) + private void dontHighlight(CallbackInfoReturnable cir) { + if (!Fixes.TConfig.INSTANCE.getHideSlotHighlights()) return; + var display = getStack().get(DataComponentTypes.TOOLTIP_DISPLAY); + if (display != null && display.hideTooltip()) + cir.setReturnValue(false); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/CustomCapeFeatureRenderer.java b/src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/CustomCapeFeatureRenderer.java new file mode 100644 index 0000000..5a92f89 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/CustomCapeFeatureRenderer.java @@ -0,0 +1,43 @@ +package moe.nea.firmament.mixins.feature.devcosmetics; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import kotlin.Unit; +import moe.nea.firmament.features.misc.CustomCapes; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.entity.feature.CapeFeatureRenderer; +import net.minecraft.client.render.entity.feature.FeatureRenderer; +import net.minecraft.client.render.entity.feature.FeatureRendererContext; +import net.minecraft.client.render.entity.model.BipedEntityModel; +import net.minecraft.client.render.entity.model.PlayerEntityModel; +import net.minecraft.client.render.entity.state.PlayerEntityRenderState; +import net.minecraft.client.util.SkinTextures; +import net.minecraft.client.util.math.MatrixStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(CapeFeatureRenderer.class) +public abstract class CustomCapeFeatureRenderer extends FeatureRenderer { + public CustomCapeFeatureRenderer(FeatureRendererContext context) { + super(context); + } + + @WrapOperation( + method = "render(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/client/render/entity/state/PlayerEntityRenderState;FF)V", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/entity/model/BipedEntityModel;render(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumer;II)V") + ) + private void onRender(BipedEntityModel instance, MatrixStack matrixStack, VertexConsumer vertexConsumer, int light, int overlay, Operation original, @Local PlayerEntityRenderState playerEntityRenderState, @Local SkinTextures skinTextures, @Local VertexConsumerProvider vertexConsumerProvider) { + CustomCapes.render( + playerEntityRenderState, + vertexConsumer, + RenderLayer.getEntitySolid(skinTextures.capeTexture()), + vertexConsumerProvider, + updatedConsumer -> { + original.call(instance, matrixStack, updatedConsumer, light, overlay); + return Unit.INSTANCE; + }); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/CustomCapeStorage.java b/src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/CustomCapeStorage.java new file mode 100644 index 0000000..428d7ec --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/CustomCapeStorage.java @@ -0,0 +1,23 @@ +package moe.nea.firmament.mixins.feature.devcosmetics; + +import moe.nea.firmament.features.misc.CustomCapes; +import net.minecraft.client.render.entity.state.PlayerEntityRenderState; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(PlayerEntityRenderState.class) +public class CustomCapeStorage implements CustomCapes.CapeStorage { + @Unique + CustomCapes.CustomCape customCape; + + @Override + public CustomCapes.@Nullable CustomCape getCape_firmament() { + return customCape; + } + + @Override + public void setCape_firmament(CustomCapes.@Nullable CustomCape customCape) { + this.customCape = customCape; + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/SaveCapeToPlayerEntityRenderState.java b/src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/SaveCapeToPlayerEntityRenderState.java new file mode 100644 index 0000000..ae9c743 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/SaveCapeToPlayerEntityRenderState.java @@ -0,0 +1,19 @@ +package moe.nea.firmament.mixins.feature.devcosmetics; + +import moe.nea.firmament.features.misc.CustomCapes; +import net.minecraft.client.network.AbstractClientPlayerEntity; +import net.minecraft.client.render.entity.PlayerEntityRenderer; +import net.minecraft.client.render.entity.state.PlayerEntityRenderState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(PlayerEntityRenderer.class) +public class SaveCapeToPlayerEntityRenderState { + @Inject(method = "updateRenderState(Lnet/minecraft/client/network/AbstractClientPlayerEntity;Lnet/minecraft/client/render/entity/state/PlayerEntityRenderState;F)V", + at = @At("TAIL")) + private void addCustomCape(AbstractClientPlayerEntity abstractClientPlayerEntity, PlayerEntityRenderState playerEntityRenderState, float f, CallbackInfo ci) { + CustomCapes.addCapeData(abstractClientPlayerEntity, playerEntityRenderState); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/ChangeColorOfLivingEntities.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/ChangeColorOfLivingEntities.java new file mode 100644 index 0000000..2b96e5c --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/ChangeColorOfLivingEntities.java @@ -0,0 +1,62 @@ +package moe.nea.firmament.mixins.render.entitytints; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import com.llamalad7.mixinextras.sugar.Local; +import moe.nea.firmament.events.EntityRenderTintEvent; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.entity.LivingEntityRenderer; +import net.minecraft.client.render.entity.model.EntityModel; +import net.minecraft.client.render.entity.state.LivingEntityRenderState; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.LivingEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyArg; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * Applies various rendering modifications from {@link EntityRenderTintEvent} + */ +@Mixin(LivingEntityRenderer.class) +public class ChangeColorOfLivingEntities> { + @ModifyReturnValue(method = "getMixColor", at = @At("RETURN")) + private int changeColor(int original, @Local(argsOnly = true) S state) { + var tintState = EntityRenderTintEvent.HasTintRenderState.cast(state); + if (tintState.getHasTintOverride_firmament()) + return tintState.getTint_firmament(); + return original; + } + + @ModifyArg( + method = "getOverlay", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/OverlayTexture;getU(F)I"), + allow = 1 + ) + private static float modifyLightOverlay(float originalWhiteOffset, @Local(argsOnly = true) LivingEntityRenderState state) { + var tintState = EntityRenderTintEvent.HasTintRenderState.cast(state); + if (tintState.getHasTintOverride_firmament() || tintState.getOverlayTexture_firmament() != null) { + return 1F; // TODO: add interpolation percentage to render state extension + } + return originalWhiteOffset; + } + + @Inject(method = "render(Lnet/minecraft/client/render/entity/state/LivingEntityRenderState;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;I)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/util/math/MatrixStack;pop()V")) + private void afterRender(S livingEntityRenderState, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i, CallbackInfo ci) { + var tintState = EntityRenderTintEvent.HasTintRenderState.cast(livingEntityRenderState); + var overlayTexture = tintState.getOverlayTexture_firmament(); + if (overlayTexture != null && vertexConsumerProvider instanceof VertexConsumerProvider.Immediate imm) { + imm.drawCurrentLayer(); + } + EntityRenderTintEvent.overlayOverride = null; + } + + @Inject(method = "render(Lnet/minecraft/client/render/entity/state/LivingEntityRenderState;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;I)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/util/math/MatrixStack;push()V")) + private void beforeRender(S livingEntityRenderState, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i, CallbackInfo ci) { + var tintState = EntityRenderTintEvent.HasTintRenderState.cast(livingEntityRenderState); + var overlayTexture = tintState.getOverlayTexture_firmament(); + if (overlayTexture != null) { + EntityRenderTintEvent.overlayOverride = overlayTexture; + } + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/EntityRenderStateTint.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/EntityRenderStateTint.java new file mode 100644 index 0000000..1019027 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/EntityRenderStateTint.java @@ -0,0 +1,55 @@ +package moe.nea.firmament.mixins.render.entitytints; + +import moe.nea.firmament.events.EntityRenderTintEvent; +import moe.nea.firmament.util.render.TintedOverlayTexture; +import net.minecraft.client.render.entity.state.EntityRenderState; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +@Mixin(EntityRenderState.class) +public class EntityRenderStateTint implements EntityRenderTintEvent.HasTintRenderState { + @Unique + int tint = -1; + @Unique + TintedOverlayTexture overlayTexture; + @Unique + boolean hasTintOverride = false; + + @Override + public int getTint_firmament() { + return tint; + } + + @Override + public void setTint_firmament(int i) { + tint = i; + hasTintOverride = true; + } + + @Override + public boolean getHasTintOverride_firmament() { + return hasTintOverride; + } + + @Override + public void setHasTintOverride_firmament(boolean b) { + hasTintOverride = b; + } + + @Override + public void reset_firmament() { + hasTintOverride = false; + overlayTexture = null; + } + + @Override + public @Nullable TintedOverlayTexture getOverlayTexture_firmament() { + return overlayTexture; + } + + @Override + public void setOverlayTexture_firmament(@Nullable TintedOverlayTexture tintedOverlayTexture) { + this.overlayTexture = tintedOverlayTexture; + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/InjectIntoRenderState.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/InjectIntoRenderState.java new file mode 100644 index 0000000..7938340 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/InjectIntoRenderState.java @@ -0,0 +1,30 @@ +package moe.nea.firmament.mixins.render.entitytints; + +import moe.nea.firmament.events.EntityRenderTintEvent; +import net.minecraft.client.render.entity.EntityRenderer; +import net.minecraft.client.render.entity.state.EntityRenderState; +import net.minecraft.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * Dispatches {@link EntityRenderTintEvent} to collect additional render state used by {@link ChangeColorOfLivingEntities} + */ +@Mixin(EntityRenderer.class) +public class InjectIntoRenderState { + + @Inject( + method = "updateRenderState", + at = @At("RETURN")) + private void onUpdateRenderState(T entity, S state, float tickDelta, CallbackInfo ci) { + var renderState = EntityRenderTintEvent.HasTintRenderState.cast(state); + renderState.reset_firmament(); + var tintEvent = new EntityRenderTintEvent( + entity, + renderState + ); + EntityRenderTintEvent.Companion.publish(tintEvent); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/ReplaceOverlayTexture.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/ReplaceOverlayTexture.java new file mode 100644 index 0000000..61e5c65 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/ReplaceOverlayTexture.java @@ -0,0 +1,24 @@ +package moe.nea.firmament.mixins.render.entitytints; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import moe.nea.firmament.events.EntityRenderTintEvent; +import net.minecraft.client.render.OverlayTexture; +import net.minecraft.client.render.RenderLayer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +/** + * Replaces the overlay texture used by rendering with the override specified in {@link EntityRenderTintEvent#overlayOverride} + */ +@Mixin(RenderLayer.Overlay.class) +public class ReplaceOverlayTexture { + @ModifyExpressionValue( + method = {"method_23555", "method_23556"}, + expect = 2, + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/GameRenderer;getOverlayTexture()Lnet/minecraft/client/render/OverlayTexture;")) + private static OverlayTexture replaceOverlayTexture(OverlayTexture original) { + if (EntityRenderTintEvent.overlayOverride != null) + return EntityRenderTintEvent.overlayOverride; + return original; + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableEquipmentRenderer.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableEquipmentRenderer.java new file mode 100644 index 0000000..d9c174c --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableEquipmentRenderer.java @@ -0,0 +1,34 @@ +package moe.nea.firmament.mixins.render.entitytints; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import moe.nea.firmament.events.EntityRenderTintEvent; +import net.minecraft.client.render.OverlayTexture; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.entity.equipment.EquipmentRenderer; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +/** + * Patch to make {@link EquipmentRenderer} use a {@link RenderLayer} that allows uses Minecraft's overlay texture, if a {@link EntityRenderTintEvent#overlayOverride} is specified. + */ +@Mixin(EquipmentRenderer.class) +public class UseOverlayableEquipmentRenderer { + @WrapOperation(method = "render(Lnet/minecraft/client/render/entity/equipment/EquipmentModel$LayerType;Lnet/minecraft/registry/RegistryKey;Lnet/minecraft/client/model/Model;Lnet/minecraft/item/ItemStack;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/util/Identifier;)V", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/RenderLayer;getArmorCutoutNoCull(Lnet/minecraft/util/Identifier;)Lnet/minecraft/client/render/RenderLayer;")) + private RenderLayer replace(Identifier texture, Operation original) { + if (EntityRenderTintEvent.overlayOverride != null) + return RenderLayer.getEntityTranslucent(texture); + return original.call(texture); + } + + @ModifyExpressionValue(method = "render(Lnet/minecraft/client/render/entity/equipment/EquipmentModel$LayerType;Lnet/minecraft/registry/RegistryKey;Lnet/minecraft/client/model/Model;Lnet/minecraft/item/ItemStack;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/util/Identifier;)V", + at = @At(value = "FIELD", target = "Lnet/minecraft/client/render/OverlayTexture;DEFAULT_UV:I")) + private int replaceUvIndex(int original) { + if (EntityRenderTintEvent.overlayOverride != null) + return OverlayTexture.packUv(15, 10); // TODO: store this info in a global alongside overlayOverride + return original; + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableHeadFeatureRenderer.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableHeadFeatureRenderer.java new file mode 100644 index 0000000..07bc5cf --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableHeadFeatureRenderer.java @@ -0,0 +1,25 @@ +package moe.nea.firmament.mixins.render.entitytints; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import moe.nea.firmament.events.EntityRenderTintEvent; +import net.minecraft.client.render.OverlayTexture; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.entity.feature.HeadFeatureRenderer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +/** + * Patch to make {@link HeadFeatureRenderer} use a {@link RenderLayer} that allows uses Minecraft's overlay texture, if a {@link EntityRenderTintEvent#overlayOverride} is specified. + * @see UseOverlayableItemRenderer + */ +@Mixin(HeadFeatureRenderer.class) +public class UseOverlayableHeadFeatureRenderer { + + @ModifyExpressionValue(method = "render(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/client/render/entity/state/LivingEntityRenderState;FF)V", + at = @At(value = "FIELD", target = "Lnet/minecraft/client/render/OverlayTexture;DEFAULT_UV:I")) + private int replaceUvIndex(int original) { + if (EntityRenderTintEvent.overlayOverride != null) + return OverlayTexture.packUv(15, 10); // TODO: store this info in a global alongside overlayOverride + return original; + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableItemRenderer.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableItemRenderer.java new file mode 100644 index 0000000..620ab2c --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableItemRenderer.java @@ -0,0 +1,25 @@ +package moe.nea.firmament.mixins.render.entitytints; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import moe.nea.firmament.events.EntityRenderTintEvent; +import net.minecraft.client.render.OverlayTexture; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.RenderPhase; +import net.minecraft.client.render.item.ItemRenderState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +/** + * Patch to make {@link ItemRenderState} use a {@link RenderLayer} that allows uses Minecraft's overlay texture. + * + * @see UseOverlayableHeadFeatureRenderer + */ +@Mixin(ItemRenderState.LayerRenderState.class) +public class UseOverlayableItemRenderer { + @ModifyExpressionValue(method = "render", at = @At(value = "FIELD", target = "Lnet/minecraft/client/render/item/ItemRenderState$LayerRenderState;renderLayer:Lnet/minecraft/client/render/RenderLayer;")) + private RenderLayer replace(RenderLayer original) { + if (EntityRenderTintEvent.overlayOverride != null && original instanceof RenderLayer.MultiPhase multiPhase && multiPhase.phases.texture instanceof RenderPhase.Texture texture && texture.getId().isPresent()) + return RenderLayer.getEntityTranslucent(texture.getId().get()); + return original; + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableSkullBlockEntityRenderer.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableSkullBlockEntityRenderer.java new file mode 100644 index 0000000..9905af1 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableSkullBlockEntityRenderer.java @@ -0,0 +1,25 @@ +package moe.nea.firmament.mixins.render.entitytints; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import moe.nea.firmament.events.EntityRenderTintEvent; +import net.minecraft.client.render.OverlayTexture; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.block.entity.SkullBlockEntityRenderer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +/** + * Patch to make {@link SkullBlockEntityRenderer} use a {@link RenderLayer} that allows uses Minecraft's overlay texture, if a {@link EntityRenderTintEvent#overlayOverride} is specified. + */ + +@Mixin(SkullBlockEntityRenderer.class) +public class UseOverlayableSkullBlockEntityRenderer { + @ModifyExpressionValue(method = "renderSkull", + at = @At(value = "FIELD", target = "Lnet/minecraft/client/render/OverlayTexture;DEFAULT_UV:I")) + private static int replaceUvIndex(int original) { + if (EntityRenderTintEvent.overlayOverride != null) + return OverlayTexture.packUv(15, 10); // TODO: store this info in a global alongside overlayOverride + return original; + } + +} diff --git a/src/main/kotlin/Compat.kt b/src/main/kotlin/Compat.kt new file mode 100644 index 0000000..ba3c88d --- /dev/null +++ b/src/main/kotlin/Compat.kt @@ -0,0 +1,11 @@ +package moe.nea.firmament + +import moe.nea.firmament.util.compatloader.CompatMeta +import moe.nea.firmament.util.compatloader.ICompatMeta + +@CompatMeta +object Compat : ICompatMeta { + override fun shouldLoad(): Boolean { + return true + } +} diff --git a/src/main/kotlin/Firmament.kt b/src/main/kotlin/Firmament.kt new file mode 100644 index 0000000..b00546a --- /dev/null +++ b/src/main/kotlin/Firmament.kt @@ -0,0 +1,182 @@ +package moe.nea.firmament + +import com.google.gson.Gson +import com.mojang.brigadier.CommandDispatcher +import io.ktor.client.HttpClient +import io.ktor.client.plugins.UserAgent +import io.ktor.client.plugins.cache.HttpCache +import io.ktor.client.plugins.compression.ContentEncoding +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logging +import io.ktor.serialization.kotlinx.json.json +import java.io.InputStream +import java.nio.file.Files +import java.nio.file.Path +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents +import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents +import net.fabricmc.fabric.api.resource.ResourceManagerHelper +import net.fabricmc.fabric.api.resource.ResourcePackActivationType +import net.fabricmc.loader.api.FabricLoader +import net.fabricmc.loader.api.Version +import net.fabricmc.loader.api.metadata.ModMetadata +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.Logger +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.plus +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import kotlin.coroutines.EmptyCoroutineContext +import net.minecraft.command.CommandRegistryAccess +import net.minecraft.util.Identifier +import moe.nea.firmament.commands.registerFirmamentCommand +import moe.nea.firmament.events.ClientInitEvent +import moe.nea.firmament.events.ClientStartedEvent +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.events.ItemTooltipEvent +import moe.nea.firmament.events.ScreenRenderPostEvent +import moe.nea.firmament.events.TickEvent +import moe.nea.firmament.events.registration.registerFirmamentEvents +import moe.nea.firmament.features.FeatureManager +import moe.nea.firmament.repo.HypixelStaticData +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.data.IDataHolder +import moe.nea.firmament.util.mc.InitLevel +import moe.nea.firmament.util.tr + +object Firmament { + val modContainer by lazy { FabricLoader.getInstance().getModContainer(MOD_ID).get() } + const val MOD_ID = "firmament" + + val DEBUG = System.getProperty("firmament.debug") == "true" + val DATA_DIR: Path = Path.of(".firmament").also { Files.createDirectories(it) } + val CONFIG_DIR: Path = Path.of("config/firmament").also { Files.createDirectories(it) } + val logger: Logger = LogManager.getLogger("Firmament") + private val metadata: ModMetadata by lazy { + FabricLoader.getInstance().getModContainer(MOD_ID).orElseThrow().metadata + } + val version: Version by lazy { metadata.version } + + private val DEFAULT_JSON_INDENT = " " + + @OptIn(ExperimentalSerializationApi::class) + val json = Json { + prettyPrint = DEBUG + isLenient = true + allowTrailingComma = true + ignoreUnknownKeys = true + encodeDefaults = true + prettyPrintIndent = if (prettyPrint) "\t" else DEFAULT_JSON_INDENT + } + + /** + * FUCK two space indentation + */ + val twoSpaceJson = Json(from = json) { + prettyPrint = true + prettyPrintIndent = " " + } + val gson = Gson() + val tightJson = Json(from = json) { + prettyPrint = false + // Reset pretty print indent back to default to prevent getting yelled at by json + prettyPrintIndent = DEFAULT_JSON_INDENT + encodeDefaults = false + explicitNulls = false + } + + + val httpClient by lazy { + HttpClient { + install(ContentNegotiation) { + json(json) + } + install(ContentEncoding) { + gzip() + deflate() + } + install(UserAgent) { + agent = "Firmament/$version" + } + if (DEBUG) + install(Logging) { + level = LogLevel.INFO + } + install(HttpCache) + } + } + + val globalJob = Job() + val coroutineScope = + CoroutineScope(EmptyCoroutineContext + CoroutineName("Firmament")) + SupervisorJob(globalJob) + + private fun registerCommands( + dispatcher: CommandDispatcher, + @Suppress("UNUSED_PARAMETER") + ctx: CommandRegistryAccess + ) { + registerFirmamentCommand(dispatcher) + CommandEvent.publish(CommandEvent(dispatcher, ctx, MC.networkHandler?.commandDispatcher)) + } + + @JvmStatic + fun onInitialize() { + } + + @JvmStatic + fun onClientInitialize() { + InitLevel.bump(InitLevel.MC_INIT) + FeatureManager.subscribeEvents() + ClientTickEvents.END_CLIENT_TICK.register(ClientTickEvents.EndTick { instance -> + TickEvent.publish(TickEvent(MC.currentTick++)) + }) + IDataHolder.registerEvents() + RepoManager.initialize() + SBData.init() + FeatureManager.autoload() + HypixelStaticData.spawnDataCollectionLoop() + ClientCommandRegistrationCallback.EVENT.register(this::registerCommands) + ClientLifecycleEvents.CLIENT_STARTED.register(ClientLifecycleEvents.ClientStarted { + ClientStartedEvent.publish(ClientStartedEvent()) + }) + ClientLifecycleEvents.CLIENT_STOPPING.register(ClientLifecycleEvents.ClientStopping { + logger.info("Shutting down Firmament coroutines") + globalJob.cancel() + }) + registerFirmamentEvents() + ItemTooltipCallback.EVENT.register { stack, context, type, lines -> + ItemTooltipEvent.publish(ItemTooltipEvent(stack, context, type, lines)) + } + ScreenEvents.AFTER_INIT.register(ScreenEvents.AfterInit { client, screen, scaledWidth, scaledHeight -> + ScreenEvents.afterRender(screen) + .register(ScreenEvents.AfterRender { screen, drawContext, mouseX, mouseY, tickDelta -> + ScreenRenderPostEvent.publish(ScreenRenderPostEvent(screen, mouseX, mouseY, tickDelta, drawContext)) + }) + }) + ClientInitEvent.publish(ClientInitEvent()) + ResourceManagerHelper.registerBuiltinResourcePack( + identifier("transparent_overlay"), + modContainer, + tr("firmament.resourcepack.transparentoverlay", "Transparent Firmament Overlay"), + ResourcePackActivationType.NORMAL + ) + } + + + fun identifier(path: String) = Identifier.of(MOD_ID, path) + inline fun tryDecodeJsonFromStream(inputStream: InputStream): Result { + return runCatching { + json.decodeFromStream(inputStream) + } + } +} diff --git a/src/main/kotlin/apis/Profiles.kt b/src/main/kotlin/apis/Profiles.kt new file mode 100644 index 0000000..a6c334b --- /dev/null +++ b/src/main/kotlin/apis/Profiles.kt @@ -0,0 +1,194 @@ + + +@file:UseSerializers(DashlessUUIDSerializer::class, InstantAsLongSerializer::class) + +package moe.nea.firmament.apis + +import io.github.moulberry.repo.constants.Leveling +import io.github.moulberry.repo.data.Rarity +import java.time.Instant +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.util.LegacyFormattingCode +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.assertNotNullOr +import moe.nea.firmament.util.json.DashlessUUIDSerializer +import moe.nea.firmament.util.json.InstantAsLongSerializer +import net.minecraft.util.DyeColor +import net.minecraft.util.Formatting +import java.util.* +import kotlin.reflect.KProperty1 + + +@Serializable +data class CollectionSkillData( + val items: Map +) + +@Serializable +data class CollectionResponse( + val success: Boolean, + val collections: Map +) + +@Serializable +data class CollectionInfo( + val name: String, + val maxTiers: Int, + val tiers: List +) + +@Serializable +data class CollectionTier( + val tier: Int, + val amountRequired: Long, + val unlocks: List, +) + + +@Serializable +data class Profiles( + val success: Boolean, + val profiles: List? +) + +@Serializable +data class Profile( + @SerialName("profile_id") + val profileId: UUID, + @SerialName("cute_name") + val cuteName: String, + val selected: Boolean = false, + val members: Map, +) + +enum class Skill(val accessor: KProperty1, val color: DyeColor, val icon: SkyblockId) { + FARMING(Member::experienceSkillFarming, DyeColor.YELLOW, SkyblockId("ROOKIE_HOE")), + FORAGING(Member::experienceSkillForaging, DyeColor.BROWN, SkyblockId("TREECAPITATOR_AXE")), + MINING(Member::experienceSkillMining, DyeColor.LIGHT_GRAY, SkyblockId("DIAMOND_PICKAXE")), + ALCHEMY(Member::experienceSkillAlchemy, DyeColor.PURPLE, SkyblockId("BREWING_STAND")), + TAMING(Member::experienceSkillTaming, DyeColor.GREEN, SkyblockId("SUPER_EGG")), + FISHING(Member::experienceSkillFishing, DyeColor.BLUE, SkyblockId("FARMER_ROD")), + RUNECRAFTING(Member::experienceSkillRunecrafting, DyeColor.PINK, SkyblockId("MUSIC_RUNE;1")), + CARPENTRY(Member::experienceSkillCarpentry, DyeColor.ORANGE, SkyblockId("WORKBENCH")), + COMBAT(Member::experienceSkillCombat, DyeColor.RED, SkyblockId("UNDEAD_SWORD")), + SOCIAL(Member::experienceSkillSocial, DyeColor.WHITE, SkyblockId("EGG_HUNT")), + ENCHANTING(Member::experienceSkillEnchanting, DyeColor.MAGENTA, SkyblockId("ENCHANTMENT_TABLE")), + ; + + fun getMaximumLevel(leveling: Leveling) = assertNotNullOr(leveling.maximumLevels[name.lowercase()]) { 50 } + + fun getLadder(leveling: Leveling): List { + if (this == SOCIAL) return leveling.socialExperienceRequiredPerLevel + if (this == RUNECRAFTING) return leveling.runecraftingExperienceRequiredPerLevel + return leveling.skillExperienceRequiredPerLevel + } +} + +enum class CollectionCategory(val skill: Skill?, val color: DyeColor, val icon: SkyblockId) { + FARMING(Skill.FARMING, DyeColor.YELLOW, SkyblockId("ROOKIE_HOE")), + FORAGING(Skill.FORAGING, DyeColor.BROWN, SkyblockId("TREECAPITATOR_AXE")), + MINING(Skill.MINING, DyeColor.LIGHT_GRAY, SkyblockId("DIAMOND_PICKAXE")), + FISHING(Skill.FISHING, DyeColor.BLUE, SkyblockId("FARMER_ROD")), + COMBAT(Skill.COMBAT, DyeColor.RED, SkyblockId("UNDEAD_SWORD")), + RIFT(null, DyeColor.PURPLE, SkyblockId("SKYBLOCK_MOTE")), +} + +@Serializable +@JvmInline +value class CollectionType(val string: String) { + val skyblockId get() = SkyblockId(string.replace(":", "-").replace("MUSHROOM_COLLECTION", "HUGE_MUSHROOM_2")) +} + +@Serializable +data class Member( + val pets: List = listOf(), + @SerialName("coop_invitation") + val coopInvitation: CoopInvitation? = null, + @SerialName("experience_skill_farming") + val experienceSkillFarming: Double = 0.0, + @SerialName("experience_skill_alchemy") + val experienceSkillAlchemy: Double = 0.0, + @SerialName("experience_skill_combat") + val experienceSkillCombat: Double = 0.0, + @SerialName("experience_skill_taming") + val experienceSkillTaming: Double = 0.0, + @SerialName("experience_skill_social2") + val experienceSkillSocial: Double = 0.0, + @SerialName("experience_skill_enchanting") + val experienceSkillEnchanting: Double = 0.0, + @SerialName("experience_skill_fishing") + val experienceSkillFishing: Double = 0.0, + @SerialName("experience_skill_foraging") + val experienceSkillForaging: Double = 0.0, + @SerialName("experience_skill_mining") + val experienceSkillMining: Double = 0.0, + @SerialName("experience_skill_runecrafting") + val experienceSkillRunecrafting: Double = 0.0, + @SerialName("experience_skill_carpentry") + val experienceSkillCarpentry: Double = 0.0, + val collection: Map = mapOf() +) + +@Serializable +data class CoopInvitation( + val timestamp: Instant, + @SerialName("invited_by") + val invitedBy: UUID? = null, + val confirmed: Boolean, +) + +@JvmInline +@Serializable +value class PetType(val name: String) + +@Serializable +data class Pet( + val uuid: UUID? = null, + val type: PetType, + val exp: Double = 0.0, + val active: Boolean = false, + val tier: Rarity, + val candyUsed: Int = 0, + val heldItem: String? = null, + val skin: String? = null, +) { + val itemId get() = SkyblockId("${type.name};${tier.ordinal}") +} + +@Serializable +data class PlayerResponse( + val success: Boolean, + val player: PlayerData, +) + +@Serializable +data class PlayerData( + val uuid: UUID, + val firstLogin: Instant, + val lastLogin: Instant? = null, + @SerialName("playername") + val playerName: String, + val achievementsOneTime: List = listOf(), + @SerialName("newPackageRank") + val packageRank: String? = null, + val monthlyPackageRank: String? = null, + val rankPlusColor: String = "GOLD" +) { + val rankPlusDyeColor = LegacyFormattingCode.values().find { it.name == rankPlusColor } ?: LegacyFormattingCode.GOLD + val rankData get() = RepoManager.neuRepo.constants.misc.ranks[if (monthlyPackageRank == "NONE" || monthlyPackageRank == null) packageRank else monthlyPackageRank] + fun getDisplayName(name: String = playerName) = rankData?.let { + ("§${it.color}[${it.tag}${rankPlusDyeColor.modern}" + + "${it.plus ?: ""}§${it.color}] $name") + } ?: "${Formatting.GRAY}$name" + + +} + +@Serializable +data class MowojangNameLookup( + val name: String, + val id: UUID, +) diff --git a/src/main/kotlin/apis/Routes.kt b/src/main/kotlin/apis/Routes.kt new file mode 100644 index 0000000..5e29402 --- /dev/null +++ b/src/main/kotlin/apis/Routes.kt @@ -0,0 +1,95 @@ + + +package moe.nea.firmament.apis + +import io.ktor.client.call.* +import io.ktor.client.request.* +import io.ktor.http.* +import io.ktor.util.* +import java.util.* +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.collections.MutableMap +import kotlin.collections.listOf +import kotlin.collections.mutableMapOf +import kotlin.collections.set +import moe.nea.firmament.Firmament +import moe.nea.firmament.util.MinecraftDispatcher + +object Routes { + private val nameToUUID: MutableMap> = CaseInsensitiveMap() + private val profiles: MutableMap> = mutableMapOf() + private val accounts: MutableMap> = mutableMapOf() + private val UUIDToName: MutableMap> = mutableMapOf() + + suspend fun getPlayerNameForUUID(uuid: UUID): String? { + return withContext(MinecraftDispatcher) { + UUIDToName.computeIfAbsent(uuid) { + async(Firmament.coroutineScope.coroutineContext) { + val response = Firmament.httpClient.get("https://mowojang.matdoes.dev/$uuid") + if (!response.status.isSuccess()) return@async null + val data = response.body() + launch(MinecraftDispatcher) { + nameToUUID[data.name] = async { data.id } + } + data.name + } + } + }.await() + } + + suspend fun getUUIDForPlayerName(name: String): UUID? { + return withContext(MinecraftDispatcher) { + nameToUUID.computeIfAbsent(name) { + async(Firmament.coroutineScope.coroutineContext) { + val response = Firmament.httpClient.get("https://mowojang.matdoes.dev/$name") + if (!response.status.isSuccess()) return@async null + val data = response.body() + launch(MinecraftDispatcher) { + UUIDToName[data.id] = async { data.name } + } + data.id + } + } + }.await() + } + + suspend fun getAccountData(uuid: UUID): PlayerData? { + return withContext(MinecraftDispatcher) { + accounts.computeIfAbsent(uuid) { + async(Firmament.coroutineScope.coroutineContext) { + val response = UrsaManager.request(listOf("v1", "hypixel","player", uuid.toString())) + if (!response.status.isSuccess()) { + launch(MinecraftDispatcher) { + @Suppress("DeferredResultUnused") + accounts.remove(uuid) + } + return@async null + } + response.body().player + } + } + }.await() + } + + suspend fun getProfiles(uuid: UUID): Profiles? { + return withContext(MinecraftDispatcher) { + profiles.computeIfAbsent(uuid) { + async(Firmament.coroutineScope.coroutineContext) { + val response = UrsaManager.request(listOf("v1", "hypixel","profiles", uuid.toString())) + if (!response.status.isSuccess()) { + launch(MinecraftDispatcher) { + @Suppress("DeferredResultUnused") + profiles.remove(uuid) + } + return@async null + } + response.body() + } + } + }.await() + } + +} diff --git a/src/main/kotlin/apis/UrsaManager.kt b/src/main/kotlin/apis/UrsaManager.kt new file mode 100644 index 0000000..13f7aef --- /dev/null +++ b/src/main/kotlin/apis/UrsaManager.kt @@ -0,0 +1,72 @@ + + +package moe.nea.firmament.apis + +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.withContext +import moe.nea.firmament.Firmament +import net.minecraft.client.MinecraftClient +import java.time.Duration +import java.time.Instant +import java.util.* + +object UrsaManager { + private data class Token( + val validUntil: Instant, + val token: String, + val obtainedFrom: String, + ) { + fun isValid(host: String) = Instant.now().plusSeconds(60) < validUntil && obtainedFrom == host + } + + private var currentToken: Token? = null + private val lock = Mutex() + private fun getToken(host: String) = currentToken?.takeIf { it.isValid(host) } + + suspend fun request(path: List): HttpResponse { + var didLock = false + try { + val host = "ursa.notenoughupdates.org" + var token = getToken(host) + if (token == null) { + lock.lock() + didLock = true + token = getToken(host) + } + val response = Firmament.httpClient.get { + url { + this.host = host + appendPathSegments(path, encodeSlash = true) + } + if (token == null) { + withContext(Dispatchers.IO) { + val mc = MinecraftClient.getInstance() + val serverId = UUID.randomUUID().toString() + mc.sessionService.joinServer(mc.session.uuidOrNull, mc.session.accessToken, serverId) + header("x-ursa-username", mc.session.username) + header("x-ursa-serverid", serverId) + } + } else { + header("x-ursa-token", token.token) + } + } + val savedToken = response.headers["x-ursa-token"] + if (savedToken != null) { + val validUntil = response.headers["x-ursa-expires"]?.toLongOrNull()?.let { Instant.ofEpochMilli(it) } + ?: (Instant.now() + Duration.ofMinutes(55)) + currentToken = Token(validUntil, savedToken, host) + } + if (response.status.value != 200) { + Firmament.logger.error("Failed to contact ursa minor: ${response.bodyAsText()}") + } + return response + } finally { + if (didLock) + lock.unlock() + } + } +} diff --git a/src/main/kotlin/commands/CaseInsensitiveLiteralCommandNode.kt b/src/main/kotlin/commands/CaseInsensitiveLiteralCommandNode.kt new file mode 100644 index 0000000..10772b0 --- /dev/null +++ b/src/main/kotlin/commands/CaseInsensitiveLiteralCommandNode.kt @@ -0,0 +1,75 @@ + + +package moe.nea.firmament.commands + +import com.mojang.brigadier.Command +import com.mojang.brigadier.RedirectModifier +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.builder.LiteralArgumentBuilder +import com.mojang.brigadier.context.CommandContextBuilder +import com.mojang.brigadier.context.StringRange +import com.mojang.brigadier.exceptions.CommandSyntaxException +import com.mojang.brigadier.tree.CommandNode +import com.mojang.brigadier.tree.LiteralCommandNode +import java.util.function.Predicate + +class CaseInsensitiveLiteralCommandNode( + literal: String, command: Command?, requirement: Predicate?, + redirect: CommandNode?, modifier: RedirectModifier?, forks: Boolean +) : LiteralCommandNode( + literal.lowercase(), command, requirement, redirect, modifier, forks +) { + class Builder(literal: String) : LiteralArgumentBuilder(literal) { + override fun build(): LiteralCommandNode { + val result = CaseInsensitiveLiteralCommandNode( + literal, + command, requirement, redirect, redirectModifier, isFork + ) + for (argument in arguments) { + result.addChild(argument) + } + return result + } + } + + override fun createBuilder(): LiteralArgumentBuilder { + return Builder(literal).also { + it.requires(requirement) + it.forward(redirect, redirectModifier, isFork) + if (command != null) + it.executes(command) + } + } + + override fun parse(reader: StringReader, contextBuilder: CommandContextBuilder) { + val start = reader.cursor + val end = parse0(reader) + if (end > -1) { + contextBuilder.withNode(this, StringRange.between(start, end)) + return + } + + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.literalIncorrect().createWithContext(reader, literal) + } + + override fun toString(): String { + return "" + } + + private fun parse0(reader: StringReader): Int { + val start = reader.cursor + if (reader.canRead(literal.length)) { + val end = start + literal.length + if (reader.string.substring(start, end).equals(literal, true)) { + reader.cursor = end + if (!reader.canRead() || reader.peek() == ' ') { + return end + } else { + reader.cursor = start + } + } + } + return -1 + } + +} diff --git a/src/main/kotlin/commands/Duration.kt b/src/main/kotlin/commands/Duration.kt new file mode 100644 index 0000000..42f143d --- /dev/null +++ b/src/main/kotlin/commands/Duration.kt @@ -0,0 +1,75 @@ +package moe.nea.firmament.commands + +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.context.CommandContext +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType +import com.mojang.brigadier.suggestion.Suggestions +import com.mojang.brigadier.suggestion.SuggestionsBuilder +import java.util.concurrent.CompletableFuture +import java.util.function.Function +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import kotlin.time.DurationUnit +import kotlin.time.toDuration +import moe.nea.firmament.util.tr + +object DurationArgumentType : ArgumentType { + val unknownTimeCode = DynamicCommandExceptionType { timeCode -> + tr("firmament.command-argument.duration.error", + "Unknown time code '$timeCode'") + } + + override fun parse(reader: StringReader): Duration { + val start = reader.cursor + val string = reader.readUnquotedString() + val matcher = regex.matcher(string) + var s = 0 + var time = 0.seconds + fun createError(till: Int) { + throw unknownTimeCode.createWithContext( + reader.also { it.cursor = start + s }, + string.substring(s, till)) + } + + while (matcher.find()) { + if (matcher.start() != s) { + createError(matcher.start()) + } + s = matcher.end() + val amount = matcher.group("count").toDouble() + val what = timeSuffixes[matcher.group("what").single()]!! + time += amount.toDuration(what) + } + if (string.length != s) { + createError(string.length) + } + return time + } + + + override fun listSuggestions( + context: CommandContext, + builder: SuggestionsBuilder + ): CompletableFuture { + val remaining = builder.remainingLowerCase.substringBefore(' ') + if (remaining.isEmpty()) return super.listSuggestions(context, builder) + if (remaining.last().isDigit()) { + for (timeSuffix in timeSuffixes.keys) { + builder.suggest(remaining + timeSuffix) + } + } + return builder.buildFuture() + } + + val timeSuffixes = mapOf( + 'm' to DurationUnit.MINUTES, + 's' to DurationUnit.SECONDS, + 'h' to DurationUnit.HOURS, + ) + val regex = "(?[0-9]+)(?[${timeSuffixes.keys.joinToString("")}])".toPattern() + + override fun getExamples(): Collection { + return listOf("3m", "20s", "1h45m") + } +} diff --git a/src/main/kotlin/commands/RestArgumentType.kt b/src/main/kotlin/commands/RestArgumentType.kt new file mode 100644 index 0000000..361907f --- /dev/null +++ b/src/main/kotlin/commands/RestArgumentType.kt @@ -0,0 +1,15 @@ + + +package moe.nea.firmament.commands + +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.arguments.ArgumentType + +object RestArgumentType : ArgumentType { + override fun parse(reader: StringReader): String { + val remaining = reader.remaining + reader.cursor += remaining.length + return remaining + } + +} diff --git a/src/main/kotlin/commands/dsl.kt b/src/main/kotlin/commands/dsl.kt new file mode 100644 index 0000000..d1f0d8c --- /dev/null +++ b/src/main/kotlin/commands/dsl.kt @@ -0,0 +1,118 @@ + + +package moe.nea.firmament.commands + +import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.builder.ArgumentBuilder +import com.mojang.brigadier.builder.RequiredArgumentBuilder +import com.mojang.brigadier.context.CommandContext +import com.mojang.brigadier.suggestion.SuggestionProvider +import kotlinx.coroutines.launch +import moe.nea.firmament.Firmament +import moe.nea.firmament.util.MinecraftDispatcher +import moe.nea.firmament.util.iterate +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type +import java.lang.reflect.TypeVariable + + +typealias DefaultSource = FabricClientCommandSource + + +inline val > T.context get() = this +operator fun > C.get(arg: TypeSafeArg): T { + return arg.get(this) +} + +fun literal( + name: String, + block: CaseInsensitiveLiteralCommandNode.Builder.() -> Unit +): CaseInsensitiveLiteralCommandNode.Builder = + CaseInsensitiveLiteralCommandNode.Builder(name).also(block) + + +private fun normalizeGeneric(argument: Type): Class<*> { + return when (argument) { + is Class<*> -> argument + is TypeVariable<*> -> normalizeGeneric(argument.bounds[0]) + is ParameterizedType -> normalizeGeneric(argument.rawType) + else -> Any::class.java + } +} + +data class TypeSafeArg(val name: String, val argument: ArgumentType) { + val argClass by lazy { + argument.javaClass + .iterate>> { + it.superclass + } + .flatMap { + it.genericInterfaces.toList() + } + .filterIsInstance() + .find { it.rawType == ArgumentType::class.java }!! + .let { normalizeGeneric(it.actualTypeArguments[0]) } + } + + @JvmName("getWithThis") + fun CommandContext.get(): T = + get(this) + + + fun get(ctx: CommandContext): T { + try { + return ctx.getArgument(name, argClass) as T + } catch (e: Exception) { + if (ctx.child != null) { + return get(ctx.child) + } + throw e + } + } +} + + +fun argument( + name: String, + argument: ArgumentType, + block: RequiredArgumentBuilder.(TypeSafeArg) -> Unit +): RequiredArgumentBuilder = + RequiredArgumentBuilder.argument(name, argument).also { block(it, TypeSafeArg(name, argument)) } + +fun , AT : Any> T.thenArgument( + name: String, + argument: ArgumentType, + block: RequiredArgumentBuilder.(TypeSafeArg) -> Unit +): T = then(argument(name, argument, block)) + +fun > T.suggestsList(provider: CommandContext.() -> Iterable) { + suggests(SuggestionProvider { context, builder -> + provider(context) + .asSequence() + .filter { it.startsWith(builder.remaining, ignoreCase = true) } + .forEach { + builder.suggest(it) + } + builder.buildFuture() + }) +} + +fun > T.thenLiteral( + name: String, + block: CaseInsensitiveLiteralCommandNode.Builder.() -> Unit +): T = + then(literal(name, block)) + +fun > T.then(node: ArgumentBuilder, block: T.() -> Unit): T = + then(node).also(block) + +fun > T.thenExecute(block: suspend CommandContext.() -> Unit): T = + executes { + Firmament.coroutineScope.launch(MinecraftDispatcher) { + block(it) + } + 1 + } + + diff --git a/src/main/kotlin/commands/rome.kt b/src/main/kotlin/commands/rome.kt new file mode 100644 index 0000000..f808231 --- /dev/null +++ b/src/main/kotlin/commands/rome.kt @@ -0,0 +1,376 @@ +package moe.nea.firmament.commands + +import com.mojang.brigadier.CommandDispatcher +import com.mojang.brigadier.arguments.IntegerArgumentType +import com.mojang.brigadier.arguments.StringArgumentType.string +import io.ktor.client.statement.bodyAsText +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource +import net.minecraft.nbt.NbtOps +import net.minecraft.text.Text +import net.minecraft.text.TextCodecs +import moe.nea.firmament.apis.UrsaManager +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.events.FirmamentEventBus +import moe.nea.firmament.features.debug.DebugLogger +import moe.nea.firmament.features.debug.DeveloperFeatures +import moe.nea.firmament.features.debug.PowerUserTools +import moe.nea.firmament.features.inventory.buttons.InventoryButtons +import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlayScreen +import moe.nea.firmament.features.inventory.storageoverlay.StorageOverviewScreen +import moe.nea.firmament.features.mining.MiningBlockInfoUi +import moe.nea.firmament.gui.config.AllConfigsGui +import moe.nea.firmament.gui.config.BooleanHandler +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.gui.config.ManagedOption +import moe.nea.firmament.init.MixinPlugin +import moe.nea.firmament.repo.HypixelStaticData +import moe.nea.firmament.repo.ItemCache +import moe.nea.firmament.repo.RepoDownloadManager +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.util.FirmFormatters +import moe.nea.firmament.util.FirmFormatters.debugPath +import moe.nea.firmament.util.FirmFormatters.formatBool +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.ScreenUtil +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.accessors.messages +import moe.nea.firmament.util.asBazaarStock +import moe.nea.firmament.util.collections.InstanceList +import moe.nea.firmament.util.collections.WeakCache +import moe.nea.firmament.util.mc.SNbtFormatter +import moe.nea.firmament.util.tr +import moe.nea.firmament.util.unformattedString + + +fun firmamentCommand() = literal("firmament") { + thenLiteral("config") { + thenExecute { + AllConfigsGui.showAllGuis() + } + thenLiteral("toggle") { + thenArgument("config", string()) { config -> + suggestsList { + ManagedConfig.allManagedConfigs.getAll().asSequence().map { it.name }.asIterable() + } + thenArgument("property", string()) { property -> + suggestsList { + (ManagedConfig.allManagedConfigs.getAll().find { it.name == this[config] } + ?: return@suggestsList listOf()) + .allOptions.entries.asSequence().filter { it.value.handler is BooleanHandler } + .map { it.key } + .asIterable() + } + thenExecute { + val config = this[config] + val property = this[property] + + val configObj = ManagedConfig.allManagedConfigs.getAll().find { it.name == config } + if (configObj == null) { + source.sendFeedback( + Text.stringifiedTranslatable( + "firmament.command.toggle.no-config-found", + config + ) + ) + return@thenExecute + } + val propertyObj = configObj.allOptions[property] + if (propertyObj == null) { + source.sendFeedback( + Text.stringifiedTranslatable("firmament.command.toggle.no-property-found", property) + ) + return@thenExecute + } + if (propertyObj.handler !is BooleanHandler) { + source.sendFeedback( + Text.stringifiedTranslatable("firmament.command.toggle.not-a-toggle", property) + ) + return@thenExecute + } + propertyObj as ManagedOption + propertyObj.value = !propertyObj.value + configObj.save() + source.sendFeedback( + Text.stringifiedTranslatable( + "firmament.command.toggle.toggled", configObj.labelText, + propertyObj.labelText, + Text.translatable("firmament.toggle.${propertyObj.value}") + ) + ) + } + } + } + } + } + thenLiteral("buttons") { + thenExecute { + InventoryButtons.openEditor() + } + } + thenLiteral("sendcoords") { + thenExecute { + val p = MC.player ?: return@thenExecute + MC.sendServerChat("x: ${p.blockX}, y: ${p.blockY}, z: ${p.blockZ}") + } + thenArgument("rest", RestArgumentType) { rest -> + thenExecute { + val p = MC.player ?: return@thenExecute + MC.sendServerChat("x: ${p.blockX}, y: ${p.blockY}, z: ${p.blockZ} ${this[rest]}") + } + } + } + thenLiteral("storageoverview") { + thenExecute { + ScreenUtil.setScreenLater(StorageOverviewScreen()) + MC.player?.networkHandler?.sendChatCommand("storage") + } + } + thenLiteral("storage") { + thenExecute { + ScreenUtil.setScreenLater(StorageOverlayScreen()) + MC.player?.networkHandler?.sendChatCommand("storage") + } + } + thenLiteral("repo") { + thenLiteral("checkpr") { + thenArgument("prnum", IntegerArgumentType.integer(1)) { prnum -> + thenExecute { + val prnum = this[prnum] + source.sendFeedback(tr("firmament.repo.reload.pr", "Temporarily reloading repo from PR #${prnum}.")) + RepoManager.downloadOverridenBranch("refs/pull/$prnum/head") + } + } + } + thenLiteral("reload") { + thenLiteral("fetch") { + thenExecute { + source.sendFeedback(Text.translatable("firmament.repo.reload.network")) // TODO better reporting + RepoManager.launchAsyncUpdate() + } + } + thenExecute { + source.sendFeedback(Text.translatable("firmament.repo.reload.disk")) + RepoManager.reload() + } + } + } + thenLiteral("price") { + thenArgument("item", string()) { item -> + suggestsList { RepoManager.neuRepo.items.items.keys } + thenExecute { + val itemName = SkyblockId(get(item)) + source.sendFeedback(Text.stringifiedTranslatable("firmament.price", itemName.neuItem)) + val bazaarData = HypixelStaticData.bazaarData[itemName.asBazaarStock] + if (bazaarData != null) { + source.sendFeedback(Text.translatable("firmament.price.bazaar")) + source.sendFeedback( + Text.stringifiedTranslatable("firmament.price.bazaar.productid", bazaarData.productId.bazaarId) + ) + source.sendFeedback( + Text.stringifiedTranslatable( + "firmament.price.bazaar.buy.price", + FirmFormatters.formatCommas(bazaarData.quickStatus.buyPrice, 1) + ) + ) + source.sendFeedback( + Text.stringifiedTranslatable( + "firmament.price.bazaar.buy.order", + bazaarData.quickStatus.buyOrders + ) + ) + source.sendFeedback( + Text.stringifiedTranslatable( + "firmament.price.bazaar.sell.price", + FirmFormatters.formatCommas(bazaarData.quickStatus.sellPrice, 1) + ) + ) + source.sendFeedback( + Text.stringifiedTranslatable( + "firmament.price.bazaar.sell.order", + bazaarData.quickStatus.sellOrders + ) + ) + } + val lowestBin = HypixelStaticData.lowestBin[itemName] + if (lowestBin != null) { + source.sendFeedback( + Text.stringifiedTranslatable( + "firmament.price.lowestbin", + FirmFormatters.formatCommas(lowestBin, 1) + ) + ) + } + } + } + } + thenLiteral(DeveloperFeatures.DEVELOPER_SUBCOMMAND) { + thenLiteral("simulate") { + thenArgument("message", RestArgumentType) { message -> + thenExecute { + MC.instance.messageHandler.onGameMessage(Text.literal(get(message)), false) + } + } + } + thenLiteral("debuglog") { + thenLiteral("toggle") { + thenArgument("tag", string()) { tag -> + suggestsList { DebugLogger.allInstances.getAll().map { it.tag } + DebugLogger.EnabledLogs.data } + thenExecute { + val tagText = this[tag] + val enabled = DebugLogger.EnabledLogs.data + if (tagText in enabled) { + enabled.remove(tagText) + source.sendFeedback(Text.literal("Disabled $tagText debug logging")) + } else { + enabled.add(tagText) + source.sendFeedback(Text.literal("Enabled $tagText debug logging")) + } + } + } + } + } + thenLiteral("screens") { + thenExecute { + MC.sendChat(Text.literal(""" + |Screen: ${MC.screen} (${MC.screen?.title}) + |Screen Handler: ${MC.handledScreen?.screenHandler} ${MC.handledScreen?.screenHandler?.syncId} + |Player Screen Handler: ${MC.player?.currentScreenHandler} ${MC.player?.currentScreenHandler?.syncId} + """.trimMargin())) + } + } + thenLiteral("blocks") { + thenExecute { + ScreenUtil.setScreenLater(MiningBlockInfoUi.makeScreen()) + } + } + thenLiteral("dumpchat") { + thenExecute { + MC.inGameHud.chatHud.messages.forEach { + val nbt = TextCodecs.CODEC.encodeStart(NbtOps.INSTANCE, it.content).orThrow + println(nbt) + } + } + thenArgument("search", string()) { search -> + thenExecute { + MC.inGameHud.chatHud.messages + .filter { this[search] in it.content.unformattedString } + .forEach { + val nbt = TextCodecs.CODEC.encodeStart(NbtOps.INSTANCE, it.content).orThrow + println(SNbtFormatter.prettify(nbt)) + } + } + } + } + thenLiteral("sbdata") { + thenExecute { + source.sendFeedback(Text.stringifiedTranslatable("firmament.sbinfo.profile", SBData.profileId)) + val locrawInfo = SBData.locraw + if (locrawInfo == null) { + source.sendFeedback(Text.translatable("firmament.sbinfo.nolocraw")) + } else { + source.sendFeedback(Text.stringifiedTranslatable("firmament.sbinfo.server", locrawInfo.server)) + source.sendFeedback(Text.stringifiedTranslatable("firmament.sbinfo.gametype", locrawInfo.gametype)) + source.sendFeedback(Text.stringifiedTranslatable("firmament.sbinfo.mode", locrawInfo.mode)) + source.sendFeedback(Text.stringifiedTranslatable("firmament.sbinfo.map", locrawInfo.map)) + source.sendFeedback(tr("firmament.sbinfo.custommining", + "Custom Mining: ${formatBool(locrawInfo.skyblockLocation?.hasCustomMining ?: false)}")) + } + } + } + thenLiteral("copyEntities") { + thenExecute { + val player = MC.player ?: return@thenExecute + player.world.getOtherEntities(player, player.boundingBox.expand(12.0)) + .forEach(PowerUserTools::showEntity) + PowerUserTools.showEntity(player) + } + } + thenLiteral("callUrsa") { + thenArgument("path", string()) { path -> + thenExecute { + source.sendFeedback(Text.translatable("firmament.ursa.debugrequest.start")) + val text = UrsaManager.request(this[path].split("/")).bodyAsText() + source.sendFeedback(Text.stringifiedTranslatable("firmament.ursa.debugrequest.result", text)) + } + } + } + thenLiteral("events") { + thenExecute { + source.sendFeedback(tr("firmament.event.start", "Event Bus Readout:")) + FirmamentEventBus.allEventBuses.forEach { eventBus -> + val prefixName = eventBus.eventType.typeName.removePrefix("moe.nea.firmament") + source.sendFeedback(tr( + "firmament.event.bustype", + "- $prefixName:")) + eventBus.handlers.forEach { handler -> + source.sendFeedback(tr( + "firmament.event.handler", + " * ${handler.label}")) + } + } + } + } + thenLiteral("caches") { + thenExecute { + source.sendFeedback(Text.literal("Caches:")) + WeakCache.allInstances.getAll().forEach { + source.sendFeedback(Text.literal(" - ${it.name}: ${it.size}")) + } + source.sendFeedback(Text.translatable("Instance lists:")) + InstanceList.allInstances.getAll().forEach { + source.sendFeedback(Text.literal(" - ${it.name}: ${it.size}")) + } + } + } + thenLiteral("mixins") { + thenExecute { + MixinPlugin.instances.forEach { plugin -> + source.sendFeedback(tr("firmament.mixins.start.package", "Mixins (base ${plugin.mixinPackage}):")) + plugin.appliedMixins + .map { it.removePrefix(plugin.mixinPackage) } + .forEach { + source.sendFeedback(Text.literal(" - ").withColor(0xD020F0) + .append(Text.literal(it).withColor(0xF6BA20))) + } + } + } + } + thenLiteral("repo") { + thenExecute { + source.sendFeedback(tr("firmament.repo.info.ref", "Repo Upstream: ${RepoManager.getRepoRef()}")) + source.sendFeedback(tr("firmament.repo.info.downloadedref", + "Downloaded ref: ${RepoDownloadManager.latestSavedVersionHash}")) + source.sendFeedback(tr("firmament.repo.info.location", + "Saved location: ${debugPath(RepoDownloadManager.repoSavedLocation)}")) + source.sendFeedback(tr("firmament.repo.info.reloadstatus", + "Incomplete: ${ + formatBool(RepoManager.neuRepo.isIncomplete, + trueIsGood = false) + }, Unstable ${formatBool(RepoManager.neuRepo.isUnstable, trueIsGood = false)}")) + source.sendFeedback(tr("firmament.repo.info.items", + "Loaded items: ${RepoManager.neuRepo.items?.items?.size}")) + source.sendFeedback(tr("firmament.repo.info.itemcache", + "ItemCache flawless: ${formatBool(ItemCache.isFlawless)}")) + source.sendFeedback(tr("firmament.repo.info.itemdir", + "Items on disk: ${debugPath(RepoDownloadManager.repoSavedLocation.resolve("items"))}")) + } + } + } + thenExecute { + AllConfigsGui.showAllGuis() + } + CommandEvent.SubCommand.publish(CommandEvent.SubCommand(this@literal)) +} + + +fun registerFirmamentCommand(dispatcher: CommandDispatcher) { + val firmament = dispatcher.register(firmamentCommand()) + dispatcher.register(literal("firm") { + redirect(firmament) + }) +} + + + + diff --git a/src/main/kotlin/events/AllowChatEvent.kt b/src/main/kotlin/events/AllowChatEvent.kt new file mode 100644 index 0000000..3069843 --- /dev/null +++ b/src/main/kotlin/events/AllowChatEvent.kt @@ -0,0 +1,16 @@ + + +package moe.nea.firmament.events + +import moe.nea.firmament.util.unformattedString +import net.minecraft.text.Text + +/** + * Filter whether the user should see a chat message altogether. May or may not be called for every chat packet sent by + * the server. When that quality is desired, consider [ProcessChatEvent] instead. + */ +data class AllowChatEvent(val text: Text) : FirmamentEvent.Cancellable() { + val unformattedString = text.unformattedString + + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/events/AttackBlockEvent.kt b/src/main/kotlin/events/AttackBlockEvent.kt new file mode 100644 index 0000000..bbaa81d --- /dev/null +++ b/src/main/kotlin/events/AttackBlockEvent.kt @@ -0,0 +1,18 @@ + +package moe.nea.firmament.events + +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.util.Hand +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Direction +import net.minecraft.world.World + +data class AttackBlockEvent( + val player: PlayerEntity, + val world: World, + val hand: Hand, + val blockPos: BlockPos, + val direction: Direction +) : FirmamentEvent.Cancellable() { + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/events/ChestInventoryUpdateEvent.kt b/src/main/kotlin/events/ChestInventoryUpdateEvent.kt new file mode 100644 index 0000000..ddf54fc --- /dev/null +++ b/src/main/kotlin/events/ChestInventoryUpdateEvent.kt @@ -0,0 +1,11 @@ +package moe.nea.firmament.events + +import net.minecraft.item.ItemStack +import moe.nea.firmament.util.MC + +sealed class ChestInventoryUpdateEvent : FirmamentEvent() { + companion object : FirmamentEventBus() + data class Single(val slot: Int, val stack: ItemStack) : ChestInventoryUpdateEvent() + data class Multi(val contents: List) : ChestInventoryUpdateEvent() + val inventory = MC.screen +} diff --git a/src/main/kotlin/events/ClientInitEvent.kt b/src/main/kotlin/events/ClientInitEvent.kt new file mode 100644 index 0000000..7d13d65 --- /dev/null +++ b/src/main/kotlin/events/ClientInitEvent.kt @@ -0,0 +1,5 @@ +package moe.nea.firmament.events + +class ClientInitEvent : FirmamentEvent() { + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/events/ClientStartedEvent.kt b/src/main/kotlin/events/ClientStartedEvent.kt new file mode 100644 index 0000000..637916d --- /dev/null +++ b/src/main/kotlin/events/ClientStartedEvent.kt @@ -0,0 +1,6 @@ + +package moe.nea.firmament.events + +class ClientStartedEvent : FirmamentEvent() { + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/events/CommandEvent.kt b/src/main/kotlin/events/CommandEvent.kt new file mode 100644 index 0000000..cc9cf45 --- /dev/null +++ b/src/main/kotlin/events/CommandEvent.kt @@ -0,0 +1,45 @@ + + +package moe.nea.firmament.events + +import com.mojang.brigadier.CommandDispatcher +import com.mojang.brigadier.tree.LiteralCommandNode +import net.minecraft.command.CommandRegistryAccess +import moe.nea.firmament.commands.CaseInsensitiveLiteralCommandNode +import moe.nea.firmament.commands.DefaultSource +import moe.nea.firmament.commands.literal +import moe.nea.firmament.commands.thenLiteral + +data class CommandEvent( + val dispatcher: CommandDispatcher, + val ctx: CommandRegistryAccess, + val serverCommands: CommandDispatcher<*>?, +) : FirmamentEvent() { + companion object : FirmamentEventBus() + + /** + * Register subcommands to `/firm`. For new top level commands use [CommandEvent]. Cannot be used to register + * subcommands to other commands. + */ + data class SubCommand( + val builder: CaseInsensitiveLiteralCommandNode.Builder, + ) : FirmamentEvent() { + companion object : FirmamentEventBus() + + fun subcommand(name: String, block: CaseInsensitiveLiteralCommandNode.Builder.() -> Unit) { + builder.thenLiteral(name, block) + } + } + + fun deleteCommand(name: String) { + dispatcher.root.children.removeIf { it.name.equals(name, ignoreCase = false) } + serverCommands?.root?.children?.removeIf { it.name.equals(name, ignoreCase = false) } + } + + fun register( + name: String, + block: CaseInsensitiveLiteralCommandNode.Builder.() -> Unit + ): LiteralCommandNode { + return dispatcher.register(literal(name, block)) + } +} diff --git a/src/main/kotlin/events/CustomItemModelEvent.kt b/src/main/kotlin/events/CustomItemModelEvent.kt new file mode 100644 index 0000000..7b86980 --- /dev/null +++ b/src/main/kotlin/events/CustomItemModelEvent.kt @@ -0,0 +1,75 @@ +package moe.nea.firmament.events + +import java.util.Objects +import java.util.Optional +import kotlin.jvm.optionals.getOrNull +import net.minecraft.component.DataComponentTypes +import net.minecraft.item.ItemStack +import net.minecraft.util.Identifier +import moe.nea.firmament.util.collections.WeakCache +import moe.nea.firmament.util.collections.WeakCache.CacheFunction +import moe.nea.firmament.util.mc.IntrospectableItemModelManager + +// TODO: assert an order on these events +data class CustomItemModelEvent( + val itemStack: ItemStack, + val itemModelManager: IntrospectableItemModelManager, + var overrideModel: Identifier? = null, +) : FirmamentEvent() { + companion object : FirmamentEventBus() { + val weakCache = + object : WeakCache>("ItemModelIdentifier") { + override fun mkRef( + key: ItemStack, + extraData: IntrospectableItemModelManager + ): WeakCache>.Ref { + return IRef(key, extraData) + } + + inner class IRef(weakInstance: ItemStack, data: IntrospectableItemModelManager) : + Ref(weakInstance, data) { + override fun shouldBeEvicted(): Boolean = false + val isSimpleStack = weakInstance.componentChanges.isEmpty || (weakInstance.componentChanges.size() == 1 && weakInstance.get( + DataComponentTypes.CUSTOM_DATA)?.isEmpty == true) + val item = weakInstance.item + override fun hashCode(): Int { + if (isSimpleStack) + return Objects.hash(item, extraData) + return super.hashCode() + } + + override fun equals(other: Any?): Boolean { + if (other is IRef && isSimpleStack) { + return other.isSimpleStack && item == other.item + } + return super.equals(other) + } + } + } + val cache = CacheFunction.WithExtraData(weakCache, ::getModelIdentifier0) + + @JvmStatic + fun getModelIdentifier(itemStack: ItemStack?, itemModelManager: IntrospectableItemModelManager): Identifier? { + if (itemStack == null) return null + return cache.invoke(itemStack, itemModelManager).getOrNull() + } + + fun getModelIdentifier0( + itemStack: ItemStack, + itemModelManager: IntrospectableItemModelManager + ): Optional { + // TODO: add an error / warning if the model does not exist + return Optional.ofNullable(publish(CustomItemModelEvent(itemStack, itemModelManager)).overrideModel) + } + } + + fun overrideIfExists(overrideModel: Identifier) { + if (itemModelManager.hasModel_firmament(overrideModel)) + this.overrideModel = overrideModel + } + + fun overrideIfEmpty(identifier: Identifier) { + if (overrideModel == null) + overrideModel = identifier + } +} diff --git a/src/main/kotlin/events/DebugInstantiateEvent.kt b/src/main/kotlin/events/DebugInstantiateEvent.kt new file mode 100644 index 0000000..3470a8c --- /dev/null +++ b/src/main/kotlin/events/DebugInstantiateEvent.kt @@ -0,0 +1,9 @@ +package moe.nea.firmament.events + +/** + * Called in a devenv after minecraft has been initialized. This event should be used to force instantiation of lazy + * variables (and similar late init) to cause any possible issues to materialize. + */ +class DebugInstantiateEvent : FirmamentEvent() { + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/events/EarlyResourceReloadEvent.kt b/src/main/kotlin/events/EarlyResourceReloadEvent.kt new file mode 100644 index 0000000..ec8377a --- /dev/null +++ b/src/main/kotlin/events/EarlyResourceReloadEvent.kt @@ -0,0 +1,10 @@ + +package moe.nea.firmament.events + +import java.util.concurrent.Executor +import net.minecraft.resource.ResourceManager + +data class EarlyResourceReloadEvent(val resourceManager: ResourceManager, val preparationExecutor: Executor) : + FirmamentEvent() { + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/events/EntityDespawnEvent.kt b/src/main/kotlin/events/EntityDespawnEvent.kt new file mode 100644 index 0000000..93dc477 --- /dev/null +++ b/src/main/kotlin/events/EntityDespawnEvent.kt @@ -0,0 +1,11 @@ + +package moe.nea.firmament.events + +import net.minecraft.entity.Entity + +data class EntityDespawnEvent( + val entity: Entity?, val entityId: Int, + val reason: Entity.RemovalReason, +) : FirmamentEvent() { + companion object: FirmamentEventBus() +} diff --git a/src/main/kotlin/events/EntityInteractionEvent.kt b/src/main/kotlin/events/EntityInteractionEvent.kt new file mode 100644 index 0000000..123ea39 --- /dev/null +++ b/src/main/kotlin/events/EntityInteractionEvent.kt @@ -0,0 +1,29 @@ + +package moe.nea.firmament.events + +import net.minecraft.entity.Entity +import net.minecraft.util.Hand + +data class EntityInteractionEvent( + val kind: InteractionKind, + val entity: Entity, + val hand: Hand, +) : FirmamentEvent() { + companion object : FirmamentEventBus() + enum class InteractionKind { + /** + * Is sent when left-clicking an entity + */ + ATTACK, + + /** + * Is a fallback when [INTERACT_AT_LOCATION] fails + */ + INTERACT, + + /** + * Is tried first on right click + */ + INTERACT_AT_LOCATION, + } +} diff --git a/src/main/kotlin/events/EntityRenderTintEvent.kt b/src/main/kotlin/events/EntityRenderTintEvent.kt new file mode 100644 index 0000000..29b888b --- /dev/null +++ b/src/main/kotlin/events/EntityRenderTintEvent.kt @@ -0,0 +1,66 @@ +package moe.nea.firmament.events + +import net.minecraft.client.render.GameRenderer +import net.minecraft.client.render.OverlayTexture +import net.minecraft.client.render.entity.state.EntityRenderState +import net.minecraft.entity.Entity +import net.minecraft.entity.LivingEntity +import moe.nea.firmament.util.render.TintedOverlayTexture + +/** + * Change the tint color of a [LivingEntity] + */ +class EntityRenderTintEvent( + val entity: Entity, + val renderState: HasTintRenderState +) : FirmamentEvent.Cancellable() { + init { + if (entity !is LivingEntity) { + cancel() + } + } + + companion object : FirmamentEventBus() { + /** + * Static variable containing an override for [GameRenderer.getOverlayTexture]. Should be only set briefly. + * + * This variable only affects render layers that naturally make use of the overlay texture, have proper overlay UVs set (`overlay u != 0`), and have a shader that makes use of the overlay (does not have the `NO_OVERLAY` flag set in its json definition). + * + * Currently supported layers: [net.minecraft.client.render.entity.equipment.EquipmentRenderer], [net.minecraft.client.render.entity.model.PlayerEntityModel], as well as some others naturally. + * + * @see moe.nea.firmament.mixins.render.entitytints.ReplaceOverlayTexture + * @see TintedOverlayTexture + */ + @JvmField + var overlayOverride: OverlayTexture? = null + } + + @Suppress("PropertyName", "FunctionName") + interface HasTintRenderState { + /** + * Multiplicative tint applied before the overlay. + */ + var tint_firmament: Int + + /** + * Must be set for [tint_firmament] to have any effect. + */ + var hasTintOverride_firmament: Boolean + + // TODO: allow for more specific selection of which layers get tinted + /** + * Specify a [TintedOverlayTexture] to be used. This does not apply to render layers not using the overlay texture. + * @see overlayOverride + */ + var overlayTexture_firmament: TintedOverlayTexture? + fun reset_firmament() + + companion object { + @JvmStatic + fun cast(state: EntityRenderState): HasTintRenderState { + return state as HasTintRenderState + } + } + } + +} diff --git a/src/main/kotlin/events/EntityUpdateEvent.kt b/src/main/kotlin/events/EntityUpdateEvent.kt new file mode 100644 index 0000000..fec2fa5 --- /dev/null +++ b/src/main/kotlin/events/EntityUpdateEvent.kt @@ -0,0 +1,60 @@ +package moe.nea.firmament.events + +import com.mojang.datafixers.util.Pair +import net.minecraft.entity.Entity +import net.minecraft.entity.EquipmentSlot +import net.minecraft.entity.LivingEntity +import net.minecraft.entity.data.DataTracker +import net.minecraft.item.ItemStack +import net.minecraft.network.packet.s2c.play.EntityAttributesS2CPacket +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.util.MC + +/** + * This event is fired when some entity properties are updated. + * It is not fired for common changes like position, but is for less common ones, + * like health, tracked data, names, equipment. It is always fired + * *after* the values have been applied to the entity. + */ +sealed class EntityUpdateEvent : FirmamentEvent() { + companion object : FirmamentEventBus() { + @Subscribe + fun onPlayerInventoryUpdate(event: PlayerInventoryUpdate) { + val p = MC.player ?: return + val updatedSlots = listOf( + EquipmentSlot.HEAD to 39, + EquipmentSlot.CHEST to 38, + EquipmentSlot.LEGS to 37, + EquipmentSlot.FEET to 36, + EquipmentSlot.OFFHAND to 40, + EquipmentSlot.MAINHAND to p.inventory.selectedSlot, // TODO: also equipment update when you swap your selected slot perhaps + ).mapNotNull { (slot, stackIndex) -> + val slotIndex = p.playerScreenHandler.getSlotIndex(p.inventory, stackIndex).asInt + event.getOrNull(slotIndex)?.let { + Pair.of(slot, it) + } + } + if (updatedSlots.isNotEmpty()) + publish(EquipmentUpdate(p, updatedSlots)) + } + } + + abstract val entity: Entity + + data class AttributeUpdate( + override val entity: LivingEntity, + val attributes: List, + ) : EntityUpdateEvent() + + data class TrackedDataUpdate( + override val entity: Entity, + val trackedValues: List>, + ) : EntityUpdateEvent() + + data class EquipmentUpdate( + override val entity: Entity, + val newEquipment: List>, + ) : EntityUpdateEvent() + +// TODO: onEntityPassengersSet, onEntityAttach?, onEntityStatusEffect +} diff --git a/src/main/kotlin/events/FeaturesInitializedEvent.kt b/src/main/kotlin/events/FeaturesInitializedEvent.kt new file mode 100644 index 0000000..ad2ad8a --- /dev/null +++ b/src/main/kotlin/events/FeaturesInitializedEvent.kt @@ -0,0 +1,8 @@ + +package moe.nea.firmament.events + +import moe.nea.firmament.features.FirmamentFeature + +data class FeaturesInitializedEvent(val features: List) : FirmamentEvent() { + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/events/FinalizeResourceManagerEvent.kt b/src/main/kotlin/events/FinalizeResourceManagerEvent.kt new file mode 100644 index 0000000..12167f8 --- /dev/null +++ b/src/main/kotlin/events/FinalizeResourceManagerEvent.kt @@ -0,0 +1,32 @@ +package moe.nea.firmament.events + +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Executor +import net.minecraft.resource.ReloadableResourceManagerImpl +import net.minecraft.resource.ResourceManager +import net.minecraft.resource.ResourceReloader + +data class FinalizeResourceManagerEvent( + val resourceManager: ReloadableResourceManagerImpl, +) : FirmamentEvent() { + companion object : FirmamentEventBus() + + inline fun registerOnApply(name: String, crossinline function: () -> Unit) { + resourceManager.registerReloader(object : ResourceReloader { + override fun reload( + synchronizer: ResourceReloader.Synchronizer, + manager: ResourceManager, + prepareExecutor: Executor, + applyExecutor: Executor + ): CompletableFuture { + return CompletableFuture.completedFuture(Unit) + .thenCompose(synchronizer::whenPrepared) + .thenAcceptAsync({ function() }, applyExecutor) + } + + override fun getName(): String { + return name + } + }) + } +} diff --git a/src/main/kotlin/events/FirmamentEvent.kt b/src/main/kotlin/events/FirmamentEvent.kt new file mode 100644 index 0000000..1a93ef5 --- /dev/null +++ b/src/main/kotlin/events/FirmamentEvent.kt @@ -0,0 +1,38 @@ + + +package moe.nea.firmament.events + +/** + * An event that can be fired by a [FirmamentEventBus]. + * + * Typically, that event bus is implemented as a companion object + * + * ``` + * class SomeEvent : FirmamentEvent() { + * companion object : FirmamentEventBus() + * } + * ``` + */ +abstract class FirmamentEvent { + /** + * A [FirmamentEvent] that can be [cancelled] + */ + abstract class Cancellable : FirmamentEvent() { + /** + * Cancels this is event. + * + * @see cancelled + */ + fun cancel() { + cancelled = true + } + + /** + * Whether this event is cancelled. + * + * Cancelled events will bypass handlers unless otherwise specified and will prevent the action that this + * event was originally fired for. + */ + var cancelled: Boolean = false + } +} diff --git a/src/main/kotlin/events/FirmamentEventBus.kt b/src/main/kotlin/events/FirmamentEventBus.kt new file mode 100644 index 0000000..af4e16a --- /dev/null +++ b/src/main/kotlin/events/FirmamentEventBus.kt @@ -0,0 +1,65 @@ +package moe.nea.firmament.events + +import java.util.concurrent.CopyOnWriteArrayList +import org.apache.commons.lang3.reflect.TypeUtils +import moe.nea.firmament.Firmament +import moe.nea.firmament.util.ErrorUtil +import moe.nea.firmament.util.MC + +/** + * A pubsub event bus. + * + * [subscribe] to events [publish]ed on this event bus. + * Subscriptions may not necessarily be delivered in the order of registering. + */ +open class FirmamentEventBus { + companion object { + val allEventBuses = mutableListOf>() + } + + val eventType = TypeUtils.getTypeArguments(javaClass, FirmamentEventBus::class.java)!!.values.single() + + init { + allEventBuses.add(this) + } + + data class Handler( + val invocation: (T) -> Unit, val receivesCancelled: Boolean, + var knownErrors: MutableSet> = mutableSetOf(), + val label: String, + ) + + private val toHandle: MutableList> = CopyOnWriteArrayList() + val handlers: List> get() = toHandle + + fun subscribe(label: String, handle: (T) -> Unit) { + subscribe(false, label, handle) + } + + fun subscribe(receivesCancelled: Boolean, label: String, handle: (T) -> Unit) { + toHandle.add(Handler(handle, receivesCancelled, label = label)) + } + + fun publish(event: T): T { + for (function in toHandle) { + if (function.receivesCancelled || event !is FirmamentEvent.Cancellable || !event.cancelled) { + try { + function.invocation(event) + } catch (e: Exception) { + val klass = e.javaClass + if (!function.knownErrors.contains(klass) || Firmament.DEBUG) { + function.knownErrors.add(klass) + ErrorUtil.softError("Caught exception during processing event $event by $function", e) + } + } + } + } + return event + } + + fun publishSync(event: T) { + MC.onMainThread { + publish(event) + } + } +} diff --git a/src/main/kotlin/events/HandledScreenClickEvent.kt b/src/main/kotlin/events/HandledScreenClickEvent.kt new file mode 100644 index 0000000..4c3003c --- /dev/null +++ b/src/main/kotlin/events/HandledScreenClickEvent.kt @@ -0,0 +1,10 @@ + + +package moe.nea.firmament.events + +import net.minecraft.client.gui.screen.ingame.HandledScreen + +data class HandledScreenClickEvent(val screen: HandledScreen<*>, val mouseX: Double, val mouseY: Double, val button: Int) : + FirmamentEvent.Cancellable() { + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/events/HandledScreenForegroundEvent.kt b/src/main/kotlin/events/HandledScreenForegroundEvent.kt new file mode 100644 index 0000000..f16d30e --- /dev/null +++ b/src/main/kotlin/events/HandledScreenForegroundEvent.kt @@ -0,0 +1,16 @@ + + +package moe.nea.firmament.events + +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.gui.screen.ingame.HandledScreen + +data class HandledScreenForegroundEvent( + val screen: HandledScreen<*>, + val context: DrawContext, + val mouseX: Int, + val mouseY: Int, + val delta: Float +) : FirmamentEvent() { + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/events/HandledScreenKeyPressedEvent.kt b/src/main/kotlin/events/HandledScreenKeyPressedEvent.kt new file mode 100644 index 0000000..183ec71 --- /dev/null +++ b/src/main/kotlin/events/HandledScreenKeyPressedEvent.kt @@ -0,0 +1,38 @@ +package moe.nea.firmament.events + +import net.minecraft.client.gui.screen.ingame.HandledScreen +import net.minecraft.client.option.KeyBinding +import moe.nea.firmament.keybindings.IKeyBinding + +sealed interface HandledScreenKeyEvent { + val screen: HandledScreen<*> + val keyCode: Int + val scanCode: Int + val modifiers: Int + + fun matches(keyBinding: KeyBinding): Boolean { + return matches(IKeyBinding.minecraft(keyBinding)) + } + + fun matches(keyBinding: IKeyBinding): Boolean { + return keyBinding.matches(keyCode, scanCode, modifiers) + } +} + +data class HandledScreenKeyPressedEvent( + override val screen: HandledScreen<*>, + override val keyCode: Int, + override val scanCode: Int, + override val modifiers: Int +) : FirmamentEvent.Cancellable(), HandledScreenKeyEvent { + companion object : FirmamentEventBus() +} + +data class HandledScreenKeyReleasedEvent( + override val screen: HandledScreen<*>, + override val keyCode: Int, + override val scanCode: Int, + override val modifiers: Int +) : FirmamentEvent.Cancellable(), HandledScreenKeyEvent { + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/events/HandledScreenPushREIEvent.kt b/src/main/kotlin/events/HandledScreenPushREIEvent.kt new file mode 100644 index 0000000..1bb495a --- /dev/null +++ b/src/main/kotlin/events/HandledScreenPushREIEvent.kt @@ -0,0 +1,18 @@ + + +package moe.nea.firmament.events + +import me.shedaniel.math.Rectangle +import net.minecraft.client.gui.screen.ingame.HandledScreen + +data class HandledScreenPushREIEvent( + val screen: HandledScreen<*>, + val rectangles: MutableList = mutableListOf() +) : FirmamentEvent() { + + fun block(rectangle: Rectangle) { + rectangles.add(rectangle) + } + + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/events/HotbarItemRenderEvent.kt b/src/main/kotlin/events/HotbarItemRenderEvent.kt new file mode 100644 index 0000000..a1940e6 --- /dev/null +++ b/src/main/kotlin/events/HotbarItemRenderEvent.kt @@ -0,0 +1,17 @@ + + +package moe.nea.firmament.events + +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.render.RenderTickCounter +import net.minecraft.item.ItemStack + +data class HotbarItemRenderEvent( + val item: ItemStack, + val context: DrawContext, + val x: Int, + val y: Int, + val tickDelta: RenderTickCounter, +) : FirmamentEvent() { + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/events/HudRenderEvent.kt b/src/main/kotlin/events/HudRenderEvent.kt new file mode 100644 index 0000000..a773a93 --- /dev/null +++ b/src/main/kotlin/events/HudRenderEvent.kt @@ -0,0 +1,17 @@ + + +package moe.nea.firmament.events + +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.render.RenderTickCounter +import net.minecraft.world.GameMode +import moe.nea.firmament.util.MC + +/** + * Called when hud elements should be rendered, before the screen, but after the world. + */ +data class HudRenderEvent(val context: DrawContext, val tickDelta: RenderTickCounter) : FirmamentEvent() { + val isRenderingHud = !MC.options.hudHidden + val isRenderingCursor = MC.interactionManager?.currentGameMode != GameMode.SPECTATOR && isRenderingHud + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/events/IsSlotProtectedEvent.kt b/src/main/kotlin/events/IsSlotProtectedEvent.kt new file mode 100644 index 0000000..8fe0a96 --- /dev/null +++ b/src/main/kotlin/events/IsSlotProtectedEvent.kt @@ -0,0 +1,65 @@ +package moe.nea.firmament.events + +import net.minecraft.item.ItemStack +import net.minecraft.screen.slot.Slot +import net.minecraft.screen.slot.SlotActionType +import net.minecraft.text.Text +import moe.nea.firmament.util.CommonSoundEffects +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.grey +import moe.nea.firmament.util.hover +import moe.nea.firmament.util.red +import moe.nea.firmament.util.tr + +data class IsSlotProtectedEvent( + val slot: Slot?, + val actionType: SlotActionType, + var isProtected: Boolean, + val itemStackOverride: ItemStack?, + val origin: MoveOrigin, + var silent: Boolean = false, +) : FirmamentEvent() { + val itemStack get() = itemStackOverride ?: slot!!.stack + + fun protect() { + isProtected = true + silent = false + } + + fun protectSilent() { + if (!isProtected) { + silent = true + } + isProtected = true + } + + enum class MoveOrigin { + DROP_FROM_HOTBAR, + SALVAGE, + INVENTORY_MOVE + ; + } + + companion object : FirmamentEventBus() { + @JvmStatic + @JvmOverloads + fun shouldBlockInteraction( + slot: Slot?, action: SlotActionType, + origin: MoveOrigin, + itemStackOverride: ItemStack? = null, + ): Boolean { + if (slot == null && itemStackOverride == null) return false + val event = IsSlotProtectedEvent(slot, action, false, itemStackOverride, origin) + publish(event) + if (event.isProtected && !event.silent) { + MC.sendChat(tr("firmament.protectitem", "Firmament protected your item: ${event.itemStack.name}.\n") + .red() + .append(tr("firmament.protectitem.hoverhint", "Hover for more info.").grey()) + .hover(tr("firmament.protectitem.hint", + "To unlock this item use the Lock Slot or Lock Item keybind from Firmament while hovering over this item."))) + CommonSoundEffects.playFailure() + } + return event.isProtected + } + } +} diff --git a/src/main/kotlin/events/ItemTooltipEvent.kt b/src/main/kotlin/events/ItemTooltipEvent.kt new file mode 100644 index 0000000..d86e06f --- /dev/null +++ b/src/main/kotlin/events/ItemTooltipEvent.kt @@ -0,0 +1,14 @@ + + +package moe.nea.firmament.events + +import net.minecraft.item.Item.TooltipContext +import net.minecraft.item.ItemStack +import net.minecraft.item.tooltip.TooltipType +import net.minecraft.text.Text + +data class ItemTooltipEvent( + val stack: ItemStack, val context: TooltipContext, val type: TooltipType, val lines: MutableList +) : FirmamentEvent() { + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/events/MaskCommands.kt b/src/main/kotlin/events/MaskCommands.kt new file mode 100644 index 0000000..35aade0 --- /dev/null +++ b/src/main/kotlin/events/MaskCommands.kt @@ -0,0 +1,13 @@ + + +package moe.nea.firmament.events + +import com.mojang.brigadier.CommandDispatcher + +data class MaskCommands(val dispatcher: CommandDispatcher<*>) : FirmamentEvent() { + companion object : FirmamentEventBus() + + fun mask(name: String) { + dispatcher.root.children.removeIf { it.name.equals(name, ignoreCase = true) } + } +} diff --git a/src/main/kotlin/events/ModifyChatEvent.kt b/src/main/kotlin/events/ModifyChatEvent.kt new file mode 100644 index 0000000..a5868e8 --- /dev/null +++ b/src/main/kotlin/events/ModifyChatEvent.kt @@ -0,0 +1,21 @@ + + +package moe.nea.firmament.events + +import moe.nea.firmament.util.unformattedString +import net.minecraft.text.Text + +/** + * Allow modification of a chat message before it is sent off to the user. Intended for display purposes. + */ +data class ModifyChatEvent(val originalText: Text) : FirmamentEvent() { + var unformattedString = originalText.unformattedString + private set + var replaceWith: Text = originalText + set(value) { + field = value + unformattedString = value.unformattedString + } + + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/events/OutgoingPacketEvent.kt b/src/main/kotlin/events/OutgoingPacketEvent.kt new file mode 100644 index 0000000..93890ea --- /dev/null +++ b/src/main/kotlin/events/OutgoingPacketEvent.kt @@ -0,0 +1,9 @@ + + +package moe.nea.firmament.events + +import net.minecraft.network.packet.Packet + +data class OutgoingPacketEvent(val packet: Packet<*>) : FirmamentEvent.Cancellable() { + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/events/ParticleSpawnEvent.kt b/src/main/kotlin/events/ParticleSpawnEvent.kt new file mode 100644 index 0000000..9359e4b --- /dev/null +++ b/src/main/kotlin/events/ParticleSpawnEvent.kt @@ -0,0 +1,18 @@ + + +package moe.nea.firmament.events + +import org.joml.Vector3f +import net.minecraft.particle.ParticleEffect +import net.minecraft.util.math.Vec3d + +data class ParticleSpawnEvent( + val particleEffect: ParticleEffect, + val position: Vec3d, + val offset: Vector3f, + val longDistance: Boolean, + val count: Int, + val speed: Float, +) : FirmamentEvent.Cancellable() { + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/events/PartyMessageReceivedEvent.kt b/src/main/kotlin/events/PartyMessageReceivedEvent.kt new file mode 100644 index 0000000..4688dfe --- /dev/null +++ b/src/main/kotlin/events/PartyMessageReceivedEvent.kt @@ -0,0 +1,9 @@ +package moe.nea.firmament.events + +data class PartyMessageReceivedEvent( + val from: ProcessChatEvent, + val message: String, + val name: String, +) : FirmamentEvent() { + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/events/PlayerInventoryUpdate.kt b/src/main/kotlin/events/PlayerInventoryUpdate.kt new file mode 100644 index 0000000..88439a9 --- /dev/null +++ b/src/main/kotlin/events/PlayerInventoryUpdate.kt @@ -0,0 +1,22 @@ +package moe.nea.firmament.events + +import net.minecraft.item.ItemStack + +sealed class PlayerInventoryUpdate : FirmamentEvent() { + companion object : FirmamentEventBus() + data class Single(val slot: Int, val stack: ItemStack) : PlayerInventoryUpdate() { + override fun getOrNull(slot: Int): ItemStack? { + if (slot == this.slot) return stack + return null + } + + } + + data class Multi(val contents: List) : PlayerInventoryUpdate() { + override fun getOrNull(slot: Int): ItemStack? { + return contents.getOrNull(slot) + } + } + + abstract fun getOrNull(slot: Int): ItemStack? +} diff --git a/src/main/kotlin/events/ProcessChatEvent.kt b/src/main/kotlin/events/ProcessChatEvent.kt new file mode 100644 index 0000000..76c0b27 --- /dev/null +++ b/src/main/kotlin/events/ProcessChatEvent.kt @@ -0,0 +1,28 @@ + + +package moe.nea.firmament.events + +import net.minecraft.text.Text +import moe.nea.firmament.util.unformattedString + +/** + * Behaves like [AllowChatEvent], but is triggered even when cancelled by other mods. Intended for data collection. + * Make sure to subscribe to cancellable events as well when using. + */ +data class ProcessChatEvent(val text: Text, val wasExternallyCancelled: Boolean) : FirmamentEvent.Cancellable() { + val unformattedString = text.unformattedString + + val nameHeuristic: String? = run { + val firstColon = unformattedString.indexOf(':') + if (firstColon < 0) return@run null + val firstSpace = unformattedString.lastIndexOf(' ', firstColon) + unformattedString.substring(firstSpace + 1 until firstColon).takeIf { it.isNotEmpty() } + } + + init { + if (wasExternallyCancelled) + cancelled = true + } + + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/events/ProfileSwitchEvent.kt b/src/main/kotlin/events/ProfileSwitchEvent.kt new file mode 100644 index 0000000..683b7dd --- /dev/null +++ b/src/main/kotlin/events/ProfileSwitchEvent.kt @@ -0,0 +1,7 @@ +package moe.nea.firmament.events + +import java.util.UUID + +data class ProfileSwitchEvent(val oldProfile: UUID?, val newProfile: UUID?) : FirmamentEvent() { + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/events/ReloadRegistrationEvent.kt b/src/main/kotlin/events/ReloadRegistrationEvent.kt new file mode 100644 index 0000000..4c3083e --- /dev/null +++ b/src/main/kotlin/events/ReloadRegistrationEvent.kt @@ -0,0 +1,7 @@ +package moe.nea.firmament.events + +import io.github.moulberry.repo.NEURepository + +data class ReloadRegistrationEvent(val repo: NEURepository) : FirmamentEvent() { + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/events/ScreenChangeEvent.kt b/src/main/kotlin/events/ScreenChangeEvent.kt new file mode 100644 index 0000000..489e487 --- /dev/null +++ b/src/main/kotlin/events/ScreenChangeEvent.kt @@ -0,0 +1,10 @@ + + +package moe.nea.firmament.events + +import net.minecraft.client.gui.screen.Screen + +data class ScreenChangeEvent(val old: Screen?, val new: Screen?) : FirmamentEvent.Cancellable() { + var overrideScreen: Screen? = null + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/events/ScreenRenderPostEvent.kt b/src/main/kotlin/events/ScreenRenderPostEvent.kt new file mode 100644 index 0000000..79f4913 --- /dev/null +++ b/src/main/kotlin/events/ScreenRenderPostEvent.kt @@ -0,0 +1,16 @@ + + +package moe.nea.firmament.events + +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.gui.screen.Screen + +data class ScreenRenderPostEvent( + val screen: Screen, + val mouseX: Int, + val mouseY: Int, + val tickDelta: Float, + val drawContext: DrawContext +) : FirmamentEvent() { + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/events/ServerConnectedEvent.kt b/src/main/kotlin/events/ServerConnectedEvent.kt new file mode 100644 index 0000000..26897f2 --- /dev/null +++ b/src/main/kotlin/events/ServerConnectedEvent.kt @@ -0,0 +1,18 @@ +package moe.nea.firmament.events + +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents +import net.minecraft.client.MinecraftClient +import net.minecraft.client.network.ClientPlayNetworkHandler +import net.minecraft.network.ClientConnection + +data class ServerConnectedEvent( + val connection: ClientConnection +) : FirmamentEvent() { + companion object : FirmamentEventBus() { + init { + ClientPlayConnectionEvents.INIT.register(ClientPlayConnectionEvents.Init { clientPlayNetworkHandler: ClientPlayNetworkHandler, minecraftClient: MinecraftClient -> + publishSync(ServerConnectedEvent(clientPlayNetworkHandler.connection)) + }) + } + } +} diff --git a/src/main/kotlin/events/SkyblockServerUpdateEvent.kt b/src/main/kotlin/events/SkyblockServerUpdateEvent.kt new file mode 100644 index 0000000..0bc5143 --- /dev/null +++ b/src/main/kotlin/events/SkyblockServerUpdateEvent.kt @@ -0,0 +1,15 @@ + + +package moe.nea.firmament.events + +import moe.nea.firmament.util.Locraw + +/** + * This event gets published whenever `/locraw` is queried and HyPixel returns a location different to the old one. + * + * **N.B.:** This event may get fired multiple times while on the server (for example, first to null, then to the + * correct location). + */ +data class SkyblockServerUpdateEvent(val oldLocraw: Locraw?, val newLocraw: Locraw?) : FirmamentEvent() { + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/events/SlotClickEvent.kt b/src/main/kotlin/events/SlotClickEvent.kt new file mode 100644 index 0000000..d4abfb0 --- /dev/null +++ b/src/main/kotlin/events/SlotClickEvent.kt @@ -0,0 +1,15 @@ + +package moe.nea.firmament.events + +import net.minecraft.item.ItemStack +import net.minecraft.screen.slot.Slot +import net.minecraft.screen.slot.SlotActionType + +data class SlotClickEvent( + val slot: Slot, + val stack: ItemStack, + val button: Int, + val actionType: SlotActionType, +) : FirmamentEvent() { + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/events/SlotRenderEvents.kt b/src/main/kotlin/events/SlotRenderEvents.kt new file mode 100644 index 0000000..5234176 --- /dev/null +++ b/src/main/kotlin/events/SlotRenderEvents.kt @@ -0,0 +1,37 @@ + + +package moe.nea.firmament.events + +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.render.RenderLayer +import net.minecraft.client.texture.Sprite +import net.minecraft.screen.slot.Slot +import net.minecraft.util.Identifier +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.render.drawGuiTexture + +interface SlotRenderEvents { + val context: DrawContext + val slot: Slot + + fun highlight(sprite: Identifier) { + context.drawGuiTexture( + slot.x, slot.y, 0, 16, 16, + sprite + ) + } + + data class Before( + override val context: DrawContext, override val slot: Slot, + ) : FirmamentEvent(), + SlotRenderEvents { + companion object : FirmamentEventBus() + } + + data class After( + override val context: DrawContext, override val slot: Slot, + ) : FirmamentEvent(), + SlotRenderEvents { + companion object : FirmamentEventBus() + } +} diff --git a/src/main/kotlin/events/SoundReceiveEvent.kt b/src/main/kotlin/events/SoundReceiveEvent.kt new file mode 100644 index 0000000..d1b85b6 --- /dev/null +++ b/src/main/kotlin/events/SoundReceiveEvent.kt @@ -0,0 +1,18 @@ + +package moe.nea.firmament.events + +import net.minecraft.registry.entry.RegistryEntry +import net.minecraft.sound.SoundCategory +import net.minecraft.sound.SoundEvent +import net.minecraft.util.math.Vec3d + +data class SoundReceiveEvent( + val sound: RegistryEntry, + val category: SoundCategory, + val position: Vec3d, + val pitch: Float, + val volume: Float, + val seed: Long +) : FirmamentEvent.Cancellable() { + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/events/TickEvent.kt b/src/main/kotlin/events/TickEvent.kt new file mode 100644 index 0000000..18007f8 --- /dev/null +++ b/src/main/kotlin/events/TickEvent.kt @@ -0,0 +1,7 @@ + + +package moe.nea.firmament.events + +data class TickEvent(val tickCount: Int) : FirmamentEvent() { + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/events/UseBlockEvent.kt b/src/main/kotlin/events/UseBlockEvent.kt new file mode 100644 index 0000000..8bbe0de --- /dev/null +++ b/src/main/kotlin/events/UseBlockEvent.kt @@ -0,0 +1,11 @@ + +package moe.nea.firmament.events + +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.util.Hand +import net.minecraft.util.hit.BlockHitResult +import net.minecraft.world.World + +data class UseBlockEvent(val player: PlayerEntity, val world: World, val hand: Hand, val hitResult: BlockHitResult) : FirmamentEvent.Cancellable() { + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/events/UseItemEvent.kt b/src/main/kotlin/events/UseItemEvent.kt new file mode 100644 index 0000000..e294bb1 --- /dev/null +++ b/src/main/kotlin/events/UseItemEvent.kt @@ -0,0 +1,11 @@ +package moe.nea.firmament.events + +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.ItemStack +import net.minecraft.util.Hand +import net.minecraft.world.World + +data class UseItemEvent(val playerEntity: PlayerEntity, val world: World, val hand: Hand) : FirmamentEvent.Cancellable() { + companion object : FirmamentEventBus() + val item: ItemStack = playerEntity.getStackInHand(hand) +} diff --git a/src/main/kotlin/events/WorldKeyboardEvent.kt b/src/main/kotlin/events/WorldKeyboardEvent.kt new file mode 100644 index 0000000..1d6a758 --- /dev/null +++ b/src/main/kotlin/events/WorldKeyboardEvent.kt @@ -0,0 +1,17 @@ +package moe.nea.firmament.events + +import net.minecraft.client.option.KeyBinding +import moe.nea.firmament.keybindings.IKeyBinding + +data class WorldKeyboardEvent(val keyCode: Int, val scanCode: Int, val modifiers: Int) : FirmamentEvent.Cancellable() { + companion object : FirmamentEventBus() + + fun matches(keyBinding: KeyBinding): Boolean { + return matches(IKeyBinding.minecraft(keyBinding)) + } + + fun matches(keyBinding: IKeyBinding, atLeast: Boolean = false): Boolean { + return if (atLeast) keyBinding.matchesAtLeast(keyCode, scanCode, modifiers) else + keyBinding.matches(keyCode, scanCode, modifiers) + } +} diff --git a/src/main/kotlin/events/WorldMouseMoveEvent.kt b/src/main/kotlin/events/WorldMouseMoveEvent.kt new file mode 100644 index 0000000..7a17ba4 --- /dev/null +++ b/src/main/kotlin/events/WorldMouseMoveEvent.kt @@ -0,0 +1,5 @@ +package moe.nea.firmament.events + +data class WorldMouseMoveEvent(val deltaX: Double, val deltaY: Double) : FirmamentEvent.Cancellable() { + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/events/WorldReadyEvent.kt b/src/main/kotlin/events/WorldReadyEvent.kt new file mode 100644 index 0000000..c79b100 --- /dev/null +++ b/src/main/kotlin/events/WorldReadyEvent.kt @@ -0,0 +1,10 @@ +package moe.nea.firmament.events + +class WorldReadyEvent : FirmamentEvent() { + companion object : FirmamentEventBus() +// class FullyLoaded : FirmamentEvent() { +// companion object : FirmamentEventBus() { +// TODO: check WorldLoadingState +// } +// } +} diff --git a/src/main/kotlin/events/WorldRenderLastEvent.kt b/src/main/kotlin/events/WorldRenderLastEvent.kt new file mode 100644 index 0000000..3c2103d --- /dev/null +++ b/src/main/kotlin/events/WorldRenderLastEvent.kt @@ -0,0 +1,24 @@ + + +package moe.nea.firmament.events + +import net.minecraft.client.render.Camera +import net.minecraft.client.render.GameRenderer +import net.minecraft.client.render.LightmapTextureManager +import net.minecraft.client.render.RenderTickCounter +import net.minecraft.client.render.VertexConsumerProvider +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.util.math.Position +import net.minecraft.util.math.Vec3d + +/** + * This event is called after all world rendering is done, but before any GUI rendering (including hand) has been done. + */ +data class WorldRenderLastEvent( + val matrices: MatrixStack, + val tickCounter: RenderTickCounter, + val camera: Camera, + val vertexConsumers: VertexConsumerProvider.Immediate, +) : FirmamentEvent() { + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/events/registration/ChatEvents.kt b/src/main/kotlin/events/registration/ChatEvents.kt new file mode 100644 index 0000000..1dcc91a --- /dev/null +++ b/src/main/kotlin/events/registration/ChatEvents.kt @@ -0,0 +1,63 @@ +package moe.nea.firmament.events.registration + +import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents +import net.fabricmc.fabric.api.event.player.AttackBlockCallback +import net.fabricmc.fabric.api.event.player.UseBlockCallback +import net.fabricmc.fabric.api.event.player.UseItemCallback +import net.minecraft.text.Text +import net.minecraft.util.ActionResult +import moe.nea.firmament.events.AllowChatEvent +import moe.nea.firmament.events.AttackBlockEvent +import moe.nea.firmament.events.ModifyChatEvent +import moe.nea.firmament.events.ProcessChatEvent +import moe.nea.firmament.events.UseBlockEvent +import moe.nea.firmament.events.UseItemEvent + +private var lastReceivedMessage: Text? = null + +fun registerFirmamentEvents() { + ClientReceiveMessageEvents.ALLOW_CHAT.register(ClientReceiveMessageEvents.AllowChat { message, signedMessage, sender, params, receptionTimestamp -> + lastReceivedMessage = message + !ProcessChatEvent.publish(ProcessChatEvent(message, false)).cancelled + && !AllowChatEvent.publish(AllowChatEvent(message)).cancelled + }) + ClientReceiveMessageEvents.ALLOW_GAME.register(ClientReceiveMessageEvents.AllowGame { message, overlay -> + lastReceivedMessage = message + overlay || (!ProcessChatEvent.publish(ProcessChatEvent(message, false)).cancelled && + !AllowChatEvent.publish(AllowChatEvent(message)).cancelled) + }) + ClientReceiveMessageEvents.MODIFY_GAME.register(ClientReceiveMessageEvents.ModifyGame { message, overlay -> + if (overlay) message + else ModifyChatEvent.publish(ModifyChatEvent(message)).replaceWith + }) + ClientReceiveMessageEvents.GAME_CANCELED.register(ClientReceiveMessageEvents.GameCanceled { message, overlay -> + if (!overlay && lastReceivedMessage !== message) { + ProcessChatEvent.publish(ProcessChatEvent(message, true)) + } + }) + ClientReceiveMessageEvents.CHAT_CANCELED.register(ClientReceiveMessageEvents.ChatCanceled { message, signedMessage, sender, params, receptionTimestamp -> + if (lastReceivedMessage !== message) { + ProcessChatEvent.publish(ProcessChatEvent(message, true)) + } + }) + + AttackBlockCallback.EVENT.register(AttackBlockCallback { player, world, hand, pos, direction -> + if (AttackBlockEvent.publish(AttackBlockEvent(player, world, hand, pos, direction)).cancelled) + ActionResult.CONSUME + else ActionResult.PASS + }) + UseBlockCallback.EVENT.register(UseBlockCallback { player, world, hand, hitResult -> + if (UseBlockEvent.publish(UseBlockEvent(player, world, hand, hitResult)).cancelled) + ActionResult.CONSUME + else ActionResult.PASS + }) + UseBlockCallback.EVENT.register(UseBlockCallback { player, world, hand, hitResult -> + if (UseItemEvent.publish(UseItemEvent(player, world, hand)).cancelled) + ActionResult.CONSUME + else ActionResult.PASS + }) + UseItemCallback.EVENT.register(UseItemCallback { playerEntity, world, hand -> + if (UseItemEvent.publish(UseItemEvent(playerEntity, world, hand)).cancelled) ActionResult.CONSUME + else ActionResult.PASS + }) +} diff --git a/src/main/kotlin/events/subscription/Subscription.kt b/src/main/kotlin/events/subscription/Subscription.kt new file mode 100644 index 0000000..1c1d3bd --- /dev/null +++ b/src/main/kotlin/events/subscription/Subscription.kt @@ -0,0 +1,17 @@ + +package moe.nea.firmament.events.subscription + +import moe.nea.firmament.events.FirmamentEvent +import moe.nea.firmament.events.FirmamentEventBus +import moe.nea.firmament.features.FirmamentFeature + +interface SubscriptionOwner { + val delegateFeature: FirmamentFeature +} + +data class Subscription( + val owner: Any, + val invoke: (T) -> Unit, + val eventBus: FirmamentEventBus, + val methodName: String, +) diff --git a/src/main/kotlin/events/subscription/SubscriptionList.kt b/src/main/kotlin/events/subscription/SubscriptionList.kt new file mode 100644 index 0000000..817efc3 --- /dev/null +++ b/src/main/kotlin/events/subscription/SubscriptionList.kt @@ -0,0 +1,28 @@ +package moe.nea.firmament.events.subscription + +import java.util.ServiceLoader +import kotlin.streams.asSequence +import moe.nea.firmament.Firmament + +interface SubscriptionList { + fun provideSubscriptions(addSubscription: (Subscription<*>) -> Unit) + + companion object { + val allLists by lazy { + ServiceLoader.load(SubscriptionList::class.java) + .stream() + .asSequence() + .mapNotNull { + kotlin.runCatching { it.get() } + .getOrElse { ex -> + Firmament.logger.error("Could not load subscriptions from ${it.type()}", ex) + null + } + } + .toList() + } + init { + require(allLists.isNotEmpty()) + } + } +} diff --git a/src/main/kotlin/features/FeatureManager.kt b/src/main/kotlin/features/FeatureManager.kt new file mode 100644 index 0000000..e0799c4 --- /dev/null +++ b/src/main/kotlin/features/FeatureManager.kt @@ -0,0 +1,133 @@ +package moe.nea.firmament.features + +import kotlinx.serialization.Serializable +import kotlinx.serialization.serializer +import moe.nea.firmament.Firmament +import moe.nea.firmament.events.FeaturesInitializedEvent +import moe.nea.firmament.events.FirmamentEvent +import moe.nea.firmament.events.subscription.Subscription +import moe.nea.firmament.events.subscription.SubscriptionList +import moe.nea.firmament.features.chat.AutoCompletions +import moe.nea.firmament.features.chat.ChatLinks +import moe.nea.firmament.features.chat.QuickCommands +import moe.nea.firmament.features.debug.DebugView +import moe.nea.firmament.features.debug.DeveloperFeatures +import moe.nea.firmament.features.debug.MinorTrolling +import moe.nea.firmament.features.debug.PowerUserTools +import moe.nea.firmament.features.diana.DianaWaypoints +import moe.nea.firmament.features.events.anniversity.AnniversaryFeatures +import moe.nea.firmament.features.events.carnival.CarnivalFeatures +import moe.nea.firmament.features.fixes.CompatibliltyFeatures +import moe.nea.firmament.features.fixes.Fixes +import moe.nea.firmament.features.inventory.CraftingOverlay +import moe.nea.firmament.features.inventory.ItemRarityCosmetics +import moe.nea.firmament.features.inventory.PetFeatures +import moe.nea.firmament.features.inventory.PriceData +import moe.nea.firmament.features.inventory.SaveCursorPosition +import moe.nea.firmament.features.inventory.SlotLocking +import moe.nea.firmament.features.inventory.WardrobeKeybinds +import moe.nea.firmament.features.inventory.buttons.InventoryButtons +import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlay +import moe.nea.firmament.features.items.EtherwarpOverlay +import moe.nea.firmament.features.mining.PickaxeAbility +import moe.nea.firmament.features.mining.PristineProfitTracker +import moe.nea.firmament.features.misc.CustomCapes +import moe.nea.firmament.features.misc.Hud +import moe.nea.firmament.features.world.FairySouls +import moe.nea.firmament.features.world.Waypoints +import moe.nea.firmament.util.compatloader.ICompatMeta +import moe.nea.firmament.util.data.DataHolder + +object FeatureManager : DataHolder(serializer(), "features", ::Config) { + @Serializable + data class Config( + val enabledFeatures: MutableMap = mutableMapOf() + ) + + private val features = mutableMapOf() + + val allFeatures: Collection get() = features.values + + private var hasAutoloaded = false + + fun autoload() { + synchronized(this) { + if (hasAutoloaded) return + loadFeature(MinorTrolling) + loadFeature(FairySouls) + loadFeature(AutoCompletions) + // TODO: loadFeature(FishingWarning) + loadFeature(SlotLocking) + loadFeature(StorageOverlay) + loadFeature(PristineProfitTracker) + loadFeature(CraftingOverlay) + loadFeature(PowerUserTools) + loadFeature(Waypoints) + loadFeature(ChatLinks) + loadFeature(CompatibliltyFeatures) + loadFeature(AnniversaryFeatures) + loadFeature(QuickCommands) + loadFeature(PetFeatures) + loadFeature(SaveCursorPosition) + loadFeature(PriceData) + loadFeature(Fixes) + loadFeature(CustomCapes) + loadFeature(Hud) + loadFeature(EtherwarpOverlay) + loadFeature(WardrobeKeybinds) + loadFeature(DianaWaypoints) + loadFeature(ItemRarityCosmetics) + loadFeature(PickaxeAbility) + loadFeature(CarnivalFeatures) + if (Firmament.DEBUG) { + loadFeature(DeveloperFeatures) + loadFeature(DebugView) + } + allFeatures.forEach { it.config } + FeaturesInitializedEvent.publish(FeaturesInitializedEvent(allFeatures.toList())) + hasAutoloaded = true + } + } + + fun subscribeEvents() { + SubscriptionList.allLists.forEach { list -> + if (ICompatMeta.shouldLoad(list.javaClass.name)) + runCatching { + list.provideSubscriptions { + it.owner.javaClass.classes.forEach { + runCatching { it.getDeclaredField("INSTANCE").get(null) } + } + subscribeSingleEvent(it) + } + }.getOrElse { + // TODO: allow annotating source sets to specifically opt out of loading for mods, maybe automatically + Firmament.logger.info("Ignoring events from $list, likely due to a missing compat mod.", it) + } + } + } + + private fun subscribeSingleEvent(it: Subscription) { + it.eventBus.subscribe(false, "${it.owner.javaClass.simpleName}:${it.methodName}", it.invoke) + } + + fun loadFeature(feature: FirmamentFeature) { + synchronized(features) { + if (feature.identifier in features) { + Firmament.logger.error("Double registering feature ${feature.identifier}. Ignoring second instance $feature") + return + } + features[feature.identifier] = feature + feature.onLoad() + } + } + + fun isEnabled(identifier: String): Boolean? = + data.enabledFeatures[identifier] + + + fun setEnabled(identifier: String, value: Boolean) { + data.enabledFeatures[identifier] = value + markDirty() + } + +} diff --git a/src/main/kotlin/features/FirmamentFeature.kt b/src/main/kotlin/features/FirmamentFeature.kt new file mode 100644 index 0000000..2cfc4fd --- /dev/null +++ b/src/main/kotlin/features/FirmamentFeature.kt @@ -0,0 +1,23 @@ + + +package moe.nea.firmament.features + +import moe.nea.firmament.events.subscription.SubscriptionOwner +import moe.nea.firmament.gui.config.ManagedConfig + +// TODO: remove this entire feature system and revamp config +interface FirmamentFeature : SubscriptionOwner { + val identifier: String + val defaultEnabled: Boolean + get() = true + var isEnabled: Boolean + get() = FeatureManager.isEnabled(identifier) ?: defaultEnabled + set(value) { + FeatureManager.setEnabled(identifier, value) + } + override val delegateFeature: FirmamentFeature + get() = this + val config: ManagedConfig? get() = null + fun onLoad() {} + +} diff --git a/src/main/kotlin/features/chat/AutoCompletions.kt b/src/main/kotlin/features/chat/AutoCompletions.kt new file mode 100644 index 0000000..9e0de40 --- /dev/null +++ b/src/main/kotlin/features/chat/AutoCompletions.kt @@ -0,0 +1,55 @@ +package moe.nea.firmament.features.chat + +import com.mojang.brigadier.arguments.StringArgumentType.string +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.get +import moe.nea.firmament.commands.suggestsList +import moe.nea.firmament.commands.thenArgument +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.events.MaskCommands +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.util.MC + +object AutoCompletions : FirmamentFeature { + + object TConfig : ManagedConfig(identifier, Category.CHAT) { + val provideWarpTabCompletion by toggle("warp-complete") { true } + val replaceWarpIsByWarpIsland by toggle("warp-is") { true } + } + + override val config: ManagedConfig? + get() = TConfig + override val identifier: String + get() = "auto-completions" + + @Subscribe + fun onMaskCommands(event: MaskCommands) { + if (TConfig.provideWarpTabCompletion) { + event.mask("warp") + } + } + + @Subscribe + fun onCommandEvent(event: CommandEvent) { + if (!TConfig.provideWarpTabCompletion) return + event.deleteCommand("warp") + event.register("warp") { + thenArgument("to", string()) { toArg -> + suggestsList { + RepoManager.neuRepo.constants?.islands?.warps?.flatMap { listOf(it.warp) + it.aliases } ?: listOf() + } + thenExecute { + val warpName = get(toArg) + if (warpName == "is" && TConfig.replaceWarpIsByWarpIsland) { + MC.sendServerCommand("warp island") + } else { + MC.sendServerCommand("warp $warpName") + } + } + } + } + } +} diff --git a/src/main/kotlin/features/chat/ChatLinks.kt b/src/main/kotlin/features/chat/ChatLinks.kt new file mode 100644 index 0000000..1fb12e1 --- /dev/null +++ b/src/main/kotlin/features/chat/ChatLinks.kt @@ -0,0 +1,165 @@ +package moe.nea.firmament.features.chat + +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsChannel +import io.ktor.utils.io.jvm.javaio.toInputStream +import java.net.URI +import java.net.URL +import java.util.Collections +import java.util.concurrent.atomic.AtomicInteger +import moe.nea.jarvis.api.Point +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlin.math.min +import net.minecraft.client.gui.screen.ChatScreen +import net.minecraft.client.texture.NativeImage +import net.minecraft.client.texture.NativeImageBackedTexture +import net.minecraft.text.ClickEvent +import net.minecraft.text.HoverEvent +import net.minecraft.text.Style +import net.minecraft.text.Text +import net.minecraft.util.Formatting +import net.minecraft.util.Identifier +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ModifyChatEvent +import moe.nea.firmament.events.ScreenRenderPostEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.render.drawTexture +import moe.nea.firmament.util.transformEachRecursively +import moe.nea.firmament.util.unformattedString + +object ChatLinks : FirmamentFeature { + override val identifier: String + get() = "chat-links" + + object TConfig : ManagedConfig(identifier, Category.CHAT) { + val enableLinks by toggle("links-enabled") { true } + val imageEnabled by toggle("image-enabled") { true } + val allowAllHosts by toggle("allow-all-hosts") { false } + val allowedHosts by string("allowed-hosts") { "cdn.discordapp.com,media.discordapp.com,media.discordapp.net,i.imgur.com" } + val actualAllowedHosts get() = allowedHosts.split(",").map { it.trim() } + val position by position("position", 16 * 20, 9 * 20) { Point(0.0, 0.0) } + } + + private fun isHostAllowed(host: String) = + TConfig.allowAllHosts || TConfig.actualAllowedHosts.any { it.equals(host, ignoreCase = true) } + + private fun isUrlAllowed(url: String) = isHostAllowed(url.removePrefix("https://").substringBefore("/")) + + override val config get() = TConfig + val urlRegex = "https://[^. ]+\\.[^ ]+(\\.?(\\s|$))".toRegex() + val nextTexId = AtomicInteger(0) + + data class Image( + val texture: Identifier, + val width: Int, + val height: Int, + ) + + val imageCache: MutableMap> = + Collections.synchronizedMap(mutableMapOf>()) + + private fun tryCacheUrl(url: String) { + if (!isUrlAllowed(url)) { + return + } + if (url in imageCache) { + return + } + imageCache[url] = Firmament.coroutineScope.async { + try { + val response = Firmament.httpClient.get(URL(url)) + if (response.status.value == 200) { + val inputStream = response.bodyAsChannel().toInputStream(Firmament.globalJob) + val image = NativeImage.read(inputStream) + val texId = Firmament.identifier("dynamic_image_preview${nextTexId.getAndIncrement()}") + MC.textureManager.registerTexture( + texId, + NativeImageBackedTexture({ texId.path }, image) + ) + Image(texId, image.width, image.height) + } else + null + } catch (exc: Exception) { + exc.printStackTrace() + null + } + } + } + + val imageExtensions = listOf("jpg", "png", "gif", "jpeg") + fun isImageUrl(url: String): Boolean { + return (url.substringAfterLast('.').lowercase() in imageExtensions) + } + + @Subscribe + @OptIn(ExperimentalCoroutinesApi::class) + fun onRender(it: ScreenRenderPostEvent) { + if (!TConfig.imageEnabled) return + if (it.screen !is ChatScreen) return + val hoveredComponent = + MC.inGameHud.chatHud.getTextStyleAt(it.mouseX.toDouble(), it.mouseY.toDouble()) ?: return + val hoverEvent = hoveredComponent.hoverEvent as? HoverEvent.ShowText ?: return + val value = hoverEvent.value + val url = urlRegex.matchEntire(value.unformattedString)?.groupValues?.get(0) ?: return + if (!isImageUrl(url)) return + val imageFuture = imageCache[url] ?: return + if (!imageFuture.isCompleted) return + val image = imageFuture.getCompleted() ?: return + it.drawContext.matrices.push() + val pos = TConfig.position + pos.applyTransformations(it.drawContext.matrices) + val scale = min(1F, min((9 * 20F) / image.height, (16 * 20F) / image.width)) + it.drawContext.matrices.scale(scale, scale, 1F) + it.drawContext.drawTexture( + image.texture, + 0, + 0, + 1F, + 1F, + image.width, + image.height, + image.width, + image.height, + ) + it.drawContext.matrices.pop() + } + + @Subscribe + fun onModifyChat(it: ModifyChatEvent) { + if (!TConfig.enableLinks) return + it.replaceWith = it.replaceWith.transformEachRecursively { child -> + val text = child.string + if ("://" !in text) return@transformEachRecursively child + val s = Text.empty().setStyle(child.style) + var index = 0 + while (index < text.length) { + val nextMatch = urlRegex.find(text, index) + val url = nextMatch?.groupValues[0] + val uri = runCatching { url?.let(::URI) }.getOrNull() + if (nextMatch == null || url == null || uri == null) { + s.append(Text.literal(text.substring(index, text.length))) + break + } + val range = nextMatch.groups[0]!!.range + s.append(Text.literal(text.substring(index, range.first))) + s.append( + Text.literal(url).setStyle( + Style.EMPTY.withUnderline(true).withColor( + Formatting.AQUA + ).withHoverEvent(HoverEvent.ShowText(Text.literal(url))) + .withClickEvent(ClickEvent.OpenUrl(uri)) + ) + ) + if (isImageUrl(url)) + tryCacheUrl(url) + index = range.last + 1 + } + s + } + } +} diff --git a/src/main/kotlin/features/chat/CopyChat.kt b/src/main/kotlin/features/chat/CopyChat.kt new file mode 100644 index 0000000..64f8734 --- /dev/null +++ b/src/main/kotlin/features/chat/CopyChat.kt @@ -0,0 +1,31 @@ +package moe.nea.firmament.features.chat + +import net.minecraft.text.OrderedText +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ClientStartedEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.reconstitute + + +object CopyChat : FirmamentFeature { + override val identifier: String + get() = "copy-chat" + + object TConfig : ManagedConfig(identifier, Category.CHAT) { + val copyChat by toggle("copy-chat") { false } + } + + @Subscribe + fun onInit(event: ClientStartedEvent) { + } + + override val config: ManagedConfig? + get() = TConfig + + fun orderedTextToString(orderedText: OrderedText): String { + return orderedText.reconstitute().string + } + + +} diff --git a/src/main/kotlin/features/chat/PartyCommands.kt b/src/main/kotlin/features/chat/PartyCommands.kt new file mode 100644 index 0000000..de3a0d9 --- /dev/null +++ b/src/main/kotlin/features/chat/PartyCommands.kt @@ -0,0 +1,134 @@ +package moe.nea.firmament.features.chat + +import com.mojang.brigadier.CommandDispatcher +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.exceptions.CommandSyntaxException +import com.mojang.brigadier.tree.LiteralCommandNode +import kotlin.time.Duration.Companion.seconds +import net.minecraft.util.math.BlockPos +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.CaseInsensitiveLiteralCommandNode +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.events.PartyMessageReceivedEvent +import moe.nea.firmament.events.ProcessChatEvent +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.ErrorUtil +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.tr +import moe.nea.firmament.util.useMatch + +object PartyCommands { + + val messageInChannel = "(?Party|Guild) >([^:]+?)? (?[^: ]+): (?.+)".toPattern() + + @Subscribe + fun onChat(event: ProcessChatEvent) { + messageInChannel.useMatch(event.unformattedString) { + val channel = group("channel") + val message = group("message") + val name = group("name") + if (channel == "Party") { + PartyMessageReceivedEvent.publish(PartyMessageReceivedEvent( + event, message, name + )) + } + } + } + + val commandPrefixes = "!-?$.&#+~€\"@°_;:³²`'´ß\\,|".toSet() + + data class PartyCommandContext( + val name: String + ) + + val dispatch = CommandDispatcher().also { dispatch -> + fun register( + name: String, + vararg alias: String, + block: CaseInsensitiveLiteralCommandNode.Builder.() -> Unit = {}, + ): LiteralCommandNode { + val node = + dispatch.register(CaseInsensitiveLiteralCommandNode.Builder(name).also(block)) + alias.forEach { register(it) { redirect(node) } } + return node + } + + register("warp", "pw", "pwarp", "partywarp") { + executes { + // TODO: add check if you are the party leader + MC.sendCommand("p warp") + 0 + } + } + + register("transfer", "pt", "ptme") { + executes { + MC.sendCommand("p transfer ${it.source.name}") + 0 + } + } + + register("allinvite", "allinv") { + executes { + MC.sendCommand("p settings allinvite") + 0 + } + } + + register("coords") { + executes { + val p = MC.player?.blockPos ?: BlockPos.ORIGIN + MC.sendCommand("pc x: ${p.x}, y: ${p.y}, z: ${p.z}") + 0 + } + } + // TODO: downtime tracker (display message again at end of dungeon) + // instance ends: kuudra, dungeons, bacte + // TODO: at TPS command + } + + object TConfig : ManagedConfig("party-commands", Category.CHAT) { + val enable by toggle("enable") { false } + val cooldown by duration("cooldown", 0.seconds, 20.seconds) { 2.seconds } + val ignoreOwnCommands by toggle("ignore-own") { false } + } + + var lastCommand = TimeMark.farPast() + + @Subscribe + fun listPartyCommands(event: CommandEvent.SubCommand) { + event.subcommand("partycommands") { + thenExecute { + // TODO: Better help, including descriptions and redirect detection + MC.sendChat(tr("firmament.partycommands.help", "Available party commands: ${dispatch.root.children.map { it.name }}. Available prefixes: $commandPrefixes")) + } + } + } + + @Subscribe + fun onPartyMessage(event: PartyMessageReceivedEvent) { + if (!TConfig.enable) return + if (event.message.firstOrNull() !in commandPrefixes) return + if (event.name == MC.playerName && TConfig.ignoreOwnCommands) return + if (lastCommand.passedTime() < TConfig.cooldown) { + MC.sendChat(tr("firmament.partycommands.cooldown", "Skipping party command. Cooldown not passed.")) + return + } + // TODO: add trust levels + val commandLine = event.message.substring(1) + try { + dispatch.execute(StringReader(commandLine), PartyCommandContext(event.name)) + } catch (ex: Exception) { + if (ex is CommandSyntaxException) { + MC.sendChat(tr("firmament.partycommands.unknowncommand", "Unknown party command.")) + return + } else { + MC.sendChat(tr("firmament.partycommands.unknownerror", "Unknown error during command execution.")) + ErrorUtil.softError("Unknown error during command execution.", ex) + } + } + lastCommand = TimeMark.now() + } +} diff --git a/src/main/kotlin/features/chat/QuickCommands.kt b/src/main/kotlin/features/chat/QuickCommands.kt new file mode 100644 index 0000000..7963171 --- /dev/null +++ b/src/main/kotlin/features/chat/QuickCommands.kt @@ -0,0 +1,154 @@ +package moe.nea.firmament.features.chat + +import com.mojang.brigadier.CommandDispatcher +import com.mojang.brigadier.context.CommandContext +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource +import net.fabricmc.fabric.impl.command.client.ClientCommandInternals +import net.minecraft.command.CommandRegistryAccess +import net.minecraft.network.packet.s2c.play.CommandTreeS2CPacket +import net.minecraft.text.Text +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.DefaultSource +import moe.nea.firmament.commands.RestArgumentType +import moe.nea.firmament.commands.get +import moe.nea.firmament.commands.thenArgument +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.gui.config.ManagedOption +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.grey +import moe.nea.firmament.util.tr + +object QuickCommands : FirmamentFeature { + override val identifier: String + get() = "quick-commands" + + object TConfig : ManagedConfig("quick-commands", Category.CHAT) { + val enableJoin by toggle("join") { true } + val enableDh by toggle("dh") { true } + override fun onChange(option: ManagedOption<*>) { + reloadCommands() + } + } + + fun reloadCommands() { + val lastPacket = lastReceivedTreePacket ?: return + val network = MC.networkHandler ?: return + val fallback = ClientCommandInternals.getActiveDispatcher() + try { + val dispatcher = CommandDispatcher() + ClientCommandInternals.setActiveDispatcher(dispatcher) + ClientCommandRegistrationCallback.EVENT.invoker() + .register(dispatcher, CommandRegistryAccess.of(network.combinedDynamicRegistries, + network.enabledFeatures)) + ClientCommandInternals.finalizeInit() + network.onCommandTree(lastPacket) + } catch (ex: Exception) { + ClientCommandInternals.setActiveDispatcher(fallback) + throw ex + } + } + + + fun removePartialPrefix(text: String, prefix: String): String? { + var lf: String? = null + for (i in 1..prefix.length) { + if (text.startsWith(prefix.substring(0, i))) { + lf = text.substring(i) + } + } + return lf + } + + var lastReceivedTreePacket: CommandTreeS2CPacket? = null + + val kuudraLevelNames = listOf("NORMAL", "HOT", "BURNING", "FIERY", "INFERNAL") + val dungeonLevelNames = listOf("ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN") + + @Subscribe + fun registerDh(event: CommandEvent) { + if (!TConfig.enableDh) return + event.register("dh") { + thenExecute { + MC.sendCommand("warp dhub") + } + } + event.register("dn") { + thenExecute { + MC.sendChat(tr("firmament.quickwarp.deez-nutz", "Warping to... Deez Nuts!").grey()) + MC.sendCommand("warp dhub") + } + } + } + + @Subscribe + fun registerJoin(it: CommandEvent) { + if (!TConfig.enableJoin) return + it.register("join") { + thenArgument("what", RestArgumentType) { what -> + thenExecute { + val what = this[what] + if (!SBData.isOnSkyblock) { + MC.sendCommand("join $what") + return@thenExecute + } + val joinName = getNameForFloor(what.replace(" ", "").lowercase()) + if (joinName == null) { + source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown", what)) + } else { + source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.success", + joinName)) + MC.sendCommand("joininstance $joinName") + } + } + } + thenExecute { + source.sendFeedback(Text.translatable("firmament.quick-commands.join.explain")) + } + } + } + + fun CommandContext.getNameForFloor(w: String): String? { + val kuudraLevel = removePartialPrefix(w, "kuudratier") ?: removePartialPrefix(w, "tier") + if (kuudraLevel != null) { + val l = kuudraLevel.toIntOrNull()?.let { it - 1 } ?: kuudraLevelNames.indexOfFirst { + it.startsWith( + kuudraLevel, + true + ) + } + if (l !in kuudraLevelNames.indices) { + source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-kuudra", + kuudraLevel)) + return null + } + return "KUUDRA_${kuudraLevelNames[l]}" + } + val masterLevel = removePartialPrefix(w, "master") + val normalLevel = + removePartialPrefix(w, "floor") ?: removePartialPrefix(w, "catacombs") ?: removePartialPrefix(w, "dungeons") + val dungeonLevel = masterLevel ?: normalLevel + if (dungeonLevel != null) { + val l = dungeonLevel.toIntOrNull()?.let { it - 1 } ?: dungeonLevelNames.indexOfFirst { + it.startsWith( + dungeonLevel, + true + ) + } + if (masterLevel == null && (l == -1 || null != removePartialPrefix(w, "entrance"))) { + return "CATACOMBS_ENTRANCE" + } + if (l !in dungeonLevelNames.indices) { + source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-catacombs", + kuudraLevel)) + return null + } + return "${if (masterLevel != null) "MASTER_" else ""}CATACOMBS_FLOOR_${dungeonLevelNames[l]}" + } + return null + } +} diff --git a/src/main/kotlin/features/debug/AnimatedClothingScanner.kt b/src/main/kotlin/features/debug/AnimatedClothingScanner.kt new file mode 100644 index 0000000..4edccfb --- /dev/null +++ b/src/main/kotlin/features/debug/AnimatedClothingScanner.kt @@ -0,0 +1,193 @@ +package moe.nea.firmament.features.debug + +import net.minecraft.command.argument.RegistryKeyArgumentType +import net.minecraft.component.ComponentType +import net.minecraft.entity.Entity +import net.minecraft.entity.decoration.ArmorStandEntity +import net.minecraft.item.ItemStack +import net.minecraft.nbt.NbtElement +import net.minecraft.nbt.NbtOps +import net.minecraft.registry.RegistryKeys +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.get +import moe.nea.firmament.commands.thenArgument +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.commands.thenLiteral +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.events.EntityUpdateEvent +import moe.nea.firmament.events.WorldReadyEvent +import moe.nea.firmament.util.ClipboardUtils +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.math.GChainReconciliation +import moe.nea.firmament.util.math.GChainReconciliation.shortenCycle +import moe.nea.firmament.util.mc.NbtPrism +import moe.nea.firmament.util.tr + +object AnimatedClothingScanner { + + data class LensOfFashionTheft( + val prism: NbtPrism, + val component: ComponentType, + ) { + fun observe(itemStack: ItemStack): Collection { + val x = itemStack.get(component) ?: return listOf() + val nbt = component.codecOrThrow.encodeStart(NbtOps.INSTANCE, x).orThrow + return prism.access(nbt) + } + } + + var lens: LensOfFashionTheft<*>? = null + var subject: Entity? = null + var history: MutableList = mutableListOf() + val metaHistory: MutableList> = mutableListOf() + + @OptIn(ExperimentalStdlibApi::class) + @Subscribe + fun onUpdate(event: EntityUpdateEvent) { + val s = subject ?: return + if (event.entity != s) return + val l = lens ?: return + if (event is EntityUpdateEvent.EquipmentUpdate) { + event.newEquipment.forEach { + val formatted = (l.observe(it.second)).joinToString() + history.add(formatted) + // TODO: add a slot filter + } + } + } + + fun reduceHistory(reducer: (List, List) -> List): List { + return metaHistory.fold(history, reducer).shortenCycle() + } + + @Subscribe + fun onSubCommand(event: CommandEvent.SubCommand) { + event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) { + thenLiteral("stealthisfit") { + thenLiteral("clear") { + thenExecute { + subject = null + metaHistory.clear() + history.clear() + MC.sendChat(tr("firmament.fitstealer.clear", "Cleared fit stealing history")) + } + } + thenLiteral("copy") { + thenExecute { + val history = reduceHistory { a, b -> a + b } + copyHistory(history) + MC.sendChat(tr("firmament.fitstealer.copied", "Copied the history")) + } + thenLiteral("deduplicated") { + thenExecute { + val history = reduceHistory { a, b -> + (a.toMutableSet() + b).toList() + } + copyHistory(history) + MC.sendChat( + tr( + "firmament.fitstealer.copied.deduplicated", + "Copied the deduplicated history" + ) + ) + } + } + thenLiteral("merged") { + thenExecute { + val history = reduceHistory(GChainReconciliation::reconcileCycles) + copyHistory(history) + MC.sendChat(tr("firmament.fitstealer.copied.merged", "Copied the merged history")) + } + } + } + thenLiteral("target") { + thenLiteral("self") { + thenExecute { + toggleObserve(MC.player!!) + } + } + thenLiteral("pet") { + thenExecute { + source.sendFeedback( + tr( + "firmament.fitstealer.stealingpet", + "Observing nearest marker armourstand" + ) + ) + val p = MC.player!! + val nearestPet = p.world.getEntitiesByClass( + ArmorStandEntity::class.java, + p.boundingBox.expand(10.0), + { it.isMarker }) + .minBy { it.squaredDistanceTo(p) } + toggleObserve(nearestPet) + } + } + thenExecute { + val ent = MC.instance.targetedEntity + if (ent == null) { + source.sendFeedback( + tr( + "firmament.fitstealer.notargetundercursor", + "No entity under cursor" + ) + ) + } else { + toggleObserve(ent) + } + } + } + thenLiteral("path") { + thenArgument( + "component", + RegistryKeyArgumentType.registryKey(RegistryKeys.DATA_COMPONENT_TYPE) + ) { component -> + thenArgument("path", NbtPrism.Argument) { path -> + thenExecute { + lens = LensOfFashionTheft( + get(path), + MC.unsafeGetRegistryEntry(get(component))!!, + ) + source.sendFeedback( + tr( + "firmament.fitstealer.lensset", + "Analyzing path ${get(path)} for component ${get(component).value}" + ) + ) + } + } + } + } + } + } + } + + private fun copyHistory(toCopy: List) { + ClipboardUtils.setTextContent(toCopy.joinToString("\n")) + } + + @Subscribe + fun onWorldSwap(event: WorldReadyEvent) { + subject = null + if (history.isNotEmpty()) { + metaHistory.add(history) + history = mutableListOf() + } + } + + private fun toggleObserve(entity: Entity?) { + subject = if (subject == null) entity else null + if (subject == null) { + metaHistory.add(history) + history = mutableListOf() + } + MC.sendChat( + subject?.let { + tr( + "firmament.fitstealer.targeted", + "Observing the equipment of ${it.name}." + ) + } ?: tr("firmament.fitstealer.targetlost", "No longer logging equipment."), + ) + } +} diff --git a/src/main/kotlin/features/debug/DebugLogger.kt b/src/main/kotlin/features/debug/DebugLogger.kt new file mode 100644 index 0000000..9115956 --- /dev/null +++ b/src/main/kotlin/features/debug/DebugLogger.kt @@ -0,0 +1,26 @@ +package moe.nea.firmament.features.debug + +import kotlinx.serialization.serializer +import net.minecraft.text.Text +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.collections.InstanceList +import moe.nea.firmament.util.data.DataHolder + +class DebugLogger(val tag: String) { + companion object { + val allInstances = InstanceList("DebugLogger") + } + + object EnabledLogs : DataHolder>(serializer(), "DebugLogs", ::mutableSetOf) + + init { + allInstances.add(this) + } + + fun isEnabled() = DeveloperFeatures.isEnabled && EnabledLogs.data.contains(tag) + fun log(text: String) = log { text } + fun log(text: () -> String) { + if (!isEnabled()) return + MC.sendChat(Text.literal(text())) + } +} diff --git a/src/main/kotlin/features/debug/DebugView.kt b/src/main/kotlin/features/debug/DebugView.kt new file mode 100644 index 0000000..ee54260 --- /dev/null +++ b/src/main/kotlin/features/debug/DebugView.kt @@ -0,0 +1,29 @@ + + +package moe.nea.firmament.features.debug + +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.TickEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.util.TimeMark + +object DebugView : FirmamentFeature { + private data class StoredVariable( + val obj: T, + val timer: TimeMark, + ) + + private val storedVariables: MutableMap> = sortedMapOf() + override val identifier: String + get() = "debug-view" + override val defaultEnabled: Boolean + get() = Firmament.DEBUG + + fun showVariable(label: String, obj: T) { + synchronized(this) { + storedVariables[label] = StoredVariable(obj, TimeMark.now()) + } + } + +} diff --git a/src/main/kotlin/features/debug/DeveloperFeatures.kt b/src/main/kotlin/features/debug/DeveloperFeatures.kt new file mode 100644 index 0000000..fd236f9 --- /dev/null +++ b/src/main/kotlin/features/debug/DeveloperFeatures.kt @@ -0,0 +1,121 @@ +package moe.nea.firmament.features.debug + +import java.io.File +import java.nio.file.Path +import java.util.concurrent.CompletableFuture +import org.objectweb.asm.ClassReader +import org.objectweb.asm.Type +import org.objectweb.asm.tree.ClassNode +import org.spongepowered.asm.mixin.Mixin +import kotlinx.serialization.json.encodeToStream +import kotlin.io.path.absolute +import kotlin.io.path.exists +import net.minecraft.client.MinecraftClient +import net.minecraft.text.Text +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.DebugInstantiateEvent +import moe.nea.firmament.events.TickEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.init.MixinPlugin +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.asm.AsmAnnotationUtil +import moe.nea.firmament.util.iterate + +object DeveloperFeatures : FirmamentFeature { + val DEVELOPER_SUBCOMMAND: String = "dev" + override val identifier: String + get() = "developer" + override val config: TConfig + get() = TConfig + override val defaultEnabled: Boolean + get() = Firmament.DEBUG + + val gradleDir = + Path.of(".").absolute() + .iterate { it.parent } + .find { it.resolve("settings.gradle.kts").exists() } + + object TConfig : ManagedConfig("developer", Category.DEV) { + val autoRebuildResources by toggle("auto-rebuild") { false } + } + + var missingTranslations: Set? = null + + @JvmStatic + fun hookMissingTranslations(missingTranslations: Set) { + this.missingTranslations = missingTranslations + } + + @Subscribe + fun loadAllMixinClasses(event: DebugInstantiateEvent) { + val allMixinClasses = mutableSetOf() + MixinPlugin.instances.forEach { plugin -> + val prefix = plugin.mixinPackage + "." + val classes = plugin.mixins.map { prefix + it } + allMixinClasses.addAll(classes) + for (cls in classes) { + val targets = javaClass.classLoader.getResourceAsStream("${cls.replace(".", "/")}.class").use { + val node = ClassNode() + ClassReader(it).accept(node, 0) + val mixins = mutableListOf() + (node.visibleAnnotations.orEmpty() + node.invisibleAnnotations.orEmpty()).forEach { + val annotationType = Type.getType(it.desc) + val mixinType = Type.getType(Mixin::class.java) + if (mixinType == annotationType) { + mixins.add(AsmAnnotationUtil.createProxy(Mixin::class.java, it)) + } + } + mixins.flatMap { it.targets.toList() } + mixins.flatMap { it.value.map { it.java.name } } + } + for (target in targets) + try { + Firmament.logger.debug("Loading ${target} to force instantiate ${cls}") + Class.forName(target, true, javaClass.classLoader) + } catch (ex: Throwable) { + Firmament.logger.error("Could not load class ${target} that has been mixind by $cls", ex) + } + } + } + Firmament.logger.info("Forceloaded all Firmament mixins:") + val applied = MixinPlugin.instances.flatMap { it.appliedMixins }.toSet() + applied.forEach { Firmament.logger.info(" - ${it}") } + require(allMixinClasses == applied) + } + + @Subscribe + fun dumpMissingTranslations(tickEvent: TickEvent) { + val toDump = missingTranslations ?: return + missingTranslations = null + File("missing_translations.json").outputStream().use { + Firmament.json.encodeToStream(toDump.associateWith { "Mis" + "sing translation" }, it) + } + } + + @JvmStatic + fun hookOnBeforeResourceReload(client: MinecraftClient): CompletableFuture { + val reloadFuture = if (TConfig.autoRebuildResources && isEnabled && gradleDir != null) { + val builder = ProcessBuilder("./gradlew", ":processResources") + builder.directory(gradleDir.toFile()) + builder.inheritIO() + val process = builder.start() + MC.sendChat(Text.translatable("firmament.dev.resourcerebuild.start")) + val startTime = TimeMark.now() + process.toHandle().onExit().thenApply { + MC.sendChat( + Text.stringifiedTranslatable( + "firmament.dev.resourcerebuild.done", + startTime.passedTime() + ) + ) + Unit + } + } else { + CompletableFuture.completedFuture(Unit) + } + return reloadFuture.thenCompose { client.reloadResources() } + } +} + diff --git a/src/main/kotlin/features/debug/ExportedTestConstantMeta.kt b/src/main/kotlin/features/debug/ExportedTestConstantMeta.kt new file mode 100644 index 0000000..f0250dc --- /dev/null +++ b/src/main/kotlin/features/debug/ExportedTestConstantMeta.kt @@ -0,0 +1,27 @@ +package moe.nea.firmament.features.debug + +import com.mojang.serialization.Codec +import com.mojang.serialization.codecs.RecordCodecBuilder +import java.util.Optional +import net.minecraft.SharedConstants +import moe.nea.firmament.Firmament + +data class ExportedTestConstantMeta( + val dataVersion: Int, + val modVersion: Optional, +) { + companion object { + val current = ExportedTestConstantMeta( + SharedConstants.getGameVersion().saveVersion.id, + Optional.of("Firmament ${Firmament.version.friendlyString}") + ) + + val CODEC: Codec = RecordCodecBuilder.create { + it.group( + Codec.INT.fieldOf("dataVersion").forGetter(ExportedTestConstantMeta::dataVersion), + Codec.STRING.optionalFieldOf("modVersion").forGetter(ExportedTestConstantMeta::modVersion), + ).apply(it, ::ExportedTestConstantMeta) + } + val SOURCE_CODEC = CODEC.fieldOf("source").codec() + } +} diff --git a/src/main/kotlin/features/debug/MinorTrolling.kt b/src/main/kotlin/features/debug/MinorTrolling.kt new file mode 100644 index 0000000..32035a6 --- /dev/null +++ b/src/main/kotlin/features/debug/MinorTrolling.kt @@ -0,0 +1,27 @@ + + +package moe.nea.firmament.features.debug + +import net.minecraft.text.Text +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ModifyChatEvent +import moe.nea.firmament.features.FirmamentFeature + + +// In memorian Dulkir +object MinorTrolling : FirmamentFeature { + override val identifier: String + get() = "minor-trolling" + + val trollers = listOf("nea89o", "lrg89") + val t = "From(?: \\[[^\\]]+])? ([^:]+): (.*)".toRegex() + + @Subscribe + fun onTroll(it: ModifyChatEvent) { + val m = t.matchEntire(it.unformattedString) ?: return + val (_, name, text) = m.groupValues + if (name !in trollers) return + if (!text.startsWith("c:")) return + it.replaceWith = Text.literal(text.substring(2).replace("&", "§")) + } +} diff --git a/src/main/kotlin/features/debug/PowerUserTools.kt b/src/main/kotlin/features/debug/PowerUserTools.kt new file mode 100644 index 0000000..0800a4f --- /dev/null +++ b/src/main/kotlin/features/debug/PowerUserTools.kt @@ -0,0 +1,245 @@ +package moe.nea.firmament.features.debug + +import com.mojang.serialization.JsonOps +import kotlin.jvm.optionals.getOrNull +import net.minecraft.block.SkullBlock +import net.minecraft.block.entity.SkullBlockEntity +import net.minecraft.component.DataComponentTypes +import net.minecraft.component.type.ProfileComponent +import net.minecraft.entity.Entity +import net.minecraft.entity.LivingEntity +import net.minecraft.item.ItemStack +import net.minecraft.item.Items +import net.minecraft.nbt.NbtList +import net.minecraft.nbt.NbtOps +import net.minecraft.predicate.NbtPredicate +import net.minecraft.text.Text +import net.minecraft.text.TextCodecs +import net.minecraft.util.Identifier +import net.minecraft.util.Nameable +import net.minecraft.util.hit.BlockHitResult +import net.minecraft.util.hit.EntityHitResult +import net.minecraft.util.hit.HitResult +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.CustomItemModelEvent +import moe.nea.firmament.events.HandledScreenKeyPressedEvent +import moe.nea.firmament.events.ItemTooltipEvent +import moe.nea.firmament.events.ScreenChangeEvent +import moe.nea.firmament.events.TickEvent +import moe.nea.firmament.events.WorldKeyboardEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.mixins.accessor.AccessorHandledScreen +import moe.nea.firmament.util.ClipboardUtils +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.focusedItemStack +import moe.nea.firmament.util.mc.IntrospectableItemModelManager +import moe.nea.firmament.util.mc.SNbtFormatter +import moe.nea.firmament.util.mc.SNbtFormatter.Companion.toPrettyString +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.iterableArmorItems +import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.tr +import moe.nea.firmament.util.grey + +object PowerUserTools : FirmamentFeature { + override val identifier: String + get() = "power-user" + + object TConfig : ManagedConfig(identifier, Category.DEV) { + val showItemIds by toggle("show-item-id") { false } + val copyItemId by keyBindingWithDefaultUnbound("copy-item-id") + val copyTexturePackId by keyBindingWithDefaultUnbound("copy-texture-pack-id") + val copyNbtData by keyBindingWithDefaultUnbound("copy-nbt-data") + val copyLoreData by keyBindingWithDefaultUnbound("copy-lore") + val copySkullTexture by keyBindingWithDefaultUnbound("copy-skull-texture") + val copyEntityData by keyBindingWithDefaultUnbound("entity-data") + val copyItemStack by keyBindingWithDefaultUnbound("copy-item-stack") + val copyTitle by keyBindingWithDefaultUnbound("copy-title") + val exportItemStackToRepo by keyBindingWithDefaultUnbound("export-item-stack") + val exportUIRecipes by keyBindingWithDefaultUnbound("export-recipe") + val exportNpcLocation by keyBindingWithDefaultUnbound("export-npc-location") + val highlightNonOverlayItems by toggle("highlight-non-overlay") { false } + val dontHighlightSemicolonItems by toggle("dont-highlight-semicolon-items") { false } + } + + override val config + get() = TConfig + + var lastCopiedStack: Pair? = null + set(value) { + field = value + if (value != null) lastCopiedStackViewTime = 2 + } + var lastCopiedStackViewTime = 0 + + @Subscribe + fun resetLastCopiedStack(event: TickEvent) { + if (lastCopiedStackViewTime-- < 0) lastCopiedStack = null + } + + @Subscribe + fun resetLastCopiedStackOnScreenChange(event: ScreenChangeEvent) { + lastCopiedStack = null + } + + fun debugFormat(itemStack: ItemStack): Text { + return Text.literal(itemStack.skyBlockId?.toString() ?: itemStack.toString()) + } + + @Subscribe + fun onEntityInfo(event: WorldKeyboardEvent) { + if (!event.matches(TConfig.copyEntityData)) return + val target = (MC.instance.crosshairTarget as? EntityHitResult)?.entity + if (target == null) { + MC.sendChat(Text.translatable("firmament.poweruser.entity.fail")) + return + } + showEntity(target) + } + + fun showEntity(target: Entity) { + val nbt = NbtPredicate.entityToNbt(target) + nbt.remove("Inventory") + nbt.put("StyledName", TextCodecs.CODEC.encodeStart(NbtOps.INSTANCE, target.styledDisplayName).orThrow) + println(SNbtFormatter.prettify(nbt)) + ClipboardUtils.setTextContent(SNbtFormatter.prettify(nbt)) + MC.sendChat(Text.translatable("firmament.poweruser.entity.type", target.type)) + MC.sendChat(Text.translatable("firmament.poweruser.entity.name", target.name)) + MC.sendChat(Text.stringifiedTranslatable("firmament.poweruser.entity.position", target.pos)) + if (target is LivingEntity) { + MC.sendChat(Text.translatable("firmament.poweruser.entity.armor")) + for ((slot, armorItem) in target.iterableArmorItems) { + MC.sendChat(Text.translatable("firmament.poweruser.entity.armor.item", debugFormat(armorItem))) + } + } + MC.sendChat(Text.stringifiedTranslatable("firmament.poweruser.entity.passengers", target.passengerList.size)) + target.passengerList.forEach { + showEntity(it) + } + } + + // TODO: leak this through some other way, maybe. + lateinit var getSkullId: (profile: ProfileComponent) -> Identifier? + + @Subscribe + fun copyInventoryInfo(it: HandledScreenKeyPressedEvent) { + if (it.screen !is AccessorHandledScreen) return + val item = it.screen.focusedItemStack ?: return + if (it.matches(TConfig.copyItemId)) { + val sbId = item.skyBlockId + if (sbId == null) { + lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.skyblockid.fail")) + return + } + ClipboardUtils.setTextContent(sbId.neuItem) + lastCopiedStack = + Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.skyblockid", sbId.neuItem)) + } else if (it.matches(TConfig.copyTexturePackId)) { + val model = CustomItemModelEvent.getModelIdentifier0(item, object : IntrospectableItemModelManager { + override fun hasModel_firmament(identifier: Identifier): Boolean { + return true + } + }).getOrNull() // TODO: remove global texture overrides, maybe + if (model == null) { + lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.modelid.fail")) + return + } + ClipboardUtils.setTextContent(model.toString()) + lastCopiedStack = + Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.modelid", model.toString())) + } else if (it.matches(TConfig.copyNbtData)) { + // TODO: copy full nbt + val nbt = item.get(DataComponentTypes.CUSTOM_DATA)?.nbt?.toPrettyString() ?: "" + ClipboardUtils.setTextContent(nbt) + lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.nbt")) + } else if (it.matches(TConfig.copyLoreData)) { + val list = mutableListOf(item.displayNameAccordingToNbt) + list.addAll(item.loreAccordingToNbt) + ClipboardUtils.setTextContent(list.joinToString("\n") { + TextCodecs.CODEC.encodeStart(JsonOps.INSTANCE, it).result().getOrNull().toString() + }) + lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.lore")) + } else if (it.matches(TConfig.copySkullTexture)) { + if (item.item != Items.PLAYER_HEAD) { + lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.skull-id.fail.no-skull")) + return + } + val profile = item.get(DataComponentTypes.PROFILE) + if (profile == null) { + lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.skull-id.fail.no-profile")) + return + } + val skullTexture = getSkullId(profile) + if (skullTexture == null) { + lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.skull-id.fail.no-texture")) + return + } + ClipboardUtils.setTextContent(skullTexture.toString()) + lastCopiedStack = + Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.skull-id", skullTexture.toString())) + println("Copied skull id: $skullTexture") + } else if (it.matches(TConfig.copyItemStack)) { + val nbt = ItemStack.CODEC + .encodeStart(MC.currentOrDefaultRegistries.getOps(NbtOps.INSTANCE), item) + .orThrow + ClipboardUtils.setTextContent(nbt.toPrettyString()) + lastCopiedStack = Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.stack")) + } else if (it.matches(TConfig.copyTitle)) { + val allTitles = NbtList() + val inventoryNames = + it.screen.screenHandler.slots + .mapNotNullTo(mutableSetOf()) { it.inventory } + .filterIsInstance() + .map { it.name } + for (it in listOf(it.screen.title) + inventoryNames) { + allTitles.add(TextCodecs.CODEC.encodeStart(NbtOps.INSTANCE, it).result().getOrNull()!!) + } + ClipboardUtils.setTextContent(allTitles.toPrettyString()) + MC.sendChat(tr("firmament.power-user.title.copied", "Copied screen and inventory titles")) + } + } + + @Subscribe + fun onCopyWorldInfo(it: WorldKeyboardEvent) { + if (it.matches(TConfig.copySkullTexture)) { + val p = MC.camera ?: return + val blockHit = p.raycast(20.0, 0.0f, false) ?: return + if (blockHit.type != HitResult.Type.BLOCK || blockHit !is BlockHitResult) { + MC.sendChat(Text.translatable("firmament.tooltip.copied.skull.fail")) + return + } + val blockAt = p.world.getBlockState(blockHit.blockPos)?.block + val entity = p.world.getBlockEntity(blockHit.blockPos) + if (blockAt !is SkullBlock || entity !is SkullBlockEntity || entity.owner == null) { + MC.sendChat(Text.translatable("firmament.tooltip.copied.skull.fail")) + return + } + val id = getSkullId(entity.owner!!) + if (id == null) { + MC.sendChat(Text.translatable("firmament.tooltip.copied.skull.fail")) + } else { + ClipboardUtils.setTextContent(id.toString()) + MC.sendChat(Text.stringifiedTranslatable("firmament.tooltip.copied.skull", id.toString())) + } + } + } + + @Subscribe + fun addItemId(it: ItemTooltipEvent) { + if (TConfig.showItemIds) { + val id = it.stack.skyBlockId ?: return + it.lines.add(Text.stringifiedTranslatable("firmament.tooltip.skyblockid", id.neuItem).grey()) + } + val (item, text) = lastCopiedStack ?: return + if (!ItemStack.areEqual(item, it.stack)) { + lastCopiedStack = null + return + } + lastCopiedStackViewTime = 0 + it.lines.add(text) + } + + +} diff --git a/src/main/kotlin/features/debug/SoundVisualizer.kt b/src/main/kotlin/features/debug/SoundVisualizer.kt new file mode 100644 index 0000000..f805e6b --- /dev/null +++ b/src/main/kotlin/features/debug/SoundVisualizer.kt @@ -0,0 +1,65 @@ +package moe.nea.firmament.features.debug + +import net.minecraft.text.Text +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.commands.thenLiteral +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.events.SoundReceiveEvent +import moe.nea.firmament.events.WorldReadyEvent +import moe.nea.firmament.events.WorldRenderLastEvent +import moe.nea.firmament.util.red +import moe.nea.firmament.util.render.RenderInWorldContext + +object SoundVisualizer { + + var showSounds = false + + var sounds = mutableListOf() + + + @Subscribe + fun onSubCommand(event: CommandEvent.SubCommand) { + event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) { + thenLiteral("sounds") { + thenExecute { + showSounds = !showSounds + if (!showSounds) { + sounds.clear() + } + } + } + } + } + + @Subscribe + fun onWorldSwap(event: WorldReadyEvent) { + sounds.clear() + } + + @Subscribe + fun onRender(event: WorldRenderLastEvent) { + RenderInWorldContext.renderInWorld(event) { + sounds.forEach { event -> + withFacingThePlayer(event.position) { + text( + Text.literal(event.sound.value().id.toString()).also { + if (event.cancelled) + it.red() + }, + verticalAlign = RenderInWorldContext.VerticalAlign.CENTER, + ) + } + } + } + } + + @Subscribe + fun onSoundReceive(event: SoundReceiveEvent) { + if (!showSounds) return + if (sounds.size > 1000) { + sounds.subList(0, 200).clear() + } + sounds.add(event) + } +} diff --git a/src/main/kotlin/features/debug/itemeditor/ExportRecipe.kt b/src/main/kotlin/features/debug/itemeditor/ExportRecipe.kt new file mode 100644 index 0000000..9356dd3 --- /dev/null +++ b/src/main/kotlin/features/debug/itemeditor/ExportRecipe.kt @@ -0,0 +1,255 @@ +package moe.nea.firmament.features.debug.itemeditor + +import kotlinx.coroutines.launch +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import net.minecraft.client.network.AbstractClientPlayerEntity +import net.minecraft.entity.decoration.ArmorStandEntity +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HandledScreenKeyPressedEvent +import moe.nea.firmament.events.WorldKeyboardEvent +import moe.nea.firmament.features.debug.PowerUserTools +import moe.nea.firmament.repo.ItemNameLookup +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.SHORT_NUMBER_FORMAT +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.async.waitForTextInput +import moe.nea.firmament.util.ifDropLast +import moe.nea.firmament.util.mc.ScreenUtil.getSlotByIndex +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.mc.setSkullOwner +import moe.nea.firmament.util.parseShortNumber +import moe.nea.firmament.util.red +import moe.nea.firmament.util.removeColorCodes +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.skyblock.SkyBlockItems +import moe.nea.firmament.util.tr +import moe.nea.firmament.util.unformattedString +import moe.nea.firmament.util.useMatch + +object ExportRecipe { + + + val xNames = "123" + val yNames = "ABC" + + val slotIndices = (0..<9).map { + val x = it % 3 + val y = it / 3 + + (yNames[y].toString() + xNames[x].toString()) to x + y * 9 + 10 + } + val resultSlot = 25 + val craftingTableSlut = resultSlot - 2 + + @Subscribe + fun exportNpcLocation(event: WorldKeyboardEvent) { + if (!event.matches(PowerUserTools.TConfig.exportNpcLocation)) { + return + } + val entity = MC.instance.targetedEntity + if (entity == null) { + MC.sendChat(tr("firmament.repo.export.npc.noentity", "Could not find entity to export")) + return + } + Firmament.coroutineScope.launch { + val guessName = entity.world.getEntitiesByClass( + ArmorStandEntity::class.java, + entity.boundingBox.expand(0.1), + { !it.name.string.contains("CLICK") }) + .firstOrNull()?.customName?.string + ?: "" + val reply = waitForTextInput("$guessName (NPC)", "Export stub") + val id = generateName(reply) + ItemExporter.exportStub(id, "§9$reply") { + val playerEntity = entity as? AbstractClientPlayerEntity + val textureUrl = playerEntity?.skinTextures?.textureUrl + if (textureUrl != null) + it.setSkullOwner(playerEntity.uuid, textureUrl) + } + ItemExporter.modifyJson(id) { + val mutJson = it.toMutableMap() + mutJson["island"] = JsonPrimitive(SBData.skyblockLocation?.locrawMode ?: "unknown") + mutJson["x"] = JsonPrimitive(entity.blockX) + mutJson["y"] = JsonPrimitive(entity.blockY) + mutJson["z"] = JsonPrimitive(entity.blockZ) + JsonObject(mutJson) + } + } + } + + @Subscribe + fun onRecipeKeyBind(event: HandledScreenKeyPressedEvent) { + if (!event.matches(PowerUserTools.TConfig.exportUIRecipes)) { + return + } + val title = event.screen.title.string + val sellSlot = event.screen.getSlotByIndex(49, false)?.stack + val craftingTableSlot = event.screen.getSlotByIndex(craftingTableSlut, false) + if (craftingTableSlot?.stack?.displayNameAccordingToNbt?.unformattedString == "Crafting Table") { + slotIndices.forEach { (_, index) -> + event.screen.getSlotByIndex(index, false)?.stack?.let(ItemExporter::ensureExported) + } + val inputs = slotIndices.associate { (name, index) -> + val id = event.screen.getSlotByIndex(index, false)?.stack?.takeIf { !it.isEmpty() }?.let { + "${it.skyBlockId?.neuItem}:${it.count}" + } ?: "" + name to JsonPrimitive(id) + } + val output = event.screen.getSlotByIndex(resultSlot, false)?.stack!! + val overrideOutputId = output.skyBlockId!!.neuItem + val count = output.count + val recipe = JsonObject( + inputs + mapOf( + "type" to JsonPrimitive("crafting"), + "count" to JsonPrimitive(count), + "overrideOutputId" to JsonPrimitive(overrideOutputId) + ) + ) + ItemExporter.appendRecipe(output.skyBlockId!!, recipe) + MC.sendChat(tr("firmament.repo.export.recipe", "Recipe for ${output.skyBlockId} exported.")) + return + } else if (sellSlot?.displayNameAccordingToNbt?.string == "Sell Item" || (sellSlot?.loreAccordingToNbt + ?: listOf()).any { it.string == "Click to buyback!" } + ) { + val shopId = SkyblockId(title.uppercase().replace(" ", "_") + "_NPC") + if (!ItemExporter.isExported(shopId)) { + // TODO: export location + skin of last clicked npc + ItemExporter.exportStub(shopId, "§9$title (NPC)") + } + for (index in (9..9 * 5)) { + val item = event.screen.getSlotByIndex(index, false)?.stack ?: continue + val skyblockId = item.skyBlockId ?: continue + val costLines = item.loreAccordingToNbt + .map { it.string.trim() } + .dropWhile { !it.startsWith("Cost") } + .dropWhile { it == "Cost" } + .takeWhile { it != "Click to trade!" } + .takeWhile { it != "Stock" } + .filter { !it.isBlank() } + .map { it.removePrefix("Cost: ") } + + + val costs = costLines.mapNotNull { lineText -> + val line = findStackableItemByName(lineText) + if (line == null) { + MC.sendChat( + tr( + "firmament.repo.itemshop.fail", + "Could not parse cost item ${lineText} for ${item.displayNameAccordingToNbt}" + ).red() + ) + } + line + } + + + ItemExporter.appendRecipe( + shopId, JsonObject( + mapOf( + "type" to JsonPrimitive("npc_shop"), + "cost" to JsonArray(costs.map { JsonPrimitive("${it.first.neuItem}:${it.second}") }), + "result" to JsonPrimitive("${skyblockId.neuItem}:${item.count}"), + ) + ) + ) + } + MC.sendChat(tr("firmament.repo.export.itemshop", "Item Shop export for ${title} complete.")) + } else { + MC.sendChat(tr("firmament.repo.export.recipe.fail", "No Recipe found")) + } + } + + private val coinRegex = "(?$SHORT_NUMBER_FORMAT) Coins?".toPattern() + private val stackedItemRegex = "(?.*) x(?$SHORT_NUMBER_FORMAT)".toPattern() + private val reverseStackedItemRegex = "(?$SHORT_NUMBER_FORMAT)x (?.*)".toPattern() + private val essenceRegex = "(?.*) Essence x(?$SHORT_NUMBER_FORMAT)".toPattern() + private val numberedItemRegex = "(?$SHORT_NUMBER_FORMAT) (?.*)".toPattern() + + private val etherialRewardPattern = "\\+(?${SHORT_NUMBER_FORMAT})x? (?.*)".toPattern() + + fun findForName(name: String, fallbackToGenerated: Boolean = true): SkyblockId? { + var id = ItemNameLookup.guessItemByName(name, true) + if (id == null && fallbackToGenerated) { + id = generateName(name) + } + return id + } + + fun skill(name: String): SkyblockId { + return SkyblockId("SKYBLOCK_SKILL_${name}") + } + + fun generateName(name: String): SkyblockId { + return SkyblockId(name.uppercase().replace(" ", "_").replace(Regex("[^A-Z_]+"), "")) + } + + fun findStackableItemByName(name: String, fallbackToGenerated: Boolean = false): Pair? { + val properName = name.removeColorCodes().trim() + if (properName == "FREE" || properName == "This Chest is Free!") { + return Pair(SkyBlockItems.COINS, 0.0) + } + coinRegex.useMatch(properName) { + return Pair(SkyBlockItems.COINS, parseShortNumber(group("amount"))) + } + etherialRewardPattern.useMatch(properName) { + val id = when (val id = group("what")) { + "Copper" -> SkyblockId("SKYBLOCK_COPPER") + "Bits" -> SkyblockId("SKYBLOCK_BIT") + "Garden Experience" -> SkyblockId("SKYBLOCK_SKILL_GARDEN") + "Farming XP" -> SkyblockId("SKYBLOCK_SKILL_FARMING") + "Gold Essence" -> SkyblockId("ESSENCE_GOLD") + "Gemstone Powder" -> SkyblockId("SKYBLOCK_POWDER_GEMSTONE") + "Mithril Powder" -> SkyblockId("SKYBLOCK_POWDER_MITHRIL") + "Pelts" -> SkyblockId("SKYBLOCK_PELT") + "Fine Flour" -> SkyblockId("FINE_FLOUR") + else -> { + id.ifDropLast(" Experience") { + skill(generateName(it).neuItem) + } ?: id.ifDropLast(" XP") { + skill(generateName(it).neuItem) + } ?: id.ifDropLast(" Powder") { + SkyblockId("SKYBLOCK_POWDER_${generateName(it).neuItem}") + } ?: id.ifDropLast(" Essence") { + SkyblockId("ESSENCE_${generateName(it).neuItem}") + } ?: generateName(id) + } + } + return Pair(id, parseShortNumber(group("amount"))) + } + essenceRegex.useMatch(properName) { + return Pair( + SkyblockId("ESSENCE_${group("essence").uppercase()}"), + parseShortNumber(group("count")) + ) + } + stackedItemRegex.useMatch(properName) { + val item = findForName(group("name"), fallbackToGenerated) + if (item != null) { + val count = parseShortNumber(group("count")) + return Pair(item, count) + } + } + reverseStackedItemRegex.useMatch(properName) { + val item = findForName(group("name"), fallbackToGenerated) + if (item != null) { + val count = parseShortNumber(group("count")) + return Pair(item, count) + } + } + numberedItemRegex.useMatch(properName) { + val item = findForName(group("what"), fallbackToGenerated) + if (item != null) { + val count = parseShortNumber(group("count")) + return Pair(item, count) + } + } + + return findForName(properName, fallbackToGenerated)?.let { Pair(it, 1.0) } + } + +} diff --git a/src/main/kotlin/features/debug/itemeditor/ItemExporter.kt b/src/main/kotlin/features/debug/itemeditor/ItemExporter.kt new file mode 100644 index 0000000..2a56204 --- /dev/null +++ b/src/main/kotlin/features/debug/itemeditor/ItemExporter.kt @@ -0,0 +1,242 @@ +package moe.nea.firmament.features.debug.itemeditor + +import kotlinx.coroutines.launch +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlin.io.path.createParentDirectories +import kotlin.io.path.exists +import kotlin.io.path.notExists +import kotlin.io.path.readText +import kotlin.io.path.relativeTo +import kotlin.io.path.writeText +import net.minecraft.item.ItemStack +import net.minecraft.item.Items +import net.minecraft.nbt.NbtString +import net.minecraft.text.Text +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.RestArgumentType +import moe.nea.firmament.commands.get +import moe.nea.firmament.commands.thenArgument +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.commands.thenLiteral +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.events.HandledScreenKeyPressedEvent +import moe.nea.firmament.events.SlotRenderEvents +import moe.nea.firmament.features.debug.DeveloperFeatures +import moe.nea.firmament.features.debug.ExportedTestConstantMeta +import moe.nea.firmament.features.debug.PowerUserTools +import moe.nea.firmament.repo.RepoDownloadManager +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.util.LegacyTagParser +import moe.nea.firmament.util.LegacyTagWriter.Companion.toLegacyString +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.focusedItemStack +import moe.nea.firmament.util.mc.SNbtFormatter.Companion.toPrettyString +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.mc.toNbtList +import moe.nea.firmament.util.render.drawGuiTexture +import moe.nea.firmament.util.setSkyBlockId +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.tr + +object ItemExporter { + + fun exportItem(itemStack: ItemStack): Text { + nonOverlayCache.clear() + val exporter = LegacyItemExporter.createExporter(itemStack) + var json = exporter.exportJson() + val fileName = json.jsonObject["internalname"]!!.jsonPrimitive.content + val itemFile = RepoDownloadManager.repoSavedLocation.resolve("items").resolve("${fileName}.json") + itemFile.createParentDirectories() + if (itemFile.exists()) { + val existing = try { + Firmament.json.decodeFromString(itemFile.readText()) + } catch (ex: Exception) { + ex.printStackTrace() + JsonObject(mapOf()) + } + val mut = json.jsonObject.toMutableMap() + for (prop in existing) { + if (prop.key !in mut || mut[prop.key]!!.let { + (it is JsonPrimitive && (it.content.isEmpty() || it.content == "0")) || (it is JsonArray && it.isEmpty()) || (it is JsonObject && it.isEmpty()) + }) + mut[prop.key] = prop.value + } + json = JsonObject(mut) + } + val jsonFormatted = Firmament.twoSpaceJson.encodeToString(json) + itemFile.writeText(jsonFormatted) + val overlayFile = RepoDownloadManager.repoSavedLocation.resolve("itemsOverlay") + .resolve(ExportedTestConstantMeta.current.dataVersion.toString()) + .resolve("${fileName}.snbt") + overlayFile.createParentDirectories() + overlayFile.writeText(exporter.exportModernSnbt().toPrettyString()) + return tr( + "firmament.repoexport.success", + "Exported item to ${itemFile.relativeTo(RepoDownloadManager.repoSavedLocation)}${ + exporter.warnings.joinToString( + "" + ) { "\nWarning: $it" } + }" + ) + } + + fun pathFor(skyBlockId: SkyblockId) = + RepoManager.neuRepo.baseFolder.resolve("items/${skyBlockId.neuItem}.json") + + fun isExported(skyblockId: SkyblockId) = + pathFor(skyblockId).exists() + + fun ensureExported(itemStack: ItemStack) { + if (!isExported(itemStack.skyBlockId ?: return)) + MC.sendChat(exportItem(itemStack)) + } + + fun modifyJson(skyblockId: SkyblockId, modify: (JsonObject) -> JsonObject) { + val oldJson = Firmament.json.decodeFromString(pathFor(skyblockId).readText()) + val newJson = modify(oldJson) + pathFor(skyblockId).writeText(Firmament.twoSpaceJson.encodeToString(JsonObject(newJson))) + } + + fun appendRecipe(skyblockId: SkyblockId, recipe: JsonObject) { + modifyJson(skyblockId) { oldJson -> + val mutableJson = oldJson.toMutableMap() + val recipes = ((mutableJson["recipes"] as JsonArray?) ?: listOf()).toMutableList() + recipes.add(recipe) + mutableJson["recipes"] = JsonArray(recipes) + JsonObject(mutableJson) + } + } + + @Subscribe + fun onCommand(event: CommandEvent.SubCommand) { + event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) { + thenLiteral("reexportlore") { + thenArgument("itemid", RestArgumentType) { itemid -> + suggests { ctx, builder -> + val spaceIndex = builder.remaining.lastIndexOf(" ") + val (before, after) = + if (spaceIndex < 0) Pair("", builder.remaining) + else Pair( + builder.remaining.substring(0, spaceIndex + 1), + builder.remaining.substring(spaceIndex + 1) + ) + RepoManager.neuRepo.items.items.keys + .asSequence() + .filter { it.startsWith(after, ignoreCase = true) } + .forEach { + builder.suggest(before + it) + } + + builder.buildFuture() + } + thenExecute { + for (itemid in get(itemid).split(" ").map { SkyblockId(it) }) { + if (pathFor(itemid).notExists()) { + MC.sendChat( + tr( + "firmament.repo.export.relore.fail", + "Could not find json file to relore for ${itemid}" + ) + ) + } + fixLoreNbtFor(itemid) + MC.sendChat( + tr( + "firmament.repo.export.relore", + "Updated lore / display name for $itemid" + ) + ) + } + } + } + thenLiteral("all") { + thenExecute { + var i = 0 + val chunkSize = 100 + val items = RepoManager.neuRepo.items.items.keys + Firmament.coroutineScope.launch { + items.chunked(chunkSize).forEach { key -> + MC.sendChat( + tr( + "firmament.repo.export.relore.progress", + "Updated lore / display for ${i * chunkSize} / ${items.size}." + ) + ) + i++ + key.forEach { + fixLoreNbtFor(SkyblockId(it)) + } + } + MC.sendChat(tr("firmament.repo.export.relore.alldone", "All lores updated.")) + } + } + } + } + } + } + + fun fixLoreNbtFor(itemid: SkyblockId) { + modifyJson(itemid) { + val mutJson = it.toMutableMap() + val legacyTag = LegacyTagParser.parse(mutJson["nbttag"]!!.jsonPrimitive.content) + val display = legacyTag.getCompoundOrEmpty("display") + legacyTag.put("display", display) + display.putString("Name", mutJson["displayname"]!!.jsonPrimitive.content) + display.put( + "Lore", + (mutJson["lore"] as JsonArray).map { NbtString.of(it.jsonPrimitive.content) } + .toNbtList() + ) + mutJson["nbttag"] = JsonPrimitive(legacyTag.toLegacyString()) + JsonObject(mutJson) + } + } + + @Subscribe + fun onKeyBind(event: HandledScreenKeyPressedEvent) { + if (event.matches(PowerUserTools.TConfig.exportItemStackToRepo)) { + val itemStack = event.screen.focusedItemStack ?: return + PowerUserTools.lastCopiedStack = (itemStack to exportItem(itemStack)) + } + } + + val nonOverlayCache = mutableMapOf() + + @Subscribe + fun onRender(event: SlotRenderEvents.Before) { + if (!PowerUserTools.TConfig.highlightNonOverlayItems) { + return + } + val stack = event.slot.stack ?: return + val id = event.slot.stack.skyBlockId?.neuItem + if (PowerUserTools.TConfig.dontHighlightSemicolonItems && id != null && id.contains(";")) return + val isExported = nonOverlayCache.getOrPut(stack.skyBlockId ?: return) { + RepoDownloadManager.repoSavedLocation.resolve("itemsOverlay") + .resolve(ExportedTestConstantMeta.current.dataVersion.toString()) + .resolve("${stack.skyBlockId}.snbt") + .exists() + } + if (!isExported) + event.context.drawGuiTexture( + Firmament.identifier("selected_pet_background"), + event.slot.x, event.slot.y, 16, 16, + ) + } + + fun exportStub(skyblockId: SkyblockId, title: String, extra: (ItemStack) -> Unit = {}) { + exportItem(ItemStack(Items.PLAYER_HEAD).also { + it.displayNameAccordingToNbt = Text.literal(title) + it.loreAccordingToNbt = listOf(Text.literal("")) + it.setSkyBlockId(skyblockId) + extra(it) // LOL + }) + MC.sendChat(tr("firmament.repo.export.stub", "Exported a stub item for $skyblockId")) + } +} diff --git a/src/main/kotlin/features/debug/itemeditor/LegacyItemData.kt b/src/main/kotlin/features/debug/itemeditor/LegacyItemData.kt new file mode 100644 index 0000000..bc8c618 --- /dev/null +++ b/src/main/kotlin/features/debug/itemeditor/LegacyItemData.kt @@ -0,0 +1,89 @@ +package moe.nea.firmament.features.debug.itemeditor + +import kotlinx.serialization.Serializable +import kotlin.jvm.optionals.getOrNull +import net.minecraft.item.ItemStack +import net.minecraft.nbt.NbtCompound +import net.minecraft.util.Identifier +import moe.nea.firmament.Firmament +import moe.nea.firmament.repo.ExpensiveItemCacheApi +import moe.nea.firmament.repo.ItemCache +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.StringUtil.camelWords + +/** + * Load data based on [prismarine.js' 1.8 item data](https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/1.8/items.json) + */ +object LegacyItemData { + @Serializable + data class ItemData( + val id: Int, + val name: String, + val displayName: String, + val stackSize: Int, + val variations: List = listOf() + ) { + val properId = if (name.contains(":")) name else "minecraft:$name" + + fun allVariants() = + variations.map { LegacyItemType(properId, it.metadata.toShort()) } + LegacyItemType(properId, 0) + } + + @Serializable + data class Variation( + val metadata: Int, val displayName: String + ) + + data class LegacyItemType( + val name: String, + val metadata: Short + ) { + override fun toString(): String { + return "$name:$metadata" + } + } + + @Serializable + data class EnchantmentData( + val id: Int, + val name: String, + val displayName: String, + ) + + inline fun getLegacyData(name: String) = + Firmament.tryDecodeJsonFromStream( + LegacyItemData::class.java.getResourceAsStream("/legacy_data/$name.json")!! + ).getOrThrow() + + val enchantmentData = getLegacyData>("enchantments") + val enchantmentLut = enchantmentData.associateBy { Identifier.ofVanilla(it.name) } + + val itemDat = getLegacyData>("items") + + @OptIn(ExpensiveItemCacheApi::class) // This is fine, we get loaded in a thread. + val itemLut = itemDat.flatMap { item -> + item.allVariants().map { legacyItemType -> + val nbt = ItemCache.convert189ToModern(NbtCompound().apply { + putString("id", legacyItemType.name) + putByte("Count", 1) + putShort("Damage", legacyItemType.metadata) + })!! + val stack = ItemStack.fromNbt(MC.defaultRegistries, nbt).getOrNull() + ?: error("Could not transform ${legacyItemType}") + stack.item to legacyItemType + } + }.toMap() + + @Serializable + data class LegacyEffect( + val id: Int, + val name: String, + val displayName: String, + val type: String + ) + + val effectList = getLegacyData>("effects") + .associateBy { + it.name.camelWords().map { it.trim().lowercase() }.joinToString("_") + } +} diff --git a/src/main/kotlin/features/debug/itemeditor/LegacyItemExporter.kt b/src/main/kotlin/features/debug/itemeditor/LegacyItemExporter.kt new file mode 100644 index 0000000..ecf3d2c --- /dev/null +++ b/src/main/kotlin/features/debug/itemeditor/LegacyItemExporter.kt @@ -0,0 +1,311 @@ +package moe.nea.firmament.features.debug.itemeditor + +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import kotlin.concurrent.thread +import kotlin.jvm.optionals.getOrNull +import net.minecraft.component.DataComponentTypes +import net.minecraft.item.ItemStack +import net.minecraft.nbt.NbtByte +import net.minecraft.nbt.NbtCompound +import net.minecraft.nbt.NbtElement +import net.minecraft.nbt.NbtInt +import net.minecraft.nbt.NbtList +import net.minecraft.nbt.NbtOps +import net.minecraft.nbt.NbtString +import net.minecraft.registry.tag.ItemTags +import net.minecraft.text.Text +import net.minecraft.util.Unit +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ClientStartedEvent +import moe.nea.firmament.features.debug.ExportedTestConstantMeta +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.util.HypixelPetInfo +import moe.nea.firmament.util.LegacyTagWriter.Companion.toLegacyString +import moe.nea.firmament.util.StringUtil.words +import moe.nea.firmament.util.directLiteralStringContent +import moe.nea.firmament.util.extraAttributes +import moe.nea.firmament.util.getLegacyFormatString +import moe.nea.firmament.util.json.toJsonArray +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.mc.toNbtList +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.skyblock.Rarity +import moe.nea.firmament.util.transformEachRecursively +import moe.nea.firmament.util.unformattedString + +class LegacyItemExporter private constructor(var itemStack: ItemStack) { + init { + require(!itemStack.isEmpty) + itemStack.count = 1 + } + + var lore = itemStack.loreAccordingToNbt + var name = itemStack.displayNameAccordingToNbt + val extraAttribs = itemStack.extraAttributes.copy() + val legacyNbt = NbtCompound() + val warnings = mutableListOf() + + // TODO: check if lore contains non 1.8.9 able hex codes and emit lore in overlay files if so + + fun preprocess() { + // TODO: split up preprocess steps into preprocess actions that can be toggled in a ui + extraAttribs.remove("timestamp") + extraAttribs.remove("uuid") + extraAttribs.remove("modifier") + extraAttribs.getString("petInfo").ifPresent { petInfoJson -> + var petInfo = Firmament.json.decodeFromString(petInfoJson) + petInfo = petInfo.copy(candyUsed = 0, heldItem = null, exp = 0.0, active = null, uuid = null) + extraAttribs.putString("petInfo", Firmament.tightJson.encodeToString(petInfo)) + } + itemStack.skyBlockId?.let { + extraAttribs.putString("id", it.neuItem) + } + trimLore() + itemStack.loreAccordingToNbt = itemStack.item.defaultStack.loreAccordingToNbt + itemStack.remove(DataComponentTypes.CUSTOM_NAME) + } + + fun trimLore() { + val rarityIdx = lore.indexOfLast { + val firstWordInLine = it.unformattedString.words().filter { it.length > 2 }.firstOrNull() + firstWordInLine?.let(Rarity::fromString) != null + } + if (rarityIdx >= 0) { + lore = lore.subList(0, rarityIdx + 1) + } + + trimStats() + + deleteLineUntilNextSpace { it.startsWith("Held Item: ") } + deleteLineUntilNextSpace { it.startsWith("Progress to Level ") } + deleteLineUntilNextSpace { it.startsWith("MAX LEVEL") } + deleteLineUntilNextSpace { it.startsWith("Click to view recipe!") } + collapseWhitespaces() + + name = name.transformEachRecursively { + var string = it.directLiteralStringContent ?: return@transformEachRecursively it + string = string.replace("Lvl \\d+".toRegex(), "Lvl {LVL}") + Text.literal(string).setStyle(it.style) + } + + if (lore.isEmpty()) + lore = listOf(Text.empty()) + } + + private fun trimStats() { + val lore = this.lore.toMutableList() + for (index in lore.indices) { + val value = lore[index] + val statLine = SBItemStack.parseStatLine(value) + if (statLine == null) break + val v = value.copy() + require(value.directLiteralStringContent == "") + v.siblings.removeIf { it.directLiteralStringContent!!.contains("(") } + val last = v.siblings.last() + v.siblings[v.siblings.lastIndex] = + Text.literal(last.directLiteralStringContent!!.trimEnd()) + .setStyle(last.style) + lore[index] = v + } + this.lore = lore + } + + fun collapseWhitespaces() { + lore = (listOf(null as Text?) + lore).zipWithNext() + .filter { !it.first?.unformattedString.isNullOrBlank() || !it.second?.unformattedString.isNullOrBlank() } + .map { it.second!! } + } + + fun deleteLineUntilNextSpace(search: (String) -> Boolean) { + val idx = lore.indexOfFirst { search(it.unformattedString) } + if (idx < 0) return + val l = lore.toMutableList() + val p = l.subList(idx, l.size) + val nextBlank = p.indexOfFirst { it.unformattedString.isEmpty() } + if (nextBlank < 0) + p.clear() + else + p.subList(0, nextBlank).clear() + lore = l + } + + fun processNbt() { + // TODO: calculate hideflags + legacyNbt.put("HideFlags", NbtInt.of(254)) + copyUnbreakable() + copyItemModel() + copyPotion() + copyExtraAttributes() + copyLegacySkullNbt() + copyDisplay() + copyColour() + copyEnchantments() + copyEnchantGlint() + // TODO: copyDisplay + } + + private fun copyPotion() { + val effects = itemStack.get(DataComponentTypes.POTION_CONTENTS) ?: return + legacyNbt.put("CustomPotionEffects", NbtList().also { + effects.effects.forEach { effect -> + val effectId = effect.effectType.key.get().value.path + val duration = effect.duration + val legacyId = LegacyItemData.effectList[effectId]!! + + it.add(NbtCompound().apply { + put("Ambient", NbtByte.of(false)) + put("Duration", NbtInt.of(duration)) + put("Id", NbtByte.of(legacyId.id.toByte())) + put("Amplifier", NbtByte.of(effect.amplifier.toByte())) + }) + } + }) + } + + fun NbtCompound.getOrPutCompound(name: String): NbtCompound { + val compound = getCompoundOrEmpty(name) + put(name, compound) + return compound + } + + private fun copyColour() { + if (!itemStack.isIn(ItemTags.DYEABLE)) { + itemStack.remove(DataComponentTypes.DYED_COLOR) + return + } + val leatherTint = itemStack.componentChanges.get(DataComponentTypes.DYED_COLOR)?.getOrNull() ?: return + legacyNbt.getOrPutCompound("display").put("color", NbtInt.of(leatherTint.rgb)) + } + + private fun copyItemModel() { + val itemModel = itemStack.get(DataComponentTypes.ITEM_MODEL) ?: return + legacyNbt.put("ItemModel", NbtString.of(itemModel.toString())) + } + + private fun copyDisplay() { + legacyNbt.getOrPutCompound("display").apply { + put("Lore", lore.map { NbtString.of(it.getLegacyFormatString(trimmed = true)) }.toNbtList()) + putString("Name", name.getLegacyFormatString(trimmed = true)) + } + } + + fun exportModernSnbt(): NbtElement { + val overlay = ItemStack.CODEC.encodeStart(NbtOps.INSTANCE, itemStack) + .orThrow + val overlayWithVersion = + ExportedTestConstantMeta.SOURCE_CODEC.encode(ExportedTestConstantMeta.current, NbtOps.INSTANCE, overlay) + .orThrow + return overlayWithVersion + } + + fun prepare() { + preprocess() + processNbt() + itemStack.extraAttributes = extraAttribs + } + + fun exportJson(): JsonElement { + return buildJsonObject { + val (itemId, damage) = legacyifyItemStack() + put("itemid", itemId) + put("displayname", name.getLegacyFormatString(trimmed = true)) + put("nbttag", legacyNbt.toLegacyString()) + put("damage", damage) + put("lore", lore.map { it.getLegacyFormatString(trimmed = true) }.toJsonArray()) + val sbId = itemStack.skyBlockId + if (sbId == null) + warnings.add("Could not find skyblock id") + put("internalname", sbId?.neuItem) + put("clickcommand", "") + put("crafttext", "") + put("modver", "Firmament ${Firmament.version.friendlyString}") + put("infoType", "") + put("info", JsonArray(listOf())) + } + + } + + companion object { + fun createExporter(itemStack: ItemStack): LegacyItemExporter { + return LegacyItemExporter(itemStack.copy()).also { it.prepare() } + } + + @Subscribe + fun load(event: ClientStartedEvent) { + thread(start = true, name = "ItemExporter Meta Load Thread") { + LegacyItemData.itemLut + } + } + } + + fun copyEnchantGlint() { + if (itemStack.get(DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE) == true) { + val ench = legacyNbt.getListOrEmpty("ench") + legacyNbt.put("ench", ench) + } + } + + private fun copyUnbreakable() { + if (itemStack.get(DataComponentTypes.UNBREAKABLE) == Unit.INSTANCE) { + legacyNbt.putBoolean("Unbreakable", true) + } + } + + fun copyEnchantments() { + val enchantments = itemStack.get(DataComponentTypes.ENCHANTMENTS)?.takeIf { !it.isEmpty } ?: return + val enchTag = legacyNbt.getListOrEmpty("ench") + legacyNbt.put("ench", enchTag) + enchantments.enchantmentEntries.forEach { entry -> + val id = entry.key.key.get().value + val legacyId = LegacyItemData.enchantmentLut[id] + if (legacyId == null) { + warnings.add("Could not find legacy enchantment id for ${id}") + return@forEach + } + enchTag.add(NbtCompound().apply { + putShort("lvl", entry.intValue.toShort()) + putShort( + "id", + legacyId.id.toShort() + ) + }) + } + } + + fun copyExtraAttributes() { + legacyNbt.put("ExtraAttributes", extraAttribs) + } + + fun copyLegacySkullNbt() { + val profile = itemStack.get(DataComponentTypes.PROFILE) ?: return + legacyNbt.put("SkullOwner", NbtCompound().apply { + profile.id.ifPresent { + putString("Id", it.toString()) + } + putBoolean("hypixelPopulated", true) + put("Properties", NbtCompound().apply { + profile.properties().forEach { prop, value -> + val list = getListOrEmpty(prop) + put(prop, list) + list.add(NbtCompound().apply { + value.signature?.let { + putString("Signature", it) + } + putString("Value", value.value) + putString("Name", value.name) + }) + } + }) + }) + } + + fun legacyifyItemStack(): LegacyItemData.LegacyItemType { + // TODO: add a default here + return LegacyItemData.itemLut[itemStack.item]!! + } +} diff --git a/src/main/kotlin/features/debug/itemeditor/PromptScreen.kt b/src/main/kotlin/features/debug/itemeditor/PromptScreen.kt new file mode 100644 index 0000000..187b70b --- /dev/null +++ b/src/main/kotlin/features/debug/itemeditor/PromptScreen.kt @@ -0,0 +1,15 @@ +package moe.nea.firmament.features.debug.itemeditor + +import io.github.notenoughupdates.moulconfig.gui.CloseEventListener +import io.github.notenoughupdates.moulconfig.gui.GuiComponentWrapper +import io.github.notenoughupdates.moulconfig.gui.GuiContext +import io.github.notenoughupdates.moulconfig.gui.component.CenterComponent +import io.github.notenoughupdates.moulconfig.gui.component.ColumnComponent +import io.github.notenoughupdates.moulconfig.gui.component.PanelComponent +import io.github.notenoughupdates.moulconfig.gui.component.TextComponent +import io.github.notenoughupdates.moulconfig.gui.component.TextFieldComponent +import io.github.notenoughupdates.moulconfig.observer.GetSetter +import kotlin.reflect.KMutableProperty0 +import moe.nea.firmament.gui.FirmButtonComponent +import moe.nea.firmament.util.MoulConfigUtils + diff --git a/src/main/kotlin/features/diana/AncestralSpadeSolver.kt b/src/main/kotlin/features/diana/AncestralSpadeSolver.kt new file mode 100644 index 0000000..ff85c00 --- /dev/null +++ b/src/main/kotlin/features/diana/AncestralSpadeSolver.kt @@ -0,0 +1,131 @@ +package moe.nea.firmament.features.diana + +import kotlin.time.Duration.Companion.seconds +import net.minecraft.particle.ParticleTypes +import net.minecraft.sound.SoundEvents +import net.minecraft.util.math.Vec3d +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ParticleSpawnEvent +import moe.nea.firmament.events.SoundReceiveEvent +import moe.nea.firmament.events.WorldKeyboardEvent +import moe.nea.firmament.events.WorldReadyEvent +import moe.nea.firmament.events.WorldRenderLastEvent +import moe.nea.firmament.events.subscription.SubscriptionOwner +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.SkyBlockIsland +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.WarpUtil +import moe.nea.firmament.util.render.RenderInWorldContext +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.skyblock.SkyBlockItems + +object AncestralSpadeSolver : SubscriptionOwner { + var lastDing = TimeMark.farPast() + private set + private val pitches = mutableListOf() + val particlePositions = mutableListOf() + var nextGuess: Vec3d? = null + private set + + private var lastTeleportAttempt = TimeMark.farPast() + + fun isEnabled() = + DianaWaypoints.TConfig.ancestralSpadeSolver + && SBData.skyblockLocation == SkyBlockIsland.HUB + && MC.player?.inventory?.containsAny { it.skyBlockId == SkyBlockItems.ANCESTRAL_SPADE } == true // TODO: add a reactive property here + + @Subscribe + fun onKeyBind(event: WorldKeyboardEvent) { + if (!isEnabled()) return + if (!event.matches(DianaWaypoints.TConfig.ancestralSpadeTeleport)) return + + if (lastTeleportAttempt.passedTime() < 3.seconds) return + WarpUtil.teleportToNearestWarp(SkyBlockIsland.HUB, nextGuess ?: return) + lastTeleportAttempt = TimeMark.now() + } + + @Subscribe + fun onParticleSpawn(event: ParticleSpawnEvent) { + if (!isEnabled()) return + if (event.particleEffect != ParticleTypes.DRIPPING_LAVA) return + if (event.offset.x != 0.0F || event.offset.y != 0F || event.offset.z != 0F) + return + particlePositions.add(event.position) + if (particlePositions.size > 20) { + particlePositions.removeFirst() + } + } + + @Subscribe + fun onPlaySound(event: SoundReceiveEvent) { + if (!isEnabled()) return + if (!SoundEvents.BLOCK_NOTE_BLOCK_HARP.matchesId(event.sound.value().id)) return + + if (lastDing.passedTime() > 1.seconds) { + particlePositions.clear() + pitches.clear() + } + lastDing = TimeMark.now() + + pitches.add(event.pitch) + if (pitches.size > 20) { + pitches.removeFirst() + } + + if (particlePositions.size < 3) { + return + } + + val averagePitchDelta = + if (pitches.isEmpty()) return + else pitches + .zipWithNext { a, b -> b - a } + .average() + + val soundDistanceEstimate = (Math.E / averagePitchDelta) - particlePositions.first().distanceTo(event.position) + + if (soundDistanceEstimate > 1000) { + return + } + + val lastParticleDirection = particlePositions + .takeLast(3) + .let { (a, _, b) -> b.subtract(a) } + .normalize() + + nextGuess = event.position.add(lastParticleDirection.multiply(soundDistanceEstimate)) + } + + @Subscribe + fun onWorldRender(event: WorldRenderLastEvent) { + if (!isEnabled()) return + RenderInWorldContext.renderInWorld(event) { + nextGuess?.let { + tinyBlock(it, 1f, 0x80FFFFFF.toInt()) + // TODO: replace this + color(1f, 1f, 0f, 1f) + tracer(it, lineWidth = 3f) + } + if (particlePositions.size > 2 && lastDing.passedTime() < 10.seconds && nextGuess != null) { + // TODO: replace this // TODO: add toggle + color(0f, 1f, 0f, 0.7f) + line(particlePositions) + } + } + } + + @Subscribe + fun onSwapWorld(event: WorldReadyEvent) { + nextGuess = null + particlePositions.clear() + pitches.clear() + lastDing = TimeMark.farPast() + } + + override val delegateFeature: FirmamentFeature + get() = DianaWaypoints + +} diff --git a/src/main/kotlin/features/diana/DianaWaypoints.kt b/src/main/kotlin/features/diana/DianaWaypoints.kt new file mode 100644 index 0000000..6d87262 --- /dev/null +++ b/src/main/kotlin/features/diana/DianaWaypoints.kt @@ -0,0 +1,31 @@ +package moe.nea.firmament.features.diana + +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.AttackBlockEvent +import moe.nea.firmament.events.UseBlockEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig + +object DianaWaypoints : FirmamentFeature { + override val identifier get() = "diana" + override val config get() = TConfig + + object TConfig : ManagedConfig(identifier, Category.EVENTS) { + val ancestralSpadeSolver by toggle("ancestral-spade") { true } + val ancestralSpadeTeleport by keyBindingWithDefaultUnbound("ancestral-teleport") + val nearbyWaypoints by toggle("nearby-waypoints") { true } + } + + + @Subscribe + fun onBlockUse(event: UseBlockEvent) { + NearbyBurrowsSolver.onBlockClick(event.hitResult.blockPos) + } + + @Subscribe + fun onBlockAttack(event: AttackBlockEvent) { + NearbyBurrowsSolver.onBlockClick(event.blockPos) + } +} + + diff --git a/src/main/kotlin/features/diana/NearbyBurrowsSolver.kt b/src/main/kotlin/features/diana/NearbyBurrowsSolver.kt new file mode 100644 index 0000000..2fb4002 --- /dev/null +++ b/src/main/kotlin/features/diana/NearbyBurrowsSolver.kt @@ -0,0 +1,144 @@ +package moe.nea.firmament.features.diana + +import me.shedaniel.math.Color +import kotlin.time.Duration.Companion.seconds +import net.minecraft.particle.ParticleTypes +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.MathHelper +import net.minecraft.util.math.Position +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ParticleSpawnEvent +import moe.nea.firmament.events.ProcessChatEvent +import moe.nea.firmament.events.WorldReadyEvent +import moe.nea.firmament.events.WorldRenderLastEvent +import moe.nea.firmament.events.subscription.SubscriptionOwner +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.collections.mutableMapWithMaxSize +import moe.nea.firmament.util.render.RenderInWorldContext.Companion.renderInWorld + +object NearbyBurrowsSolver : SubscriptionOwner { + + + private val recentlyDugBurrows: MutableMap = mutableMapWithMaxSize(20) + private val recentEnchantParticles: MutableMap = mutableMapWithMaxSize(500) + private var lastBlockClick: BlockPos? = null + + enum class BurrowType { + START, MOB, TREASURE + } + + val burrows = mutableMapOf() + + @Subscribe + fun onChatEvent(event: ProcessChatEvent) { + val lastClickedBurrow = lastBlockClick ?: return + if (event.unformattedString.startsWith("You dug out a Griffin Burrow!") || + event.unformattedString.startsWith(" ☠ You were killed by") || + event.unformattedString.startsWith("You finished the Griffin burrow chain!") + ) { + markAsDug(lastClickedBurrow) + burrows.remove(lastClickedBurrow) + } + } + + + fun wasRecentlyDug(blockPos: BlockPos): Boolean { + val lastDigTime = recentlyDugBurrows[blockPos] ?: TimeMark.farPast() + return lastDigTime.passedTime() < 10.seconds + } + + fun markAsDug(blockPos: BlockPos) { + recentlyDugBurrows[blockPos] = TimeMark.now() + } + + fun wasRecentlyEnchanted(blockPos: BlockPos): Boolean { + val lastEnchantTime = recentEnchantParticles[blockPos] ?: TimeMark.farPast() + return lastEnchantTime.passedTime() < 4.seconds + } + + fun markAsEnchanted(blockPos: BlockPos) { + recentEnchantParticles[blockPos] = TimeMark.now() + } + + @Subscribe + fun onParticles(event: ParticleSpawnEvent) { + if (!DianaWaypoints.TConfig.nearbyWaypoints) return + + val position: BlockPos = event.position.toBlockPos().down() + + if (wasRecentlyDug(position)) return + + val isEven50Spread = (event.offset.x == 0.5f && event.offset.z == 0.5f) + + if (event.particleEffect.type == ParticleTypes.ENCHANT) { + if (event.count == 5 && event.speed == 0.05F && event.offset.y == 0.4F && isEven50Spread) { + markAsEnchanted(position) + } + return + } + + if (!wasRecentlyEnchanted(position)) return + + if (event.particleEffect.type == ParticleTypes.ENCHANTED_HIT + && event.count == 4 + && event.speed == 0.01F + && event.offset.y == 0.1f + && isEven50Spread + ) { + burrows[position] = BurrowType.START + } + if (event.particleEffect.type == ParticleTypes.CRIT + && event.count == 3 + && event.speed == 0.01F + && event.offset.y == 0.1F + && isEven50Spread + ) { + burrows[position] = BurrowType.MOB + } + if (event.particleEffect.type == ParticleTypes.DRIPPING_LAVA + && event.count == 2 + && event.speed == 0.01F + && event.offset.y == 0.1F + && event.offset.x == 0.35F && event.offset.z == 0.35f + ) { + burrows[position] = BurrowType.TREASURE + } + } + + @Subscribe + fun onRender(event: WorldRenderLastEvent) { + if (!DianaWaypoints.TConfig.nearbyWaypoints) return + renderInWorld(event) { + for ((location, burrow) in burrows) { + val color = when (burrow) { + BurrowType.START -> Color.ofRGBA(.2f, .8f, .2f, 0.4f) + BurrowType.MOB -> Color.ofRGBA(0.3f, 0.4f, 0.9f, 0.4f) + BurrowType.TREASURE -> Color.ofRGBA(1f, 0.7f, 0.2f, 0.4f) + } + block(location, color.color) + } + } + } + + @Subscribe + fun onSwapWorld(worldReadyEvent: WorldReadyEvent) { + burrows.clear() + recentEnchantParticles.clear() + recentlyDugBurrows.clear() + lastBlockClick = null + } + + fun onBlockClick(blockPos: BlockPos) { + if (!DianaWaypoints.TConfig.nearbyWaypoints) return + burrows.remove(blockPos) + lastBlockClick = blockPos + } + + override val delegateFeature: FirmamentFeature + get() = DianaWaypoints +} + +fun Position.toBlockPos(): BlockPos { + return BlockPos(MathHelper.floor(x), MathHelper.floor(y), MathHelper.floor(z)) +} diff --git a/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt b/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt new file mode 100644 index 0000000..0cfaeba --- /dev/null +++ b/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt @@ -0,0 +1,226 @@ + +package moe.nea.firmament.features.events.anniversity + +import io.github.notenoughupdates.moulconfig.observer.ObservableList +import io.github.notenoughupdates.moulconfig.xml.Bind +import moe.nea.jarvis.api.Point +import kotlin.time.Duration.Companion.seconds +import net.minecraft.entity.passive.PigEntity +import net.minecraft.util.math.BlockPos +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.EntityInteractionEvent +import moe.nea.firmament.events.ProcessChatEvent +import moe.nea.firmament.events.TickEvent +import moe.nea.firmament.events.WorldReadyEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.gui.hud.MoulConfigHud +import moe.nea.firmament.repo.ExpensiveItemCacheApi +import moe.nea.firmament.repo.ItemNameLookup +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SHORT_NUMBER_FORMAT +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.parseShortNumber +import moe.nea.firmament.util.useMatch + +object AnniversaryFeatures : FirmamentFeature { + override val identifier: String + get() = "anniversary" + + object TConfig : ManagedConfig(identifier, Category.EVENTS) { + val enableShinyPigTracker by toggle("shiny-pigs") {true} + val trackPigCooldown by position("pig-hud", 200, 300) { Point(0.1, 0.2) } + } + + override val config: ManagedConfig? + get() = TConfig + + data class ClickedPig( + val clickedAt: TimeMark, + val startLocation: BlockPos, + val pigEntity: PigEntity + ) { + @Bind("timeLeft") + fun getTimeLeft(): Double = 1 - clickedAt.passedTime() / pigDuration + } + + val clickedPigs = ObservableList(mutableListOf()) + var lastClickedPig: PigEntity? = null + + val pigDuration = 90.seconds + + @Subscribe + fun onTick(event: TickEvent) { + clickedPigs.removeIf { it.clickedAt.passedTime() > pigDuration } + } + + val pattern = "SHINY! You extracted (?.*) from the piglet's orb!".toPattern() + + @Subscribe + fun onChat(event: ProcessChatEvent) { + if(!TConfig.enableShinyPigTracker)return + if (event.unformattedString == "Oink! Bring the pig back to the Shiny Orb!") { + val pig = lastClickedPig ?: return + // TODO: store proper location based on the orb location, maybe + val startLocation = pig.blockPos ?: return + clickedPigs.add(ClickedPig(TimeMark.now(), startLocation, pig)) + lastClickedPig = null + } + if (event.unformattedString == "SHINY! The orb is charged! Click on it for loot!") { + val player = MC.player ?: return + val lowest = + clickedPigs.minByOrNull { it.startLocation.getSquaredDistance(player.pos) } ?: return + clickedPigs.remove(lowest) + } + pattern.useMatch(event.unformattedString) { + val reward = group("reward") + val parsedReward = parseReward(reward) + addReward(parsedReward) + PigCooldown.rewards.atOnce { + PigCooldown.rewards.clear() + rewards.mapTo(PigCooldown.rewards) { PigCooldown.DisplayReward(it) } + } + } + } + + fun addReward(reward: Reward) { + val it = rewards.listIterator() + while (it.hasNext()) { + val merged = reward.mergeWith(it.next()) ?: continue + it.set(merged) + return + } + rewards.add(reward) + } + + val rewards = mutableListOf() + + fun ObservableList.atOnce(block: () -> Unit) { + val oldObserver = observer + observer = null + block() + observer = oldObserver + update() + } + + sealed interface Reward { + fun mergeWith(other: Reward): Reward? + data class EXP(val amount: Double, val skill: String) : Reward { + override fun mergeWith(other: Reward): Reward? { + if (other is EXP && other.skill == skill) + return EXP(amount + other.amount, skill) + return null + } + } + + data class Coins(val amount: Double) : Reward { + override fun mergeWith(other: Reward): Reward? { + if (other is Coins) + return Coins(other.amount + amount) + return null + } + } + + data class Items(val amount: Int, val item: SkyblockId) : Reward { + override fun mergeWith(other: Reward): Reward? { + if (other is Items && other.item == item) + return Items(amount + other.amount, item) + return null + } + } + + data class Unknown(val text: String) : Reward { + override fun mergeWith(other: Reward): Reward? { + return null + } + } + } + + val expReward = "\\+(?$SHORT_NUMBER_FORMAT) (?[^ ]+) XP".toPattern() + val coinReward = "\\+(?$SHORT_NUMBER_FORMAT) coins".toPattern() + val itemReward = "(?:(?[0-9]+)x )?(?.*)".toPattern() + fun parseReward(string: String): Reward { + expReward.useMatch(string) { + val exp = parseShortNumber(group("exp")) + val kind = group("kind") + return Reward.EXP(exp, kind) + } + coinReward.useMatch(string) { + val coins = parseShortNumber(group("amount")) + return Reward.Coins(coins) + } + itemReward.useMatch(string) { + val amount = group("amount")?.toIntOrNull() ?: 1 + val name = group("name") + val item = ItemNameLookup.guessItemByName(name, false) ?: return@useMatch + return Reward.Items(amount, item) + } + return Reward.Unknown(string) + } + + @Subscribe + fun onWorldClear(event: WorldReadyEvent) { + lastClickedPig = null + clickedPigs.clear() + } + + @Subscribe + fun onEntityClick(event: EntityInteractionEvent) { + if (event.entity is PigEntity) { + lastClickedPig = event.entity + } + } + + @Subscribe + fun init(event: WorldReadyEvent) { + PigCooldown.forceInit() + } + + object PigCooldown : MoulConfigHud("anniversary_pig", TConfig.trackPigCooldown) { + override fun shouldRender(): Boolean { + return clickedPigs.isNotEmpty() && TConfig.enableShinyPigTracker + } + + @Bind("pigs") + fun getPigs() = clickedPigs + + class DisplayReward(val backedBy: Reward) { + @Bind + fun count(): String { + return when (backedBy) { + is Reward.Coins -> backedBy.amount + is Reward.EXP -> backedBy.amount + is Reward.Items -> backedBy.amount + is Reward.Unknown -> 0 + }.toString() + } + + val itemStack = if (backedBy is Reward.Items) { + SBItemStack(backedBy.item, backedBy.amount) + } else { + SBItemStack(SkyblockId.NULL) + } + + @OptIn(ExpensiveItemCacheApi::class) + @Bind + fun name(): String { + return when (backedBy) { + is Reward.Coins -> "Coins" + is Reward.EXP -> backedBy.skill + is Reward.Items -> itemStack.asImmutableItemStack().name.string + is Reward.Unknown -> backedBy.text + } + } + + @Bind + fun isKnown() = backedBy !is Reward.Unknown + } + + @get:Bind("rewards") + val rewards = ObservableList(mutableListOf()) + + } + +} diff --git a/src/main/kotlin/features/events/anniversity/CenturyRaffleFeatures.kt b/src/main/kotlin/features/events/anniversity/CenturyRaffleFeatures.kt new file mode 100644 index 0000000..9935051 --- /dev/null +++ b/src/main/kotlin/features/events/anniversity/CenturyRaffleFeatures.kt @@ -0,0 +1,63 @@ +package moe.nea.firmament.features.events.anniversity + +import java.util.Optional +import me.shedaniel.math.Color +import kotlin.jvm.optionals.getOrNull +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.text.Style +import net.minecraft.util.Formatting +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.EntityRenderTintEvent +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.render.TintedOverlayTexture +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.skyblock.SkyBlockItems + +object CenturyRaffleFeatures { + object TConfig : ManagedConfig("centuryraffle", Category.EVENTS) { + val highlightPlayersForSlice by toggle("highlight-cake-players") { true } +// val highlightAllPlayers by toggle("highlight-all-cake-players") { true } + } + + val cakeIcon = "⛃" + + val cakeColors = listOf( + CakeTeam(SkyBlockItems.SLICE_OF_BLUEBERRY_CAKE, Formatting.BLUE), + CakeTeam(SkyBlockItems.SLICE_OF_CHEESECAKE, Formatting.YELLOW), + CakeTeam(SkyBlockItems.SLICE_OF_GREEN_VELVET_CAKE, Formatting.GREEN), + CakeTeam(SkyBlockItems.SLICE_OF_RED_VELVET_CAKE, Formatting.RED), + CakeTeam(SkyBlockItems.SLICE_OF_STRAWBERRY_SHORTCAKE, Formatting.LIGHT_PURPLE), + ) + + data class CakeTeam( + val id: SkyblockId, + val formatting: Formatting, + ) { + val searchedTextRgb = formatting.colorValue!! + val brightenedRgb = Color.ofOpaque(searchedTextRgb)//.brighter(2.0) + val tintOverlay by lazy { + TintedOverlayTexture().setColor(brightenedRgb) + } + } + + val sliceToColor = cakeColors.associateBy { it.id } + + @Subscribe + fun onEntityRender(event: EntityRenderTintEvent) { + if (!TConfig.highlightPlayersForSlice) return + val requestedCakeTeam = sliceToColor[MC.stackInHand?.skyBlockId] ?: return + // TODO: cache the requested color + val player = event.entity as? PlayerEntity ?: return + val cakeColor: Style = player.styledDisplayName.visit( + { style, text -> + if (text == cakeIcon) Optional.of(style) + else Optional.empty() + }, Style.EMPTY).getOrNull() ?: return + if (cakeColor.color?.rgb == requestedCakeTeam.searchedTextRgb) { + event.renderState.overlayTexture_firmament = requestedCakeTeam.tintOverlay + } + } + +} diff --git a/src/main/kotlin/features/events/carnival/CarnivalFeatures.kt b/src/main/kotlin/features/events/carnival/CarnivalFeatures.kt new file mode 100644 index 0000000..840fb8c --- /dev/null +++ b/src/main/kotlin/features/events/carnival/CarnivalFeatures.kt @@ -0,0 +1,17 @@ + +package moe.nea.firmament.features.events.carnival + +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig + +object CarnivalFeatures : FirmamentFeature { + object TConfig : ManagedConfig(identifier, Category.EVENTS) { + val enableBombSolver by toggle("bombs-solver") { true } + val displayTutorials by toggle("tutorials") { true } + } + + override val config: ManagedConfig? + get() = TConfig + override val identifier: String + get() = "carnival" +} diff --git a/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt b/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt new file mode 100644 index 0000000..cfc05cc --- /dev/null +++ b/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt @@ -0,0 +1,276 @@ + +package moe.nea.firmament.features.events.carnival + +import io.github.notenoughupdates.moulconfig.observer.ObservableList +import io.github.notenoughupdates.moulconfig.platform.ModernItemStack +import io.github.notenoughupdates.moulconfig.xml.Bind +import java.util.UUID +import net.minecraft.block.Blocks +import net.minecraft.item.Item +import net.minecraft.item.ItemStack +import net.minecraft.item.Items +import net.minecraft.text.ClickEvent +import net.minecraft.text.Text +import net.minecraft.util.math.BlockPos +import net.minecraft.world.WorldAccess +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.events.AttackBlockEvent +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.events.EntityUpdateEvent +import moe.nea.firmament.events.ProcessChatEvent +import moe.nea.firmament.events.WorldReadyEvent +import moe.nea.firmament.events.WorldRenderLastEvent +import moe.nea.firmament.features.debug.DebugLogger +import moe.nea.firmament.util.LegacyFormattingCode +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.MoulConfigUtils +import moe.nea.firmament.util.ScreenUtil +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.mc.createSkullItem +import moe.nea.firmament.util.render.RenderInWorldContext +import moe.nea.firmament.util.setSkyBlockFirmamentUiId +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.useMatch + +object MinesweeperHelper { + val sandBoxLow = BlockPos(-112, 72, -11) + val sandBoxHigh = BlockPos(-106, 72, -5) + val boardSize = Pair(sandBoxHigh.x - sandBoxLow.x, sandBoxHigh.z - sandBoxLow.z) + + val gameStartMessage = "[NPC] Carnival Pirateman: Good luck, matey!" + val gameEndMessage = "Fruit Digging" + val bombPattern = "MINES! There (are|is) (?[0-8]) bombs? hidden nearby\\.".toPattern() + val startGameQuestion = "[NPC] Carnival Pirateman: Would ye like to do some Fruit Digging?" + + + enum class Piece( + @get:Bind("fruitName") + val fruitName: String, + val points: Int, + val specialAbility: String, + val totalPerBoard: Int, + val textureHash: String, + val fruitColor: LegacyFormattingCode, + ) { + COCONUT("Coconut", + 200, + "Prevents a bomb from exploding next turn", + 3, + "10ceb1455b471d016a9f06d25f6e468df9fcf223e2c1e4795b16e84fcca264ee", + LegacyFormattingCode.DARK_PURPLE), + APPLE("Apple", + 100, + "Gains 100 points for each apple dug up", + 8, + "17ea278d6225c447c5943d652798d0bbbd1418434ce8c54c54fdac79994ddd6c", + LegacyFormattingCode.GREEN), + WATERMELON("Watermelon", + 100, + "Blows up an adjacent fruit for half the points", + 4, + "efe4ef83baf105e8dee6cf03dfe7407f1911b3b9952c891ae34139560f2931d6", + LegacyFormattingCode.DARK_BLUE), + DURIAN("Durian", + 800, + "Halves the points earned in the next turn", + 2, + "ac268d36c2c6047ffeec00124096376b56dbb4d756a55329363a1b27fcd659cd", + LegacyFormattingCode.DARK_PURPLE), + MANGO("Mango", + 300, + "Just an ordinary fruit", + 10, + "f363a62126a35537f8189343a22660de75e810c6ac004a7d3da65f1c040a839", + LegacyFormattingCode.GREEN), + DRAGON_FRUIT("Dragonfruit", + 1200, + "Halves the points earned in the next turn", + 1, + "3cc761bcb0579763d9b8ab6b7b96fa77eb6d9605a804d838fec39e7b25f95591", + LegacyFormattingCode.LIGHT_PURPLE), + POMEGRANATE("Pomegranate", + 200, + "Grants an extra 50% more points in the next turn", + 4, + "40824d18079042d5769f264f44394b95b9b99ce689688cc10c9eec3f882ccc08", + LegacyFormattingCode.DARK_BLUE), + CHERRY("Cherry", + 200, + "The second cherry grants 300 bonus points", + 2, + "c92b099a62cd2fbf8ada09dec145c75d7fda4dc57b968bea3a8fa11e37aa48b2", + LegacyFormattingCode.DARK_PURPLE), + BOMB("Bomb", + -1, + "Destroys nearby fruit", + 15, + "a76a2811d1e176a07b6d0a657b910f134896ce30850f6e80c7c83732d85381ea", + LegacyFormattingCode.DARK_RED), + RUM("Rum", + -1, + "Stops your dowsing ability for one turn", + 5, + "407b275d28b927b1bf7f6dd9f45fbdad2af8571c54c8f027d1bff6956fbf3c16", + LegacyFormattingCode.YELLOW), + ; + + val textureUrl = "http://textures.minecraft.net/texture/$textureHash" + val itemStack = createSkullItem(UUID.randomUUID(), textureUrl) + .setSkyBlockFirmamentUiId("MINESWEEPER_$name") + + @Bind + fun getIcon() = ModernItemStack.of(itemStack) + + @Bind + fun pieceLabel() = fruitColor.formattingCode + fruitName + + @Bind + fun boardLabel() = "§a$totalPerBoard§7/§rboard" + + @Bind("description") + fun getDescription() = buildString { + append(specialAbility) + if (points >= 0) { + append(" Default points: $points.") + } + } + } + + object TutorialScreen { + @get:Bind("pieces") + val pieces = ObservableList(Piece.entries.toList().reversed()) + + @get:Bind("modes") + val modes = ObservableList(DowsingMode.entries.toList()) + } + + enum class DowsingMode( + val itemType: Item, + @get:Bind("feature") + val feature: String, + @get:Bind("description") + val description: String, + ) { + MINES(Items.IRON_SHOVEL, "Bomb detection", "Tells you how many bombs are near the block"), + ANCHOR(Items.DIAMOND_SHOVEL, "Lowest fruit", "Shows you which block nearby contains the lowest scoring fruit"), + TREASURE(Items.GOLDEN_SHOVEL, "Highest fruit", "Tells you which kind of fruit is the highest scoring nearby"), + ; + + @Bind("itemType") + fun getItemStack() = ModernItemStack.of(ItemStack(itemType)) + + companion object { + val id = SkyblockId("CARNIVAL_SHOVEL") + fun fromItem(itemStack: ItemStack): DowsingMode? { + if (itemStack.skyBlockId != id) return null + return DowsingMode.entries.find { it.itemType == itemStack.item } + } + } + } + + data class BoardPosition( + val x: Int, + val y: Int + ) { + fun toBlockPos() = BlockPos(sandBoxLow.x + x, sandBoxLow.y, sandBoxLow.z + y) + + fun getBlock(world: WorldAccess) = world.getBlockState(toBlockPos()).block + fun isUnopened(world: WorldAccess) = getBlock(world) == Blocks.SAND + fun isOpened(world: WorldAccess) = getBlock(world) == Blocks.SANDSTONE + fun isScorched(world: WorldAccess) = getBlock(world) == Blocks.SANDSTONE_STAIRS + + companion object { + fun fromBlockPos(blockPos: BlockPos): BoardPosition? { + if (blockPos.y != sandBoxLow.y) return null + val x = blockPos.x - sandBoxLow.x + val y = blockPos.z - sandBoxLow.z + if (x < 0 || x >= boardSize.first) return null + if (y < 0 || y >= boardSize.second) return null + return BoardPosition(x, y) + } + } + } + + data class GameState( + val nearbyBombs: MutableMap = mutableMapOf(), + val knownBombPositions: MutableSet = mutableSetOf(), + var lastClickedPosition: BoardPosition? = null, + var lastDowsingMode: DowsingMode? = null, + ) + + var gameState: GameState? = null + val log = DebugLogger("minesweeper") + + @Subscribe + fun onCommand(event: CommandEvent.SubCommand) { + event.subcommand("minesweepertutorial") { + thenExecute { + ScreenUtil.setScreenLater(MoulConfigUtils.loadScreen("carnival/minesweeper_tutorial", + TutorialScreen, + null)) + } + } + } + + @Subscribe + fun onWorldChange(event: WorldReadyEvent) { + gameState = null + } + + @Subscribe + fun onChat(event: ProcessChatEvent) { + if (CarnivalFeatures.TConfig.displayTutorials && event.unformattedString == startGameQuestion) { + MC.sendChat(Text.translatable("firmament.carnival.tutorial.minesweeper").styled { + it.withClickEvent(ClickEvent.RunCommand("/firm minesweepertutorial")) + }) + } + if (!CarnivalFeatures.TConfig.enableBombSolver) { + gameState = null // TODO: replace this which a watchable property + return + } + if (event.unformattedString == gameStartMessage) { + gameState = GameState() + log.log { "Game started" } + } + if (event.unformattedString.trim() == gameEndMessage) { + gameState = null // TODO: add a loot tracker maybe? probably not, i dont think people care + log.log { "Finished game" } + } + val gs = gameState ?: return + bombPattern.useMatch(event.unformattedString) { + val bombCount = group("bombCount").toInt() + log.log { "Marking ${gs.lastClickedPosition} as having $bombCount nearby" } + val pos = gs.lastClickedPosition ?: return + gs.nearbyBombs[pos] = bombCount + } + } + + @Subscribe + fun onMobChange(event: EntityUpdateEvent) { + val gs = gameState ?: return + if (event !is EntityUpdateEvent.TrackedDataUpdate) return + // TODO: listen to state + } + + @Subscribe + fun onBlockClick(event: AttackBlockEvent) { + val gs = gameState ?: return + val boardPosition = BoardPosition.fromBlockPos(event.blockPos) + log.log { "Breaking block at ${event.blockPos} ($boardPosition)" } + gs.lastClickedPosition = boardPosition + gs.lastDowsingMode = DowsingMode.fromItem(event.player.mainHandStack) + } + + @Subscribe + fun onRender(event: WorldRenderLastEvent) { + val gs = gameState ?: return + RenderInWorldContext.renderInWorld(event) { + for ((pos, bombCount) in gs.nearbyBombs) { + this.text(pos.toBlockPos().up().toCenterPos(), Text.literal("§a$bombCount \uD83D\uDCA3")) + } + } + } + + +} diff --git a/src/main/kotlin/features/fixes/CompatibliltyFeatures.kt b/src/main/kotlin/features/fixes/CompatibliltyFeatures.kt new file mode 100644 index 0000000..76f6ed4 --- /dev/null +++ b/src/main/kotlin/features/fixes/CompatibliltyFeatures.kt @@ -0,0 +1,41 @@ +package moe.nea.firmament.features.fixes + +import net.minecraft.particle.ParticleTypes +import net.minecraft.util.math.Vec3d +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ParticleSpawnEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.compatloader.CompatLoader + +object CompatibliltyFeatures : FirmamentFeature { + override val identifier: String + get() = "compatibility" + + object TConfig : ManagedConfig(identifier, Category.INTEGRATIONS) { + val enhancedExplosions by toggle("explosion-enabled") { false } + val explosionSize by integer("explosion-power", 10, 50) { 1 } + } + + override val config: ManagedConfig? + get() = TConfig + + interface ExplosiveApiWrapper { + fun spawnParticle(vec3d: Vec3d, power: Float) + + companion object : CompatLoader(ExplosiveApiWrapper::class.java) + } + + private val explosiveApiWrapper = ExplosiveApiWrapper.singleInstance + + @Subscribe + fun onExplosion(it: ParticleSpawnEvent) { + if (TConfig.enhancedExplosions && + it.particleEffect.type == ParticleTypes.EXPLOSION_EMITTER && + explosiveApiWrapper != null + ) { + it.cancel() + explosiveApiWrapper.spawnParticle(it.position, 2F) + } + } +} diff --git a/src/main/kotlin/features/fixes/Fixes.kt b/src/main/kotlin/features/fixes/Fixes.kt new file mode 100644 index 0000000..0105624 --- /dev/null +++ b/src/main/kotlin/features/fixes/Fixes.kt @@ -0,0 +1,82 @@ +package moe.nea.firmament.features.fixes + +import moe.nea.jarvis.api.Point +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable +import net.minecraft.client.MinecraftClient +import net.minecraft.client.option.KeyBinding +import net.minecraft.text.Text +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HudRenderEvent +import moe.nea.firmament.events.WorldKeyboardEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.tr + +object Fixes : FirmamentFeature { + override val identifier: String + get() = "fixes" + + object TConfig : ManagedConfig(identifier, Category.MISC) { // TODO: split this config + val fixUnsignedPlayerSkins by toggle("player-skins") { true } + var autoSprint by toggle("auto-sprint") { false } + val autoSprintKeyBinding by keyBindingWithDefaultUnbound("auto-sprint-keybinding") + val autoSprintUnderWater by toggle("auto-sprint-underwater") { true } + val autoSprintHud by position("auto-sprint-hud", 80, 10) { Point(0.0, 1.0) } + val peekChat by keyBindingWithDefaultUnbound("peek-chat") + val hidePotionEffects by toggle("hide-mob-effects") { false } + val hidePotionEffectsHud by toggle("hide-potion-effects-hud") { false } + val noHurtCam by toggle("disable-hurt-cam") { false } + val hideSlotHighlights by toggle("hide-slot-highlights") { false } + val hideRecipeBook by toggle("hide-recipe-book") { false } + val hideOffHand by toggle("hide-off-hand") { false } + } + + override val config: ManagedConfig + get() = TConfig + + fun handleIsPressed( + keyBinding: KeyBinding, + cir: CallbackInfoReturnable + ) { + if (keyBinding !== MinecraftClient.getInstance().options.sprintKey) return + if (!TConfig.autoSprint) return + val player = MC.player ?: return + if (player.isSprinting) return + if (!TConfig.autoSprintUnderWater && player.isTouchingWater) return + cir.returnValue = true + } + + @Subscribe + fun onRenderHud(it: HudRenderEvent) { + if (!TConfig.autoSprintKeyBinding.isBound) return + it.context.matrices.push() + TConfig.autoSprintHud.applyTransformations(it.context.matrices) + it.context.drawText( + MC.font, ( + if (MC.player?.isSprinting == true) { + Text.translatable("firmament.fixes.auto-sprint.sprinting") + } else if (TConfig.autoSprint) { + if (!TConfig.autoSprintUnderWater && MC.player?.isTouchingWater == true) + tr("firmament.fixes.auto-sprint.under-water", "In Water") + else + Text.translatable("firmament.fixes.auto-sprint.on") + } else { + Text.translatable("firmament.fixes.auto-sprint.not-sprinting") + } + ), 0, 0, -1, true + ) + it.context.matrices.pop() + } + + @Subscribe + fun onWorldKeyboard(it: WorldKeyboardEvent) { + if (it.matches(TConfig.autoSprintKeyBinding)) { + TConfig.autoSprint = !TConfig.autoSprint + } + } + + fun shouldPeekChat(): Boolean { + return TConfig.peekChat.isPressed(atLeast = true) + } +} diff --git a/src/main/kotlin/features/garden/HideComposterNoises.kt b/src/main/kotlin/features/garden/HideComposterNoises.kt new file mode 100644 index 0000000..69207a9 --- /dev/null +++ b/src/main/kotlin/features/garden/HideComposterNoises.kt @@ -0,0 +1,32 @@ +package moe.nea.firmament.features.garden + +import net.minecraft.entity.passive.WolfSoundVariants +import net.minecraft.sound.SoundEvent +import net.minecraft.sound.SoundEvents +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.SoundReceiveEvent +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.SkyBlockIsland + +object HideComposterNoises { + object TConfig : ManagedConfig("composter", Category.GARDEN) { + val hideComposterNoises by toggle("no-more-noises") { false } + } + + val composterSoundEvents: List = listOf( + SoundEvents.BLOCK_PISTON_EXTEND, + SoundEvents.BLOCK_WATER_AMBIENT, + SoundEvents.ENTITY_CHICKEN_EGG, + SoundEvents.WOLF_SOUNDS[WolfSoundVariants.Type.CLASSIC]!!.growlSound().value(), + ) + + @Subscribe + fun onNoise(event: SoundReceiveEvent) { + if (!TConfig.hideComposterNoises) return + if (SBData.skyblockLocation == SkyBlockIsland.GARDEN) { + if (event.sound.value() in composterSoundEvents) + event.cancel() + } + } +} diff --git a/src/main/kotlin/features/inventory/CraftingOverlay.kt b/src/main/kotlin/features/inventory/CraftingOverlay.kt new file mode 100644 index 0000000..f823086 --- /dev/null +++ b/src/main/kotlin/features/inventory/CraftingOverlay.kt @@ -0,0 +1,83 @@ +package moe.nea.firmament.features.inventory + +import io.github.moulberry.repo.data.NEUCraftingRecipe +import net.minecraft.client.gui.screen.ingame.GenericContainerScreen +import net.minecraft.item.ItemStack +import net.minecraft.util.Formatting +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ScreenChangeEvent +import moe.nea.firmament.events.SlotRenderEvents +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.repo.ExpensiveItemCacheApi +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.skyblockId + +object CraftingOverlay : FirmamentFeature { + + private var screen: GenericContainerScreen? = null + private var recipe: NEUCraftingRecipe? = null + private var useNextScreen = false + private val craftingOverlayIndices = listOf( + 10, 11, 12, + 19, 20, 21, + 28, 29, 30, + ) + val CRAFTING_SCREEN_NAME = "Craft Item" + + fun setOverlay(screen: GenericContainerScreen?, recipe: NEUCraftingRecipe) { + this.screen = screen + if (screen == null) { + useNextScreen = true + } + this.recipe = recipe + } + + @Subscribe + fun onScreenChange(event: ScreenChangeEvent) { + if (useNextScreen && event.new is GenericContainerScreen + && event.new.title?.string == "Craft Item" + ) { + useNextScreen = false + screen = event.new + } + } + + override val identifier: String + get() = "crafting-overlay" + + @OptIn(ExpensiveItemCacheApi::class) + @Subscribe + fun onSlotRender(event: SlotRenderEvents.After) { + val slot = event.slot + val recipe = this.recipe ?: return + if (slot.inventory != screen?.screenHandler?.inventory) return + val recipeIndex = craftingOverlayIndices.indexOf(slot.index) + if (recipeIndex < 0) return + val expectedItem = recipe.inputs[recipeIndex] + val actualStack = slot.stack ?: ItemStack.EMPTY!! + val actualEntry = SBItemStack(actualStack) + if ((actualEntry.skyblockId != expectedItem.skyblockId || actualEntry.getStackSize() < expectedItem.amount) + && expectedItem.amount.toInt() != 0 + ) { + event.context.fill( + event.slot.x, + event.slot.y, + event.slot.x + 16, + event.slot.y + 16, + 0x80FF0000.toInt() + ) + } + if (!slot.hasStack()) { + val itemStack = SBItemStack(expectedItem)?.asImmutableItemStack() ?: return + event.context.drawItem(itemStack, event.slot.x, event.slot.y) + event.context.drawStackOverlay( + MC.font, + itemStack, + event.slot.x, + event.slot.y, + "${Formatting.RED}${expectedItem.amount.toInt()}" + ) + } + } +} diff --git a/src/main/kotlin/features/inventory/ItemHotkeys.kt b/src/main/kotlin/features/inventory/ItemHotkeys.kt new file mode 100644 index 0000000..e826b31 --- /dev/null +++ b/src/main/kotlin/features/inventory/ItemHotkeys.kt @@ -0,0 +1,42 @@ +package moe.nea.firmament.features.inventory + +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HandledScreenKeyPressedEvent +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.repo.ExpensiveItemCacheApi +import moe.nea.firmament.repo.HypixelStaticData +import moe.nea.firmament.repo.ItemCache +import moe.nea.firmament.repo.ItemCache.asItemStack +import moe.nea.firmament.repo.ItemCache.isBroken +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.asBazaarStock +import moe.nea.firmament.util.focusedItemStack +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.skyblock.SBItemUtil.getSearchName + +object ItemHotkeys { + object TConfig : ManagedConfig("item-hotkeys", Category.INVENTORY) { + val openGlobalTradeInterface by keyBindingWithDefaultUnbound("global-trade-interface") + } + + @OptIn(ExpensiveItemCacheApi::class) + @Subscribe + fun onHandledInventoryPress(event: HandledScreenKeyPressedEvent) { + if (!event.matches(TConfig.openGlobalTradeInterface)) { + return + } + var item = event.screen.focusedItemStack ?: return + val skyblockId = item.skyBlockId ?: return + item = RepoManager.getNEUItem(skyblockId)?.asItemStack()?.takeIf { !it.isBroken } ?: item + if (HypixelStaticData.hasBazaarStock(skyblockId.asBazaarStock)) { + MC.sendCommand("bz ${item.getSearchName()}") + } else if (HypixelStaticData.hasAuctionHouseOffers(skyblockId)) { + MC.sendCommand("ahs ${item.getSearchName()}") + } else { + return + } + event.cancel() + } + +} diff --git a/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt b/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt new file mode 100644 index 0000000..fdc378a --- /dev/null +++ b/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt @@ -0,0 +1,63 @@ +package moe.nea.firmament.features.inventory + +import java.awt.Color +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.render.RenderLayer +import net.minecraft.item.ItemStack +import net.minecraft.util.Formatting +import net.minecraft.util.Identifier +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HotbarItemRenderEvent +import moe.nea.firmament.events.SlotRenderEvents +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.collections.lastNotNullOfOrNull +import moe.nea.firmament.util.collections.memoizeIdentity +import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.skyblock.Rarity +import moe.nea.firmament.util.unformattedString + +object ItemRarityCosmetics : FirmamentFeature { + override val identifier: String + get() = "item-rarity-cosmetics" + + object TConfig : ManagedConfig(identifier, Category.INVENTORY) { + val showItemRarityBackground by toggle("background") { false } + val showItemRarityInHotbar by toggle("background-hotbar") { false } + } + + override val config: ManagedConfig + get() = TConfig + + private val rarityToColor = Rarity.colourMap.mapValues { + val c = Color(it.value.colorValue!!) + c.rgb + } + + fun drawItemStackRarity(drawContext: DrawContext, x: Int, y: Int, item: ItemStack) { + val rarity = Rarity.fromItem(item) ?: return + val rgb = rarityToColor[rarity] ?: 0xFF00FF80.toInt() + drawContext.drawGuiTexture( + RenderLayer::getGuiTextured, + Identifier.of("firmament:item_rarity_background"), + x, y, + 16, 16, + rgb + ) + } + + + @Subscribe + fun onRenderSlot(it: SlotRenderEvents.Before) { + if (!TConfig.showItemRarityBackground) return + val stack = it.slot.stack ?: return + drawItemStackRarity(it.context, it.slot.x, it.slot.y, stack) + } + + @Subscribe + fun onRenderHotbarItem(it: HotbarItemRenderEvent) { + if (!TConfig.showItemRarityInHotbar) return + val stack = it.item + drawItemStackRarity(it.context, it.x, it.y, stack) + } +} diff --git a/src/main/kotlin/features/inventory/JunkHighlighter.kt b/src/main/kotlin/features/inventory/JunkHighlighter.kt new file mode 100644 index 0000000..80fd99d --- /dev/null +++ b/src/main/kotlin/features/inventory/JunkHighlighter.kt @@ -0,0 +1,29 @@ +package moe.nea.firmament.features.inventory + +import org.lwjgl.glfw.GLFW +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.SlotRenderEvents +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.skyblock.SBItemUtil.getSearchName +import moe.nea.firmament.util.useMatch + +object JunkHighlighter : FirmamentFeature { + override val identifier: String + get() = "junk-highlighter" + + object TConfig : ManagedConfig(identifier, Category.INVENTORY) { + val junkRegex by string("regex") { "" } + val highlightBind by keyBinding("highlight") { GLFW.GLFW_KEY_LEFT_CONTROL } + } + + @Subscribe + fun onDrawSlot(event: SlotRenderEvents.After) { + if(!TConfig.highlightBind.isPressed() || TConfig.junkRegex.isEmpty()) return + val junkRegex = TConfig.junkRegex.toPattern() + val slot = event.slot + junkRegex.useMatch(slot.stack.getSearchName()) { + event.context.fill(slot.x, slot.y, slot.x + 16, slot.y + 16, 0xffff0000.toInt()) + } + } +} diff --git a/src/main/kotlin/features/inventory/PetFeatures.kt b/src/main/kotlin/features/inventory/PetFeatures.kt new file mode 100644 index 0000000..9393b03 --- /dev/null +++ b/src/main/kotlin/features/inventory/PetFeatures.kt @@ -0,0 +1,86 @@ +package moe.nea.firmament.features.inventory + +import moe.nea.jarvis.api.Point +import net.minecraft.item.ItemStack +import net.minecraft.text.Text +import net.minecraft.util.Formatting +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HudRenderEvent +import moe.nea.firmament.events.SlotRenderEvents +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.FirmFormatters.formatPercent +import moe.nea.firmament.util.FirmFormatters.shortFormat +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.petData +import moe.nea.firmament.util.render.drawGuiTexture +import moe.nea.firmament.util.skyblock.Rarity +import moe.nea.firmament.util.titleCase +import moe.nea.firmament.util.useMatch +import moe.nea.firmament.util.withColor + +object PetFeatures : FirmamentFeature { + override val identifier: String + get() = "pets" + + override val config: ManagedConfig? + get() = TConfig + + object TConfig : ManagedConfig(identifier, Category.INVENTORY) { + val highlightEquippedPet by toggle("highlight-pet") { true } + var petOverlay by toggle("pet-overlay") { false } + val petOverlayHud by position("pet-overlay-hud", 80, 10) { Point(0.5, 1.0) } + } + + val petMenuTitle = "Pets(?: \\([0-9]+/[0-9]+\\))?".toPattern() + var petItemStack: ItemStack? = null + + @Subscribe + fun onSlotRender(event: SlotRenderEvents.Before) { + if (!TConfig.highlightEquippedPet) return + val stack = event.slot.stack + if (stack.petData?.active == true) + petMenuTitle.useMatch(MC.screenName ?: return) { + petItemStack = stack + event.context.drawGuiTexture( + Firmament.identifier("selected_pet_background"), + event.slot.x, event.slot.y, 16, 16, + ) + } + } + + @Subscribe + fun onRenderHud(it: HudRenderEvent) { + if (!TConfig.petOverlay || !SBData.isOnSkyblock) return + val itemStack = petItemStack ?: return + val petData = petItemStack?.petData ?: return + val rarity = Rarity.fromNeuRepo(petData.tier) + val rarityCode = Rarity.colourMap[rarity] ?: Formatting.WHITE + val xp = petData.level + val petType = titleCase(petData.type) + val heldItem = petData.heldItem?.let { item -> "Held Item: ${titleCase(item)}" } + + it.context.matrices.push() + TConfig.petOverlayHud.applyTransformations(it.context.matrices) + + val lines = mutableListOf() + it.context.matrices.push() + it.context.matrices.translate(-0.5, -0.5, 0.0) + it.context.matrices.scale(2f, 2f, 1f) + it.context.drawItem(itemStack, 0, 0) + it.context.matrices.pop() + + lines.add(Text.literal("[Lvl ${xp.currentLevel}] ").append(Text.literal(petType).withColor(rarityCode))) + if (heldItem != null) lines.add(Text.literal(heldItem)) + if (xp.currentLevel != xp.maxLevel) lines.add(Text.literal("Required L${xp.currentLevel + 1}: ${shortFormat(xp.expInCurrentLevel.toDouble())}/${shortFormat(xp.expRequiredForNextLevel.toDouble())} (${formatPercent(xp.percentageToNextLevel.toDouble())})")) + lines.add(Text.literal("Required L100: ${shortFormat(xp.expTotal.toDouble())}/${shortFormat(xp.expRequiredForMaxLevel.toDouble())} (${formatPercent(xp.percentageToMaxLevel.toDouble())})")) + + for ((index, line) in lines.withIndex()) { + it.context.drawText(MC.font, line.copy().withColor(Formatting.GRAY), 36, MC.font.fontHeight * index, -1, true) + } + + it.context.matrices.pop() + } +} diff --git a/src/main/kotlin/features/inventory/PriceData.kt b/src/main/kotlin/features/inventory/PriceData.kt new file mode 100644 index 0000000..241fb43 --- /dev/null +++ b/src/main/kotlin/features/inventory/PriceData.kt @@ -0,0 +1,120 @@ +package moe.nea.firmament.features.inventory + +import org.lwjgl.glfw.GLFW +import net.minecraft.text.Text +import net.minecraft.util.StringIdentifiable +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ItemTooltipEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.repo.HypixelStaticData +import moe.nea.firmament.util.FirmFormatters.formatCommas +import moe.nea.firmament.util.asBazaarStock +import moe.nea.firmament.util.bold +import moe.nea.firmament.util.darkGrey +import moe.nea.firmament.util.gold +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.tr +import moe.nea.firmament.util.yellow + +object PriceData : FirmamentFeature { + override val identifier: String + get() = "price-data" + + object TConfig : ManagedConfig(identifier, Category.INVENTORY) { + val tooltipEnabled by toggle("enable-always") { true } + val enableKeybinding by keyBindingWithDefaultUnbound("enable-keybind") + val stackSizeKey by keyBinding("stack-size-keybind") { GLFW.GLFW_KEY_LEFT_SHIFT } + val avgLowestBin by choice( + "avg-lowest-bin-days", + ) { + AvgLowestBin.THREEDAYAVGLOWESTBIN + } + } + + enum class AvgLowestBin : StringIdentifiable { + OFF, + ONEDAYAVGLOWESTBIN, + THREEDAYAVGLOWESTBIN, + SEVENDAYAVGLOWESTBIN; + + override fun asString(): String { + return name + } + } + + override val config get() = TConfig + + fun formatPrice(label: Text, price: Double): Text { + return Text.literal("") + .yellow() + .bold() + .append(label) + .append(": ") + .append( + Text.literal(formatCommas(price, fractionalDigits = 1)) + .append(if (price != 1.0) " coins" else " coin") + .gold() + .bold() + ) + } + + @Subscribe + fun onItemTooltip(it: ItemTooltipEvent) { + if (!TConfig.tooltipEnabled) return + if (TConfig.enableKeybinding.isBound && !TConfig.enableKeybinding.isPressed()) return + val sbId = it.stack.skyBlockId + val stackSize = it.stack.count + val isShowingStack = TConfig.stackSizeKey.isPressed() + val multiplier = if (isShowingStack) stackSize else 1 + val multiplierText = + if (isShowingStack) + tr("firmament.tooltip.multiply", "Showing prices for x${stackSize}").darkGrey() + else + tr( + "firmament.tooltip.multiply.hint", + "[${TConfig.stackSizeKey.format()}] to show x${stackSize}" + ).darkGrey() + val bazaarData = HypixelStaticData.bazaarData[sbId?.asBazaarStock] + val lowestBin = HypixelStaticData.lowestBin[sbId] + val avgBinValue: Double? = when (TConfig.avgLowestBin) { + AvgLowestBin.ONEDAYAVGLOWESTBIN -> HypixelStaticData.avg1dlowestBin[sbId] + AvgLowestBin.THREEDAYAVGLOWESTBIN -> HypixelStaticData.avg3dlowestBin[sbId] + AvgLowestBin.SEVENDAYAVGLOWESTBIN -> HypixelStaticData.avg7dlowestBin[sbId] + AvgLowestBin.OFF -> null + } + if (bazaarData != null) { + it.lines.add(Text.literal("")) + it.lines.add(multiplierText) + it.lines.add( + formatPrice( + tr("firmament.tooltip.bazaar.buy-order", "Bazaar Buy Order"), + bazaarData.quickStatus.sellPrice * multiplier + ) + ) + it.lines.add( + formatPrice( + tr("firmament.tooltip.bazaar.sell-order", "Bazaar Sell Order"), + bazaarData.quickStatus.buyPrice * multiplier + ) + ) + } else if (lowestBin != null) { + it.lines.add(Text.literal("")) + it.lines.add(multiplierText) + it.lines.add( + formatPrice( + tr("firmament.tooltip.ah.lowestbin", "Lowest BIN"), + lowestBin * multiplier + ) + ) + if (avgBinValue != null) { + it.lines.add( + formatPrice( + tr("firmament.tooltip.ah.avg-lowestbin", "AVG Lowest BIN"), + avgBinValue * multiplier + ) + ) + } + } + } +} diff --git a/src/main/kotlin/features/inventory/REIDependencyWarner.kt b/src/main/kotlin/features/inventory/REIDependencyWarner.kt new file mode 100644 index 0000000..476759a --- /dev/null +++ b/src/main/kotlin/features/inventory/REIDependencyWarner.kt @@ -0,0 +1,85 @@ +package moe.nea.firmament.features.inventory + +import java.net.URI +import net.fabricmc.loader.api.FabricLoader +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.seconds +import net.minecraft.SharedConstants +import net.minecraft.text.ClickEvent +import net.minecraft.text.Text +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.events.SkyblockServerUpdateEvent +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.aqua +import moe.nea.firmament.util.bold +import moe.nea.firmament.util.clickCommand +import moe.nea.firmament.util.grey +import moe.nea.firmament.util.lime +import moe.nea.firmament.util.red +import moe.nea.firmament.util.white +import moe.nea.firmament.util.yellow + +object REIDependencyWarner { + val reiModId = "roughlyenoughitems" + val hasREI = FabricLoader.getInstance().isModLoaded(reiModId) + var sentWarning = false + + fun modrinthLink(slug: String) = + "https://modrinth.com/mod/$slug/versions?g=${SharedConstants.getGameVersion().name}&l=fabric" + + fun downloadButton(modName: String, modId: String, slug: String): Text { + val alreadyDownloaded = FabricLoader.getInstance().isModLoaded(modId) + return Text.literal(" - ") + .white() + .append(Text.literal("[").aqua()) + .append(Text.translatable("firmament.download", modName) + .styled { it.withClickEvent(ClickEvent.OpenUrl(URI (modrinthLink(slug)))) } + .yellow() + .also { + if (alreadyDownloaded) + it.append(Text.translatable("firmament.download.already", modName) + .lime()) + }) + .append(Text.literal("]").aqua()) + } + + @Subscribe + fun checkREIDependency(event: SkyblockServerUpdateEvent) { + if (!SBData.isOnSkyblock) return + if (!RepoManager.Config.warnForMissingItemListMod) return + if (hasREI) return + if (sentWarning) return + sentWarning = true + Firmament.coroutineScope.launch { + delay(2.seconds) + // TODO: should we offer an automatic install that actually downloads the JARs and places them into the mod folder? + MC.sendChat( + Text.translatable("firmament.reiwarning").red().bold().append("\n") + .append(downloadButton("RoughlyEnoughItems", reiModId, "rei")).append("\n") + .append(downloadButton("Architectury API", "architectury", "architectury-api")).append("\n") + .append(downloadButton("Cloth Config API", "cloth-config", "cloth-config")).append("\n") + .append(Text.translatable("firmament.reiwarning.disable") + .clickCommand("/firm disablereiwarning") + .grey()) + ) + } + } + + @Subscribe + fun onSubcommand(event: CommandEvent.SubCommand) { + if (hasREI) return + event.subcommand("disablereiwarning") { + thenExecute { + RepoManager.Config.warnForMissingItemListMod = false + RepoManager.Config.save() + MC.sendChat(Text.translatable("firmament.reiwarning.disabled").yellow()) + } + } + } +} diff --git a/src/main/kotlin/features/inventory/SaveCursorPosition.kt b/src/main/kotlin/features/inventory/SaveCursorPosition.kt new file mode 100644 index 0000000..c47867b --- /dev/null +++ b/src/main/kotlin/features/inventory/SaveCursorPosition.kt @@ -0,0 +1,66 @@ + + +package moe.nea.firmament.features.inventory + +import kotlin.math.absoluteValue +import kotlin.time.Duration.Companion.milliseconds +import net.minecraft.client.util.InputUtil +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.assertNotNullOr + +object SaveCursorPosition : FirmamentFeature { + override val identifier: String + get() = "save-cursor-position" + + object TConfig : ManagedConfig(identifier, Category.INVENTORY) { + val enable by toggle("enable") { true } + val tolerance by duration("tolerance", 10.milliseconds, 5000.milliseconds) { 500.milliseconds } + } + + override val config: TConfig + get() = TConfig + + var savedPositionedP1: Pair? = null + var savedPosition: SavedPosition? = null + + data class SavedPosition( + val middle: Pair, + val cursor: Pair, + val savedAt: TimeMark = TimeMark.now() + ) + + @JvmStatic + fun saveCursorOriginal(positionedX: Double, positionedY: Double) { + savedPositionedP1 = Pair(positionedX, positionedY) + } + + @JvmStatic + fun loadCursor(middleX: Double, middleY: Double): Pair? { + if (!TConfig.enable) return null + val lastPosition = savedPosition?.takeIf { it.savedAt.passedTime() < TConfig.tolerance } + savedPosition = null + if (lastPosition != null && + (lastPosition.middle.first - middleX).absoluteValue < 1 && + (lastPosition.middle.second - middleY).absoluteValue < 1 + ) { + InputUtil.setCursorParameters( + MC.window.handle, + InputUtil.GLFW_CURSOR_NORMAL, + lastPosition.cursor.first, + lastPosition.cursor.second + ) + return lastPosition.cursor + } + return null + } + + @JvmStatic + fun saveCursorMiddle(middleX: Double, middleY: Double) { + if (!TConfig.enable) return + val cursorPos = assertNotNullOr(savedPositionedP1) { return } + savedPosition = SavedPosition(Pair(middleX, middleY), cursorPos) + } +} diff --git a/src/main/kotlin/features/inventory/SlotLocking.kt b/src/main/kotlin/features/inventory/SlotLocking.kt new file mode 100644 index 0000000..0a3f01b --- /dev/null +++ b/src/main/kotlin/features/inventory/SlotLocking.kt @@ -0,0 +1,504 @@ +@file:UseSerializers(DashlessUUIDSerializer::class) + +package moe.nea.firmament.features.inventory + +import java.util.UUID +import org.lwjgl.glfw.GLFW +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.int +import kotlinx.serialization.serializer +import net.minecraft.client.gui.screen.ingame.HandledScreen +import net.minecraft.client.render.RenderLayer +import net.minecraft.entity.player.PlayerInventory +import net.minecraft.item.ItemStack +import net.minecraft.screen.GenericContainerScreenHandler +import net.minecraft.screen.slot.Slot +import net.minecraft.screen.slot.SlotActionType +import net.minecraft.util.Identifier +import net.minecraft.util.StringIdentifiable +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.FeaturesInitializedEvent +import moe.nea.firmament.events.HandledScreenForegroundEvent +import moe.nea.firmament.events.HandledScreenKeyPressedEvent +import moe.nea.firmament.events.HandledScreenKeyReleasedEvent +import moe.nea.firmament.events.IsSlotProtectedEvent +import moe.nea.firmament.events.ScreenChangeEvent +import moe.nea.firmament.events.SlotRenderEvents +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.keybindings.SavedKeyBinding +import moe.nea.firmament.mixins.accessor.AccessorHandledScreen +import moe.nea.firmament.util.CommonSoundEffects +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.SkyBlockIsland +import moe.nea.firmament.util.data.ProfileSpecificDataHolder +import moe.nea.firmament.util.extraAttributes +import moe.nea.firmament.util.json.DashlessUUIDSerializer +import moe.nea.firmament.util.lime +import moe.nea.firmament.util.mc.ScreenUtil.getSlotByIndex +import moe.nea.firmament.util.mc.SlotUtils.swapWithHotBar +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.red +import moe.nea.firmament.util.render.drawLine +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.skyblock.DungeonUtil +import moe.nea.firmament.util.skyblock.SkyBlockItems +import moe.nea.firmament.util.skyblockUUID +import moe.nea.firmament.util.tr +import moe.nea.firmament.util.unformattedString + +object SlotLocking : FirmamentFeature { + override val identifier: String + get() = "slot-locking" + + @Serializable + data class Data( + val lockedSlots: MutableSet = mutableSetOf(), + val lockedSlotsRift: MutableSet = mutableSetOf(), + val lockedUUIDs: MutableSet = mutableSetOf(), + val boundSlots: BoundSlots = BoundSlots() + ) + + @Serializable + data class BoundSlot( + val hotbar: Int, + val inventory: Int, + ) + + @Serializable(with = BoundSlots.Serializer::class) + data class BoundSlots( + val pairs: MutableSet = mutableSetOf() + ) { + fun findMatchingSlots(index: Int): List { + return pairs.filter { it.hotbar == index || it.inventory == index } + } + + fun removeDuplicateForInventory(index: Int) { + pairs.removeIf { it.inventory == index } + } + + fun removeAllInvolving(index: Int): Boolean { + return pairs.removeIf { it.inventory == index || it.hotbar == index } + } + + fun insert(hotbar: Int, inventory: Int) { + if (!TConfig.allowMultiBinding) { + removeAllInvolving(hotbar) + removeAllInvolving(inventory) + } + pairs.add(BoundSlot(hotbar, inventory)) + } + + object Serializer : KSerializer { + override val descriptor: SerialDescriptor + get() = serializer().descriptor + + override fun serialize( + encoder: Encoder, + value: BoundSlots + ) { + serializer>() + .serialize(encoder, value.pairs) + } + + override fun deserialize(decoder: Decoder): BoundSlots { + decoder as JsonDecoder + val json = decoder.decodeJsonElement() + if (json is JsonObject) { + return BoundSlots(json.entries.map { + BoundSlot(it.key.toInt(), (it.value as JsonPrimitive).int) + }.toMutableSet()) + } + return BoundSlots(decoder.json.decodeFromJsonElement(serializer>(), json)) + + } + } + } + + + object TConfig : ManagedConfig(identifier, Category.INVENTORY) { + val lockSlot by keyBinding("lock") { GLFW.GLFW_KEY_L } + val lockUUID by keyBindingWithOutDefaultModifiers("lock-uuid") { + SavedKeyBinding(GLFW.GLFW_KEY_L, shift = true) + } + val slotBind by keyBinding("bind") { GLFW.GLFW_KEY_L } + val slotBindRequireShift by toggle("require-quick-move") { true } + val slotRenderLines by choice("bind-render") { SlotRenderLinesMode.ONLY_BOXES } + val allowMultiBinding by toggle("multi-bind") { true } // TODO: filter based on this option + val protectAllHuntingBoxes by toggle("hunting-box") { false } + val allowDroppingInDungeons by toggle("drop-in-dungeons") { true } + } + + enum class SlotRenderLinesMode : StringIdentifiable { + EVERYTHING, + ONLY_BOXES, + NOTHING; + + override fun asString(): String { + return name + } + } + + override val config: TConfig + get() = TConfig + + object DConfig : ProfileSpecificDataHolder(serializer(), "locked-slots", ::Data) + + val lockedUUIDs get() = DConfig.data?.lockedUUIDs + + val lockedSlots + get() = when (SBData.skyblockLocation) { + SkyBlockIsland.RIFT -> DConfig.data?.lockedSlotsRift + null -> null + else -> DConfig.data?.lockedSlots + } + + fun isSalvageScreen(screen: HandledScreen<*>?): Boolean { + if (screen == null) return false + return screen.title.unformattedString.contains("Salvage Item") + } + + fun isTradeScreen(screen: HandledScreen<*>?): Boolean { + if (screen == null) return false + val handler = screen.screenHandler as? GenericContainerScreenHandler ?: return false + if (handler.inventory.size() < 9) return false + val middlePane = handler.inventory.getStack(handler.inventory.size() - 5) + if (middlePane == null) return false + return middlePane.displayNameAccordingToNbt?.unformattedString == "⇦ Your stuff" + } + + fun isNpcShop(screen: HandledScreen<*>?): Boolean { + if (screen == null) return false + val handler = screen.screenHandler as? GenericContainerScreenHandler ?: return false + if (handler.inventory.size() < 9) return false + val sellItem = handler.inventory.getStack(handler.inventory.size() - 5) + if (sellItem == null) return false + if (sellItem.displayNameAccordingToNbt.unformattedString == "Sell Item") return true + val lore = sellItem.loreAccordingToNbt + return (lore.lastOrNull() ?: return false).unformattedString == "Click to buyback!" + } + + @Subscribe + fun onSalvageProtect(event: IsSlotProtectedEvent) { + if (event.slot == null) return + if (!event.slot.hasStack()) return + if (event.slot.stack.displayNameAccordingToNbt.unformattedString != "Salvage Items") return + val inv = event.slot.inventory + var anyBlocked = false + for (i in 0 until event.slot.index) { + val stack = inv.getStack(i) + if (IsSlotProtectedEvent.shouldBlockInteraction( + null, + SlotActionType.THROW, + IsSlotProtectedEvent.MoveOrigin.SALVAGE, + stack + ) + ) + anyBlocked = true + } + if (anyBlocked) { + event.protectSilent() + } + } + + @Subscribe + fun onProtectUuidItems(event: IsSlotProtectedEvent) { + val doesNotDeleteItem = event.actionType == SlotActionType.SWAP + || event.actionType == SlotActionType.PICKUP + || event.actionType == SlotActionType.QUICK_MOVE + || event.actionType == SlotActionType.QUICK_CRAFT + || event.actionType == SlotActionType.CLONE + || event.actionType == SlotActionType.PICKUP_ALL + val isSellOrTradeScreen = + isNpcShop(MC.handledScreen) || isTradeScreen(MC.handledScreen) || isSalvageScreen(MC.handledScreen) + if ((!isSellOrTradeScreen || event.slot?.inventory !is PlayerInventory) + && doesNotDeleteItem + ) return + val stack = event.itemStack ?: return + if (TConfig.protectAllHuntingBoxes && (stack.isHuntingBox())) { + event.protect() + return + } + val uuid = stack.skyblockUUID ?: return + if (uuid in (lockedUUIDs ?: return)) { + event.protect() + } + } + + fun ItemStack.isHuntingBox(): Boolean { + return skyBlockId == SkyBlockItems.HUNTING_TOOLKIT || extraAttributes.get("tool_kit") != null + } + + @Subscribe + fun onProtectSlot(it: IsSlotProtectedEvent) { + if (it.slot != null && it.slot.inventory is PlayerInventory && it.slot.index in (lockedSlots ?: setOf())) { + it.protect() + } + } + + @Subscribe + fun onEvent(event: FeaturesInitializedEvent) { + IsSlotProtectedEvent.subscribe(receivesCancelled = true, "SlotLocking:unlockInDungeons") { + if (it.isProtected + && it.origin == IsSlotProtectedEvent.MoveOrigin.DROP_FROM_HOTBAR + && DungeonUtil.isInActiveDungeon + && TConfig.allowDroppingInDungeons + ) { + it.isProtected = false + } + } + } + + @Subscribe + fun onQuickMoveBoundSlot(it: IsSlotProtectedEvent) { + val boundSlots = DConfig.data?.boundSlots ?: BoundSlots() + val isValidAction = + it.actionType == SlotActionType.QUICK_MOVE || (it.actionType == SlotActionType.PICKUP && !TConfig.slotBindRequireShift) + if (!isValidAction) return + val handler = MC.handledScreen?.screenHandler ?: return + val slot = it.slot + if (slot != null && it.slot.inventory is PlayerInventory) { + val matchingSlots = boundSlots.findMatchingSlots(slot.index) + if (matchingSlots.isEmpty()) return + it.protectSilent() + val boundSlot = matchingSlots.singleOrNull() ?: return + val inventorySlot = MC.handledScreen?.getSlotByIndex(boundSlot.inventory, true) + inventorySlot?.swapWithHotBar(handler, boundSlot.hotbar) + } + } + + @Subscribe + fun onLockUUID(it: HandledScreenKeyPressedEvent) { + if (!it.matches(TConfig.lockUUID)) return + val inventory = MC.handledScreen ?: return + inventory as AccessorHandledScreen + + val slot = inventory.focusedSlot_Firmament ?: return + val stack = slot.stack ?: return + if (stack.isHuntingBox()) { + MC.sendChat( + tr( + "firmament.slot-locking.hunting-box-unbindable-hint", + "The hunting box cannot be UUID bound reliably. It changes its own UUID frequently when switching tools. " + ).red().append( + tr( + "firmament.slot-locking.hunting-box-unbindable-hint.solution", + "Use the Firmament config option for locking all hunting boxes instead." + ).lime() + ) + ) + CommonSoundEffects.playFailure() + return + } + val uuid = stack.skyblockUUID ?: return + val lockedUUIDs = lockedUUIDs ?: return + if (uuid in lockedUUIDs) { + lockedUUIDs.remove(uuid) + } else { + lockedUUIDs.add(uuid) + } + DConfig.markDirty() + CommonSoundEffects.playSuccess() + it.cancel() + } + + + @Subscribe + fun onLockSlotKeyRelease(it: HandledScreenKeyReleasedEvent) { + val inventory = MC.handledScreen ?: return + inventory as AccessorHandledScreen + val slot = inventory.focusedSlot_Firmament + val storedSlot = storedLockingSlot ?: return + + if (it.matches(TConfig.slotBind) && slot != storedSlot && slot != null && slot.isHotbar() != storedSlot.isHotbar()) { + storedLockingSlot = null + val hotBarSlot = if (slot.isHotbar()) slot else storedSlot + val invSlot = if (slot.isHotbar()) storedSlot else slot + val boundSlots = DConfig.data?.boundSlots ?: return + lockedSlots?.remove(hotBarSlot.index) + lockedSlots?.remove(invSlot.index) + boundSlots.removeDuplicateForInventory(invSlot.index) + boundSlots.insert(hotBarSlot.index, invSlot.index) + DConfig.markDirty() + CommonSoundEffects.playSuccess() + return + } + if (it.matches(TConfig.lockSlot) && slot == storedSlot) { + storedLockingSlot = null + toggleSlotLock(slot) + return + } + if (it.matches(TConfig.slotBind)) { + storedLockingSlot = null + val boundSlots = DConfig.data?.boundSlots ?: return + if (slot != null) + boundSlots.removeAllInvolving(slot.index) + } + } + + @Subscribe + fun onRenderAllBoundSlots(event: HandledScreenForegroundEvent) { + val boundSlots = DConfig.data?.boundSlots ?: return + fun findByIndex(index: Int) = event.screen.getSlotByIndex(index, true) + val accScreen = event.screen as AccessorHandledScreen + val sx = accScreen.x_Firmament + val sy = accScreen.y_Firmament + val highlitSlots = mutableSetOf() + for (it in boundSlots.pairs) { + val hotbarSlot = findByIndex(it.hotbar) ?: continue + val inventorySlot = findByIndex(it.inventory) ?: continue + + val (hotX, hotY) = hotbarSlot.lineCenter() + val (invX, invY) = inventorySlot.lineCenter() + val anyHovered = accScreen.focusedSlot_Firmament === hotbarSlot + || accScreen.focusedSlot_Firmament === inventorySlot + if (!anyHovered && TConfig.slotRenderLines == SlotRenderLinesMode.NOTHING) + continue + if (anyHovered) { + highlitSlots.add(hotbarSlot) + highlitSlots.add(inventorySlot) + } + fun color(highlit: Boolean) = + if (highlit) + me.shedaniel.math.Color.ofOpaque(0x00FF00) + else + me.shedaniel.math.Color.ofTransparent(0xc0a0f000.toInt()) + if (TConfig.slotRenderLines == SlotRenderLinesMode.EVERYTHING || anyHovered) + event.context.drawLine( + invX + sx, invY + sy, + hotX + sx, hotY + sy, + color(anyHovered) + ) + event.context.drawBorder( + hotbarSlot.x + sx, + hotbarSlot.y + sy, + 16, 16, color(hotbarSlot in highlitSlots).color + ) + event.context.drawBorder( + inventorySlot.x + sx, + inventorySlot.y + sy, + 16, 16, color(inventorySlot in highlitSlots).color + ) + } + } + + @Subscribe + fun onRenderCurrentDraggingSlot(event: HandledScreenForegroundEvent) { + val draggingSlot = storedLockingSlot ?: return + val accScreen = event.screen as AccessorHandledScreen + val hoveredSlot = accScreen.focusedSlot_Firmament + ?.takeIf { it.inventory is PlayerInventory } + ?.takeIf { it == draggingSlot || it.isHotbar() != draggingSlot.isHotbar() } + val sx = accScreen.x_Firmament + val sy = accScreen.y_Firmament + val (borderX, borderY) = draggingSlot.lineCenter() + event.context.drawBorder(draggingSlot.x + sx, draggingSlot.y + sy, 16, 16, 0xFF00FF00u.toInt()) + if (hoveredSlot == null) { + event.context.drawLine( + borderX + sx, borderY + sy, + event.mouseX, event.mouseY, + me.shedaniel.math.Color.ofOpaque(0x00FF00) + ) + } else if (hoveredSlot != draggingSlot) { + val (hovX, hovY) = hoveredSlot.lineCenter() + event.context.drawLine( + borderX + sx, borderY + sy, + hovX + sx, hovY + sy, + me.shedaniel.math.Color.ofOpaque(0x00FF00) + ) + event.context.drawBorder( + hoveredSlot.x + sx, + hoveredSlot.y + sy, + 16, 16, 0xFF00FF00u.toInt() + ) + } + } + + fun Slot.lineCenter(): Pair { + return if (isHotbar()) { + x + 9 to y + } else { + x + 9 to y + 17 + } + } + + + fun Slot.isHotbar(): Boolean { + return index < 9 + } + + @Subscribe + fun onScreenChange(event: ScreenChangeEvent) { + storedLockingSlot = null + } + + var storedLockingSlot: Slot? = null + + fun toggleSlotLock(slot: Slot) { + val lockedSlots = lockedSlots ?: return + val boundSlots = DConfig.data?.boundSlots ?: BoundSlots() + if (slot.inventory is PlayerInventory) { + if (boundSlots.removeAllInvolving(slot.index)) { + // intentionally do nothing + } else if (slot.index in lockedSlots) { + lockedSlots.remove(slot.index) + } else { + lockedSlots.add(slot.index) + } + DConfig.markDirty() + CommonSoundEffects.playSuccess() + } + } + + @Subscribe + fun onLockSlot(it: HandledScreenKeyPressedEvent) { + val inventory = MC.handledScreen ?: return + inventory as AccessorHandledScreen + + val slot = inventory.focusedSlot_Firmament ?: return + if (slot.inventory !is PlayerInventory) return + if (it.matches(TConfig.slotBind)) { + storedLockingSlot = storedLockingSlot ?: slot + return + } + if (!it.matches(TConfig.lockSlot)) { + return + } + toggleSlotLock(slot) + } + + @Subscribe + fun onRenderSlotOverlay(it: SlotRenderEvents.After) { + val isSlotLocked = it.slot.inventory is PlayerInventory && it.slot.index in (lockedSlots ?: setOf()) + val isUUIDLocked = (it.slot.stack?.skyblockUUID) in (lockedUUIDs ?: setOf()) + if (isSlotLocked || isUUIDLocked) { + it.context.drawGuiTexture( + RenderLayer::getGuiTexturedOverlay, + when { + isSlotLocked -> + (Identifier.of("firmament:slot_locked")) + + isUUIDLocked -> + (Identifier.of("firmament:uuid_locked")) + + else -> + error("unreachable") + }, + it.slot.x, it.slot.y, + 16, 16, + -1 + ) + } + } +} diff --git a/src/main/kotlin/features/inventory/TimerInLore.kt b/src/main/kotlin/features/inventory/TimerInLore.kt new file mode 100644 index 0000000..e939404 --- /dev/null +++ b/src/main/kotlin/features/inventory/TimerInLore.kt @@ -0,0 +1,150 @@ +package moe.nea.firmament.features.inventory + +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.time.format.DateTimeFormatterBuilder +import java.time.format.FormatStyle +import java.time.format.TextStyle +import java.time.temporal.ChronoField +import net.minecraft.text.Text +import net.minecraft.util.StringIdentifiable +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ItemTooltipEvent +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.aqua +import moe.nea.firmament.util.grey +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.timestamp +import moe.nea.firmament.util.tr +import moe.nea.firmament.util.unformattedString + +object TimerInLore { + object TConfig : ManagedConfig("lore-timers", Category.INVENTORY) { + val showTimers by toggle("show") { true } + val showCreationTimestamp by toggle("show-creation") { true } + val timerFormat by choice("format") { TimerFormat.SOCIALIST } + } + + enum class TimerFormat(val formatter: DateTimeFormatter) : StringIdentifiable { + RFC(DateTimeFormatter.RFC_1123_DATE_TIME), + LOCAL(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)), + SOCIALIST( + { + appendText(ChronoField.DAY_OF_WEEK, TextStyle.SHORT) + appendLiteral(" ") + appendValue(ChronoField.DAY_OF_MONTH, 2) + appendLiteral(".") + appendValue(ChronoField.MONTH_OF_YEAR, 2) + appendLiteral(".") + appendValue(ChronoField.YEAR, 4) + appendLiteral(" ") + appendValue(ChronoField.HOUR_OF_DAY, 2) + appendLiteral(":") + appendValue(ChronoField.MINUTE_OF_HOUR, 2) + appendLiteral(":") + appendValue(ChronoField.SECOND_OF_MINUTE, 2) + }), + AMERICAN("EEEE, MMM d h:mm a yyyy"), + RFCPrecise(DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss.SSS Z")), + ; + + constructor(block: DateTimeFormatterBuilder.() -> Unit) + : this(DateTimeFormatterBuilder().also(block).toFormatter()) + + constructor(format: String) : this(DateTimeFormatter.ofPattern(format)) + + override fun asString(): String { + return name + } + } + + enum class CountdownTypes( + val match: String, + val label: String, // TODO: convert to a string + val isRelative: Boolean = false, + ) { + STARTING("Starting in:", "Starts at"), + STARTS("Starts in:", "Starts at"), + INTEREST("Interest in:", "Interest at"), + UNTILINTEREST("Until interest:", "Interest at"), + ENDS("Ends in:", "Ends at"), + REMAINING("Remaining:", "Ends at"), + DURATION("Duration:", "Finishes at"), + TIMELEFT("Time left:", "Ends at"), + EVENTTIMELEFT("Event lasts for", "Ends at", isRelative = true), + SHENSUCKS("Auction ends in:", "Auction ends at"), + ENDS_PET_LEVELING( + "Ends:", + "Finishes at" + ), + CALENDARDETAILS(" (§e", "Starts at"), + COMMUNITYPROJECTS("Contribute again", "Come back at"), + CHOCOLATEFACTORY("Next Charge", "Available at"), + STONKSAUCTION("Auction ends in", "Ends at"), + LIZSTONKREDEMPTION("Resets in:", "Resets at"), + TIMEREMAININGS("Time Remaining:", "Ends at"), + COOLDOWN("Cooldown:", "Come back at"), + ONCOOLDOWN("On cooldown:", "Available at"), + EVENTENDING("Event ends in:", "Ends at"); + } + + val regex = + "(?i)(?:(?[0-9]+) ?(y|years?) )?(?:(?[0-9]+) ?(d|days?))? ?(?:(?[0-9]+) ?(h|hours?))? ?(?:(?[0-9]+) ?(m|minutes?))? ?(?:(?[0-9]+) ?(s|seconds?))?\\b".toRegex() + + @Subscribe + fun creationInLore(event: ItemTooltipEvent) { + if (!TConfig.showCreationTimestamp) return + val timestamp = event.stack.timestamp ?: return + val formattedTimestamp = TConfig.timerFormat.formatter.format(ZonedDateTime.ofInstant(timestamp, ZoneId.systemDefault())) + event.lines.add(tr("firmament.lore.creationtimestamp", "Created at: $formattedTimestamp").grey()) + } + + @Subscribe + fun modifyLore(event: ItemTooltipEvent) { + if (!TConfig.showTimers) return + var lastTimer: ZonedDateTime? = null + for (i in event.lines.indices) { + val line = event.lines[i].unformattedString + val countdownType = CountdownTypes.entries.find { it.match in line } ?: continue + if (countdownType == CountdownTypes.CALENDARDETAILS + && !event.stack.displayNameAccordingToNbt.unformattedString.startsWith("Day ") + ) continue + + val countdownMatch = regex.findAll(line).filter { it.value.isNotBlank() }.lastOrNull() ?: continue + val (years, days, hours, minutes, seconds) = + listOf("years", "days", "hours", "minutes", "seconds") + .map { + countdownMatch.groups[it]?.value?.toLong() ?: 0L + } + if (years + days + hours + minutes + seconds == 0L) continue + var baseLine = ZonedDateTime.now(SBData.hypixelTimeZone) + if (countdownType.isRelative) { + if (lastTimer == null) { + event.lines.add( + i + 1, + tr( + "firmament.loretimer.missingrelative", + "Found a relative countdown with no baseline (Firmament)" + ).grey() + ) + continue + } + baseLine = lastTimer + } + val timer = + baseLine.plusYears(years).plusDays(days).plusHours(hours).plusMinutes(minutes).plusSeconds(seconds) + lastTimer = timer + val localTimer = timer.withZoneSameInstant(ZoneId.systemDefault()) + // TODO: install approximate time stabilization algorithm + event.lines.add( + i + 1, + Text.literal("${countdownType.label}: ") + .grey() + .append(Text.literal(TConfig.timerFormat.formatter.format(localTimer)).aqua()) + ) + } + } + +} diff --git a/src/main/kotlin/features/inventory/WardrobeKeybinds.kt b/src/main/kotlin/features/inventory/WardrobeKeybinds.kt new file mode 100644 index 0000000..6e2b4a9 --- /dev/null +++ b/src/main/kotlin/features/inventory/WardrobeKeybinds.kt @@ -0,0 +1,80 @@ +package moe.nea.firmament.features.inventory + +import org.lwjgl.glfw.GLFW +import net.minecraft.item.Items +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HandledScreenKeyPressedEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.mc.SlotUtils.clickLeftMouseButton + +object WardrobeKeybinds : FirmamentFeature { + override val identifier: String + get() = "wardrobe-keybinds" + + object TConfig : ManagedConfig(identifier, Category.INVENTORY) { + val wardrobeKeybinds by toggle("wardrobe-keybinds") { false } + val changePageKeybind by keyBinding("change-page") { GLFW.GLFW_KEY_ENTER } + val nextPage by keyBinding("next-page") { GLFW.GLFW_KEY_D } + val previousPage by keyBinding("previous-page") { GLFW.GLFW_KEY_A } + val slotKeybinds = (1..9).map { + keyBinding("slot-$it") { GLFW.GLFW_KEY_0 + it } + } + } + + override val config: ManagedConfig? + get() = TConfig + + val slotKeybindsWithSlot = TConfig.slotKeybinds.withIndex().map { (index, keybinding) -> + index + 36 to keybinding + } + + @Subscribe + fun switchSlot(event: HandledScreenKeyPressedEvent) { + if (MC.player == null || MC.world == null || MC.interactionManager == null) return + + val regex = Regex("Wardrobe \\([12]/2\\)") + if (!regex.matches(event.screen.title.string)) return + if (!TConfig.wardrobeKeybinds) return + + if ( + event.matches(TConfig.changePageKeybind) || + event.matches(TConfig.previousPage) || + event.matches(TConfig.nextPage) + ) { + event.cancel() + + val handler = event.screen.screenHandler + val previousSlot = handler.getSlot(45) + val nextSlot = handler.getSlot(53) + + val backPressed = event.matches(TConfig.changePageKeybind) || event.matches(TConfig.previousPage) + val nextPressed = event.matches(TConfig.changePageKeybind) || event.matches(TConfig.nextPage) + + if (backPressed && previousSlot.stack.item == Items.ARROW) { + previousSlot.clickLeftMouseButton(handler) + } else if (nextPressed && nextSlot.stack.item == Items.ARROW) { + nextSlot.clickLeftMouseButton(handler) + } + } + + + + val slot = + slotKeybindsWithSlot + .find { event.matches(it.second.get()) } + ?.first ?: return + + event.cancel() + + val handler = event.screen.screenHandler + val invSlot = handler.getSlot(slot) + + val itemStack = invSlot.stack + if (itemStack.item != Items.PINK_DYE && itemStack.item != Items.LIME_DYE) return + + invSlot.clickLeftMouseButton(handler) + } + +} diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButton.kt b/src/main/kotlin/features/inventory/buttons/InventoryButton.kt new file mode 100644 index 0000000..955ae88 --- /dev/null +++ b/src/main/kotlin/features/inventory/buttons/InventoryButton.kt @@ -0,0 +1,103 @@ +package moe.nea.firmament.features.inventory.buttons + +import com.mojang.brigadier.StringReader +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import kotlinx.serialization.Serializable +import net.minecraft.client.gui.DrawContext +import net.minecraft.command.CommandRegistryAccess +import net.minecraft.command.argument.ItemStackArgumentType +import net.minecraft.item.ItemStack +import net.minecraft.resource.featuretoggle.FeatureFlags +import net.minecraft.util.Identifier +import moe.nea.firmament.repo.ExpensiveItemCacheApi +import moe.nea.firmament.repo.ItemCache.asItemStack +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.collections.memoize +import moe.nea.firmament.util.mc.arbitraryUUID +import moe.nea.firmament.util.mc.createSkullItem +import moe.nea.firmament.util.render.drawGuiTexture + +@Serializable +data class InventoryButton( + var x: Int, + var y: Int, + var anchorRight: Boolean, + var anchorBottom: Boolean, + var icon: String? = "", + var command: String? = "", +) { + companion object { + val itemStackParser by lazy { + ItemStackArgumentType.itemStack( + CommandRegistryAccess.of( + MC.defaultRegistries, + FeatureFlags.VANILLA_FEATURES + ) + ) + } + val dimensions = Dimension(18, 18) + val getItemForName = ::getItemForName0.memoize(1024) + @OptIn(ExpensiveItemCacheApi::class) + fun getItemForName0(icon: String): ItemStack { + val repoItem = RepoManager.getNEUItem(SkyblockId(icon)) + var itemStack = repoItem.asItemStack(idHint = SkyblockId(icon)) + if (repoItem == null) { + when { + icon.startsWith("skull:") -> { + itemStack = createSkullItem( + arbitraryUUID, + "https://textures.minecraft.net/texture/${icon.substring("skull:".length)}" + ) + } + + else -> { + val giveSyntaxItem = if (icon.startsWith("/give") || icon.startsWith("give")) + icon.split(" ", limit = 3).getOrNull(2) ?: icon + else icon + val componentItem = + runCatching { + itemStackParser.parse(StringReader(giveSyntaxItem)).createStack(1, false) + }.getOrNull() + if (componentItem != null) + itemStack = componentItem + } + } + } + return itemStack + } + } + + fun render(context: DrawContext) { + context.drawGuiTexture( + 0, + 0, + 0, + dimensions.width, + dimensions.height, + Identifier.of("firmament:inventory_button_background") + ) + context.drawItem(getItem(), 1, 1) + } + + fun isValid() = !icon.isNullOrBlank() && !command.isNullOrBlank() + + fun getPosition(guiRect: Rectangle): Point { + return Point( + (if (anchorRight) guiRect.maxX else guiRect.minX) + x, + (if (anchorBottom) guiRect.maxY else guiRect.minY) + y, + ) + } + + fun getBounds(guiRect: Rectangle): Rectangle { + return Rectangle(getPosition(guiRect), dimensions) + } + + fun getItem(): ItemStack { + return getItemForName(icon ?: "") + } + +} diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt b/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt new file mode 100644 index 0000000..eecbd17 --- /dev/null +++ b/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt @@ -0,0 +1,300 @@ +package moe.nea.firmament.features.inventory.buttons + +import io.github.notenoughupdates.moulconfig.common.IItemStack +import io.github.notenoughupdates.moulconfig.gui.component.PanelComponent +import io.github.notenoughupdates.moulconfig.platform.ModernItemStack +import io.github.notenoughupdates.moulconfig.platform.ModernRenderContext +import io.github.notenoughupdates.moulconfig.xml.Bind +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import org.lwjgl.glfw.GLFW +import net.minecraft.client.MinecraftClient +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.gui.widget.ButtonWidget +import net.minecraft.client.gui.widget.TextWidget +import net.minecraft.client.util.InputUtil +import net.minecraft.text.Text +import net.minecraft.util.math.MathHelper +import net.minecraft.util.math.Vec2f +import moe.nea.firmament.util.ClipboardUtils +import moe.nea.firmament.util.FragmentGuiScreen +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.MoulConfigUtils +import moe.nea.firmament.util.tr + +class InventoryButtonEditor( + val lastGuiRect: Rectangle, +) : FragmentGuiScreen() { + inner class Editor(val originalButton: InventoryButton) { + @field:Bind + var command: String = originalButton.command ?: "" + + @field:Bind + var icon: String = originalButton.icon ?: "" + + @Bind + fun getItemIcon(): IItemStack { + save() + return ModernItemStack.of(InventoryButton.getItemForName(icon)) + } + + @Bind + fun delete() { + buttons.removeIf { it === originalButton } + popup = null + } + + fun save() { + originalButton.icon = icon + originalButton.command = command + } + } + + var buttons: MutableList = + InventoryButtons.DConfig.data.buttons.map { it.copy() }.toMutableList() + + override fun close() { + InventoryButtons.DConfig.data.buttons = buttons + InventoryButtons.DConfig.markDirty() + super.close() + } + + override fun resize(client: MinecraftClient, width: Int, height: Int) { + lastGuiRect.move( + MC.window.scaledWidth / 2 - lastGuiRect.width / 2, + MC.window.scaledHeight / 2 - lastGuiRect.height / 2 + ) + super.resize(client, width, height) + } + + override fun init() { + super.init() + addDrawableChild( + TextWidget( + lastGuiRect.minX, + 25, + lastGuiRect.width, + 9, + Text.translatable("firmament.inventory-buttons.delete"), + MC.font + ).alignCenter() + ) + addDrawableChild( + TextWidget( + lastGuiRect.minX, + 40, + lastGuiRect.width, + 9, + Text.translatable("firmament.inventory-buttons.info"), + MC.font + ).alignCenter() + ) + addDrawableChild( + ButtonWidget.builder(Text.translatable("firmament.inventory-buttons.reset")) { + val newButtons = InventoryButtonTemplates.loadTemplate("TkVVQlVUVE9OUy9bXQ==") + if (newButtons != null) + buttons = moveButtons(newButtons.map { it.copy(command = it.command?.removePrefix("/")) }) + } + .position(lastGuiRect.minX + 10, lastGuiRect.minY + 10) + .width(lastGuiRect.width - 20) + .build() + ) + addDrawableChild( + ButtonWidget.builder(Text.translatable("firmament.inventory-buttons.load-preset")) { + val t = ClipboardUtils.getTextContents() + val newButtons = InventoryButtonTemplates.loadTemplate(t) + if (newButtons != null) + buttons = moveButtons(newButtons.map { it.copy(command = it.command?.removePrefix("/")) }) + } + .position(lastGuiRect.minX + 10, lastGuiRect.minY + 35) + .width(lastGuiRect.width - 20) + .build() + ) + addDrawableChild( + ButtonWidget.builder(Text.translatable("firmament.inventory-buttons.save-preset")) { + ClipboardUtils.setTextContent(InventoryButtonTemplates.saveTemplate(buttons)) + } + .position(lastGuiRect.minX + 10, lastGuiRect.minY + 60) + .width(lastGuiRect.width - 20) + .build() + ) + addDrawableChild( + ButtonWidget.builder(Text.translatable("firmament.inventory-buttons.simple-preset")) { + // Preset from NEU + // Credit: https://github.com/NotEnoughUpdates/NotEnoughUpdates/blob/9b1fcfebc646e9fb69f99006327faa3e734e5f51/src/main/resources/assets/notenoughupdates/invbuttons/presets.json#L900-L1348 + val newButtons = InventoryButtonTemplates.loadTemplate("TkVVQlVUVE9OUy9bIntcblx0XCJ4XCI6IDE2MCxcblx0XCJ5XCI6IC0yMCxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcImJvbmVcIixcblx0XCJjb21tYW5kXCI6IFwicGV0c1wiXG59Iiwie1xuXHRcInhcIjogMTQwLFxuXHRcInlcIjogLTIwLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiBmYWxzZSxcblx0XCJpY29uXCI6IFwiYXJtb3Jfc3RhbmRcIixcblx0XCJjb21tYW5kXCI6IFwid2FyZHJvYmVcIlxufSIsIntcblx0XCJ4XCI6IDEyMCxcblx0XCJ5XCI6IC0yMCxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcImVuZGVyX2NoZXN0XCIsXG5cdFwiY29tbWFuZFwiOiBcInN0b3JhZ2VcIlxufSIsIntcblx0XCJ4XCI6IDEwMCxcblx0XCJ5XCI6IC0yMCxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcInNrdWxsOmQ3Y2M2Njg3NDIzZDA1NzBkNTU2YWM1M2UwNjc2Y2I1NjNiYmRkOTcxN2NkODI2OWJkZWJlZDZmNmQ0ZTdiZjhcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBpc2xhbmRcIlxufSIsIntcblx0XCJ4XCI6IDgwLFxuXHRcInlcIjogLTIwLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiBmYWxzZSxcblx0XCJpY29uXCI6IFwic2t1bGw6MzVmNGI0MGNlZjllMDE3Y2Q0MTEyZDI2YjYyNTU3ZjhjMWQ1YjE4OWRhMmU5OTUzNDIyMmJjOGNlYzdkOTE5NlwiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGh1YlwiXG59Il0=") + if (newButtons != null) + buttons = moveButtons(newButtons.map { it.copy(command = it.command?.removePrefix("/")) }) + } + .position(lastGuiRect.minX + 10, lastGuiRect.minY + 85) + .width(lastGuiRect.width - 20) + .build() + ) + addDrawableChild( + ButtonWidget.builder(Text.translatable("firmament.inventory-buttons.all-warps-preset")) { + // Preset from NEU + // Credit: https://github.com/NotEnoughUpdates/NotEnoughUpdates/blob/9b1fcfebc646e9fb69f99006327faa3e734e5f51/src/main/resources/assets/notenoughupdates/invbuttons/presets.json#L1817-L2276 + val newButtons = InventoryButtonTemplates.loadTemplate("TkVVQlVUVE9OUy9bIntcblx0XCJ4XCI6IDIsXG5cdFwieVwiOiAtODQsXG5cdFwiYW5jaG9yUmlnaHRcIjogdHJ1ZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6YzljODg4MWU0MjkxNWE5ZDI5YmI2MWExNmZiMjZkMDU5OTEzMjA0ZDI2NWRmNWI0MzliM2Q3OTJhY2Q1NlwiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGhvbWVcIlxufSIsIntcblx0XCJ4XCI6IDIsXG5cdFwieVwiOiAtNjQsXG5cdFwiYW5jaG9yUmlnaHRcIjogdHJ1ZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6ZDdjYzY2ODc0MjNkMDU3MGQ1NTZhYzUzZTA2NzZjYjU2M2JiZGQ5NzE3Y2Q4MjY5YmRlYmVkNmY2ZDRlN2JmOFwiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGh1YlwiXG59Iiwie1xuXHRcInhcIjogMixcblx0XCJ5XCI6IC00NCxcblx0XCJhbmNob3JSaWdodFwiOiB0cnVlLFxuXHRcImFuY2hvckJvdHRvbVwiOiB0cnVlLFxuXHRcImljb25cIjogXCJza3VsbDo5YjU2ODk1Yjk2NTk4OTZhZDY0N2Y1ODU5OTIzOGFmNTMyZDQ2ZGI5YzFiMDM4OWI4YmJlYjcwOTk5ZGFiMzNkXCIsXG5cdFwiY29tbWFuZFwiOiBcIndhcnAgZHVuZ2Vvbl9odWJcIlxufSIsIntcblx0XCJ4XCI6IDIsXG5cdFwieVwiOiAtMjQsXG5cdFwiYW5jaG9yUmlnaHRcIjogdHJ1ZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6Nzg0MGI4N2Q1MjI3MWQyYTc1NWRlZGM4Mjg3N2UwZWQzZGY2N2RjYzQyZWE0NzllYzE0NjE3NmIwMjc3OWE1XCIsXG5cdFwiY29tbWFuZFwiOiBcIndhcnAgZW5kXCJcbn0iLCJ7XG5cdFwieFwiOiAxMDksXG5cdFwieVwiOiAtMTksXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IGZhbHNlLFxuXHRcImljb25cIjogXCJza3VsbDo4NmYwNmVhYTMwMDRhZWVkMDliM2Q1YjQ1ZDk3NmRlNTg0ZTY5MWMwZTljYWRlMTMzNjM1ZGU5M2QyM2I5ZWRiXCIsXG5cdFwiY29tbWFuZFwiOiBcImhvdG1cIlxufSIsIntcblx0XCJ4XCI6IDEzMCxcblx0XCJ5XCI6IC0xOSxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcIkVOREVSX0NIRVNUXCIsXG5cdFwiY29tbWFuZFwiOiBcInN0b3JhZ2VcIlxufSIsIntcblx0XCJ4XCI6IDE1MSxcblx0XCJ5XCI6IC0xOSxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcIkJPTkVcIixcblx0XCJjb21tYW5kXCI6IFwicGV0c1wiXG59Iiwie1xuXHRcInhcIjogLTE5LFxuXHRcInlcIjogMixcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcIkdPTERfQkxPQ0tcIixcblx0XCJjb21tYW5kXCI6IFwiYWhcIlxufSIsIntcblx0XCJ4XCI6IC0xOSxcblx0XCJ5XCI6IDIyLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiBmYWxzZSxcblx0XCJpY29uXCI6IFwiR09MRF9CQVJESU5HXCIsXG5cdFwiY29tbWFuZFwiOiBcImJ6XCJcbn0iLCJ7XG5cdFwieFwiOiAtMTksXG5cdFwieVwiOiAtODQsXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IHRydWUsXG5cdFwiaWNvblwiOiBcInNrdWxsOjQzOGNmM2Y4ZTU0YWZjM2IzZjkxZDIwYTQ5ZjMyNGRjYTE0ODYwMDdmZTU0NTM5OTA1NTUyNGMxNzk0MWY0ZGNcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBtdXNldW1cIlxufSIsIntcblx0XCJ4XCI6IC0xOSxcblx0XCJ5XCI6IC02NCxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6ZjQ4ODBkMmMxZTdiODZlODc1MjJlMjA4ODI2NTZmNDViYWZkNDJmOTQ5MzJiMmM1ZTBkNmVjYWE0OTBjYjRjXCIsXG5cdFwiY29tbWFuZFwiOiBcIndhcnAgZ2FyZGVuXCJcbn0iLCJ7XG5cdFwieFwiOiAtMTksXG5cdFwieVwiOiAtNDQsXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IHRydWUsXG5cdFwiaWNvblwiOiBcInNrdWxsOjRkM2E2YmQ5OGFjMTgzM2M2NjRjNDkwOWZmOGQyZGM2MmNlODg3YmRjZjNjYzViMzg0ODY1MWFlNWFmNmJcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBiYXJuXCJcbn0iLCJ7XG5cdFwieFwiOiAtMTksXG5cdFwieVwiOiAtMjQsXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IHRydWUsXG5cdFwiaWNvblwiOiBcInNrdWxsOjUxNTM5ZGRkZjllZDI1NWVjZTYzNDgxOTNjZDc1MDEyYzgyYzkzYWVjMzgxZjA1NTcyY2VjZjczNzk3MTFiM2JcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBkZXNlcnRcIlxufSIsIntcblx0XCJ4XCI6IDQsXG5cdFwieVwiOiAyLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiB0cnVlLFxuXHRcImljb25cIjogXCJza3VsbDo3M2JjOTY1ZDU3OWMzYzYwMzlmMGExN2ViN2MyZTZmYWY1MzhjN2E1ZGU4ZTYwZWM3YTcxOTM2MGQwYTg1N2E5XCIsXG5cdFwiY29tbWFuZFwiOiBcIndhcnAgZ29sZFwiXG59Iiwie1xuXHRcInhcIjogMjUsXG5cdFwieVwiOiAyLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiB0cnVlLFxuXHRcImljb25cIjogXCJza3VsbDo1NjlhMWYxMTQxNTFiNDUyMTM3M2YzNGJjMTRjMjk2M2E1MDExY2RjMjVhNjU1NGM0OGM3MDhjZDk2ZWJmY1wiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGRlZXBcIlxufSIsIntcblx0XCJ4XCI6IDQ2LFxuXHRcInlcIjogMixcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6MjFkYmUzMGIwMjdhY2JjZWI2MTI1NjNiZDg3N2NkN2ViYjcxOWVhNmVkMTM5OTAyN2RjZWU1OGJiOTA0OWQ0YVwiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGNyeXN0YWxzXCJcbn0iLCJ7XG5cdFwieFwiOiA2Nyxcblx0XCJ5XCI6IDIsXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IHRydWUsXG5cdFwiaWNvblwiOiBcInNrdWxsOjVjYmQ5ZjVlYzFlZDAwNzI1OTk5NjQ5MWU2OWZmNjQ5YTMxMDZjZjkyMDIyN2IxYmIzYTcxZWU3YTg5ODYzZlwiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGZvcmdlXCJcbn0iLCJ7XG5cdFwieFwiOiA4OCxcblx0XCJ5XCI6IDIsXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IHRydWUsXG5cdFwiaWNvblwiOiBcInNrdWxsOjZiMjBiMjNjMWFhMmJlMDI3MGYwMTZiNGM5MGQ2ZWU2YjgzMzBhMTdjZmVmODc4NjlkNmFkNjBiMmZmYmYzYjVcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBtaW5lc1wiXG59Iiwie1xuXHRcInhcIjogMTA5LFxuXHRcInlcIjogMixcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6YTIyMWY4MTNkYWNlZTBmZWY4YzU5Zjc2ODk0ZGJiMjY0MTU0NzhkOWRkZmM0NGMyZTcwOGE2ZDNiNzU0OWJcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBwYXJrXCJcbn0iLCJ7XG5cdFwieFwiOiAxMzAsXG5cdFwieVwiOiAyLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiB0cnVlLFxuXHRcImljb25cIjogXCJza3VsbDo5ZDdlM2IxOWFjNGYzZGVlOWM1Njc3YzEzNTMzM2I5ZDM1YTdmNTY4YjYzZDFlZjRhZGE0YjA2OGI1YTI1XCIsXG5cdFwiY29tbWFuZFwiOiBcIndhcnAgc3BpZGVyXCJcbn0iLCJ7XG5cdFwieFwiOiAxNTEsXG5cdFwieVwiOiAyLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiB0cnVlLFxuXHRcImljb25cIjogXCJza3VsbDpjMzY4N2UyNWM2MzJiY2U4YWE2MWUwZDY0YzI0ZTY5NGMzZWVhNjI5ZWE5NDRmNGNmMzBkY2ZiNGZiY2UwNzFcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBuZXRoZXJcIlxufSJd") + if (newButtons != null) + buttons = moveButtons(newButtons.map { it.copy(command = it.command?.removePrefix("/")) }) + } + .position(lastGuiRect.minX + 10, lastGuiRect.minY + 110) + .width(lastGuiRect.width - 20) + .build() + ) + } + + private fun moveButtons(buttons: List): MutableList { + val newButtons: MutableList = ArrayList(buttons.size) + val movedButtons = mutableListOf() + for (button in buttons) { + if ((!button.anchorBottom && !button.anchorRight && button.x > 0 && button.y > 0)) { + MC.sendChat( + tr( + "firmament.inventory-buttons.button-moved", + "One of your imported buttons intersects with the inventory and has been moved to the top left." + ) + ) + movedButtons.add( + button.copy( + x = 0, + y = -InventoryButton.dimensions.width, + anchorRight = false, + anchorBottom = false + ) + ) + } else { + newButtons.add(button) + } + } + var i = 0 + val zeroRect = Rectangle(0, 0, 1, 1) + for (movedButton in movedButtons) { + fun getPosition(button: InventoryButton, index: Int) = + button.copy( + x = (index % 10) * InventoryButton.dimensions.width, + y = (index / 10) * -InventoryButton.dimensions.height, + anchorRight = false, anchorBottom = false + ) + while (true) { + val newPos = getPosition(movedButton, i++) + val newBounds = newPos.getBounds(zeroRect) + if (newButtons.none { it.getBounds(zeroRect).intersects(newBounds) }) { + newButtons.add(newPos) + break + } + } + } + return newButtons + } + + override fun renderBackground(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + context.matrices.push() + context.matrices.translate(0F, 0F, -15F) + super.renderBackground(context, mouseX, mouseY, delta) + context.matrices.pop() + } + + override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + super.render(context, mouseX, mouseY, delta) + context.matrices.push() + context.matrices.translate(0f, 0f, -10f) + PanelComponent.DefaultBackgroundRenderer.VANILLA + .render( + ModernRenderContext(context), + lastGuiRect.minX, lastGuiRect.minY, + lastGuiRect.width, lastGuiRect.height, + ) + context.matrices.pop() + for (button in buttons) { + val buttonPosition = button.getBounds(lastGuiRect) + context.matrices.push() + context.matrices.translate(buttonPosition.minX.toFloat(), buttonPosition.minY.toFloat(), 0F) + button.render(context) + context.matrices.pop() + } + } + + override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + if (super.keyPressed(keyCode, scanCode, modifiers)) return true + if (keyCode == GLFW.GLFW_KEY_ESCAPE) { + close() + return true + } + return false + } + + override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { + if (super.mouseReleased(mouseX, mouseY, button)) return true + val clickedButton = buttons.firstOrNull { it.getBounds(lastGuiRect).contains(Point(mouseX, mouseY)) } + if (clickedButton != null && !justPerformedAClickAction) { + if (InputUtil.isKeyPressed(MC.window.handle, InputUtil.GLFW_KEY_LEFT_CONTROL)) Editor(clickedButton).delete() + else createPopup(MoulConfigUtils.loadGui("button_editor_fragment", Editor(clickedButton)), Point(mouseX, mouseY)) + return true + } + justPerformedAClickAction = false + lastDraggedButton = null + return false + } + + override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean { + if (super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)) return true + + if (initialDragMousePosition.distanceSquared(Vec2f(mouseX.toFloat(), mouseY.toFloat())) >= 4 * 4) { + initialDragMousePosition = Vec2f(-10F, -10F) + lastDraggedButton?.let { dragging -> + justPerformedAClickAction = true + val (anchorRight, anchorBottom, offsetX, offsetY) = getCoordsForMouse(mouseX.toInt(), mouseY.toInt()) + ?: return true + dragging.x = offsetX + dragging.y = offsetY + dragging.anchorRight = anchorRight + dragging.anchorBottom = anchorBottom + } + } + return false + } + + var lastDraggedButton: InventoryButton? = null + var justPerformedAClickAction = false + var initialDragMousePosition = Vec2f(-10F, -10F) + + data class AnchoredCoords( + val anchorRight: Boolean, + val anchorBottom: Boolean, + val offsetX: Int, + val offsetY: Int, + ) + + fun getCoordsForMouse(mx: Int, my: Int): AnchoredCoords? { + val anchorRight = mx > lastGuiRect.maxX + val anchorBottom = my > lastGuiRect.maxY + var offsetX = mx - if (anchorRight) lastGuiRect.maxX else lastGuiRect.minX + var offsetY = my - if (anchorBottom) lastGuiRect.maxY else lastGuiRect.minY + if (InputUtil.isKeyPressed(MC.window.handle, InputUtil.GLFW_KEY_LEFT_SHIFT)) { + offsetX = MathHelper.floor(offsetX / 20F) * 20 + offsetY = MathHelper.floor(offsetY / 20F) * 20 + } + val rect = InventoryButton(offsetX, offsetY, anchorRight, anchorBottom).getBounds(lastGuiRect) + if (rect.intersects(lastGuiRect)) return null + val anchoredCoords = AnchoredCoords(anchorRight, anchorBottom, offsetX, offsetY) + return anchoredCoords + } + + override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { + if (super.mouseClicked(mouseX, mouseY, button)) return true + val clickedButton = buttons.firstOrNull { it.getBounds(lastGuiRect).contains(Point(mouseX, mouseY)) } + if (clickedButton != null) { + lastDraggedButton = clickedButton + initialDragMousePosition = Vec2f(mouseX.toFloat(), mouseY.toFloat()) + return true + } + val mx = mouseX.toInt() + val my = mouseY.toInt() + val (anchorRight, anchorBottom, offsetX, offsetY) = getCoordsForMouse(mx, my) ?: return true + buttons.add(InventoryButton(offsetX, offsetY, anchorRight, anchorBottom, null, null)) + justPerformedAClickAction = true + return true + } + +} diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButtonTemplates.kt b/src/main/kotlin/features/inventory/buttons/InventoryButtonTemplates.kt new file mode 100644 index 0000000..d282157 --- /dev/null +++ b/src/main/kotlin/features/inventory/buttons/InventoryButtonTemplates.kt @@ -0,0 +1,33 @@ +package moe.nea.firmament.features.inventory.buttons + +import kotlinx.serialization.encodeToString +import net.minecraft.text.Text +import moe.nea.firmament.Firmament +import moe.nea.firmament.util.ErrorUtil +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TemplateUtil + +object InventoryButtonTemplates { + + val legacyPrefix = "NEUBUTTONS/" + val modernPrefix = "MAYBEONEDAYIWILLHAVEMYOWNFORMAT" + + fun loadTemplate(t: String): List? { + val buttons = TemplateUtil.maybeDecodeTemplate>(legacyPrefix, t) ?: return null + return buttons.mapNotNull { + ErrorUtil.catch("Could not import button") { + Firmament.json.decodeFromString(it).also { + if (it.icon?.startsWith("extra:") == true) { + MC.sendChat(Text.translatable("firmament.inventory-buttons.import-failed")) + } + } + }.or { + null + } + } + } + + fun saveTemplate(buttons: List): String { + return TemplateUtil.encodeTemplate(legacyPrefix, buttons.map { Firmament.json.encodeToString(it) }) + } +} diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt b/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt new file mode 100644 index 0000000..ab80d97 --- /dev/null +++ b/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt @@ -0,0 +1,114 @@ + + +package moe.nea.firmament.features.inventory.buttons + +import me.shedaniel.math.Rectangle +import kotlinx.serialization.Serializable +import kotlinx.serialization.serializer +import kotlin.time.Duration.Companion.seconds +import net.minecraft.client.gui.screen.ingame.HandledScreen +import net.minecraft.client.gui.screen.ingame.InventoryScreen +import net.minecraft.text.Text +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HandledScreenClickEvent +import moe.nea.firmament.events.HandledScreenForegroundEvent +import moe.nea.firmament.events.HandledScreenPushREIEvent +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.ScreenUtil +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.data.DataHolder +import moe.nea.firmament.util.accessors.getRectangle +import moe.nea.firmament.util.gold + +object InventoryButtons { + + object TConfig : ManagedConfig("inventory-buttons-config", Category.INVENTORY) { + val _openEditor by button("open-editor") { + openEditor() + } + val hoverText by toggle("hover-text") { true } + val onlyInv by toggle("only-inv") { false } + } + + object DConfig : DataHolder(serializer(), "inventory-buttons", ::Data) + + @Serializable + data class Data( + var buttons: MutableList = mutableListOf() + ) + + fun getValidButtons(screen: HandledScreen<*>): Sequence { + return DConfig.data.buttons.asSequence().filter { button -> + button.isValid() && (!TConfig.onlyInv || screen is InventoryScreen) + } + } + + + @Subscribe + fun onRectangles(it: HandledScreenPushREIEvent) { + val bounds = it.screen.getRectangle() + for (button in getValidButtons(it.screen)) { + val buttonBounds = button.getBounds(bounds) + it.block(buttonBounds) + } + } + + @Subscribe + fun onClickScreen(it: HandledScreenClickEvent) { + val bounds = it.screen.getRectangle() + for (button in getValidButtons(it.screen)) { + val buttonBounds = button.getBounds(bounds) + if (buttonBounds.contains(it.mouseX, it.mouseY)) { + MC.sendCommand(button.command!! /* non null invariant covered by getValidButtons */) + break + } + } + } + + var lastHoveredComponent: InventoryButton? = null + var lastMouseMove = TimeMark.farPast() + + @Subscribe + fun onRenderForeground(it: HandledScreenForegroundEvent) { + val bounds = it.screen.getRectangle() + + var hoveredComponent: InventoryButton? = null + for (button in getValidButtons(it.screen)) { + val buttonBounds = button.getBounds(bounds) + it.context.matrices.push() + it.context.matrices.translate(buttonBounds.minX.toFloat(), buttonBounds.minY.toFloat(), 0F) + button.render(it.context) + it.context.matrices.pop() + + if (buttonBounds.contains(it.mouseX, it.mouseY) && TConfig.hoverText && hoveredComponent == null) { + hoveredComponent = button + if (lastMouseMove.passedTime() > 0.6.seconds && lastHoveredComponent === button) { + it.context.drawTooltip( + MC.font, + listOf(Text.literal(button.command).gold()), + buttonBounds.minX - 15, + buttonBounds.maxY + 20, + ) + } + } + } + if (hoveredComponent !== lastHoveredComponent) + lastMouseMove = TimeMark.now() + lastHoveredComponent = hoveredComponent + lastRectangle = bounds + } + + var lastRectangle: Rectangle? = null + fun openEditor() { + ScreenUtil.setScreenLater( + InventoryButtonEditor( + lastRectangle ?: Rectangle( + MC.window.scaledWidth / 2 - 88, + MC.window.scaledHeight / 2 - 83, + 176, 166, + ) + ) + ) + } +} diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt new file mode 100644 index 0000000..d7346c2 --- /dev/null +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt @@ -0,0 +1,59 @@ +@file:OptIn(ExperimentalContracts::class) + +package moe.nea.firmament.features.inventory.storageoverlay + +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract +import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.gui.screen.ingame.GenericContainerScreen +import net.minecraft.screen.GenericContainerScreenHandler +import moe.nea.firmament.util.ifMatches +import moe.nea.firmament.util.unformattedString + +/** + * A handle representing the state of the "server side" screens. + */ +sealed interface StorageBackingHandle { + + sealed interface HasBackingScreen { + val handler: GenericContainerScreenHandler + } + + /** + * The main storage overview is open. Clicking on a slot will open that page. This page is accessible via `/storage` + */ + data class Overview(override val handler: GenericContainerScreenHandler) : StorageBackingHandle, HasBackingScreen + + /** + * An individual storage page is open. This may be a backpack or an enderchest page. This page is accessible via + * the [Overview] or via `/ec ` for enderchest pages. + */ + data class Page(override val handler: GenericContainerScreenHandler, val storagePageSlot: StoragePageSlot) : + StorageBackingHandle, HasBackingScreen + + companion object { + private val enderChestName = "^Ender Chest (?:✦ )?\\(([1-9])/[1-9]\\)$".toRegex() + private val backPackName = "^.+Backpack (?:✦ )?\\(Slot #([0-9]+)\\)$".toRegex() + + /** + * Parse a screen into a [StorageBackingHandle]. If this returns null it means that the screen is not + * representable as a [StorageBackingHandle], meaning another screen is open, for example the enderchest icon + * selection screen. + */ + @OptIn(ExperimentalContracts::class) + fun fromScreen(screen: Screen?): StorageBackingHandle? { + contract { + returnsNotNull() implies (screen != null) + } + if (screen == null) return null + if (screen !is GenericContainerScreen) return null + val title = screen.title.unformattedString + if (title == "Storage") return Overview(screen.screenHandler) + return title.ifMatches(enderChestName) { + Page(screen.screenHandler, StoragePageSlot.ofEnderChestPage(it.groupValues[1].toInt())) + } ?: title.ifMatches(backPackName) { + Page(screen.screenHandler, StoragePageSlot.ofBackPackPage(it.groupValues[1].toInt())) + } + } + } +} diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageData.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageData.kt new file mode 100644 index 0000000..7555c56 --- /dev/null +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageData.kt @@ -0,0 +1,21 @@ + + +@file:UseSerializers(SortedMapSerializer::class) +package moe.nea.firmament.features.inventory.storageoverlay + +import java.util.SortedMap +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers +import moe.nea.firmament.util.SortedMapSerializer + +@Serializable +data class StorageData( + val storageInventories: SortedMap = sortedMapOf() +) { + @Serializable + data class StorageInventory( + var title: String, + val slot: StoragePageSlot, + var inventory: VirtualInventory?, + ) +} diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt new file mode 100644 index 0000000..f59b293 --- /dev/null +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt @@ -0,0 +1,200 @@ +package moe.nea.firmament.features.inventory.storageoverlay + +import io.github.notenoughupdates.moulconfig.ChromaColour +import java.util.SortedMap +import kotlinx.serialization.serializer +import net.minecraft.client.gui.screen.ingame.GenericContainerScreen +import net.minecraft.client.gui.screen.ingame.HandledScreen +import net.minecraft.entity.player.PlayerInventory +import net.minecraft.item.Items +import net.minecraft.network.packet.c2s.play.CloseHandledScreenC2SPacket +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ScreenChangeEvent +import moe.nea.firmament.events.SlotClickEvent +import moe.nea.firmament.events.SlotRenderEvents +import moe.nea.firmament.events.TickEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.customgui.customGui +import moe.nea.firmament.util.data.ProfileSpecificDataHolder + +object StorageOverlay : FirmamentFeature { + + + object Data : ProfileSpecificDataHolder(serializer(), "storage-data", ::StorageData) + + override val identifier: String + get() = "storage-overlay" + + object TConfig : ManagedConfig(identifier, Category.INVENTORY) { + val alwaysReplace by toggle("always-replace") { true } + val outlineActiveStoragePage by toggle("outline-active-page") { false } + val outlineActiveStoragePageColour by colour("outline-active-page-colour") { + ChromaColour.fromRGB( + 255, + 255, + 0, + 0, + 255 + ) + } + val columns by integer("rows", 1, 10) { 3 } + val height by integer("height", 80, 3000) { 3 * 18 * 6 } + val retainScroll by toggle("retain-scroll") { true } + val scrollSpeed by integer("scroll-speed", 1, 50) { 10 } + val inverseScroll by toggle("inverse-scroll") { false } + val padding by integer("padding", 1, 20) { 5 } + val margin by integer("margin", 1, 60) { 20 } + val itemsBlockScrolling by toggle("block-item-scrolling") { true } + val highlightSearchResults by toggle("highlight-search-results") { true } + val highlightSearchResultsColour by colour("highlight-search-results-colour") { + ChromaColour.fromRGB( + 0, + 176, + 0, + 0, + 255 + ) + } + } + + @Subscribe + fun highlightSlots(event: SlotRenderEvents.Before) { + if (!TConfig.highlightSearchResults) return + val storageOverlayScreen = + (MC.screen as? StorageOverlayScreen) + ?: (MC.handledScreen?.customGui as? StorageOverlayCustom)?.overview + ?: return + val stack = event.slot.stack ?: return + val search = storageOverlayScreen.searchText.get().takeIf { it.isNotBlank() } ?: return + if (storageOverlayScreen.matchesSearch(stack, search)) { + event.context.fill( + event.slot.x, + event.slot.y, + event.slot.x + 16, + event.slot.y + 16, + TConfig.highlightSearchResultsColour.getEffectiveColourRGB() + ) + } + } + + + fun adjustScrollSpeed(amount: Double): Double { + return amount * TConfig.scrollSpeed * (if (TConfig.inverseScroll) 1 else -1) + } + + override val config: TConfig + get() = TConfig + + var lastStorageOverlay: StorageOverviewScreen? = null + var skipNextStorageOverlayBackflip = false + var currentHandler: StorageBackingHandle? = null + + @Subscribe + fun onTick(event: TickEvent) { + rememberContent(currentHandler ?: return) + } + + @Subscribe + fun onClick(event: SlotClickEvent) { + if (lastStorageOverlay != null && event.slot.inventory !is PlayerInventory && event.slot.index < 9 + && event.stack.item != Items.BLACK_STAINED_GLASS_PANE + ) { + skipNextStorageOverlayBackflip = true + } + } + + @Subscribe + fun onScreenChange(it: ScreenChangeEvent) { + if (it.old == null && it.new == null) return + val storageOverlayScreen = it.old as? StorageOverlayScreen + ?: ((it.old as? HandledScreen<*>)?.customGui as? StorageOverlayCustom)?.overview + var storageOverviewScreen = it.old as? StorageOverviewScreen + val screen = it.new as? GenericContainerScreen + val oldHandler = currentHandler + currentHandler = StorageBackingHandle.fromScreen(screen) + rememberContent(currentHandler) + if (storageOverviewScreen != null && oldHandler is StorageBackingHandle.HasBackingScreen) { + val player = MC.player + assert(player != null) + player?.networkHandler?.sendPacket(CloseHandledScreenC2SPacket(oldHandler.handler.syncId)) + if (player?.currentScreenHandler === oldHandler.handler) { + player.currentScreenHandler = player.playerScreenHandler + } + } + storageOverviewScreen = storageOverviewScreen ?: lastStorageOverlay + if (it.new == null && storageOverlayScreen != null && !storageOverlayScreen.isExiting) { + it.overrideScreen = storageOverlayScreen + return + } + if (storageOverviewScreen != null + && !storageOverviewScreen.isClosing + && (currentHandler is StorageBackingHandle.Overview || currentHandler == null) + ) { + if (skipNextStorageOverlayBackflip) { + skipNextStorageOverlayBackflip = false + } else { + it.overrideScreen = storageOverviewScreen + lastStorageOverlay = null + } + return + } + screen ?: return + if (storageOverlayScreen?.isExiting == true) return + screen.customGui = StorageOverlayCustom( + currentHandler ?: return, + screen, + storageOverlayScreen ?: (if (TConfig.alwaysReplace) StorageOverlayScreen() else return) + ) + } + + fun rememberContent(handler: StorageBackingHandle?) { + handler ?: return + // TODO: Make all of these functions work on deltas / updates instead of the entire contents + val data = Data.data?.storageInventories ?: return + when (handler) { + is StorageBackingHandle.Overview -> rememberStorageOverview(handler, data) + is StorageBackingHandle.Page -> rememberPage(handler, data) + } + Data.markDirty() + } + + private fun rememberStorageOverview( + handler: StorageBackingHandle.Overview, + data: SortedMap + ) { + for ((index, stack) in handler.handler.stacks.withIndex()) { + // Ignore unloaded item stacks + if (stack.isEmpty) continue + val slot = StoragePageSlot.fromOverviewSlotIndex(index) ?: continue + val isEmpty = stack.item in StorageOverviewScreen.emptyStorageSlotItems + if (slot in data) { + if (isEmpty) + data.remove(slot) + continue + } + if (!isEmpty) { + data[slot] = StorageData.StorageInventory(slot.defaultName(), slot, null) + } + } + } + + private fun rememberPage( + handler: StorageBackingHandle.Page, + data: SortedMap + ) { + // TODO: FIXME: FIXME NOW: Definitely don't copy all of this every tick into persistence + val newStacks = + VirtualInventory(handler.handler.stacks.take(handler.handler.rows * 9).drop(9).map { it.copy() }) + data.compute(handler.storagePageSlot) { slot, existingInventory -> + (existingInventory ?: StorageData.StorageInventory( + slot.defaultName(), + slot, + null + )).also { + it.inventory = newStacks + } + } + } +} diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt new file mode 100644 index 0000000..e4d4e42 --- /dev/null +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt @@ -0,0 +1,122 @@ +package moe.nea.firmament.features.inventory.storageoverlay + +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.client.MinecraftClient +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.gui.screen.ingame.GenericContainerScreen +import net.minecraft.entity.player.PlayerInventory +import net.minecraft.screen.slot.Slot +import moe.nea.firmament.mixins.accessor.AccessorHandledScreen +import moe.nea.firmament.util.customgui.CustomGui +import moe.nea.firmament.util.focusedItemStack + +class StorageOverlayCustom( + val handler: StorageBackingHandle, + val screen: GenericContainerScreen, + val overview: StorageOverlayScreen, +) : CustomGui() { + override fun onVoluntaryExit(): Boolean { + overview.isExiting = true + StorageOverlayScreen.resetScroll() + return super.onVoluntaryExit() + } + + override fun getBounds(): List { + return overview.getBounds() + } + + override fun afterSlotRender(context: DrawContext, slot: Slot) { + if (slot.inventory !is PlayerInventory) + context.disableScissor() + } + + override fun beforeSlotRender(context: DrawContext, slot: Slot) { + if (slot.inventory !is PlayerInventory) + overview.createScissors(context) + } + + override fun onInit() { + overview.init(MinecraftClient.getInstance(), screen.width, screen.height) + overview.init() + screen as AccessorHandledScreen + screen.x_Firmament = overview.measurements.x + screen.y_Firmament = overview.measurements.y + screen.backgroundWidth_Firmament = overview.measurements.totalWidth + screen.backgroundHeight_Firmament = overview.measurements.totalHeight + } + + override fun isPointOverSlot(slot: Slot, xOffset: Int, yOffset: Int, pointX: Double, pointY: Double): Boolean { + if (!super.isPointOverSlot(slot, xOffset, yOffset, pointX, pointY)) + return false + if (slot.inventory !is PlayerInventory) { + if (!overview.getScrollPanelInner().contains(pointX, pointY)) + return false + } + return true + } + + override fun shouldDrawForeground(): Boolean { + return false + } + + override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { + return overview.mouseReleased(mouseX, mouseY, button) + } + + override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean { + return overview.mouseDragged(mouseX, mouseY, button, deltaX, deltaY) + } + + override fun keyReleased(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + return overview.keyReleased(keyCode, scanCode, modifiers) + } + + override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + return overview.keyPressed(keyCode, scanCode, modifiers) + } + + override fun charTyped(chr: Char, modifiers: Int): Boolean { + return overview.charTyped(chr, modifiers) + } + + override fun mouseClick(mouseX: Double, mouseY: Double, button: Int): Boolean { + return overview.mouseClicked(mouseX, mouseY, button, (handler as? StorageBackingHandle.Page)?.storagePageSlot) + } + + override fun render(drawContext: DrawContext, delta: Float, mouseX: Int, mouseY: Int) { + overview.drawBackgrounds(drawContext) + overview.drawPages(drawContext, + mouseX, + mouseY, + delta, + (handler as? StorageBackingHandle.Page)?.storagePageSlot, + screen.screenHandler.slots.take(screen.screenHandler.rows * 9).drop(9), + Point((screen as AccessorHandledScreen).x_Firmament, screen.y_Firmament)) + overview.drawScrollBar(drawContext) + overview.drawControls(drawContext, mouseX, mouseY) + } + + override fun moveSlot(slot: Slot) { + val index = slot.index + if (index in 0..<36) { + val (x, y) = overview.getPlayerInventorySlotPosition(index) + slot.x = x - (screen as AccessorHandledScreen).x_Firmament + slot.y = y - screen.y_Firmament + } else { + slot.x = -100000 + slot.y = -100000 + } + } + + override fun mouseScrolled( + mouseX: Double, + mouseY: Double, + horizontalAmount: Double, + verticalAmount: Double + ): Boolean { + if (screen.focusedItemStack != null && StorageOverlay.TConfig.itemsBlockScrolling) + return false + return overview.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount) + } +} diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt new file mode 100644 index 0000000..267799d --- /dev/null +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt @@ -0,0 +1,562 @@ +package moe.nea.firmament.features.inventory.storageoverlay + +import io.github.notenoughupdates.moulconfig.common.IMinecraft +import io.github.notenoughupdates.moulconfig.gui.GuiContext +import io.github.notenoughupdates.moulconfig.gui.KeyboardEvent +import io.github.notenoughupdates.moulconfig.gui.MouseEvent +import io.github.notenoughupdates.moulconfig.gui.component.ColumnComponent +import io.github.notenoughupdates.moulconfig.gui.component.PanelComponent +import io.github.notenoughupdates.moulconfig.gui.component.TextComponent +import io.github.notenoughupdates.moulconfig.gui.component.TextFieldComponent +import io.github.notenoughupdates.moulconfig.observer.GetSetter +import io.github.notenoughupdates.moulconfig.observer.Property +import java.util.TreeSet +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.gui.screen.ingame.HandledScreen +import net.minecraft.item.ItemStack +import net.minecraft.screen.slot.Slot +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.SlotRenderEvents +import moe.nea.firmament.gui.EmptyComponent +import moe.nea.firmament.gui.FirmButtonComponent +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.MoulConfigUtils.adopt +import moe.nea.firmament.util.MoulConfigUtils.clickMCComponentInPlace +import moe.nea.firmament.util.MoulConfigUtils.drawMCComponentInPlace +import moe.nea.firmament.util.MoulConfigUtils.typeMCComponentInPlace +import moe.nea.firmament.util.StringUtil.words +import moe.nea.firmament.util.assertTrueOr +import moe.nea.firmament.util.customgui.customGui +import moe.nea.firmament.util.mc.FakeSlot +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.render.drawGuiTexture +import moe.nea.firmament.util.render.enableScissorWithoutTranslation +import moe.nea.firmament.util.tr +import moe.nea.firmament.util.unformattedString + +class StorageOverlayScreen : Screen(Text.literal("")) { + + companion object { + val PLAYER_WIDTH = 184 + val PLAYER_HEIGHT = 91 + val PLAYER_Y_INSET = 3 + val SLOT_SIZE = 18 + val PADDING = 10 + val PAGE_SLOTS_WIDTH = SLOT_SIZE * 9 + val PAGE_WIDTH = PAGE_SLOTS_WIDTH + 4 + val HOTBAR_X = 12 + val HOTBAR_Y = 67 + val MAIN_INVENTORY_Y = 9 + val SCROLL_BAR_WIDTH = 8 + val SCROLL_BAR_HEIGHT = 16 + val CONTROL_X_INSET = 3 + val CONTROL_Y_INSET = 5 + val CONTROL_WIDTH = 70 + val CONTROL_BACKGROUND_WIDTH = CONTROL_WIDTH + CONTROL_X_INSET + 1 + val CONTROL_HEIGHT = 50 + + var scroll: Float = 0F + var lastRenderedInnerHeight = 0 + + fun resetScroll() { + if (!StorageOverlay.TConfig.retainScroll) scroll = 0F + } + } + + var isExiting: Boolean = false + var pageWidthCount = StorageOverlay.TConfig.columns + + inner class Measurements { + val innerScrollPanelWidth = PAGE_WIDTH * pageWidthCount + (pageWidthCount - 1) * PADDING + val overviewWidth = innerScrollPanelWidth + 3 * PADDING + SCROLL_BAR_WIDTH + val x = width / 2 - overviewWidth / 2 + val overviewHeight = minOf( + height - PLAYER_HEIGHT - minOf(80, height / 10), + StorageOverlay.TConfig.height + ) + val innerScrollPanelHeight = overviewHeight - PADDING * 2 + val y = height / 2 - (overviewHeight + PLAYER_HEIGHT) / 2 + val playerX = width / 2 - PLAYER_WIDTH / 2 + val playerY = y + overviewHeight - PLAYER_Y_INSET + val controlX = playerX - CONTROL_WIDTH + CONTROL_X_INSET + val controlY = playerY - CONTROL_Y_INSET + val totalWidth = overviewWidth + val totalHeight = overviewHeight - PLAYER_Y_INSET + PLAYER_HEIGHT + } + + var measurements = Measurements() + + public override fun init() { + super.init() + pageWidthCount = StorageOverlay.TConfig.columns + .coerceAtMost((width - PADDING) / (PAGE_WIDTH + PADDING)) + .coerceAtLeast(1) + measurements = Measurements() + scroll = scroll.coerceAtMost(getMaxScroll()).coerceAtLeast(0F) + } + + override fun mouseScrolled( + mouseX: Double, + mouseY: Double, + horizontalAmount: Double, + verticalAmount: Double + ): Boolean { + coerceScroll(StorageOverlay.adjustScrollSpeed(verticalAmount).toFloat()) + return true + } + + fun coerceScroll(offset: Float) { + scroll = (scroll + offset) + .coerceAtMost(getMaxScroll()) + .coerceAtLeast(0F) + } + + fun getMaxScroll() = lastRenderedInnerHeight.toFloat() - getScrollPanelInner().height + + val playerInventorySprite = Identifier.of("firmament:storageoverlay/player_inventory") + val upperBackgroundSprite = Identifier.of("firmament:storageoverlay/upper_background") + val slotRowSprite = Identifier.of("firmament:storageoverlay/storage_row") + val scrollbarBackground = Identifier.of("firmament:storageoverlay/scroll_bar_background") + val scrollbarKnob = Identifier.of("firmament:storageoverlay/scroll_bar_knob") + val controllerBackground = Identifier.of("firmament:storageoverlay/storage_controls") + + override fun close() { + isExiting = true + resetScroll() + super.close() + } + + override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + super.render(context, mouseX, mouseY, delta) + drawBackgrounds(context) + drawPages(context, mouseX, mouseY, delta, null, null, Point()) + drawScrollBar(context) + drawPlayerInventory(context, mouseX, mouseY, delta) + drawControls(context, mouseX, mouseY) + } + + fun getScrollbarPercentage(): Float { + return scroll / getMaxScroll() + } + + fun drawScrollBar(context: DrawContext) { + val sbRect = getScrollBarRect() + context.drawGuiTexture( + scrollbarBackground, + sbRect.minX, sbRect.minY, + sbRect.width, sbRect.height, + ) + context.drawGuiTexture( + scrollbarKnob, + sbRect.minX, sbRect.minY + (getScrollbarPercentage() * (sbRect.height - SCROLL_BAR_HEIGHT)).toInt(), + SCROLL_BAR_WIDTH, SCROLL_BAR_HEIGHT + ) + } + + fun editPages() { + isExiting = true + MC.instance.send { + val hs = MC.screen as? HandledScreen<*> + if (StorageBackingHandle.fromScreen(hs) is StorageBackingHandle.Overview) { + hs.customGui = null + hs.init(MC.instance, width, height) + } else { + MC.sendCommand("storage") + } + } + } + + val guiContext = GuiContext(EmptyComponent()) + private val knobStub = EmptyComponent() + val editButton = FirmButtonComponent( + TextComponent(tr("firmament.storage-overlay.edit-pages", "Edit Pages").string), + action = ::editPages + ) + val searchText = Property.of("") // TODO: sync with REI + val searchField = TextFieldComponent( + searchText, 100, GetSetter.constant(true), + tr("firmament.storage-overlay.search.suggestion", "Search...").string, + IMinecraft.instance.defaultFontRenderer + ) + val controlComponent = PanelComponent( + ColumnComponent( + searchField, + editButton, + ), + 8, PanelComponent.DefaultBackgroundRenderer.TRANSPARENT + ) + + init { + searchText.addObserver { _, _ -> + layoutedForEach(StorageOverlay.Data.data ?: StorageData(), { _, _, _ -> }) + coerceScroll(0F) + } + guiContext.adopt(knobStub) + guiContext.adopt(controlComponent) + } + + fun drawControls(context: DrawContext, mouseX: Int, mouseY: Int) { + context.drawGuiTexture( + controllerBackground, + measurements.controlX, + measurements.controlY, + CONTROL_BACKGROUND_WIDTH, CONTROL_HEIGHT + ) + context.drawMCComponentInPlace( + controlComponent, + measurements.controlX, measurements.controlY, + CONTROL_WIDTH, CONTROL_HEIGHT, + mouseX, mouseY + ) + } + + fun drawBackgrounds(context: DrawContext) { + context.drawGuiTexture( + upperBackgroundSprite, + measurements.x, + measurements.y, + measurements.overviewWidth, + measurements.overviewHeight + ) + context.drawGuiTexture( + playerInventorySprite, + measurements.playerX, + measurements.playerY, + PLAYER_WIDTH, + PLAYER_HEIGHT + ) + } + + fun getPlayerInventorySlotPosition(int: Int): Pair { + if (int < 9) { + return Pair(measurements.playerX + int * SLOT_SIZE + HOTBAR_X, HOTBAR_Y + measurements.playerY) + } + return Pair( + measurements.playerX + (int % 9) * SLOT_SIZE + HOTBAR_X, + measurements.playerY + (int / 9 - 1) * SLOT_SIZE + MAIN_INVENTORY_Y + ) + } + + fun drawPlayerInventory(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + val items = MC.player?.inventory?.mainStacks ?: return + items.withIndex().forEach { (index, item) -> + val (x, y) = getPlayerInventorySlotPosition(index) + context.drawItem(item, x, y, 0) + context.drawStackOverlay(textRenderer, item, x, y) + } + } + + fun getScrollBarRect(): Rectangle { + return Rectangle( + measurements.x + PADDING + measurements.innerScrollPanelWidth + PADDING, + measurements.y + PADDING, + SCROLL_BAR_WIDTH, + measurements.innerScrollPanelHeight + ) + } + + fun getScrollPanelInner(): Rectangle { + return Rectangle( + measurements.x + PADDING, + measurements.y + PADDING, + measurements.innerScrollPanelWidth, + measurements.innerScrollPanelHeight + ) + } + + fun createScissors(context: DrawContext) { + val rect = getScrollPanelInner() + context.enableScissorWithoutTranslation( + rect.minX.toFloat(), rect.minY.toFloat(), + rect.maxX.toFloat(), rect.maxY.toFloat(), + ) + } + + fun drawPages( + context: DrawContext, mouseX: Int, mouseY: Int, delta: Float, + excluding: StoragePageSlot?, + slots: List?, + slotOffset: Point + ) { + createScissors(context) + val data = StorageOverlay.Data.data ?: StorageData() + layoutedForEach(data) { rect, page, inventory -> + drawPage( + context, + rect.x, + rect.y, + page, inventory, + if (excluding == page) slots else null, + slotOffset + ) + } + context.disableScissor() + } + + + var knobGrabbed: Boolean + get() = guiContext.focusedElement == knobStub + set(value) = knobStub.setFocus(value) + + override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { + return mouseClicked(mouseX, mouseY, button, null) + } + + override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { + if (knobGrabbed) { + knobGrabbed = false + return true + } + if (clickMCComponentInPlace( + controlComponent, + measurements.controlX, measurements.controlY, + CONTROL_WIDTH, CONTROL_HEIGHT, + mouseX.toInt(), mouseY.toInt(), + MouseEvent.Click(button, false) + ) + ) return true + return super.mouseReleased(mouseX, mouseY, button) + } + + override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean { + if (knobGrabbed) { + val sbRect = getScrollBarRect() + val percentage = (mouseY - sbRect.getY()) / sbRect.getHeight() + scroll = (getMaxScroll() * percentage).toFloat() + mouseScrolled(0.0, 0.0, 0.0, 0.0) + return true + } + return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY) + } + + fun mouseClicked(mouseX: Double, mouseY: Double, button: Int, activePage: StoragePageSlot?): Boolean { + guiContext.setFocusedElement(null) // Blur all elements. They will be refocused by clickMCComponentInPlace if in doubt, and we don't have any double click components. + if (getScrollPanelInner().contains(mouseX, mouseY)) { + val data = StorageOverlay.Data.data ?: StorageData() + layoutedForEach(data) { rect, page, _ -> + if (rect.contains(mouseX, mouseY) && activePage != page && button == 0) { + page.navigateTo() + return true + } + } + return false + } + val sbRect = getScrollBarRect() + if (sbRect.contains(mouseX, mouseY)) { + val percentage = (mouseY - sbRect.getY()) / sbRect.getHeight() + scroll = (getMaxScroll() * percentage).toFloat() + mouseScrolled(0.0, 0.0, 0.0, 0.0) + knobGrabbed = true + return true + } + if (clickMCComponentInPlace( + controlComponent, + measurements.controlX, measurements.controlY, + CONTROL_WIDTH, CONTROL_HEIGHT, + mouseX.toInt(), mouseY.toInt(), + MouseEvent.Click(button, true) + ) + ) return true + return false + } + + override fun charTyped(chr: Char, modifiers: Int): Boolean { + if (typeMCComponentInPlace( + controlComponent, + measurements.controlX, measurements.controlY, + CONTROL_WIDTH, CONTROL_HEIGHT, + KeyboardEvent.CharTyped(chr) + ) + ) { + return true + } + return super.charTyped(chr, modifiers) + } + + override fun keyReleased(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + if (typeMCComponentInPlace( + controlComponent, + measurements.controlX, measurements.controlY, + CONTROL_WIDTH, CONTROL_HEIGHT, + KeyboardEvent.KeyPressed(keyCode, false) + ) + ) { + return true + } + return super.keyReleased(keyCode, scanCode, modifiers) + } + + override fun shouldCloseOnEsc(): Boolean { + return this === MC.screen // Fixes this UI closing the handled screen on Escape press. + } + + override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + if (typeMCComponentInPlace( + controlComponent, + measurements.controlX, measurements.controlY, + CONTROL_WIDTH, CONTROL_HEIGHT, + KeyboardEvent.KeyPressed(keyCode, true) + ) + ) { + return true + } + return super.keyPressed(keyCode, scanCode, modifiers) + } + + + var searchCache: String? = null + var filteredPagesCache = setOf() + + fun getFilteredPages(): Set { + val searchValue = searchText.get() + val data = StorageOverlay.Data.data ?: return filteredPagesCache // Do not update cache if data is missing + if (searchCache == searchValue) return filteredPagesCache + val result = + data.storageInventories + .entries.asSequence() + .filter { it.value.inventory?.stacks?.any { matchesSearch(it, searchValue) } ?: true } + .map { it.key } + .toSet() + searchCache = searchValue + filteredPagesCache = result + return result + } + + + fun matchesSearch(itemStack: ItemStack, search: String): Boolean { + val searchWords = search.words().toCollection(TreeSet()) + fun removePrefixes(value: String) { + searchWords.removeIf { value.contains(it, ignoreCase = true) } + } + itemStack.displayNameAccordingToNbt.unformattedString.words().forEach(::removePrefixes) + if (searchWords.isEmpty()) return true + itemStack.loreAccordingToNbt.forEach { + it.unformattedString.words().forEach(::removePrefixes) + } + return searchWords.isEmpty() + } + + private inline fun layoutedForEach( + data: StorageData, + func: ( + rectangle: Rectangle, + page: StoragePageSlot, inventory: StorageData.StorageInventory, + ) -> Unit + ) { + var yOffset = -scroll.toInt() + var xOffset = 0 + var maxHeight = 0 + val filter = getFilteredPages() + for ((page, inventory) in data.storageInventories.entries) { + if (page !in filter) continue + val currentHeight = inventory.inventory?.let { it.rows * SLOT_SIZE + 6 + textRenderer.fontHeight } + ?: 18 + maxHeight = maxOf(maxHeight, currentHeight) + val rect = Rectangle( + measurements.x + PADDING + (PAGE_WIDTH + PADDING) * xOffset, + yOffset + measurements.y + PADDING, + PAGE_WIDTH, + currentHeight + ) + func(rect, page, inventory) + xOffset++ + if (xOffset >= pageWidthCount) { + yOffset += maxHeight + xOffset = 0 + maxHeight = 0 + } + } + lastRenderedInnerHeight = maxHeight + yOffset + scroll.toInt() + } + + fun drawPage( + context: DrawContext, + x: Int, + y: Int, + page: StoragePageSlot, + inventory: StorageData.StorageInventory, + slots: List?, + slotOffset: Point, + ): Int { + val inv = inventory.inventory + if (inv == null) { + context.drawGuiTexture(upperBackgroundSprite, x, y, PAGE_WIDTH, 18) + context.drawText( + textRenderer, + Text.literal("TODO: open this page"), + x + 4, + y + 4, + -1, + true + ) + return 18 + } + assertTrueOr(slots == null || slots.size == inv.stacks.size) { return 0 } + val name = page.defaultName() + val pageHeight = inv.rows * SLOT_SIZE + 8 + textRenderer.fontHeight + if (slots != null && StorageOverlay.TConfig.outlineActiveStoragePage) + context.drawBorder( + x, + y + 3 + textRenderer.fontHeight, + PAGE_WIDTH, + inv.rows * SLOT_SIZE + 4, + StorageOverlay.TConfig.outlineActiveStoragePageColour.getEffectiveColourRGB() + ) + context.drawText( + textRenderer, Text.literal(name), x + 6, y + 3, + if (slots == null) 0xFFFFFFFF.toInt() else 0xFFFFFF00.toInt(), true + ) + context.drawGuiTexture( + slotRowSprite, + x + 2, + y + 5 + textRenderer.fontHeight, + PAGE_SLOTS_WIDTH, + inv.rows * SLOT_SIZE + ) + inv.stacks.forEachIndexed { index, stack -> + val slotX = (index % 9) * SLOT_SIZE + x + 3 + val slotY = (index / 9) * SLOT_SIZE + y + 5 + textRenderer.fontHeight + 1 + val fakeSlot = FakeSlot(stack, slotX, slotY) + if (slots == null) { + SlotRenderEvents.Before.publish(SlotRenderEvents.Before(context, fakeSlot)) + context.drawItem(stack, slotX, slotY) + context.drawStackOverlay(textRenderer, stack, slotX, slotY) + SlotRenderEvents.After.publish(SlotRenderEvents.After(context, fakeSlot)) + } else { + val slot = slots[index] + slot.x = slotX - slotOffset.x + slot.y = slotY - slotOffset.y + } + } + return pageHeight + 6 + } + + fun getBounds(): List { + return listOf( + Rectangle( + measurements.x, + measurements.y, + measurements.overviewWidth, + measurements.overviewHeight + ), + Rectangle( + measurements.playerX, + measurements.playerY, + PLAYER_WIDTH, + PLAYER_HEIGHT + ), + Rectangle( + measurements.controlX, + measurements.controlY, + CONTROL_WIDTH, + CONTROL_HEIGHT + ) + ) + } +} diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverviewScreen.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverviewScreen.kt new file mode 100644 index 0000000..3462d3d --- /dev/null +++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverviewScreen.kt @@ -0,0 +1,135 @@ + + +package moe.nea.firmament.features.inventory.storageoverlay + +import org.lwjgl.glfw.GLFW +import kotlin.math.max +import net.minecraft.block.Blocks +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.gui.screen.Screen +import net.minecraft.item.Item +import net.minecraft.item.Items +import net.minecraft.text.Text +import net.minecraft.util.DyeColor +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.toShedaniel + +class StorageOverviewScreen() : Screen(Text.empty()) { + companion object { + val emptyStorageSlotItems = listOf( + Blocks.RED_STAINED_GLASS_PANE.asItem(), + Blocks.BROWN_STAINED_GLASS_PANE.asItem(), + Items.GRAY_DYE + ) + val pageWidth get() = 19 * 9 + + var scroll = 0 + var lastRenderedHeight = 0 + } + + val content = StorageOverlay.Data.data ?: StorageData() + var isClosing = false + + override fun init() { + super.init() + scroll = scroll.coerceAtMost(getMaxScroll()).coerceAtLeast(0) + } + + override fun close() { + if (!StorageOverlay.TConfig.retainScroll) scroll = 0 + super.close() + } + + override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + super.render(context, mouseX, mouseY, delta) + context.fill(0, 0, width, height, 0x90000000.toInt()) + layoutedForEach { (key, value), offsetX, offsetY -> + context.matrices.push() + context.matrices.translate(offsetX.toFloat(), offsetY.toFloat(), 0F) + renderStoragePage(context, value, mouseX - offsetX, mouseY - offsetY) + context.matrices.pop() + } + } + + inline fun layoutedForEach(onEach: (data: Pair, offsetX: Int, offsetY: Int) -> Unit) { + var offsetY = 0 + var currentMaxHeight = StorageOverlay.config.margin - StorageOverlay.config.padding - scroll + var totalHeight = -currentMaxHeight + content.storageInventories.onEachIndexed { index, (key, value) -> + val pageX = (index % StorageOverlay.config.columns) + if (pageX == 0) { + currentMaxHeight += StorageOverlay.config.padding + offsetY += currentMaxHeight + totalHeight += currentMaxHeight + currentMaxHeight = 0 + } + val xPosition = + width / 2 - (StorageOverlay.config.columns * (pageWidth + StorageOverlay.config.padding) - StorageOverlay.config.padding) / 2 + pageX * (pageWidth + StorageOverlay.config.padding) + onEach(Pair(key, value), xPosition, offsetY) + val height = getStorePageHeight(value) + currentMaxHeight = max(currentMaxHeight, height) + } + lastRenderedHeight = totalHeight + currentMaxHeight + } + + override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { + layoutedForEach { (k, p), x, y -> + val rx = mouseX - x + val ry = mouseY - y + if (rx in (0.0..pageWidth.toDouble()) && ry in (0.0..getStorePageHeight(p).toDouble())) { + close() + StorageOverlay.lastStorageOverlay = this + k.navigateTo() + return true + } + } + return super.mouseClicked(mouseX, mouseY, button) + } + + fun getStorePageHeight(page: StorageData.StorageInventory): Int { + return page.inventory?.rows?.let { it * 19 + MC.font.fontHeight + 2 } ?: 60 + } + + override fun mouseScrolled( + mouseX: Double, + mouseY: Double, + horizontalAmount: Double, + verticalAmount: Double + ): Boolean { + scroll = + (scroll + StorageOverlay.adjustScrollSpeed(verticalAmount)).toInt() + .coerceAtMost(getMaxScroll()).coerceAtLeast(0) + return true + } + + private fun getMaxScroll() = lastRenderedHeight - height + 2 * StorageOverlay.config.margin + + private fun renderStoragePage(context: DrawContext, page: StorageData.StorageInventory, mouseX: Int, mouseY: Int) { + context.drawText(MC.font, page.title, 2, 2, -1, true) + val inventory = page.inventory + if (inventory == null) { + // TODO: Missing texture + context.fill(0, 0, pageWidth, 60, DyeColor.RED.toShedaniel().darker(4.0).color) + context.drawCenteredTextWithShadow(MC.font, Text.literal("Not loaded yet"), pageWidth / 2, 30, -1) + return + } + + for ((index, stack) in inventory.stacks.withIndex()) { + val x = (index % 9) * 19 + val y = (index / 9) * 19 + MC.font.fontHeight + 2 + if (((mouseX - x) in 0 until 18) && ((mouseY - y) in 0 until 18)) { + context.fill(x, y, x + 18, y + 18, 0x80808080.toInt()) + } else { + context.fill(x, y, x + 18, y + 18, 0x40808080.toInt()) + } + context.drawItem(stack, x + 1, y + 1) + context.drawStackOverlay(MC.font, stack, x + 1, y + 1) + } + } + + override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + if (keyCode == GLFW.GLFW_KEY_ESCAPE) + isClosing = true + return super.keyPressed(keyCode, scanCode, modifiers) + } +} diff --git a/src/main/kotlin/features/inventory/storageoverlay/StoragePageSlot.kt b/src/main/kotlin/features/inventory/storageoverlay/StoragePageSlot.kt new file mode 100644 index 0000000..9259415 --- /dev/null +++ b/src/main/kotlin/features/inventory/storageoverlay/StoragePageSlot.kt @@ -0,0 +1,66 @@ + + +package moe.nea.firmament.features.inventory.storageoverlay + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import moe.nea.firmament.util.MC + +@Serializable(with = StoragePageSlot.Serializer::class) +data class StoragePageSlot(val index: Int) : Comparable { + object Serializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("StoragePageSlot", PrimitiveKind.INT) + + override fun deserialize(decoder: Decoder): StoragePageSlot { + return StoragePageSlot(decoder.decodeInt()) + } + + override fun serialize(encoder: Encoder, value: StoragePageSlot) { + encoder.encodeInt(value.index) + } + } + + init { + assert(index in 0 until (3 * 9)) + } + + val isEnderChest get() = index < 9 + val isBackPack get() = !isEnderChest + val slotIndexInOverviewPage get() = if (isEnderChest) index + 9 else index + 18 + fun defaultName(): String = if (isEnderChest) "Ender Chest #${index + 1}" else "Backpack #${index - 9 + 1}" + + fun navigateTo() { + if (isBackPack) { + MC.sendCommand("backpack ${index - 9 + 1}") + } else { + MC.sendCommand("enderchest ${index + 1}") + } + } + + companion object { + fun fromOverviewSlotIndex(slot: Int): StoragePageSlot? { + if (slot in 9 until 18) return StoragePageSlot(slot - 9) + if (slot in 27 until 45) return StoragePageSlot(slot - 27 + 9) + return null + } + + fun ofEnderChestPage(slot: Int): StoragePageSlot { + assert(slot in 1..9) + return StoragePageSlot(slot - 1) + } + + fun ofBackPackPage(slot: Int): StoragePageSlot { + assert(slot in 1..18) + return StoragePageSlot(slot - 1 + 9) + } + } + + override fun compareTo(other: StoragePageSlot): Int { + return this.index - other.index + } +} diff --git a/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt b/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt new file mode 100644 index 0000000..d99acd7 --- /dev/null +++ b/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt @@ -0,0 +1,76 @@ +package moe.nea.firmament.features.inventory.storageoverlay + +import io.ktor.util.decodeBase64Bytes +import io.ktor.util.encodeBase64 +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlin.jvm.optionals.getOrNull +import net.minecraft.item.ItemStack +import net.minecraft.nbt.NbtCompound +import net.minecraft.nbt.NbtIo +import net.minecraft.nbt.NbtList +import net.minecraft.nbt.NbtOps +import net.minecraft.nbt.NbtSizeTracker +import net.minecraft.registry.RegistryOps +import moe.nea.firmament.util.ErrorUtil +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.mc.TolerantRegistriesOps + +@Serializable(with = VirtualInventory.Serializer::class) +data class VirtualInventory( + val stacks: List +) { + val rows = stacks.size / 9 + + init { + assert(stacks.size % 9 == 0) + assert(stacks.size / 9 in 1..5) + } + + + object Serializer : KSerializer { + const val INVENTORY = "INVENTORY" + override val descriptor: SerialDescriptor + get() = PrimitiveSerialDescriptor("VirtualInventory", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): VirtualInventory { + val s = decoder.decodeString() + val n = NbtIo.readCompressed(ByteArrayInputStream(s.decodeBase64Bytes()), NbtSizeTracker.of(100_000_000)) + val items = n.getList(INVENTORY).getOrNull() + val ops = getOps() + return VirtualInventory(items?.map { + it as NbtCompound + if (it.isEmpty) ItemStack.EMPTY + else ErrorUtil.catch("Could not deserialize item") { + ItemStack.CODEC.parse(ops, it).orThrow + }.or { ItemStack.EMPTY } + } ?: listOf()) + } + + fun getOps() = TolerantRegistriesOps(NbtOps.INSTANCE, MC.currentOrDefaultRegistries) + + override fun serialize(encoder: Encoder, value: VirtualInventory) { + val list = NbtList() + val ops = getOps() + value.stacks.forEach { + if (it.isEmpty) list.add(NbtCompound()) + else list.add(ErrorUtil.catch("Could not serialize item") { + ItemStack.CODEC.encode(it, + ops, + NbtCompound()).orThrow + } + .or { NbtCompound() }) + } + val baos = ByteArrayOutputStream() + NbtIo.writeCompressed(NbtCompound().also { it.put(INVENTORY, list) }, baos) + encoder.encodeString(baos.toByteArray().encodeBase64()) + } + } +} diff --git a/src/main/kotlin/features/items/BlockZapperOverlay.kt b/src/main/kotlin/features/items/BlockZapperOverlay.kt new file mode 100644 index 0000000..c207d67 --- /dev/null +++ b/src/main/kotlin/features/items/BlockZapperOverlay.kt @@ -0,0 +1,146 @@ +package moe.nea.firmament.features.items + +import io.github.notenoughupdates.moulconfig.ChromaColour +import java.util.LinkedList +import net.minecraft.block.Block +import net.minecraft.block.BlockState +import net.minecraft.block.Blocks +import net.minecraft.util.hit.BlockHitResult +import net.minecraft.util.hit.HitResult +import net.minecraft.util.math.BlockPos +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ClientStartedEvent +import moe.nea.firmament.events.WorldKeyboardEvent +import moe.nea.firmament.events.WorldRenderLastEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.render.RenderInWorldContext +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.skyblock.SkyBlockItems + +object BlockZapperOverlay : FirmamentFeature { + override val identifier: String + get() = "block-zapper-overlay" + + object TConfig : ManagedConfig(identifier, Category.ITEMS) { + var blockZapperOverlay by toggle("block-zapper-overlay") { false } + val color by colour("color") { ChromaColour.fromStaticRGB(160, 0, 0, 60) } + var undoKey by keyBindingWithDefaultUnbound("undo-key") + } + + @Subscribe + fun onInit(event: ClientStartedEvent) { + } + + override val config: ManagedConfig + get() = TConfig + + val bannedZapper: List = listOf( + Blocks.WHEAT, + Blocks.CARROTS, + Blocks.POTATOES, + Blocks.PUMPKIN, + Blocks.PUMPKIN_STEM, + Blocks.MELON, + Blocks.MELON_STEM, + Blocks.CACTUS, + Blocks.SUGAR_CANE, + Blocks.NETHER_WART, + Blocks.TALL_GRASS, + Blocks.SUNFLOWER, + Blocks.FARMLAND, + Blocks.BREWING_STAND, + Blocks.SNOW, + Blocks.RED_MUSHROOM, + Blocks.BROWN_MUSHROOM, + ) + + private val zapperOffsets: List = listOf( + BlockPos(0, 0, -1), + BlockPos(0, 0, 1), + BlockPos(-1, 0, 0), + BlockPos(1, 0, 0), + BlockPos(0, 1, 0), + BlockPos(0, -1, 0) + ) + + // Skidded from NEU + // Credit: https://github.com/NotEnoughUpdates/NotEnoughUpdates/blob/9b1fcfebc646e9fb69f99006327faa3e734e5f51/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/CustomItemEffects.java#L1281-L1355 (Modified) + @Subscribe + fun renderBlockZapperOverlay(event: WorldRenderLastEvent) { + if (!TConfig.blockZapperOverlay) return + val player = MC.player ?: return + val world = player.world ?: return + val heldItem = MC.stackInHand + if (heldItem.skyBlockId != SkyBlockItems.BLOCK_ZAPPER) return + val hitResult = MC.instance.crosshairTarget ?: return + + val zapperBlocks: HashSet = HashSet() + val returnablePositions = LinkedList() + + if (hitResult is BlockHitResult && hitResult.type == HitResult.Type.BLOCK) { + var pos: BlockPos = hitResult.blockPos + val firstBlockState: BlockState = world.getBlockState(pos) + val block = firstBlockState.block + + val initialAboveBlock = world.getBlockState(pos.up()).block + if (!bannedZapper.contains(initialAboveBlock) && !bannedZapper.contains(block)) { + var i = 0 + while (i < 164) { + zapperBlocks.add(pos) + returnablePositions.remove(pos) + + val availableNeighbors: MutableList = ArrayList() + + for (offset in zapperOffsets) { + val newPos = pos.add(offset) + + if (zapperBlocks.contains(newPos)) continue + + val state: BlockState? = world.getBlockState(newPos) + if (state != null && state.block === block) { + val above = newPos.up() + val aboveBlock = world.getBlockState(above).block + if (!bannedZapper.contains(aboveBlock)) { + availableNeighbors.add(newPos) + } + } + } + + if (availableNeighbors.size >= 2) { + returnablePositions.add(pos) + pos = availableNeighbors[0] + } else if (availableNeighbors.size == 1) { + pos = availableNeighbors[0] + } else if (returnablePositions.isEmpty()) { + break + } else { + i-- + pos = returnablePositions.last() + } + + i++ + } + } + + RenderInWorldContext.renderInWorld(event) { + if (MC.player?.isSneaking ?: false) { + zapperBlocks.forEach { + block(it, TConfig.color.getEffectiveColourRGB()) + } + } else { + sharedVoxelSurface(zapperBlocks, TConfig.color.getEffectiveColourRGB()) + } + } + } + } + + @Subscribe + fun onWorldKeyboard(it: WorldKeyboardEvent) { + if (!TConfig.undoKey.isBound) return + if (!it.matches(TConfig.undoKey)) return + if (MC.stackInHand.skyBlockId != SkyBlockItems.BLOCK_ZAPPER) return + MC.sendCommand("undozap") + } +} diff --git a/src/main/kotlin/features/items/BonemerangOverlay.kt b/src/main/kotlin/features/items/BonemerangOverlay.kt new file mode 100644 index 0000000..ffdffe3 --- /dev/null +++ b/src/main/kotlin/features/items/BonemerangOverlay.kt @@ -0,0 +1,101 @@ +package moe.nea.firmament.features.items + +import me.shedaniel.math.Color +import moe.nea.jarvis.api.Point +import net.minecraft.entity.LivingEntity +import net.minecraft.entity.decoration.ArmorStandEntity +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.util.Formatting +import net.minecraft.util.math.Box +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ClientStartedEvent +import moe.nea.firmament.events.EntityRenderTintEvent +import moe.nea.firmament.events.HudRenderEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.render.TintedOverlayTexture +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.skyblock.SkyBlockItems +import moe.nea.firmament.util.tr + +object BonemerangOverlay : FirmamentFeature { + override val identifier: String + get() = "bonemerang-overlay" + + object TConfig : ManagedConfig(identifier, Category.ITEMS) { + var bonemerangOverlay by toggle("bonemerang-overlay") { false } + val bonemerangOverlayHud by position("bonemerang-overlay-hud", 80, 10) { Point(0.1, 1.0) } + var highlightHitEntities by toggle("highlight-hit-entities") { false } + } + + @Subscribe + fun onInit(event: ClientStartedEvent) { + } + + override val config: ManagedConfig + get() = TConfig + + fun getEntities(): MutableSet { + val entities = mutableSetOf() + val camera = MC.camera as? PlayerEntity ?: return entities + val player = MC.player ?: return entities + val world = player.world ?: return entities + + val cameraPos = camera.eyePos + val rayDirection = camera.rotationVector.normalize() + val endPos = cameraPos.add(rayDirection.multiply(15.0)) + val foundEntities = world.getOtherEntities(camera, Box(cameraPos, endPos).expand(1.0)) + + for (entity in foundEntities) { + if (entity !is LivingEntity || entity is ArmorStandEntity || entity.isInvisible) continue + val hitResult = entity.boundingBox.expand(0.35).raycast(cameraPos, endPos).orElse(null) + if (hitResult != null) entities.add(entity) + } + + return entities + } + + + val throwableWeapons = listOf( + SkyBlockItems.BONE_BOOMERANG, SkyBlockItems.STARRED_BONE_BOOMERANG, + SkyBlockItems.TRIBAL_SPEAR, + ) + + + @Subscribe + fun onEntityRender(event: EntityRenderTintEvent) { + if (!TConfig.highlightHitEntities) return + if (MC.stackInHand.skyBlockId !in throwableWeapons) return + + val entities = getEntities() + if (entities.isEmpty()) return + if (event.entity !in entities) return + + val tintOverlay by lazy { + TintedOverlayTexture().setColor(Color.ofOpaque(Formatting.BLUE.colorValue!!)) + } + + event.renderState.overlayTexture_firmament = tintOverlay + } + + + @Subscribe + fun onRenderHud(it: HudRenderEvent) { + if (!TConfig.bonemerangOverlay) return + if (MC.stackInHand.skyBlockId !in throwableWeapons) return + + val entities = getEntities() + + it.context.matrices.push() + TConfig.bonemerangOverlayHud.applyTransformations(it.context.matrices) + it.context.drawText( + MC.font, String.format( + tr( + "firmament.bonemerang-overlay.bonemerang-overlay.display", "Bonemerang Targets: %s" + ).string, entities.size + ), 0, 0, -1, true + ) + it.context.matrices.pop() + } +} diff --git a/src/main/kotlin/features/items/EtherwarpOverlay.kt b/src/main/kotlin/features/items/EtherwarpOverlay.kt new file mode 100644 index 0000000..f6ab1a2 --- /dev/null +++ b/src/main/kotlin/features/items/EtherwarpOverlay.kt @@ -0,0 +1,54 @@ +package moe.nea.firmament.features.items + +import io.github.notenoughupdates.moulconfig.ChromaColour +import me.shedaniel.math.Color +import net.minecraft.util.hit.BlockHitResult +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.WorldRenderLastEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.extraAttributes +import moe.nea.firmament.util.render.RenderInWorldContext +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.skyblock.SkyBlockItems + +object EtherwarpOverlay : FirmamentFeature { + override val identifier: String + get() = "etherwarp-overlay" + + object TConfig : ManagedConfig(identifier, Category.ITEMS) { + var etherwarpOverlay by toggle("etherwarp-overlay") { false } + var onlyShowWhileSneaking by toggle("only-show-while-sneaking") { true } + var cube by toggle("cube") { true } + val cubeColour by colour("cube-colour") { ChromaColour.fromStaticRGB(172, 0, 255, 60) } + var wireframe by toggle("wireframe") { false } + } + + override val config: ManagedConfig + get() = TConfig + + + @Subscribe + fun renderEtherwarpOverlay(event: WorldRenderLastEvent) { + if (!TConfig.etherwarpOverlay) return + val player = MC.player ?: return + if (TConfig.onlyShowWhileSneaking && !player.isSneaking) return + val world = player.world + val camera = MC.camera ?: return + val heldItem = MC.stackInHand + if (heldItem.skyBlockId !in listOf(SkyBlockItems.ASPECT_OF_THE_VOID, SkyBlockItems.ASPECT_OF_THE_END)) return + if (!heldItem.extraAttributes.contains("ethermerge")) return + + val hitResult = camera.raycast(61.0, 0.0f, false) + if (hitResult !is BlockHitResult) return + val blockPos = hitResult.blockPos + if (camera.squaredDistanceTo(blockPos.toCenterPos()) > 61 * 61) return + if (!world.getBlockState(blockPos.up()).isAir) return + if (!world.getBlockState(blockPos.up(2)).isAir) return + RenderInWorldContext.renderInWorld(event) { + if (TConfig.cube) block(blockPos, TConfig.cubeColour.getEffectiveColourRGB()) + if (TConfig.wireframe) wireframeCube(blockPos, 10f) + } + } +} diff --git a/src/main/kotlin/features/macros/ComboProcessor.kt b/src/main/kotlin/features/macros/ComboProcessor.kt new file mode 100644 index 0000000..5c5ac0e --- /dev/null +++ b/src/main/kotlin/features/macros/ComboProcessor.kt @@ -0,0 +1,114 @@ +package moe.nea.firmament.features.macros + +import kotlin.time.Duration.Companion.seconds +import net.minecraft.client.util.InputUtil +import net.minecraft.text.Text +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HudRenderEvent +import moe.nea.firmament.events.TickEvent +import moe.nea.firmament.events.WorldKeyboardEvent +import moe.nea.firmament.keybindings.SavedKeyBinding +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.tr + +object ComboProcessor { + + var rootTrie: Branch = Branch(mapOf()) + private set + + var activeTrie: Branch = rootTrie + private set + + var isInputting = false + var lastInput = TimeMark.farPast() + val breadCrumbs = mutableListOf() + + init { + val f = SavedKeyBinding(InputUtil.GLFW_KEY_F) + val one = SavedKeyBinding(InputUtil.GLFW_KEY_1) + val two = SavedKeyBinding(InputUtil.GLFW_KEY_2) + setActions( + MacroData.DConfig.data.comboActions + ) + } + + fun setActions(actions: List) { + rootTrie = KeyComboTrie.fromComboList(actions) + reset() + } + + fun reset() { + activeTrie = rootTrie + lastInput = TimeMark.now() + isInputting = false + breadCrumbs.clear() + } + + @Subscribe + fun onTick(event: TickEvent) { + if (isInputting && lastInput.passedTime() > 3.seconds) + reset() + } + + + @Subscribe + fun onRender(event: HudRenderEvent) { + if (!isInputting) return + if (!event.isRenderingHud) return + event.context.matrices.push() + val width = 120 + event.context.matrices.translate( + (MC.window.scaledWidth - width) / 2F, + (MC.window.scaledHeight) / 2F + 8, + 0F + ) + val breadCrumbText = breadCrumbs.joinToString(" > ") + event.context.drawText( + MC.font, + tr("firmament.combo.active", "Current Combo: ").append(breadCrumbText), + 0, + 0, + -1, + true + ) + event.context.matrices.translate(0F, MC.font.fontHeight + 2F, 0F) + for ((key, value) in activeTrie.nodes) { + event.context.drawText( + MC.font, + Text.literal("$breadCrumbText > $key: ").append(value.label), + 0, + 0, + -1, + true + ) + event.context.matrices.translate(0F, MC.font.fontHeight + 1F, 0F) + } + event.context.matrices.pop() + } + + @Subscribe + fun onKeyBinding(event: WorldKeyboardEvent) { + val nextEntry = activeTrie.nodes.entries + .find { event.matches(it.key) } + if (nextEntry == null) { + reset() + return + } + event.cancel() + breadCrumbs.add(nextEntry.key) + lastInput = TimeMark.now() + isInputting = true + val value = nextEntry.value + when (value) { + is Branch -> { + activeTrie = value + } + + is Leaf -> { + value.execute() + reset() + } + }.let { } + } +} diff --git a/src/main/kotlin/features/macros/HotkeyAction.kt b/src/main/kotlin/features/macros/HotkeyAction.kt new file mode 100644 index 0000000..011f797 --- /dev/null +++ b/src/main/kotlin/features/macros/HotkeyAction.kt @@ -0,0 +1,40 @@ +package moe.nea.firmament.features.macros + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import net.minecraft.text.Text +import moe.nea.firmament.util.MC + +@Serializable +sealed interface HotkeyAction { + // TODO: execute + val label: Text + fun execute() +} + +@Serializable +@SerialName("command") +data class CommandAction(val command: String) : HotkeyAction { + override val label: Text + get() = Text.literal("/$command") + + override fun execute() { + MC.sendCommand(command) + } +} + +// Mit onscreen anzeige: +// F -> 1 /equipment +// F -> 2 /wardrobe +// Bei Combos: Keys buffern! (für wardrobe hotkeys beispielsweiße) + +// Radial menu +// Hold F +// Weight (mach eins doppelt so groß) +// /equipment +// /wardrobe + +// Bei allen: Filter! +// - Nur in Dungeons / andere Insel +// - Nur wenn ich Item X im inventar habe (fishing rod) + diff --git a/src/main/kotlin/features/macros/KeyComboTrie.kt b/src/main/kotlin/features/macros/KeyComboTrie.kt new file mode 100644 index 0000000..452bc56 --- /dev/null +++ b/src/main/kotlin/features/macros/KeyComboTrie.kt @@ -0,0 +1,73 @@ +package moe.nea.firmament.features.macros + +import kotlinx.serialization.Serializable +import net.minecraft.text.Text +import moe.nea.firmament.keybindings.SavedKeyBinding +import moe.nea.firmament.util.ErrorUtil + +sealed interface KeyComboTrie { + val label: Text + + companion object { + fun fromComboList( + combos: List, + ): Branch { + val root = Branch(mutableMapOf()) + for (combo in combos) { + var p = root + if (combo.keys.isEmpty()) { + ErrorUtil.softUserError("Key Combo for ${combo.action.label.string} is empty") + continue + } + for ((index, key) in combo.keys.withIndex()) { + val m = (p.nodes as MutableMap) + if (index == combo.keys.lastIndex) { + if (key in m) { + ErrorUtil.softUserError("Overlapping actions found for ${combo.keys.joinToString(" > ")} (another action ${m[key]} already exists).") + break + } + + m[key] = Leaf(combo.action) + } else { + val c = m.getOrPut(key) { Branch(mutableMapOf()) } + if (c !is Branch) { + ErrorUtil.softUserError("Overlapping actions found for ${combo.keys} (final node exists at index $index) through another action already") + break + } else { + p = c + } + } + } + } + return root + } + } +} + +@Serializable +data class MacroWheel( + val key: SavedKeyBinding, + val options: List +) + +@Serializable +data class ComboKeyAction( + val action: HotkeyAction, + val keys: List, +) + +data class Leaf(val action: HotkeyAction) : KeyComboTrie { + override val label: Text + get() = action.label + + fun execute() { + action.execute() + } +} + +data class Branch( + val nodes: Map +) : KeyComboTrie { + override val label: Text + get() = Text.literal("...") // TODO: better labels +} diff --git a/src/main/kotlin/features/macros/MacroData.kt b/src/main/kotlin/features/macros/MacroData.kt new file mode 100644 index 0000000..91de423 --- /dev/null +++ b/src/main/kotlin/features/macros/MacroData.kt @@ -0,0 +1,12 @@ +package moe.nea.firmament.features.macros + +import kotlinx.serialization.Serializable +import moe.nea.firmament.util.data.DataHolder + +@Serializable +data class MacroData( + var comboActions: List = listOf(), + var wheels: List = listOf(), +) { + object DConfig : DataHolder(kotlinx.serialization.serializer(), "macros", ::MacroData) +} diff --git a/src/main/kotlin/features/macros/MacroUI.kt b/src/main/kotlin/features/macros/MacroUI.kt new file mode 100644 index 0000000..8c22c5c --- /dev/null +++ b/src/main/kotlin/features/macros/MacroUI.kt @@ -0,0 +1,285 @@ +package moe.nea.firmament.features.macros + +import io.github.notenoughupdates.moulconfig.gui.CloseEventListener +import io.github.notenoughupdates.moulconfig.observer.ObservableList +import io.github.notenoughupdates.moulconfig.xml.Bind +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.gui.config.AllConfigsGui.toObservableList +import moe.nea.firmament.gui.config.KeyBindingStateManager +import moe.nea.firmament.keybindings.SavedKeyBinding +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.MoulConfigUtils +import moe.nea.firmament.util.ScreenUtil + +class MacroUI { + + + companion object { + @Subscribe + fun onCommands(event: CommandEvent.SubCommand) { + // TODO: add button in config + event.subcommand("macros") { + thenExecute { + ScreenUtil.setScreenLater(MoulConfigUtils.loadScreen("config/macros/index", MacroUI(), null)) + } + } + } + + } + + @field:Bind("combos") + val combos = Combos() + + @field:Bind("wheels") + val wheels = Wheels() + var dontSave = false + + @Bind + fun beforeClose(): CloseEventListener.CloseAction { + if (!dontSave) + save() + return CloseEventListener.CloseAction.NO_OBJECTIONS_TO_CLOSE + } + + fun save() { + MacroData.DConfig.data.comboActions = combos.actions.map { it.asSaveable() } + MacroData.DConfig.data.wheels = wheels.wheels.map { it.asSaveable() } + MacroData.DConfig.markDirty() + RadialMacros.setWheels(MacroData.DConfig.data.wheels) + ComboProcessor.setActions(MacroData.DConfig.data.comboActions) + } + + fun discard() { + dontSave = true + MC.screen?.close() + } + + class Command( + @field:Bind("text") + var text: String, + val parent: Wheel, + ) { + @Bind + fun delete() { + parent.editableCommands.removeIf { it === this } + parent.editableCommands.update() + parent.commands.update() + } + + fun asCommandAction() = CommandAction(text) + } + + inner class Wheel( + val parent: Wheels, + var binding: SavedKeyBinding, + commands: List, + ) { + + fun asSaveable(): MacroWheel { + return MacroWheel(binding, commands.map { it.asCommandAction() }) + } + + @Bind("keyCombo") + fun text() = binding.format().string + + @field:Bind("commands") + val commands = commands.mapTo(ObservableList(mutableListOf())) { Command(it.command, this) } + + @field:Bind("editableCommands") + val editableCommands = this.commands.toObservableList() + + @Bind + fun addOption() { + editableCommands.add(Command("", this)) + } + + @Bind + fun back() { + MC.screen?.close() + } + + @Bind + fun edit() { + MC.screen = MoulConfigUtils.loadScreen("config/macros/editor_wheel", this, MC.screen) + } + + @Bind + fun delete() { + parent.wheels.removeIf { it === this } + parent.wheels.update() + } + + val sm = KeyBindingStateManager( + { binding }, + { binding = it }, + ::blur, + ::requestFocus + ) + + @field:Bind + val button = sm.createButton() + + init { + sm.updateLabel() + } + + fun blur() { + button.blur() + } + + + fun requestFocus() { + button.requestFocus() + } + } + + inner class Wheels { + @field:Bind("wheels") + val wheels: ObservableList = MacroData.DConfig.data.wheels.mapTo(ObservableList(mutableListOf())) { + Wheel(this, it.key, it.options.map { CommandAction((it as CommandAction).command) }) + } + + @Bind + fun discard() { + this@MacroUI.discard() + } + + @Bind + fun saveAndClose() { + this@MacroUI.saveAndClose() + } + + @Bind + fun save() { + this@MacroUI.save() + } + + @Bind + fun addWheel() { + wheels.add(Wheel(this, SavedKeyBinding.unbound(), listOf())) + } + } + + fun saveAndClose() { + save() + MC.screen?.close() + } + + inner class Combos { + @field:Bind("actions") + val actions: ObservableList = ObservableList( + MacroData.DConfig.data.comboActions.mapTo(mutableListOf()) { + ActionEditor(it, this) + } + ) + + @Bind + fun addCommand() { + actions.add( + ActionEditor( + ComboKeyAction( + CommandAction("ac Hello from a Firmament Hotkey"), + listOf() + ), + this + ) + ) + } + + @Bind + fun discard() { + this@MacroUI.discard() + } + + @Bind + fun saveAndClose() { + this@MacroUI.saveAndClose() + } + + @Bind + fun save() { + this@MacroUI.save() + } + } + + class KeyBindingEditor(var binding: SavedKeyBinding, val parent: ActionEditor) { + val sm = KeyBindingStateManager( + { binding }, + { binding = it }, + ::blur, + ::requestFocus + ) + + @field:Bind + val button = sm.createButton() + + init { + sm.updateLabel() + } + + fun blur() { + button.blur() + } + + + fun requestFocus() { + button.requestFocus() + } + + @Bind + fun delete() { + parent.combo.removeIf { it === this } + parent.combo.update() + } + } + + class ActionEditor(val action: ComboKeyAction, val parent: Combos) { + fun asSaveable(): ComboKeyAction { + return ComboKeyAction( + CommandAction(command), + combo.map { it.binding } + ) + } + + @field:Bind("command") + var command: String = (action.action as CommandAction).command + + @field:Bind("combo") + val combo = action.keys.map { KeyBindingEditor(it, this) }.toObservableList() + + @Bind + fun formattedCombo() = + combo.joinToString(" > ") { it.binding.toString() } + + @Bind + fun addStep() { + combo.add(KeyBindingEditor(SavedKeyBinding.unbound(), this)) + } + + @Bind + fun back() { + MC.screen?.close() + } + + @Bind + fun delete() { + parent.actions.removeIf { it === this } + parent.actions.update() + } + + @Bind + fun edit() { + MC.screen = MoulConfigUtils.loadScreen("config/macros/editor_combo", this, MC.screen) + } + } +} + +private fun ObservableList.setAll(ts: Collection) { + val observer = this.observer + this.clear() + this.addAll(ts) + this.observer = observer + this.update() +} diff --git a/src/main/kotlin/features/macros/RadialMenu.kt b/src/main/kotlin/features/macros/RadialMenu.kt new file mode 100644 index 0000000..9e5222f --- /dev/null +++ b/src/main/kotlin/features/macros/RadialMenu.kt @@ -0,0 +1,153 @@ +package moe.nea.firmament.features.macros + +import org.joml.Vector2f +import util.render.CustomRenderLayers +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.sin +import kotlin.math.sqrt +import net.minecraft.client.gui.DrawContext +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HudRenderEvent +import moe.nea.firmament.events.TickEvent +import moe.nea.firmament.events.WorldKeyboardEvent +import moe.nea.firmament.events.WorldMouseMoveEvent +import moe.nea.firmament.features.macros.RadialMenuViewer.RadialMenu +import moe.nea.firmament.features.macros.RadialMenuViewer.RadialMenuOption +import moe.nea.firmament.keybindings.SavedKeyBinding +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.render.RenderCircleProgress +import moe.nea.firmament.util.render.drawLine +import moe.nea.firmament.util.render.lerpAngle +import moe.nea.firmament.util.render.wrapAngle +import moe.nea.firmament.util.render.τ + +object RadialMenuViewer { + interface RadialMenu { + val key: SavedKeyBinding + val options: List + } + + interface RadialMenuOption { + val isEnabled: Boolean + fun resolve() + fun renderSlice(drawContext: DrawContext) + } + + var activeMenu: RadialMenu? = null + set(value) { + if (value?.options.isNullOrEmpty()) { + field = null + } else { + field = value + } + delta = Vector2f(0F, 0F) + } + var delta = Vector2f(0F, 0F) + val maxSelectionSize = 100F + + @Subscribe + fun onMouseMotion(event: WorldMouseMoveEvent) { + val menu = activeMenu ?: return + event.cancel() + delta.add(event.deltaX.toFloat(), event.deltaY.toFloat()) + val m = delta.lengthSquared() + if (m > maxSelectionSize * maxSelectionSize) { + delta.mul(maxSelectionSize / sqrt(m)) + } + } + + val INNER_CIRCLE_RADIUS = 16 + + @Subscribe + fun onRender(event: HudRenderEvent) { + val menu = activeMenu ?: return + val mat = event.context.matrices + mat.push() + mat.translate( + (MC.window.scaledWidth) / 2F, + (MC.window.scaledHeight) / 2F, + 0F + ) + val sliceWidth = (τ / menu.options.size).toFloat() + var selectedAngle = wrapAngle(atan2(delta.y, delta.x)) + if (delta.lengthSquared() < INNER_CIRCLE_RADIUS * INNER_CIRCLE_RADIUS) + selectedAngle = Float.NaN + for ((idx, option) in menu.options.withIndex()) { + val range = (sliceWidth * idx)..(sliceWidth * (idx + 1)) + mat.push() + mat.scale(64F, 64F, 1F) + val cutout = INNER_CIRCLE_RADIUS / 64F / 2 + RenderCircleProgress.renderCircularSlice( + event.context, + CustomRenderLayers.TRANSLUCENT_CIRCLE_GUI, + 0F, 1F, 0F, 1F, + range, + color = if (selectedAngle in range) 0x70A0A0A0 else 0x70FFFFFF, + innerCutoutRadius = cutout + ) + mat.pop() + mat.push() + val centreAngle = lerpAngle(range.start, range.endInclusive, 0.5F) + val vec = Vector2f(cos(centreAngle), sin(centreAngle)).mul(40F) + mat.translate(vec.x, vec.y, 0F) + option.renderSlice(event.context) + mat.pop() + } + event.context.drawLine(1, 1, delta.x.toInt(), delta.y.toInt(), me.shedaniel.math.Color.ofOpaque(0x00FF00)) + mat.pop() + } + + @Subscribe + fun onTick(event: TickEvent) { + val menu = activeMenu ?: return + if (!menu.key.isPressed(true)) { + val angle = atan2(delta.y, delta.x) + + val choiceIndex = (wrapAngle(angle) * menu.options.size / τ).toInt() + val choice = menu.options[choiceIndex] + val selectedAny = delta.lengthSquared() > INNER_CIRCLE_RADIUS * INNER_CIRCLE_RADIUS + activeMenu = null + if (selectedAny) + choice.resolve() + } + } + +} + +object RadialMacros { + var wheels = MacroData.DConfig.data.wheels + private set + + fun setWheels(wheels: List) { + this.wheels = wheels + RadialMenuViewer.activeMenu = null + } + + @Subscribe + fun onOpen(event: WorldKeyboardEvent) { + if (RadialMenuViewer.activeMenu != null) return + wheels.forEach { wheel -> + if (event.matches(wheel.key, atLeast = true)) { + class R(val action: HotkeyAction) : RadialMenuOption { + override val isEnabled: Boolean + get() = true + + override fun resolve() { + action.execute() + } + + override fun renderSlice(drawContext: DrawContext) { + drawContext.drawCenteredTextWithShadow(MC.font, action.label, 0, 0, -1) + } + } + RadialMenuViewer.activeMenu = object : RadialMenu { + override val key: SavedKeyBinding + get() = wheel.key + override val options: List = + wheel.options.map { R(it) } + } + } + } + } +} diff --git a/src/main/kotlin/features/mining/CommissionFeatures.kt b/src/main/kotlin/features/mining/CommissionFeatures.kt new file mode 100644 index 0000000..faba253 --- /dev/null +++ b/src/main/kotlin/features/mining/CommissionFeatures.kt @@ -0,0 +1,26 @@ +package moe.nea.firmament.features.mining + +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.SlotRenderEvents +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.unformattedString + +object CommissionFeatures { + object Config : ManagedConfig("commissions", Category.MINING) { + val highlightCompletedCommissions by toggle("highlight-completed") { true } + } + + + @Subscribe + fun onSlotRender(event: SlotRenderEvents.Before) { + if (!Config.highlightCompletedCommissions) return + if (MC.screenName != "Commissions") return + val stack = event.slot.stack + if (stack.loreAccordingToNbt.any { it.unformattedString == "COMPLETED" }) { + event.highlight(Firmament.identifier("completed_commission_background")) + } + } +} diff --git a/src/main/kotlin/features/mining/Histogram.kt b/src/main/kotlin/features/mining/Histogram.kt new file mode 100644 index 0000000..ed48437 --- /dev/null +++ b/src/main/kotlin/features/mining/Histogram.kt @@ -0,0 +1,81 @@ + +package moe.nea.firmament.features.mining + +import java.util.* +import kotlin.time.Duration +import moe.nea.firmament.util.TimeMark + +class Histogram( + val maxSize: Int, + val maxDuration: Duration, +) { + + data class OrderedTimestamp(val timestamp: TimeMark, val order: Int) : Comparable { + override fun compareTo(other: OrderedTimestamp): Int { + val o = timestamp.compareTo(other.timestamp) + if (o != 0) return o + return order.compareTo(other.order) + } + } + + val size: Int get() = dataPoints.size + private val dataPoints: NavigableMap = TreeMap() + + private var order = Int.MIN_VALUE + + fun record(entry: T, timestamp: TimeMark = TimeMark.now()) { + dataPoints[OrderedTimestamp(timestamp, order++)] = entry + trim() + } + + fun oldestUpdate(): TimeMark { + trim() + return if (dataPoints.isEmpty()) TimeMark.now() else dataPoints.firstKey().timestamp + } + + fun latestUpdate(): TimeMark { + trim() + return if (dataPoints.isEmpty()) TimeMark.farPast() else dataPoints.lastKey().timestamp + } + + fun averagePer(valueExtractor: (T) -> Double, perDuration: Duration): Double? { + return aggregate( + seed = 0.0, + operator = { accumulator, entry, _ -> accumulator + valueExtractor(entry) }, + finish = { sum, beginning, end -> + val timespan = end - beginning + if (timespan > perDuration) + sum / (timespan / perDuration) + else null + }) + } + + fun aggregate( + seed: V, + operator: (V, T, TimeMark) -> V, + finish: (V, TimeMark, TimeMark) -> R + ): R? { + trim() + var accumulator = seed + var min: TimeMark? = null + var max: TimeMark? = null + dataPoints.forEach { (key, value) -> + max = key.timestamp + if (min == null) + min = key.timestamp + accumulator = operator(accumulator, value, key.timestamp) + } + if (min == null) + return null + return finish(accumulator, min!!, max!!) + } + + private fun trim() { + while (maxSize < dataPoints.size) { + dataPoints.pollFirstEntry() + } + dataPoints.headMap(OrderedTimestamp(TimeMark.ago(maxDuration), Int.MAX_VALUE)).clear() + } + + +} diff --git a/src/main/kotlin/features/mining/HotmPresets.kt b/src/main/kotlin/features/mining/HotmPresets.kt new file mode 100644 index 0000000..2241fee --- /dev/null +++ b/src/main/kotlin/features/mining/HotmPresets.kt @@ -0,0 +1,218 @@ +package moe.nea.firmament.features.mining + +import me.shedaniel.math.Rectangle +import kotlinx.serialization.Serializable +import kotlin.time.Duration.Companion.seconds +import net.minecraft.block.Blocks +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.gui.screen.ingame.HandledScreen +import net.minecraft.entity.player.PlayerInventory +import net.minecraft.item.Items +import net.minecraft.screen.GenericContainerScreenHandler +import net.minecraft.screen.slot.Slot +import net.minecraft.text.Text +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.events.ChestInventoryUpdateEvent +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.events.ScreenChangeEvent +import moe.nea.firmament.events.SlotRenderEvents +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.mixins.accessor.AccessorHandledScreen +import moe.nea.firmament.util.ClipboardUtils +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TemplateUtil +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.customgui.CustomGui +import moe.nea.firmament.util.customgui.customGui +import moe.nea.firmament.util.mc.CommonTextures +import moe.nea.firmament.util.mc.SlotUtils.clickRightMouseButton +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.render.drawGuiTexture +import moe.nea.firmament.util.unformattedString +import moe.nea.firmament.util.useMatch + +object HotmPresets { + val SHARE_PREFIX = "FIRMHOTM/" + + @Serializable + data class HotmPreset( + val perks: List, + ) + + @Serializable + data class PerkPreset(val perkName: String) + + var hotmCommandSent = TimeMark.farPast() + val hotmInventoryName = "Heart of the Mountain" + + @Subscribe + fun onScreenOpen(event: ScreenChangeEvent) { + val title = event.new?.title?.unformattedString + if (title != hotmInventoryName) return + val screen = event.new as? HandledScreen<*> ?: return + val oldHandler = (event.old as? HandledScreen<*>)?.customGui + if (oldHandler is HotmScrollPrompt) { + event.new.customGui = oldHandler + oldHandler.setNewScreen(screen) + return + } + if (hotmCommandSent.passedTime() > 5.seconds) return + hotmCommandSent = TimeMark.farPast() + screen.customGui = HotmScrollPrompt(screen) + } + + class HotmScrollPrompt(var screen: HandledScreen<*>) : CustomGui() { + var bounds = Rectangle( + 0, 0, 0, 0 + ) + + fun setNewScreen(screen: HandledScreen<*>) { + this.screen = screen + onInit() + hasScrolled = false + } + + override fun render(drawContext: DrawContext, delta: Float, mouseX: Int, mouseY: Int) { + drawContext.drawGuiTexture( + CommonTextures.genericWidget(), + bounds.x, bounds.y, + bounds.width, + bounds.height, + ) + drawContext.drawCenteredTextWithShadow( + MC.font, + if (hasAll) { + Text.translatable("firmament.hotmpreset.copied") + } else if (!hasScrolled) { + Text.translatable("firmament.hotmpreset.scrollprompt") + } else { + Text.translatable("firmament.hotmpreset.scrolled") + }, + bounds.centerX, + bounds.centerY - 5, + -1 + ) + } + + + var hasScrolled = false + var hasAll = false + + override fun mouseClick(mouseX: Double, mouseY: Double, button: Int): Boolean { + if (!hasScrolled) { + val slot = screen.screenHandler.getSlot(8) + println("Clicking ${slot.stack}") + slot.clickRightMouseButton(screen.screenHandler) + } + hasScrolled = true + return super.mouseClick(mouseX, mouseY, button) + } + + override fun shouldDrawForeground(): Boolean { + return false + } + + override fun getBounds(): List { + return listOf(bounds) + } + + override fun onInit() { + bounds = Rectangle( + screen.width / 2 - 150, + screen.height / 2 - 100, + 300, 200 + ) + val screen = screen as AccessorHandledScreen + screen.x_Firmament = bounds.x + screen.y_Firmament = bounds.y + screen.backgroundWidth_Firmament = bounds.width + screen.backgroundHeight_Firmament = bounds.height + } + + override fun moveSlot(slot: Slot) { + slot.x = -10000 + } + + val coveredRows = mutableSetOf() + val unlockedPerks = mutableSetOf() + val allRows = (1..10).toSet() + + fun onNewItems(event: ChestInventoryUpdateEvent) { + val handler = screen.screenHandler as? GenericContainerScreenHandler ?: return + for (it in handler.slots) { + if (it.inventory is PlayerInventory) continue + val stack = it.stack + val name = stack.displayNameAccordingToNbt.unformattedString + tierRegex.useMatch(name) { + coveredRows.add(group("tier").toInt()) + } + if (stack.item == Items.DIAMOND + || stack.item == Items.EMERALD + || stack.item == Blocks.EMERALD_BLOCK.asItem() + ) { + unlockedPerks.add(name) + } + } + if (allRows == coveredRows) { + ClipboardUtils.setTextContent(TemplateUtil.encodeTemplate(SHARE_PREFIX, HotmPreset( + unlockedPerks.map { PerkPreset(it) } + ))) + hasAll = true + } + } + } + + val tierRegex = "Tier (?[0-9]+)".toPattern() + var highlightedPerks: Set = emptySet() + + @Subscribe + fun onSlotUpdates(event: ChestInventoryUpdateEvent) { + val customGui = (event.inventory as? HandledScreen<*>)?.customGui + if (customGui is HotmScrollPrompt) { + customGui.onNewItems(event) + } + } + + @Subscribe + fun resetOnScreen(event: ScreenChangeEvent) { + if (event.new != null && event.new.title.unformattedString != hotmInventoryName) { + highlightedPerks = emptySet() + } + } + + @Subscribe + fun onSlotRender(event: SlotRenderEvents.Before) { + if (hotmInventoryName == MC.screenName + && event.slot.stack.displayNameAccordingToNbt.unformattedString in highlightedPerks + ) { + event.highlight((Firmament.identifier("hotm_perk_preset"))) + } + } + + @Subscribe + fun onCommand(event: CommandEvent.SubCommand) { + event.subcommand("exporthotm") { + thenExecute { + hotmCommandSent = TimeMark.now() + MC.sendCommand("hotm") + source.sendFeedback(Text.translatable("firmament.hotmpreset.openinghotm")) + } + } + event.subcommand("importhotm") { + thenExecute { + val template = + TemplateUtil.maybeDecodeTemplate(SHARE_PREFIX, ClipboardUtils.getTextContents()) + if (template == null) { + source.sendFeedback(Text.translatable("firmament.hotmpreset.failedimport")) + } else { + highlightedPerks = template.perks.mapTo(mutableSetOf()) { it.perkName } + source.sendFeedback(Text.translatable("firmament.hotmpreset.okayimport")) + MC.sendCommand("hotm") + } + } + } + } + +} diff --git a/src/main/kotlin/features/mining/MiningBlockInfoUi.kt b/src/main/kotlin/features/mining/MiningBlockInfoUi.kt new file mode 100644 index 0000000..e8ea4f4 --- /dev/null +++ b/src/main/kotlin/features/mining/MiningBlockInfoUi.kt @@ -0,0 +1,54 @@ +package moe.nea.firmament.features.mining + +import io.github.notenoughupdates.moulconfig.observer.ObservableList +import io.github.notenoughupdates.moulconfig.observer.Property +import io.github.notenoughupdates.moulconfig.platform.ModernItemStack +import io.github.notenoughupdates.moulconfig.xml.Bind +import net.minecraft.client.gui.screen.Screen +import net.minecraft.item.ItemStack +import moe.nea.firmament.repo.MiningRepoData +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.util.MoulConfigUtils +import moe.nea.firmament.util.SkyBlockIsland + +object MiningBlockInfoUi { + class MiningInfo(miningData: MiningRepoData) { + @field:Bind("search") + @JvmField + var search = "" + + @get:Bind("ores") + val blocks = miningData.customMiningBlocks.mapTo(ObservableList(mutableListOf())) { OreInfo(it, this) } + } + + class OreInfo(block: MiningRepoData.CustomMiningBlock, info: MiningInfo) { + @get:Bind("oreName") + val oreName = block.name ?: "No Name" + + @get:Bind("blocks") + val res = ObservableList(block.blocks189.map { BlockInfo(it, info) }) + } + + class BlockInfo(val block: MiningRepoData.Block189, val info: MiningInfo) { + @get:Bind("item") + val item = ModernItemStack.of(block.block?.let { ItemStack(it) } ?: ItemStack.EMPTY) + + @get:Bind("isSelected") + val isSelected get() = info.search.let { block.isActiveIn(SkyBlockIsland.forMode(it)) } + + @get:Bind("itemName") + val itemName get() = item.getDisplayName() + + @get:Bind("restrictions") + val res = ObservableList( + if (block.onlyIn != null) + block.onlyIn.map { " §r- §a${it.userFriendlyName}" } + else + listOf("Everywhere") + ) + } + + fun makeScreen(): Screen { + return MoulConfigUtils.loadScreen("mining_block_info/index", MiningInfo(RepoManager.miningData), null) + } +} diff --git a/src/main/kotlin/features/mining/PickaxeAbility.kt b/src/main/kotlin/features/mining/PickaxeAbility.kt new file mode 100644 index 0000000..a049001 --- /dev/null +++ b/src/main/kotlin/features/mining/PickaxeAbility.kt @@ -0,0 +1,244 @@ +package moe.nea.firmament.features.mining + +import java.util.regex.Pattern +import kotlin.jvm.optionals.getOrNull +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import net.minecraft.client.MinecraftClient +import net.minecraft.client.toast.SystemToast +import net.minecraft.item.ItemStack +import net.minecraft.util.DyeColor +import net.minecraft.util.Hand +import net.minecraft.util.Identifier +import net.minecraft.util.StringIdentifiable +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HudRenderEvent +import moe.nea.firmament.events.ProcessChatEvent +import moe.nea.firmament.events.ProfileSwitchEvent +import moe.nea.firmament.events.SlotClickEvent +import moe.nea.firmament.events.UseItemEvent +import moe.nea.firmament.events.WorldReadyEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.DurabilityBarEvent +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.SHORT_NUMBER_FORMAT +import moe.nea.firmament.util.SkyBlockIsland +import moe.nea.firmament.util.TIME_PATTERN +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.extraAttributes +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.parseShortNumber +import moe.nea.firmament.util.parseTimePattern +import moe.nea.firmament.util.red +import moe.nea.firmament.util.render.RenderCircleProgress +import moe.nea.firmament.util.render.lerp +import moe.nea.firmament.util.skyblock.AbilityUtils +import moe.nea.firmament.util.skyblock.DungeonUtil +import moe.nea.firmament.util.skyblock.ItemType +import moe.nea.firmament.util.toShedaniel +import moe.nea.firmament.util.tr +import moe.nea.firmament.util.unformattedString +import moe.nea.firmament.util.useMatch + +object PickaxeAbility : FirmamentFeature { + override val identifier: String + get() = "pickaxe-info" + + + object TConfig : ManagedConfig(identifier, Category.MINING) { + val cooldownEnabled by toggle("ability-cooldown") { false } + val disableInDungeons by toggle("disable-in-dungeons") { true } + val cooldownScale by integer("ability-scale", 16, 64) { 16 } + val cooldownReadyToast by toggle("ability-cooldown-toast") { false } + val drillFuelBar by toggle("fuel-bar") { true } + val blockOnPrivateIsland by choice( + "block-on-dynamic", + ) { + BlockPickaxeAbility.ONLY_DESTRUCTIVE + } + } + + enum class BlockPickaxeAbility : StringIdentifiable { + NEVER, + ALWAYS, + ONLY_DESTRUCTIVE; + + override fun asString(): String { + return name + } + } + + var lobbyJoinTime = TimeMark.farPast() + var lastUsage = mutableMapOf() + var abilityOverride: String? = null + var defaultAbilityDurations = mutableMapOf( + "Mining Speed Boost" to 120.seconds, + "Pickobulus" to 110.seconds, + "Gemstone Infusion" to 140.seconds, + "Hazardous Miner" to 140.seconds, + "Maniac Miner" to 59.seconds, + "Vein Seeker" to 60.seconds + ) + val destructiveAbilities = setOf("Pickobulus") + val pickaxeTypes = setOf(ItemType.PICKAXE, ItemType.DRILL, ItemType.GAUNTLET) + + override val config: ManagedConfig + get() = TConfig + + fun getCooldownPercentage(name: String, cooldown: Duration): Double { + val sinceLastUsage = lastUsage[name]?.passedTime() ?: Duration.INFINITE + val sinceLobbyJoin = lobbyJoinTime.passedTime() + if (SBData.skyblockLocation == SkyBlockIsland.MINESHAFT) { + if (sinceLobbyJoin < sinceLastUsage) { + return 1.0 + } + } + if (sinceLastUsage < cooldown) + return sinceLastUsage / cooldown + return 1.0 + } + + @Subscribe + fun onPickaxeRightClick(event: UseItemEvent) { + if (TConfig.blockOnPrivateIsland == BlockPickaxeAbility.NEVER) return + if (SBData.skyblockLocation != SkyBlockIsland.PRIVATE_ISLAND && SBData.skyblockLocation != SkyBlockIsland.GARDEN) return + val itemType = ItemType.fromItemStack(event.item) + if (itemType !in pickaxeTypes) return + val ability = AbilityUtils.getAbilities(event.item) + val shouldBlock = when (TConfig.blockOnPrivateIsland) { + BlockPickaxeAbility.NEVER -> false + BlockPickaxeAbility.ALWAYS -> ability.any() + BlockPickaxeAbility.ONLY_DESTRUCTIVE -> ability.any { it.name in destructiveAbilities } + } + if (shouldBlock) { + MC.sendChat(tr("firmament.pickaxe.blocked", + "Firmament blocked a pickaxe ability from being used on a private island.") + .red() // TODO: .clickCommand("firm confignavigate ${TConfig.identifier} block-on-dynamic") + ) + event.cancel() + } + } + + @Subscribe + fun onSlotClick(it: SlotClickEvent) { + if (MC.screen?.title?.unformattedString == "Heart of the Mountain") { + val name = it.stack.displayNameAccordingToNbt.unformattedString + val cooldown = it.stack.loreAccordingToNbt.firstNotNullOfOrNull { + cooldownPattern.useMatch(it.unformattedString) { + parseTimePattern(group("cooldown")) + } + } ?: return + defaultAbilityDurations[name] = cooldown + } + } + + @Subscribe + fun onDurabilityBar(it: DurabilityBarEvent) { + if (!TConfig.drillFuelBar) return + val lore = it.item.loreAccordingToNbt + if (lore.lastOrNull()?.unformattedString?.contains("DRILL") != true) return + val maxFuel = lore.firstNotNullOfOrNull { + fuelPattern.useMatch(it.unformattedString) { + parseShortNumber(group("maxFuel")) + } + } ?: return + val extra = it.item.extraAttributes + val fuel = extra.getInt("drill_fuel").getOrNull() ?: return + var percentage = fuel / maxFuel.toFloat() + if (percentage > 1f) percentage = 1f + it.barOverride = DurabilityBarEvent.DurabilityBar( + lerp( + DyeColor.RED.toShedaniel(), + DyeColor.GREEN.toShedaniel(), + percentage + ), percentage + ) + } + + @Subscribe + fun onChatMessage(it: ProcessChatEvent) { + abilityUsePattern.useMatch(it.unformattedString) { + lastUsage[group("name")] = TimeMark.now() + abilityOverride = group("name") + } + abilitySwitchPattern.useMatch(it.unformattedString) { + abilityOverride = group("ability") + } + pickaxeAbilityCooldownPattern.useMatch(it.unformattedString) { + val ability = abilityOverride ?: return@useMatch + val remainingCooldown = parseTimePattern(group("remainingCooldown")) + val length = defaultAbilityDurations[ability] ?: return@useMatch + lastUsage[ability] = TimeMark.ago(length - remainingCooldown) + } + nowAvailable.useMatch(it.unformattedString) { + val ability = group("name") + lastUsage[ability] = TimeMark.farPast() + if (!TConfig.cooldownReadyToast) return + val mc: MinecraftClient = MinecraftClient.getInstance() + mc.toastManager.add( + SystemToast.create(mc, SystemToast.Type.NARRATOR_TOGGLE, tr("firmament.pickaxe.ability-ready","Pickaxe Cooldown"), tr("firmament.pickaxe.ability-ready.desc", "Pickaxe ability is ready!")) + ) + } + } + + @Subscribe + fun onWorldReady(event: WorldReadyEvent) { + lobbyJoinTime = TimeMark.now() + abilityOverride = null + } + + @Subscribe + fun onProfileSwitch(event: ProfileSwitchEvent) { + lastUsage.entries.removeIf { + it.value < lobbyJoinTime + } + } + + val abilityUsePattern = Pattern.compile("You used your (?.*) Pickaxe Ability!") + val fuelPattern = Pattern.compile("Fuel: .*/(?$SHORT_NUMBER_FORMAT)") + val pickaxeAbilityCooldownPattern = + Pattern.compile("Your pickaxe ability is on cooldown for (?$TIME_PATTERN)\\.") + val nowAvailable = Pattern.compile("(?[a-zA-Z0-9 ]+) is now available!") + + data class PickaxeAbilityData( + val name: String, + val cooldown: Duration, + ) + + fun getCooldownFromLore(itemStack: ItemStack): PickaxeAbilityData? { + val lore = itemStack.loreAccordingToNbt + if (!lore.any { it.unformattedString.contains("Breaking Power") }) + return null + val ability = AbilityUtils.getAbilities(itemStack).firstOrNull() ?: return null + return PickaxeAbilityData(ability.name, ability.cooldown ?: return null) + } + + val cooldownPattern = Pattern.compile("Cooldown: (?$TIME_PATTERN)") + val abilitySwitchPattern = + Pattern.compile("You selected (?.*) as your Pickaxe Ability\\. This ability will apply to all of your pickaxes!") + + @Subscribe + fun renderHud(event: HudRenderEvent) { + if (!TConfig.cooldownEnabled) return + if (TConfig.disableInDungeons && DungeonUtil.isInDungeonIsland) return + if (!event.isRenderingCursor) return + var ability = getCooldownFromLore(MC.player?.getStackInHand(Hand.MAIN_HAND) ?: return) ?: return + defaultAbilityDurations[ability.name] = ability.cooldown + val ao = abilityOverride + if (ao != ability.name && ao != null) { + ability = PickaxeAbilityData(ao, defaultAbilityDurations[ao] ?: 120.seconds) + } + event.context.matrices.push() + event.context.matrices.translate(MC.window.scaledWidth / 2F, MC.window.scaledHeight / 2F, 0F) + event.context.matrices.scale(TConfig.cooldownScale.toFloat(), TConfig.cooldownScale.toFloat(), 1F) + RenderCircleProgress.renderCircle( + event.context, Identifier.of("firmament", "textures/gui/circle.png"), + getCooldownPercentage(ability.name, ability.cooldown).toFloat(), + 0f, 1f, 0f, 1f + ) + event.context.matrices.pop() + } +} diff --git a/src/main/kotlin/features/mining/PristineProfitTracker.kt b/src/main/kotlin/features/mining/PristineProfitTracker.kt new file mode 100644 index 0000000..377a470 --- /dev/null +++ b/src/main/kotlin/features/mining/PristineProfitTracker.kt @@ -0,0 +1,148 @@ +package moe.nea.firmament.features.mining + +import io.github.notenoughupdates.moulconfig.xml.Bind +import moe.nea.jarvis.api.Point +import kotlinx.serialization.Serializable +import kotlinx.serialization.serializer +import kotlin.time.Duration.Companion.seconds +import net.minecraft.text.Text +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ProcessChatEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.gui.hud.MoulConfigHud +import moe.nea.firmament.util.BazaarPriceStrategy +import moe.nea.firmament.util.FirmFormatters.formatCommas +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.StringUtil.parseIntWithComma +import moe.nea.firmament.util.data.ProfileSpecificDataHolder +import moe.nea.firmament.util.formattedString +import moe.nea.firmament.util.useMatch + +object PristineProfitTracker : FirmamentFeature { + override val identifier: String + get() = "pristine-profit" + + enum class GemstoneKind( + val label: String, + ) { + SAPPHIRE("Sapphire"), + RUBY("Ruby"), + AMETHYST("Amethyst"), + AMBER("Amber"), + TOPAZ("Topaz"), + JADE("Jade"), + JASPER("Jasper"), + OPAL("Opal"), + PERIDOT("Peridot"), + ONXY("Onyx"), + AQUAMARINE("Aquamarine"), + CITRINE("Citrine"), + ; + + val flawedId: SkyblockId = SkyblockId("FLAWED_${name}_GEM") + val fineId: SkyblockId = SkyblockId("FINE_${name}_GEM") + } + + @Serializable + data class Data( + var maxMoneyPerSecond: Double = 1.0, + var maxCollectionPerSecond: Double = 1.0, + ) + + object DConfig : ProfileSpecificDataHolder(serializer(), identifier, ::Data) + + override val config: ManagedConfig? + get() = TConfig + + object TConfig : ManagedConfig(identifier, Category.MINING) { + val timeout by duration("timeout", 0.seconds, 120.seconds) { 30.seconds } + val gui by position("position", 100, 30) { Point(0.05, 0.2) } + val useFineGemstones by toggle("fine-gemstones") { false } + } + + val sellingStrategy = BazaarPriceStrategy.SELL_ORDER + + val pristineRegex = + "PRISTINE! You found . Flawed (?${ + GemstoneKind.entries.joinToString("|") { it.label } + }) Gemstone x(?[0-9,]+)!".toPattern() + + val collectionHistogram = Histogram(10000, 180.seconds) + + /** + * Separate histogram for money, since money changes based on gemstone, therefore we cannot calculate money from collection. + */ + val moneyHistogram = Histogram(10000, 180.seconds) + + object ProfitHud : MoulConfigHud("pristine_profit", TConfig.gui) { + @field:Bind + var moneyCurrent: Double = 0.0 + + @field:Bind + var moneyMax: Double = 1.0 + + @field:Bind + var moneyText = "" + + @field:Bind + var collectionCurrent = 0.0 + + @field:Bind + var collectionMax = 1.0 + + @field:Bind + var collectionText = "" + override fun shouldRender(): Boolean = collectionHistogram.latestUpdate().passedTime() < TConfig.timeout + } + + val SECONDS_PER_HOUR = 3600 + val ROUGHS_PER_FLAWED = 80 + val FLAWED_PER_FINE = 80 + val ROUGHS_PER_FINE = ROUGHS_PER_FLAWED * FLAWED_PER_FINE + + fun updateUi() { + val collectionPerSecond = collectionHistogram.averagePer({ it }, 1.seconds) + val moneyPerSecond = moneyHistogram.averagePer({ it }, 1.seconds) + if (collectionPerSecond == null || moneyPerSecond == null) return + ProfitHud.collectionCurrent = collectionPerSecond + ProfitHud.collectionText = Text.stringifiedTranslatable("firmament.pristine-profit.collection", + formatCommas(collectionPerSecond * SECONDS_PER_HOUR, + 1)).formattedString() + ProfitHud.moneyCurrent = moneyPerSecond + ProfitHud.moneyText = Text.stringifiedTranslatable("firmament.pristine-profit.money", + formatCommas(moneyPerSecond * SECONDS_PER_HOUR, 1)) + .formattedString() + val data = DConfig.data + if (data != null) { + if (data.maxCollectionPerSecond < collectionPerSecond && collectionHistogram.oldestUpdate() + .passedTime() > 30.seconds + ) { + data.maxCollectionPerSecond = collectionPerSecond + DConfig.markDirty() + } + if (data.maxMoneyPerSecond < moneyPerSecond && moneyHistogram.oldestUpdate().passedTime() > 30.seconds) { + data.maxMoneyPerSecond = moneyPerSecond + DConfig.markDirty() + } + ProfitHud.collectionMax = maxOf(data.maxCollectionPerSecond, collectionPerSecond) + ProfitHud.moneyMax = maxOf(data.maxMoneyPerSecond, moneyPerSecond) + } + } + + + @Subscribe + fun onMessage(it: ProcessChatEvent) { + pristineRegex.useMatch(it.unformattedString) { + val gemstoneKind = GemstoneKind.valueOf(group("kind").uppercase()) + val flawedCount = parseIntWithComma(group("count")) + val moneyAmount = + if (TConfig.useFineGemstones) sellingStrategy.getSellPrice(gemstoneKind.fineId) * flawedCount / FLAWED_PER_FINE + else sellingStrategy.getSellPrice(gemstoneKind.flawedId) * flawedCount + moneyHistogram.record(moneyAmount) + val collectionAmount = flawedCount * ROUGHS_PER_FLAWED + collectionHistogram.record(collectionAmount.toDouble()) + updateUi() + } + } +} diff --git a/src/main/kotlin/features/misc/CustomCapes.kt b/src/main/kotlin/features/misc/CustomCapes.kt new file mode 100644 index 0000000..dc5187a --- /dev/null +++ b/src/main/kotlin/features/misc/CustomCapes.kt @@ -0,0 +1,192 @@ +package moe.nea.firmament.features.misc + +import com.mojang.blaze3d.systems.RenderSystem +import java.util.OptionalDouble +import java.util.OptionalInt +import util.render.CustomRenderPipelines +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import net.minecraft.client.network.AbstractClientPlayerEntity +import net.minecraft.client.render.BufferBuilder +import net.minecraft.client.render.RenderLayer +import net.minecraft.client.render.VertexConsumer +import net.minecraft.client.render.VertexConsumerProvider +import net.minecraft.client.render.entity.state.PlayerEntityRenderState +import net.minecraft.client.util.BufferAllocator +import net.minecraft.client.util.SkinTextures +import net.minecraft.util.Identifier +import moe.nea.firmament.Firmament +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TimeMark + +object CustomCapes : FirmamentFeature { + override val identifier: String + get() = "developer-capes" + + object TConfig : ManagedConfig(identifier, Category.DEV) { + val showCapes by toggle("show-cape") { true } + } + + override val config: ManagedConfig + get() = TConfig + + interface CustomCapeRenderer { + fun replaceRender( + renderLayer: RenderLayer, + vertexConsumerProvider: VertexConsumerProvider, + model: (VertexConsumer) -> Unit + ) + } + + data class TexturedCapeRenderer( + val location: Identifier + ) : CustomCapeRenderer { + override fun replaceRender( + renderLayer: RenderLayer, + vertexConsumerProvider: VertexConsumerProvider, + model: (VertexConsumer) -> Unit + ) { + model(vertexConsumerProvider.getBuffer(RenderLayer.getEntitySolid(location))) + } + } + + data class ParallaxedHighlightCapeRenderer( + val template: Identifier, + val background: Identifier, + val overlay: Identifier, + val animationSpeed: Duration, + ) : CustomCapeRenderer { + override fun replaceRender( + renderLayer: RenderLayer, + vertexConsumerProvider: VertexConsumerProvider, + model: (VertexConsumer) -> Unit + ) { + BufferAllocator(2048).use { allocator -> + val bufferBuilder = BufferBuilder(allocator, renderLayer.drawMode, renderLayer.vertexFormat) + model(bufferBuilder) + bufferBuilder.end().use { buffer -> + val commandEncoder = RenderSystem.getDevice().createCommandEncoder() + val vertexBuffer = renderLayer.vertexFormat.uploadImmediateVertexBuffer(buffer.buffer) + val indexBufferConstructor = RenderSystem.getSequentialBuffer(renderLayer.drawMode) + val indexBuffer = indexBufferConstructor.getIndexBuffer(buffer.drawParameters.indexCount) + val templateTexture = MC.textureManager.getTexture(template) + val backgroundTexture = MC.textureManager.getTexture(background) + val foregroundTexture = MC.textureManager.getTexture(overlay) + commandEncoder.createRenderPass( + MC.instance.framebuffer.colorAttachment, + OptionalInt.empty(), + MC.instance.framebuffer.depthAttachment, + OptionalDouble.empty(), + ).use { renderPass -> + // TODO: account for lighting + renderPass.setPipeline(CustomRenderPipelines.PARALLAX_CAPE_SHADER) + renderPass.bindSampler("Sampler0", templateTexture.glTexture) + renderPass.bindSampler("Sampler1", backgroundTexture.glTexture) + renderPass.bindSampler("Sampler3", foregroundTexture.glTexture) + val animationValue = (startTime.passedTime() / animationSpeed).mod(1F) + renderPass.setUniform("Animation", animationValue.toFloat()) + renderPass.setIndexBuffer(indexBuffer, indexBufferConstructor.indexType) + renderPass.setVertexBuffer(0, vertexBuffer) + renderPass.drawIndexed(0, buffer.drawParameters.indexCount) + } + } + } + } + } + + interface CapeStorage { + companion object { + @JvmStatic + fun cast(playerEntityRenderState: PlayerEntityRenderState) = + playerEntityRenderState as CapeStorage + + } + + var cape_firmament: CustomCape? + } + + data class CustomCape( + val id: String, + val label: String, + val render: CustomCapeRenderer, + ) + + enum class AllCapes(val label: String, val render: CustomCapeRenderer) { + FIRMAMENT_ANIMATED( + "Animated Firmament", ParallaxedHighlightCapeRenderer( + Firmament.identifier("textures/cape/parallax_template.png"), + Firmament.identifier("textures/cape/parallax_background.png"), + Firmament.identifier("textures/cape/firmament_star.png"), + 110.seconds + ) + ), + + FURFSKY_STATIC( + "FurfSky", + TexturedCapeRenderer(Firmament.identifier("textures/cape/fsr_static.png")) + ), + + FIRMAMENT_STATIC( + "Firmament", + TexturedCapeRenderer(Firmament.identifier("textures/cape/firm_static.png")) + ) + ; + + val cape = CustomCape(name, label, render) + } + + val byId = AllCapes.entries.associateBy { it.cape.id } + val byUuid = + listOf( + listOf( + Devs.nea to AllCapes.FIRMAMENT_ANIMATED, + Devs.kath to AllCapes.FIRMAMENT_STATIC, + Devs.jani to AllCapes.FIRMAMENT_ANIMATED, + ), + Devs.FurfSky.all.map { it to AllCapes.FURFSKY_STATIC }, + ).flatten().flatMap { (dev, cape) -> dev.uuids.map { it to cape.cape } }.toMap() + + @JvmStatic + fun render( + playerEntityRenderState: PlayerEntityRenderState, + vertexConsumer: VertexConsumer, + renderLayer: RenderLayer, + vertexConsumerProvider: VertexConsumerProvider, + model: (VertexConsumer) -> Unit + ) { + val capeStorage = CapeStorage.cast(playerEntityRenderState) + val firmCape = capeStorage.cape_firmament + if (firmCape != null) { + firmCape.render.replaceRender(renderLayer, vertexConsumerProvider, model) + } else { + model(vertexConsumer) + } + } + + @JvmStatic + fun addCapeData( + player: AbstractClientPlayerEntity, + playerEntityRenderState: PlayerEntityRenderState + ) { + val cape = if (TConfig.showCapes) byUuid[player.uuid] else null + val capeStorage = CapeStorage.cast(playerEntityRenderState) + if (cape == null) { + capeStorage.cape_firmament = null + } else { + capeStorage.cape_firmament = cape + playerEntityRenderState.skinTextures = SkinTextures( + playerEntityRenderState.skinTextures.texture, + playerEntityRenderState.skinTextures.textureUrl, + Firmament.identifier("placeholder/fake_cape"), + playerEntityRenderState.skinTextures.elytraTexture, + playerEntityRenderState.skinTextures.model, + playerEntityRenderState.skinTextures.secure, + ) + playerEntityRenderState.capeVisible = true + } + } + + val startTime = TimeMark.now() +} diff --git a/src/main/kotlin/features/misc/Devs.kt b/src/main/kotlin/features/misc/Devs.kt new file mode 100644 index 0000000..1f16400 --- /dev/null +++ b/src/main/kotlin/features/misc/Devs.kt @@ -0,0 +1,38 @@ +package moe.nea.firmament.features.misc + +import java.util.UUID + +object Devs { + data class Dev( + val uuids: List, + ) { + constructor(vararg uuid: UUID) : this(uuid.toList()) + constructor(vararg uuid: String) : this(uuid.map { UUID.fromString(it) }) + } + + val nea = Dev("d3cb85e2-3075-48a1-b213-a9bfb62360c1", "842204e6-6880-487b-ae5a-0595394f9948") + val kath = Dev("add71246-c46e-455c-8345-c129ea6f146c", "b491990d-53fd-4c5f-a61e-19d58cc7eddf") + val jani = Dev("8a9f1841-48e9-48ed-b14f-76a124e6c9df") + + object FurfSky { + val smolegit = Dev("02b38b96-eb19-405a-b319-d6bc00b26ab3") + val itsCen = Dev("ada70b5a-ac37-49d2-b18c-1351672f8051") + val webster = Dev("02166f1b-9e8d-4e48-9e18-ea7a4499492d") + val vrachel = Dev("22e98637-ba97-4b6b-a84f-fb57a461ce43") + val cunuduh = Dev("2a15e3b3-c46e-4718-b907-166e1ab2efdc") + val eiiies = Dev("2ae162f2-81a7-4f91-a4b2-104e78a0a7e1") + val june = Dev("2584a4e3-f917-4493-8ced-618391f3b44f") + val denasu = Dev("313cbd25-8ade-4e41-845c-5cab555a30c9") + val libyKiwii = Dev("4265c52e-bd6f-4d3c-9cf6-bdfc8fb58023") + val madeleaan = Dev("bcb119a3-6000-4324-bda1-744f00c44b31") + val turtleSP = Dev("f1ca1934-a582-4723-8283-89921d008657") + val papayamm = Dev("ae0eea30-f6a2-40fe-ac17-9c80b3423409") + val persuasiveViksy = Dev("ba7ac144-28e0-4f55-a108-1a72fe744c9e") + val all = listOf( + smolegit, itsCen, webster, vrachel, cunuduh, eiiies, + june, denasu, libyKiwii, madeleaan, turtleSP, papayamm, + persuasiveViksy + ) + } + +} diff --git a/src/main/kotlin/features/misc/Hud.kt b/src/main/kotlin/features/misc/Hud.kt new file mode 100644 index 0000000..9661fc5 --- /dev/null +++ b/src/main/kotlin/features/misc/Hud.kt @@ -0,0 +1,77 @@ +package moe.nea.firmament.features.misc + +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HudRenderEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.tr +import moe.nea.jarvis.api.Point +import net.minecraft.client.network.PlayerListEntry +import net.minecraft.text.Text + +object Hud : FirmamentFeature { + override val identifier: String + get() = "hud" + + object TConfig : ManagedConfig(identifier, Category.MISC) { + var dayCount by toggle("day-count") { false } + val dayCountHud by position("day-count-hud", 80, 10) { Point(0.5, 0.8) } + var fpsCount by toggle("fps-count") { false } + val fpsCountHud by position("fps-count-hud", 80, 10) { Point(0.5, 0.9) } + var pingCount by toggle("ping-count") { false } + val pingCountHud by position("ping-count-hud", 80, 10) { Point(0.5, 1.0) } + } + + override val config: ManagedConfig + get() = TConfig + + @Subscribe + fun onRenderHud(it: HudRenderEvent) { + if (TConfig.dayCount) { + it.context.matrices.push() + TConfig.dayCountHud.applyTransformations(it.context.matrices) + val day = (MC.world?.timeOfDay ?: 0L) / 24000 + it.context.drawText( + MC.font, + Text.literal(String.format(tr("firmament.config.hud.day-count-hud.display", "Day: %s").string, day)), + 36, + MC.font.fontHeight, + -1, + true + ) + it.context.matrices.pop() + } + + if (TConfig.fpsCount) { + it.context.matrices.push() + TConfig.fpsCountHud.applyTransformations(it.context.matrices) + it.context.drawText( + MC.font, Text.literal( + String.format( + tr("firmament.config.hud.fps-count-hud.display", "FPS: %s").string, MC.instance.currentFps + ) + ), 36, MC.font.fontHeight, -1, true + ) + it.context.matrices.pop() + } + + if (TConfig.pingCount) { + it.context.matrices.push() + TConfig.pingCountHud.applyTransformations(it.context.matrices) + val ping = MC.player?.let { + val entry: PlayerListEntry? = MC.networkHandler?.getPlayerListEntry(it.uuid) + entry?.latency ?: -1 + } ?: -1 + it.context.drawText( + MC.font, Text.literal( + String.format( + tr("firmament.config.hud.ping-count-hud.display", "Ping: %s ms").string, ping + ) + ), 36, MC.font.fontHeight, -1, true + ) + + it.context.matrices.pop() + } + } +} diff --git a/src/main/kotlin/features/misc/LicenseViewer.kt b/src/main/kotlin/features/misc/LicenseViewer.kt new file mode 100644 index 0000000..4219177 --- /dev/null +++ b/src/main/kotlin/features/misc/LicenseViewer.kt @@ -0,0 +1,128 @@ +package moe.nea.firmament.features.misc + +import io.github.notenoughupdates.moulconfig.observer.ObservableList +import io.github.notenoughupdates.moulconfig.xml.Bind +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlinx.serialization.json.decodeFromStream +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.util.ErrorUtil +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.MoulConfigUtils +import moe.nea.firmament.util.ScreenUtil +import moe.nea.firmament.util.tr + +object LicenseViewer { + @Serializable + data class Software( + val licenses: List = listOf(), + val webPresence: String? = null, + val projectName: String, + val projectDescription: String? = null, + val developers: List = listOf(), + ) { + + @Bind + fun hasWebPresence() = webPresence != null + + @Bind + fun webPresence() = webPresence ?: "" + @Bind + fun open() { + MC.openUrl(webPresence ?: return) + } + + @Bind + fun projectName() = projectName + + @Bind + fun projectDescription() = projectDescription ?: "" + + @get:Bind("developers") + @Transient + val developersO = ObservableList(developers) + + @get:Bind("licenses") + @Transient + val licenses0 = ObservableList(licenses) + } + + @Serializable + data class Developer( + @get:Bind("name") val name: String, + val webPresence: String? = null + ) { + + @Bind + fun open() { + MC.openUrl(webPresence ?: return) + } + + @Bind + fun hasWebPresence() = webPresence != null + + @Bind + fun webPresence() = webPresence ?: "" + } + + @Serializable + data class License( + @get:Bind("name") val licenseName: String, + val licenseUrl: String? = null + ) { + @Bind + fun open() { + MC.openUrl(licenseUrl ?: return) + } + + @Bind + fun hasUrl() = licenseUrl != null + + @Bind + fun url() = licenseUrl ?: "" + } + + data class LicenseList( + val softwares: List + ) { + @get:Bind("softwares") + val obs = ObservableList(softwares) + } + + @OptIn(ExperimentalSerializationApi::class) + val licenses: LicenseList? = ErrorUtil.catch("Could not load licenses") { + Firmament.json.decodeFromStream?>( + javaClass.getResourceAsStream("/LICENSES-FIRMAMENT.json") ?: error("Could not find LICENSES-FIRMAMENT.json") + )?.let { LicenseList(it) } + }.orNull() + + fun showLicenses() { + ErrorUtil.catch("Could not display licenses") { + ScreenUtil.setScreenLater( + MoulConfigUtils.loadScreen( + "license_viewer/index", licenses!!, null + ) + ) + }.or { + MC.sendChat( + tr( + "firmament.licenses.notfound", + "Could not load licenses. Please check the Firmament source code for information directly." + ) + ) + } + } + + @Subscribe + fun onSubcommand(event: CommandEvent.SubCommand) { + event.subcommand("licenses") { + thenExecute { + showLicenses() + } + } + } +} diff --git a/src/main/kotlin/features/misc/TimerFeature.kt b/src/main/kotlin/features/misc/TimerFeature.kt new file mode 100644 index 0000000..7c4833d --- /dev/null +++ b/src/main/kotlin/features/misc/TimerFeature.kt @@ -0,0 +1,124 @@ +package moe.nea.firmament.features.misc + +import com.mojang.brigadier.arguments.IntegerArgumentType +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.DurationArgumentType +import moe.nea.firmament.commands.RestArgumentType +import moe.nea.firmament.commands.get +import moe.nea.firmament.commands.thenArgument +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.events.TickEvent +import moe.nea.firmament.util.CommonSoundEffects +import moe.nea.firmament.util.FirmFormatters +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.MinecraftDispatcher +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.clickCommand +import moe.nea.firmament.util.lime +import moe.nea.firmament.util.red +import moe.nea.firmament.util.tr +import moe.nea.firmament.util.yellow + +object TimerFeature { + data class Timer( + val start: TimeMark, + val duration: Duration, + val message: String, + val timerId: Int, + ) { + fun timeLeft() = (duration - start.passedTime()).coerceAtLeast(0.seconds) + fun isDone() = start.passedTime() >= duration + } + + // Theoretically for optimal performance this could be a treeset keyed to the end time + val timers = mutableListOf() + + @Subscribe + fun tick(event: TickEvent) { + timers.removeAll { + if (it.isDone()) { + MC.sendChat(tr("firmament.timer.finished", + "The timer you set ${FirmFormatters.formatTimespan(it.duration)} ago just went off: ${it.message}") + .yellow()) + Firmament.coroutineScope.launch { + withContext(MinecraftDispatcher) { + repeat(5) { + CommonSoundEffects.playSuccess() + delay(0.2.seconds) + } + } + } + true + } else { + false + } + } + } + + fun startTimer(duration: Duration, message: String) { + val timerId = createTimerId++ + timers.add(Timer(TimeMark.now(), duration, message, timerId)) + MC.sendChat( + tr("firmament.timer.start", + "Timer started for $message in ${FirmFormatters.formatTimespan(duration)}.").lime() + .append(" ") + .append( + tr("firmament.timer.cancelbutton", + "Click here to cancel the timer." + ).clickCommand("/firm timer clear $timerId").red() + ) + ) + } + + fun clearTimer(timerId: Int) { + val timer = timers.indexOfFirst { it.timerId == timerId } + if (timer < 0) { + MC.sendChat(tr("firmament.timer.cancel.fail", + "Could not cancel that timer. Maybe it was already cancelled?").red()) + } else { + val timerData = timers[timer] + timers.removeAt(timer) + MC.sendChat(tr("firmament.timer.cancel.done", + "Cancelled timer ${timerData.message}. It would have been done in ${ + FirmFormatters.formatTimespan(timerData.timeLeft()) + }.").lime()) + } + } + + var createTimerId = 0 + + @Subscribe + fun onCommands(event: CommandEvent.SubCommand) { + event.subcommand("cleartimer") { + thenArgument("timerId", IntegerArgumentType.integer(0)) { timerId -> + thenExecute { + clearTimer(this[timerId]) + } + } + thenExecute { + timers.map { it.timerId }.forEach { + clearTimer(it) + } + } + } + event.subcommand("timer") { + thenArgument("time", DurationArgumentType) { duration -> + thenExecute { + startTimer(this[duration], "no message") + } + thenArgument("message", RestArgumentType) { message -> + thenExecute { + startTimer(this[duration], this[message]) + } + } + } + } + } +} diff --git a/src/main/kotlin/features/notifications/Notifications.kt b/src/main/kotlin/features/notifications/Notifications.kt new file mode 100644 index 0000000..8d912d1 --- /dev/null +++ b/src/main/kotlin/features/notifications/Notifications.kt @@ -0,0 +1,7 @@ + +package moe.nea.firmament.features.notifications + +import moe.nea.firmament.features.FirmamentFeature + +object Notifications { +} diff --git a/src/main/kotlin/features/world/ColeWeightCompat.kt b/src/main/kotlin/features/world/ColeWeightCompat.kt new file mode 100644 index 0000000..f7f1317 --- /dev/null +++ b/src/main/kotlin/features/world/ColeWeightCompat.kt @@ -0,0 +1,125 @@ +package moe.nea.firmament.features.world + +import kotlinx.serialization.Serializable +import net.minecraft.text.Text +import net.minecraft.util.math.BlockPos +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.DefaultSource +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.commands.thenLiteral +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.util.ClipboardUtils +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.tr + +object ColeWeightCompat { + @Serializable + data class ColeWeightWaypoint( + val x: Int?, + val y: Int?, + val z: Int?, + val r: Int = 0, + val g: Int = 0, + val b: Int = 0, + ) + + fun fromFirm(waypoints: FirmWaypoints, relativeTo: BlockPos): List { + return waypoints.waypoints.map { + ColeWeightWaypoint(it.x - relativeTo.x, it.y - relativeTo.y, it.z - relativeTo.z) + } + } + + fun intoFirm(waypoints: List, relativeTo: BlockPos): FirmWaypoints { + val w = waypoints + .filter { it.x != null && it.y != null && it.z != null } + .map { FirmWaypoints.Waypoint(it.x!! + relativeTo.x, it.y!! + relativeTo.y, it.z!! + relativeTo.z) } + return FirmWaypoints( + "Imported Waypoints", + "imported", + null, + w.toMutableList(), + false + ) + } + + fun copyAndInform( + source: DefaultSource, + origin: BlockPos, + positiveFeedback: (Int) -> Text, + ) { + val waypoints = Waypoints.useNonEmptyWaypoints() + ?.let { fromFirm(it, origin) } + if (waypoints == null) { + source.sendError(Waypoints.textNothingToExport()) + return + } + val data = + Firmament.tightJson.encodeToString>(waypoints) + ClipboardUtils.setTextContent(data) + source.sendFeedback(positiveFeedback(waypoints.size)) + } + + fun importAndInform( + source: DefaultSource, + pos: BlockPos?, + positiveFeedback: (Int) -> Text + ) { + val text = ClipboardUtils.getTextContents() + val wr = tryParse(text).map { intoFirm(it, pos ?: BlockPos.ORIGIN) } + val waypoints = wr.getOrElse { + source.sendError( + tr("firmament.command.waypoint.import.cw.error", + "Could not import ColeWeight waypoints.")) + Firmament.logger.error(it) + return + } + waypoints.lastRelativeImport = pos + Waypoints.waypoints = waypoints + source.sendFeedback(positiveFeedback(waypoints.size)) + } + + @Subscribe + fun onEvent(event: CommandEvent.SubCommand) { + event.subcommand(Waypoints.WAYPOINTS_SUBCOMMAND) { + thenLiteral("exportcw") { + thenExecute { + copyAndInform(source, BlockPos.ORIGIN) { + tr("firmament.command.waypoint.export.cw", + "Copied $it waypoints to clipboard in ColeWeight format.") + } + } + } + thenLiteral("exportrelativecw") { + thenExecute { + copyAndInform(source, MC.player?.blockPos ?: BlockPos.ORIGIN) { + tr("firmament.command.waypoint.export.cw.relative", + "Copied $it relative waypoints to clipboard in ColeWeight format. Make sure to stand in the same position when importing.") + } + } + } + thenLiteral("importcw") { + thenExecute { + importAndInform(source, null) { + tr("firmament.command.waypoint.import.cw.success", + "Imported $it waypoints from ColeWeight.") + } + } + } + thenLiteral("importrelativecw") { + thenExecute { + importAndInform(source, MC.player!!.blockPos) { + tr("firmament.command.waypoint.import.cw.relative", + "Imported $it relative waypoints from clipboard. Make sure you stand in the same position as when you exported these waypoints for them to line up correctly.") + } + } + } + } + } + + fun tryParse(string: String): Result> { + return runCatching { + Firmament.tightJson.decodeFromString>(string) + } + } +} diff --git a/src/main/kotlin/features/world/FairySouls.kt b/src/main/kotlin/features/world/FairySouls.kt new file mode 100644 index 0000000..d4bf560 --- /dev/null +++ b/src/main/kotlin/features/world/FairySouls.kt @@ -0,0 +1,132 @@ + + +package moe.nea.firmament.features.world + +import io.github.moulberry.repo.data.Coordinate +import me.shedaniel.math.Color +import kotlinx.serialization.Serializable +import kotlinx.serialization.serializer +import net.minecraft.text.Text +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Vec3d +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ProcessChatEvent +import moe.nea.firmament.events.SkyblockServerUpdateEvent +import moe.nea.firmament.events.WorldRenderLastEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.SkyBlockIsland +import moe.nea.firmament.util.blockPos +import moe.nea.firmament.util.data.ProfileSpecificDataHolder +import moe.nea.firmament.util.render.RenderInWorldContext +import moe.nea.firmament.util.render.RenderInWorldContext.Companion.renderInWorld +import moe.nea.firmament.util.unformattedString + + +object FairySouls : FirmamentFeature { + + + @Serializable + data class Data( + val foundSouls: MutableMap> = mutableMapOf() + ) + + override val config: ManagedConfig + get() = TConfig + + object DConfig : ProfileSpecificDataHolder(serializer(), "found-fairysouls", ::Data) + + + object TConfig : ManagedConfig("fairy-souls", Category.MISC) { + val displaySouls by toggle("show") { false } + val resetSouls by button("reset") { + DConfig.data?.foundSouls?.clear() != null + updateMissingSouls() + } + } + + + override val identifier: String get() = "fairy-souls" + + val playerReach = 5 + val playerReachSquared = playerReach * playerReach + + var currentLocationName: SkyBlockIsland? = null + var currentLocationSouls: List = emptyList() + var currentMissingSouls: List = emptyList() + + fun updateMissingSouls() { + currentMissingSouls = emptyList() + val c = DConfig.data ?: return + val fi = c.foundSouls[currentLocationName] ?: setOf() + val cms = currentLocationSouls.toMutableList() + fi.asSequence().sortedDescending().filter { it in cms.indices }.forEach { cms.removeAt(it) } + currentMissingSouls = cms + } + + fun updateWorldSouls() { + currentLocationSouls = emptyList() + val loc = currentLocationName ?: return + currentLocationSouls = RepoManager.neuRepo.constants.fairySouls.soulLocations[loc.locrawMode] ?: return + } + + fun findNearestClickableSoul(): Coordinate? { + val player = MC.player ?: return null + val pos = player.pos + val location = SBData.skyblockLocation ?: return null + val soulLocations: List = + RepoManager.neuRepo.constants.fairySouls.soulLocations[location.locrawMode] ?: return null + return soulLocations + .map { it to it.blockPos.getSquaredDistance(pos) } + .filter { it.second < playerReachSquared } + .minByOrNull { it.second } + ?.first + } + + private fun markNearestSoul() { + val nearestSoul = findNearestClickableSoul() ?: return + val c = DConfig.data ?: return + val loc = currentLocationName ?: return + val idx = currentLocationSouls.indexOf(nearestSoul) + c.foundSouls.computeIfAbsent(loc) { mutableSetOf() }.add(idx) + DConfig.markDirty() + updateMissingSouls() + } + + @Subscribe + fun onWorldRender(it: WorldRenderLastEvent) { + if (!TConfig.displaySouls) return + renderInWorld(it) { + currentMissingSouls.forEach { + block(it.blockPos, Color.ofRGBA(176, 0, 255, 128).color) + } + color(1f, 0f, 1f, 1f) + currentLocationSouls.forEach { + wireframeCube(it.blockPos) + } + } + } + + @Subscribe + fun onProcessChat(it: ProcessChatEvent) { + when (it.text.unformattedString) { + "You have already found that Fairy Soul!" -> { + markNearestSoul() + } + + "SOUL! You found a Fairy Soul!" -> { + markNearestSoul() + } + } + } + + @Subscribe + fun onLocationChange(it: SkyblockServerUpdateEvent) { + currentLocationName = it.newLocraw?.skyblockLocation + updateWorldSouls() + updateMissingSouls() + } +} diff --git a/src/main/kotlin/features/world/FirmWaypointManager.kt b/src/main/kotlin/features/world/FirmWaypointManager.kt new file mode 100644 index 0000000..d18483c --- /dev/null +++ b/src/main/kotlin/features/world/FirmWaypointManager.kt @@ -0,0 +1,168 @@ +package moe.nea.firmament.features.world + +import com.mojang.brigadier.arguments.StringArgumentType +import kotlinx.serialization.serializer +import net.minecraft.text.Text +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.DefaultSource +import moe.nea.firmament.commands.RestArgumentType +import moe.nea.firmament.commands.get +import moe.nea.firmament.commands.suggestsList +import moe.nea.firmament.commands.thenArgument +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.commands.thenLiteral +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.util.ClipboardUtils +import moe.nea.firmament.util.FirmFormatters +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TemplateUtil +import moe.nea.firmament.util.data.MultiFileDataHolder +import moe.nea.firmament.util.tr + +object FirmWaypointManager { + object DataHolder : MultiFileDataHolder(serializer(), "waypoints") + + val SHARE_PREFIX = "FIRM_WAYPOINTS/" + val ENCODED_SHARE_PREFIX = TemplateUtil.getPrefixComparisonSafeBase64Encoding(SHARE_PREFIX) + + fun createExportableCopy( + waypoints: FirmWaypoints, + ): FirmWaypoints { + val copy = waypoints.copy(waypoints = waypoints.waypoints.toMutableList()) + if (waypoints.isRelativeTo != null) { + val origin = waypoints.lastRelativeImport + if (origin != null) { + copy.waypoints.replaceAll { + it.copy( + x = it.x - origin.x, + y = it.y - origin.y, + z = it.z - origin.z, + ) + } + } else { + TODO("Add warning!") + } + } + return copy + } + + fun loadWaypoints(waypoints: FirmWaypoints, sendFeedback: (Text) -> Unit) { + val copy = waypoints.deepCopy() + if (copy.isRelativeTo != null) { + val origin = MC.player!!.blockPos + copy.waypoints.replaceAll { + it.copy( + x = it.x + origin.x, + y = it.y + origin.y, + z = it.z + origin.z, + ) + } + copy.lastRelativeImport = origin.toImmutable() + sendFeedback(tr("firmament.command.waypoint.import.ordered.success", + "Imported ${copy.size} relative waypoints. Make sure you stand in the correct spot while loading the waypoints: ${copy.isRelativeTo}.")) + } else { + sendFeedback(tr("firmament.command.waypoint.import.success", + "Imported ${copy.size} waypoints.")) + } + Waypoints.waypoints = copy + } + + fun setOrigin(source: DefaultSource, text: String?) { + val waypoints = Waypoints.useEditableWaypoints() + waypoints.isRelativeTo = text ?: waypoints.isRelativeTo ?: "" + val pos = MC.player!!.blockPos + waypoints.lastRelativeImport = pos + source.sendFeedback(tr("firmament.command.waypoint.originset", + "Set the origin of waypoints to ${FirmFormatters.formatPosition(pos)}. Run /firm waypoints export to save the waypoints relative to this position.")) + } + + @Subscribe + fun onCommands(event: CommandEvent.SubCommand) { + event.subcommand(Waypoints.WAYPOINTS_SUBCOMMAND) { + thenLiteral("setorigin") { + thenExecute { + setOrigin(source, null) + } + thenArgument("hint", RestArgumentType) { text -> + thenExecute { + setOrigin(source, this[text]) + } + } + } + thenLiteral("clearorigin") { + thenExecute { + val waypoints = Waypoints.useEditableWaypoints() + waypoints.lastRelativeImport = null + waypoints.isRelativeTo = null + source.sendFeedback(tr("firmament.command.waypoint.originunset", + "Unset the origin of the waypoints. Run /firm waypoints export to save the waypoints with absolute coordinates.")) + } + } + thenLiteral("save") { + thenArgument("name", StringArgumentType.string()) { name -> + suggestsList { DataHolder.list().keys } + thenExecute { + val waypoints = Waypoints.useNonEmptyWaypoints() + if (waypoints == null) { + source.sendError(Waypoints.textNothingToExport()) + return@thenExecute + } + waypoints.id = get(name) + val exportableWaypoints = createExportableCopy(waypoints) + DataHolder.insert(get(name), exportableWaypoints) + DataHolder.save() + source.sendFeedback(tr("firmament.command.waypoint.saved", + "Saved waypoints locally as ${get(name)}. Use /firm waypoints load to load them again.")) + } + } + } + thenLiteral("load") { + thenArgument("name", StringArgumentType.string()) { name -> + suggestsList { DataHolder.list().keys } + thenExecute { + val name = get(name) + val waypoints = DataHolder.list()[name] + if (waypoints == null) { + source.sendError( + tr("firmament.command.waypoint.nosaved", + "No saved waypoint for ${name}. Use tab completion to see available names.")) + return@thenExecute + } + loadWaypoints(waypoints, source::sendFeedback) + } + } + } + thenLiteral("export") { + thenExecute { + val waypoints = Waypoints.useNonEmptyWaypoints() + if (waypoints == null) { + source.sendError(Waypoints.textNothingToExport()) + return@thenExecute + } + val exportableWaypoints = createExportableCopy(waypoints) + val data = TemplateUtil.encodeTemplate(SHARE_PREFIX, exportableWaypoints) + ClipboardUtils.setTextContent(data) + source.sendFeedback(tr("firmament.command.waypoint.export", + "Copied ${exportableWaypoints.size} waypoints to clipboard in Firmament format.")) + } + } + thenLiteral("import") { + thenExecute { + val text = ClipboardUtils.getTextContents() + if (text.startsWith("[")) { + source.sendError(tr("firmament.command.waypoint.import.lookslikecw", + "The waypoints in your clipboard look like they might be ColeWeight waypoints. If so, use /firm waypoints importcw or /firm waypoints importrelativecw.")) + return@thenExecute + } + val waypoints = TemplateUtil.maybeDecodeTemplate(SHARE_PREFIX, text) + if (waypoints == null) { + source.sendError(tr("firmament.command.waypoint.import.error", + "Could not import Firmament waypoints from your clipboard. Make sure they are Firmament compatible waypoints.")) + return@thenExecute + } + loadWaypoints(waypoints, source::sendFeedback) + } + } + } + } +} diff --git a/src/main/kotlin/features/world/FirmWaypoints.kt b/src/main/kotlin/features/world/FirmWaypoints.kt new file mode 100644 index 0000000..d0cd55a --- /dev/null +++ b/src/main/kotlin/features/world/FirmWaypoints.kt @@ -0,0 +1,37 @@ +package moe.nea.firmament.features.world + +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import net.minecraft.util.math.BlockPos + +@Serializable +data class FirmWaypoints( + var label: String, + var id: String, + /** + * A hint to indicate where to stand while loading the waypoints. + */ + var isRelativeTo: String?, + var waypoints: MutableList, + var isOrdered: Boolean, + // TODO: val resetOnSwap: Boolean, +) { + + fun deepCopy() = copy(waypoints = waypoints.toMutableList()) + @Transient + var lastRelativeImport: BlockPos? = null + + val size get() = waypoints.size + @Serializable + data class Waypoint( + val x: Int, + val y: Int, + val z: Int, + ) { + val blockPos get() = BlockPos(x, y, z) + + companion object { + fun from(blockPos: BlockPos) = Waypoint(blockPos.x, blockPos.y, blockPos.z) + } + } +} diff --git a/src/main/kotlin/features/world/NPCWaypoints.kt b/src/main/kotlin/features/world/NPCWaypoints.kt new file mode 100644 index 0000000..592b8fa --- /dev/null +++ b/src/main/kotlin/features/world/NPCWaypoints.kt @@ -0,0 +1,40 @@ +package moe.nea.firmament.features.world + +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.events.ReloadRegistrationEvent +import moe.nea.firmament.util.MoulConfigUtils +import moe.nea.firmament.util.ScreenUtil + +object NPCWaypoints { + + var allNpcWaypoints = listOf() + + @Subscribe + fun onRepoReloadRegistration(event: ReloadRegistrationEvent) { + event.repo.registerReloadListener { + allNpcWaypoints = it.items.items.values + .asSequence() + .filter { !it.island.isNullOrBlank() } + .map { + NavigableWaypoint.NPCWaypoint(it) + } + .toList() + } + } + + @Subscribe + fun onOpenGui(event: CommandEvent.SubCommand) { + event.subcommand("npcs") { + thenExecute { + ScreenUtil.setScreenLater(MoulConfigUtils.loadScreen( + "npc_waypoints", + NpcWaypointGui(allNpcWaypoints), + null)) + } + } + } + + +} diff --git a/src/main/kotlin/features/world/NavigableWaypoint.kt b/src/main/kotlin/features/world/NavigableWaypoint.kt new file mode 100644 index 0000000..28a517f --- /dev/null +++ b/src/main/kotlin/features/world/NavigableWaypoint.kt @@ -0,0 +1,22 @@ +package moe.nea.firmament.features.world + +import io.github.moulberry.repo.data.NEUItem +import net.minecraft.util.math.BlockPos +import moe.nea.firmament.util.SkyBlockIsland + +abstract class NavigableWaypoint { + abstract val name: String + abstract val position: BlockPos + abstract val island: SkyBlockIsland + + data class NPCWaypoint( + val item: NEUItem, + ) : NavigableWaypoint() { + override val name: String + get() = item.displayName + override val position: BlockPos + get() = BlockPos(item.x, item.y, item.z) + override val island: SkyBlockIsland + get() = SkyBlockIsland.forMode(item.island) + } +} diff --git a/src/main/kotlin/features/world/NavigationHelper.kt b/src/main/kotlin/features/world/NavigationHelper.kt new file mode 100644 index 0000000..acdfb86 --- /dev/null +++ b/src/main/kotlin/features/world/NavigationHelper.kt @@ -0,0 +1,121 @@ +package moe.nea.firmament.features.world + +import io.github.moulberry.repo.constants.Islands +import net.minecraft.text.Text +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Position +import net.minecraft.util.math.Vec3i +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.SkyblockServerUpdateEvent +import moe.nea.firmament.events.TickEvent +import moe.nea.firmament.events.WorldRenderLastEvent +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.SkyBlockIsland +import moe.nea.firmament.util.WarpUtil +import moe.nea.firmament.util.render.RenderInWorldContext + +object NavigationHelper { + var targetWaypoint: NavigableWaypoint? = null + set(value) { + field = value + recalculateRoute() + } + + var nextTeleporter: Islands.Teleporter? = null + private set + + val Islands.Teleporter.toIsland get() = SkyBlockIsland.forMode(this.getTo()) + val Islands.Teleporter.fromIsland get() = SkyBlockIsland.forMode(this.getFrom()) + val Islands.Teleporter.blockPos get() = BlockPos(x.toInt(), y.toInt(), z.toInt()) + + @Subscribe + fun onWorldSwitch(event: SkyblockServerUpdateEvent) { + recalculateRoute() + } + + fun recalculateRoute() { + val tp = targetWaypoint + val currentIsland = SBData.skyblockLocation + if (tp == null || currentIsland == null) { + nextTeleporter = null + return + } + val route = findRoute(currentIsland, tp.island, mutableSetOf()) + nextTeleporter = route?.get(0) + } + + private fun findRoute( + fromIsland: SkyBlockIsland, + targetIsland: SkyBlockIsland, + visitedIslands: MutableSet + ): MutableList? { + var shortestChain: MutableList? = null + for (it in RepoManager.neuRepo.constants.islands.teleporters) { + if (it.toIsland in visitedIslands) continue + if (it.fromIsland != fromIsland) continue + if (it.toIsland == targetIsland) return mutableListOf(it) + visitedIslands.add(fromIsland) + val nextRoute = findRoute(it.toIsland, targetIsland, visitedIslands) ?: continue + nextRoute.add(0, it) + if (shortestChain == null || shortestChain.size > nextRoute.size) { + shortestChain = nextRoute + } + visitedIslands.remove(fromIsland) + } + return shortestChain + } + + + @Subscribe + fun onMovement(event: TickEvent) { // TODO: add a movement tick event maybe? + val tp = targetWaypoint ?: return + val p = MC.player ?: return + if (p.squaredDistanceTo(tp.position.toCenterPos()) < 5 * 5) { + targetWaypoint = null + } + } + + @Subscribe + fun drawWaypoint(event: WorldRenderLastEvent) { + val tp = targetWaypoint ?: return + val nt = nextTeleporter + RenderInWorldContext.renderInWorld(event) { + if (nt != null) { + waypoint(nt.blockPos, + Text.literal("Teleporter to " + nt.toIsland.userFriendlyName), + Text.literal("(towards " + tp.name + "§f)")) + } else if (tp.island == SBData.skyblockLocation) { + waypoint(tp.position, + Text.literal(tp.name)) + } + } + } + + fun tryWarpNear() { + val tp = targetWaypoint + if (tp == null) { + MC.sendChat(Text.literal("Could not find a waypoint to warp you to. Select one first.")) + return + } + WarpUtil.teleportToNearestWarp(tp.island, tp.position.asPositionView()) + } + +} + +fun Vec3i.asPositionView(): Position { + return object : Position { + override fun getX(): Double { + return this@asPositionView.x.toDouble() + } + + override fun getY(): Double { + return this@asPositionView.y.toDouble() + } + + override fun getZ(): Double { + return this@asPositionView.z.toDouble() + } + } +} diff --git a/src/main/kotlin/features/world/NpcWaypointGui.kt b/src/main/kotlin/features/world/NpcWaypointGui.kt new file mode 100644 index 0000000..6146e50 --- /dev/null +++ b/src/main/kotlin/features/world/NpcWaypointGui.kt @@ -0,0 +1,68 @@ +package moe.nea.firmament.features.world + +import io.github.notenoughupdates.moulconfig.observer.ObservableList +import io.github.notenoughupdates.moulconfig.xml.Bind +import moe.nea.firmament.features.events.anniversity.AnniversaryFeatures.atOnce +import moe.nea.firmament.keybindings.SavedKeyBinding + +class NpcWaypointGui( + val allWaypoints: List, +) { + + data class NavigableWaypointW(val waypoint: NavigableWaypoint) { + @Bind + fun name() = waypoint.name + + @Bind + fun isSelected() = NavigationHelper.targetWaypoint == waypoint + + @Bind + fun click() { + if (SavedKeyBinding.isShiftDown()) { + NavigationHelper.targetWaypoint = waypoint + NavigationHelper.tryWarpNear() + } else if (isSelected()) { + NavigationHelper.targetWaypoint = null + } else { + NavigationHelper.targetWaypoint = waypoint + } + } + } + + @JvmField + @field:Bind + var search: String = "" + var lastSearch: String? = null + + @Bind("results") + fun results(): ObservableList { + return results + } + + @Bind + fun tick() { + if (search != lastSearch) { + updateSearch() + lastSearch = search + } + } + + val results: ObservableList = ObservableList(mutableListOf()) + + fun updateSearch() { + val split = search.split(" +".toRegex()) + results.atOnce { + results.clear() + allWaypoints.filter { waypoint -> + if (search.isBlank()) { + true + } else { + split.all { waypoint.name.contains(it, ignoreCase = true) } + } + }.mapTo(results) { + NavigableWaypointW(it) + } + } + } + +} diff --git a/src/main/kotlin/features/world/TemporaryWaypoints.kt b/src/main/kotlin/features/world/TemporaryWaypoints.kt new file mode 100644 index 0000000..3c8e895 --- /dev/null +++ b/src/main/kotlin/features/world/TemporaryWaypoints.kt @@ -0,0 +1,74 @@ +package moe.nea.firmament.features.world + +import me.shedaniel.math.Color +import kotlin.compareTo +import kotlin.text.clear +import kotlin.time.Duration.Companion.seconds +import net.minecraft.text.Text +import net.minecraft.util.math.BlockPos +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ProcessChatEvent +import moe.nea.firmament.events.WorldReadyEvent +import moe.nea.firmament.events.WorldRenderLastEvent +import moe.nea.firmament.features.world.Waypoints.TConfig +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.TimeMark +import moe.nea.firmament.util.render.RenderInWorldContext + +object TemporaryWaypoints { + data class TemporaryWaypoint( + val pos: BlockPos, + val postedAt: TimeMark, + ) + val temporaryPlayerWaypointList = mutableMapOf() + val temporaryPlayerWaypointMatcher = "(?i)x: (-?[0-9]+),? y: (-?[0-9]+),? z: (-?[0-9]+)".toPattern() + @Subscribe + fun onProcessChat(it: ProcessChatEvent) { + val matcher = temporaryPlayerWaypointMatcher.matcher(it.unformattedString) + if (it.nameHeuristic != null && TConfig.tempWaypointDuration > 0.seconds && matcher.find()) { + temporaryPlayerWaypointList[it.nameHeuristic] = TemporaryWaypoint(BlockPos( + matcher.group(1).toInt(), + matcher.group(2).toInt(), + matcher.group(3).toInt(), + ), TimeMark.now()) + } + } + @Subscribe + fun onRenderTemporaryWaypoints(event: WorldRenderLastEvent) { + temporaryPlayerWaypointList.entries.removeIf { it.value.postedAt.passedTime() > TConfig.tempWaypointDuration } + if (temporaryPlayerWaypointList.isEmpty()) return + RenderInWorldContext.renderInWorld(event) { + temporaryPlayerWaypointList.forEach { (_, waypoint) -> + block(waypoint.pos, Color.ofRGBA(255, 255, 0, 128).color) + } + temporaryPlayerWaypointList.forEach { (player, waypoint) -> + val skin = + MC.networkHandler?.listedPlayerListEntries?.find { it.profile.name == player }?.skinTextures?.texture + withFacingThePlayer(waypoint.pos.toCenterPos()) { + waypoint(waypoint.pos, Text.stringifiedTranslatable("firmament.waypoint.temporary", player)) + if (skin != null) { + matrixStack.translate(0F, -20F, 0F) + // Head front + texture( + skin, 16, 16, + 1 / 8f, 1 / 8f, + 2 / 8f, 2 / 8f, + ) + // Head overlay + texture( + skin, 16, 16, + 5 / 8f, 1 / 8f, + 6 / 8f, 2 / 8f, + ) + } + } + } + } + } + + @Subscribe + fun onWorldReady(event: WorldReadyEvent) { + temporaryPlayerWaypointList.clear() + } + +} diff --git a/src/main/kotlin/features/world/Waypoints.kt b/src/main/kotlin/features/world/Waypoints.kt new file mode 100644 index 0000000..b4f91b0 --- /dev/null +++ b/src/main/kotlin/features/world/Waypoints.kt @@ -0,0 +1,234 @@ +package moe.nea.firmament.features.world + +import com.mojang.brigadier.arguments.IntegerArgumentType +import me.shedaniel.math.Color +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.seconds +import net.minecraft.command.argument.BlockPosArgumentType +import net.minecraft.text.Text +import net.minecraft.util.math.Vec3d +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.get +import moe.nea.firmament.commands.thenArgument +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.commands.thenLiteral +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.events.TickEvent +import moe.nea.firmament.events.WorldReadyEvent +import moe.nea.firmament.events.WorldRenderLastEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.mc.asFakeServer +import moe.nea.firmament.util.render.RenderInWorldContext +import moe.nea.firmament.util.tr + +object Waypoints : FirmamentFeature { + override val identifier: String + get() = "waypoints" + + object TConfig : ManagedConfig(identifier, Category.MINING) { // TODO: add to misc + val tempWaypointDuration by duration("temp-waypoint-duration", 0.seconds, 1.hours) { 30.seconds } + val showIndex by toggle("show-index") { true } + val skipToNearest by toggle("skip-to-nearest") { false } + val resetWaypointOrderOnWorldSwap by toggle("reset-order-on-swap") { true } + // TODO: look ahead size + } + + override val config get() = TConfig + var waypoints: FirmWaypoints? = null + var orderedIndex = 0 + + @Subscribe + fun onRenderOrderedWaypoints(event: WorldRenderLastEvent) { + val w = useNonEmptyWaypoints() ?: return + RenderInWorldContext.renderInWorld(event) { + if (!w.isOrdered) { + w.waypoints.withIndex().forEach { + block(it.value.blockPos, Color.ofRGBA(0, 80, 160, 128).color) + if (TConfig.showIndex) withFacingThePlayer(it.value.blockPos.toCenterPos()) { + text(Text.literal(it.index.toString())) + } + } + } else { + orderedIndex %= w.waypoints.size + val firstColor = Color.ofRGBA(0, 200, 40, 180) + color(firstColor) + tracer(w.waypoints[orderedIndex].blockPos.toCenterPos(), lineWidth = 3f) + w.waypoints.withIndex().toList().wrappingWindow(orderedIndex, 3).zip(listOf( + firstColor, + Color.ofRGBA(180, 200, 40, 150), + Color.ofRGBA(180, 80, 20, 140), + )).reversed().forEach { (waypoint, col) -> + val (index, pos) = waypoint + block(pos.blockPos, col.color) + if (TConfig.showIndex) withFacingThePlayer(pos.blockPos.toCenterPos()) { + text(Text.literal(index.toString())) + } + } + } + } + } + + @Subscribe + fun onTick(event: TickEvent) { + val w = useNonEmptyWaypoints() ?: return + if (!w.isOrdered) return + orderedIndex %= w.waypoints.size + val p = MC.player?.pos ?: return + if (TConfig.skipToNearest) { + orderedIndex = + (w.waypoints.withIndex().minBy { it.value.blockPos.getSquaredDistance(p) }.index + 1) % w.waypoints.size + + } else { + if (w.waypoints[orderedIndex].blockPos.isWithinDistance(p, 3.0)) { + orderedIndex = (orderedIndex + 1) % w.waypoints.size + } + } + } + + + fun useEditableWaypoints(): FirmWaypoints { + var w = waypoints + if (w == null) { + w = FirmWaypoints("Unlabeled", "unknown", null, mutableListOf(), false) + waypoints = w + } + return w + } + + fun useNonEmptyWaypoints(): FirmWaypoints? { + val w = waypoints + if (w == null) return null + if (w.waypoints.isEmpty()) return null + return w + } + + val WAYPOINTS_SUBCOMMAND = "waypoints" + + @Subscribe + fun onWorldSwap(event: WorldReadyEvent) { + if (TConfig.resetWaypointOrderOnWorldSwap) { + orderedIndex = 0 + } + } + + @Subscribe + fun onCommand(event: CommandEvent.SubCommand) { + event.subcommand("waypoint") { + thenArgument("pos", BlockPosArgumentType.blockPos()) { pos -> + thenExecute { + source + val position = pos.get(this).toAbsoluteBlockPos(source.asFakeServer()) + val w = useEditableWaypoints() + w.waypoints.add(FirmWaypoints.Waypoint.from(position)) + source.sendFeedback(Text.stringifiedTranslatable("firmament.command.waypoint.added", + position.x, + position.y, + position.z)) + } + } + } + event.subcommand(WAYPOINTS_SUBCOMMAND) { + thenLiteral("reset") { + thenExecute { + orderedIndex = 0 + source.sendFeedback(tr( + "firmament.command.waypoint.reset", + "Reset your ordered waypoint index back to 0. If you want to delete all waypoints use /firm waypoints clear instead.")) + } + } + thenLiteral("changeindex") { + thenArgument("from", IntegerArgumentType.integer(0)) { fromIndex -> + thenArgument("to", IntegerArgumentType.integer(0)) { toIndex -> + thenExecute { + val w = useEditableWaypoints() + val toIndex = toIndex.get(this) + val fromIndex = fromIndex.get(this) + if (fromIndex !in w.waypoints.indices) { + source.sendError(textInvalidIndex(fromIndex)) + return@thenExecute + } + if (toIndex !in w.waypoints.indices) { + source.sendError(textInvalidIndex(toIndex)) + return@thenExecute + } + val waypoint = w.waypoints.removeAt(fromIndex) + w.waypoints.add( + if (toIndex > fromIndex) toIndex - 1 + else toIndex, + waypoint) + source.sendFeedback( + tr("firmament.command.waypoint.indexchange", + "Moved waypoint from index $fromIndex to $toIndex. Note that this only matters for ordered waypoints.") + ) + } + } + } + } + thenLiteral("clear") { + thenExecute { + waypoints = null + source.sendFeedback(Text.translatable("firmament.command.waypoint.clear")) + } + } + thenLiteral("toggleordered") { + thenExecute { + val w = useEditableWaypoints() + w.isOrdered = !w.isOrdered + if (w.isOrdered) { + val p = MC.player?.pos ?: Vec3d.ZERO + orderedIndex = // TODO: this should be extracted to a utility method + w.waypoints.withIndex().minByOrNull { it.value.blockPos.getSquaredDistance(p) }?.index ?: 0 + } + source.sendFeedback(Text.translatable("firmament.command.waypoint.ordered.toggle.${w.isOrdered}")) + } + } + thenLiteral("skip") { + thenExecute { + val w = useNonEmptyWaypoints() + if (w != null && w.isOrdered) { + orderedIndex = (orderedIndex + 1) % w.size + source.sendFeedback(Text.translatable("firmament.command.waypoint.skip")) + } else { + source.sendError(Text.translatable("firmament.command.waypoint.skip.error")) + } + } + } + thenLiteral("remove") { + thenArgument("index", IntegerArgumentType.integer(0)) { indexArg -> + thenExecute { + val index = get(indexArg) + val w = useNonEmptyWaypoints() + if (w != null && index in w.waypoints.indices) { + w.waypoints.removeAt(index) + source.sendFeedback(Text.stringifiedTranslatable("firmament.command.waypoint.remove", + index)) + } else { + source.sendError(Text.stringifiedTranslatable("firmament.command.waypoint.remove.error")) + } + } + } + } + } + } + + fun textInvalidIndex(index: Int) = + tr("firmament.command.waypoint.invalid-index", + "Invalid index $index provided.") + + fun textNothingToExport(): Text = + tr("firmament.command.waypoint.export.nowaypoints", + "No waypoints to export found. Add some with /firm waypoint ~ ~ ~.") +} + +fun List.wrappingWindow(startIndex: Int, windowSize: Int): List { + val result = ArrayList(windowSize) + if (startIndex + windowSize < size) { + result.addAll(subList(startIndex, startIndex + windowSize)) + } else { + result.addAll(subList(startIndex, size)) + result.addAll(subList(0, minOf(windowSize - (size - startIndex), startIndex))) + } + return result +} diff --git a/src/main/kotlin/gui/BarComponent.kt b/src/main/kotlin/gui/BarComponent.kt new file mode 100644 index 0000000..da781da --- /dev/null +++ b/src/main/kotlin/gui/BarComponent.kt @@ -0,0 +1,115 @@ +package moe.nea.firmament.gui + +import com.mojang.blaze3d.systems.RenderSystem +import io.github.notenoughupdates.moulconfig.common.MyResourceLocation +import io.github.notenoughupdates.moulconfig.common.RenderContext +import io.github.notenoughupdates.moulconfig.gui.GuiComponent +import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext +import io.github.notenoughupdates.moulconfig.observer.GetSetter +import io.github.notenoughupdates.moulconfig.platform.ModernRenderContext +import me.shedaniel.math.Color +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.render.RenderLayer +import net.minecraft.util.Identifier +import moe.nea.firmament.Firmament + +class BarComponent( + val progress: GetSetter, val total: GetSetter, + val fillColor: Color, + val emptyColor: Color, +) : GuiComponent() { + override fun getWidth(): Int { + return 80 + } + + override fun getHeight(): Int { + return 8 + } + + data class Texture( + val identifier: Identifier, + val u1: Float, val v1: Float, + val u2: Float, val v2: Float, + ) { + fun draw(context: DrawContext, x: Int, y: Int, width: Int, height: Int, color: Color) { + context.drawTexturedQuad( + RenderLayer::getGuiTextured, + identifier, + x, y, x + width, x + height, + u1, u2, v1, v2, + color.color + ) + } + } + + companion object { + val resource = Firmament.identifier("textures/gui/bar.png") + val left = Texture(resource, 0 / 64F, 0 / 64F, 4 / 64F, 8 / 64F) + val middle = Texture(resource, 4 / 64F, 0 / 64F, 8 / 64F, 8 / 64F) + val right = Texture(resource, 8 / 64F, 0 / 64F, 12 / 64F, 8 / 64F) + val segmentOverlay = Texture(resource, 12 / 64F, 0 / 64F, 15 / 64F, 8 / 64F) + } + + private fun drawSection( + context: DrawContext, + texture: Texture, + x: Int, + y: Int, + width: Int, + sectionStart: Double, + sectionEnd: Double + ) { + if (sectionEnd < progress.get() && width == 4) { + texture.draw(context, x, y, 4, 8, fillColor) + return + } + if (sectionStart > progress.get() && width == 4) { + texture.draw(context, x, y, 4, 8, emptyColor) + return + } + val increasePerPixel = (sectionEnd - sectionStart) / width + var valueAtPixel = sectionStart + for (i in (0 until width)) { + val newTex = + Texture(texture.identifier, texture.u1 + i / 64F, texture.v1, texture.u1 + (i + 1) / 64F, texture.v2) + newTex.draw( + context, x + i, y, 1, 8, + if (valueAtPixel < progress.get()) fillColor else emptyColor + ) + valueAtPixel += increasePerPixel + } + } + + override fun render(context: GuiImmediateContext) { + val renderContext = (context.renderContext as ModernRenderContext).drawContext + var i = 0 + val x = 0 + val y = 0 + while (i < context.width - 4) { + drawSection( + renderContext, + if (i == 0) left else middle, + x + i, y, + (context.width - (i + 4)).coerceAtMost(4), + i * total.get() / context.width, (i + 4) * total.get() / context.width + ) + i += 4 + } + drawSection( + renderContext, + right, + x + context.width - 4, + y, + 4, + (context.width - 4) * total.get() / context.width, + total.get() + ) + RenderSystem.setShaderColor(1F, 1F, 1F, 1F) + + } + +} + +fun Identifier.toMoulConfig(): MyResourceLocation { + return MyResourceLocation(this.namespace, this.path) +} diff --git a/src/main/kotlin/gui/CheckboxComponent.kt b/src/main/kotlin/gui/CheckboxComponent.kt new file mode 100644 index 0000000..fc48661 --- /dev/null +++ b/src/main/kotlin/gui/CheckboxComponent.kt @@ -0,0 +1,57 @@ +package moe.nea.firmament.gui + +import io.github.notenoughupdates.moulconfig.gui.GuiComponent +import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext +import io.github.notenoughupdates.moulconfig.gui.MouseEvent +import io.github.notenoughupdates.moulconfig.observer.GetSetter +import io.github.notenoughupdates.moulconfig.platform.ModernRenderContext +import net.minecraft.client.render.RenderLayer +import moe.nea.firmament.Firmament + +class CheckboxComponent( + val state: GetSetter, + val value: T, +) : GuiComponent() { + override fun getWidth(): Int { + return 16 + } + + override fun getHeight(): Int { + return 16 + } + + fun isEnabled(): Boolean { + return state.get() == value + } + + override fun render(context: GuiImmediateContext) { + val ctx = (context.renderContext as ModernRenderContext).drawContext + ctx.drawGuiTexture( + RenderLayer::getGuiTextured, + if (isEnabled()) Firmament.identifier("widget/checkbox_checked") + else Firmament.identifier("widget/checkbox_unchecked"), + 0, 0, + 16, 16 + ) + } + + var isClicking = false + + override fun mouseEvent(mouseEvent: MouseEvent, context: GuiImmediateContext): Boolean { + if (mouseEvent is MouseEvent.Click) { + if (isClicking && !mouseEvent.mouseState && mouseEvent.mouseButton == 0) { + isClicking = false + if (context.isHovered) + state.set(value) + blur() + return true + } + if (mouseEvent.mouseState && mouseEvent.mouseButton == 0 && context.isHovered) { + requestFocus() + isClicking = true + return true + } + } + return false + } +} diff --git a/src/main/kotlin/gui/EmptyComponent.kt b/src/main/kotlin/gui/EmptyComponent.kt new file mode 100644 index 0000000..13efa89 --- /dev/null +++ b/src/main/kotlin/gui/EmptyComponent.kt @@ -0,0 +1,17 @@ +package moe.nea.firmament.gui + +import io.github.notenoughupdates.moulconfig.gui.GuiComponent +import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext + +class EmptyComponent : GuiComponent() { + override fun getWidth(): Int { + return 0 + } + + override fun getHeight(): Int { + return 0 + } + + override fun render(context: GuiImmediateContext) { + } +} diff --git a/src/main/kotlin/gui/FirmButtonComponent.kt b/src/main/kotlin/gui/FirmButtonComponent.kt new file mode 100644 index 0000000..fe9b476 --- /dev/null +++ b/src/main/kotlin/gui/FirmButtonComponent.kt @@ -0,0 +1,81 @@ + +package moe.nea.firmament.gui + +import io.github.notenoughupdates.moulconfig.common.MyResourceLocation +import io.github.notenoughupdates.moulconfig.deps.libninepatch.NinePatch +import io.github.notenoughupdates.moulconfig.gui.GuiComponent +import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext +import io.github.notenoughupdates.moulconfig.gui.MouseEvent +import io.github.notenoughupdates.moulconfig.gui.component.PanelComponent +import io.github.notenoughupdates.moulconfig.observer.GetSetter + + +open class FirmButtonComponent( + child: GuiComponent, + val isEnabled: GetSetter = GetSetter.constant(true), + val noBackground: Boolean = false, + val action: Runnable, +) : PanelComponent(child, if (noBackground) 0 else 2, DefaultBackgroundRenderer.TRANSPARENT) { + + /* TODO: make use of vanillas built in nine slicer */ + val hoveredBg = + NinePatch.builder(MyResourceLocation("minecraft", "textures/gui/sprites/widget/button_highlighted.png")) + .cornerSize(5) + .cornerUv(5 / 200F, 5 / 20F) + .mode(NinePatch.Mode.STRETCHING) + .build() + val unhoveredBg = NinePatch.builder(MyResourceLocation("minecraft", "textures/gui/sprites/widget/button.png")) + .cornerSize(5) + .cornerUv(5 / 200F, 5 / 20F) + .mode(NinePatch.Mode.STRETCHING) + .build() + val disabledBg = + NinePatch.builder(MyResourceLocation("minecraft", "textures/gui/sprites/widget/button_disabled.png")) + .cornerSize(5) + .cornerUv(5 / 200F, 5 / 20F) + .mode(NinePatch.Mode.STRETCHING) + .build() + val activeBg = NinePatch.builder(MyResourceLocation("firmament", "textures/gui/sprites/widget/button_active.png")) + .cornerSize(5) + .cornerUv(5 / 200F, 5 / 20F) + .mode(NinePatch.Mode.STRETCHING) + .build() + var isClicking = false + override fun mouseEvent(mouseEvent: MouseEvent, context: GuiImmediateContext): Boolean { + if (!isEnabled.get()) return false + if (isClicking) { + if (mouseEvent is MouseEvent.Click && !mouseEvent.mouseState && mouseEvent.mouseButton == 0) { + isClicking = false + if (context.isHovered) { + action.run() + } + return true + } + } + if (!context.isHovered) return false + if (mouseEvent !is MouseEvent.Click) return false + if (mouseEvent.mouseState && mouseEvent.mouseButton == 0) { + requestFocus() + isClicking = true + return true + } + return false + } + + open fun getBackground(context: GuiImmediateContext): NinePatch = + if (!isEnabled.get()) disabledBg + else if (context.isHovered || isClicking) hoveredBg + else unhoveredBg + + override fun render(context: GuiImmediateContext) { + context.renderContext.pushMatrix() + if (!noBackground) + context.renderContext.drawNinePatch( + getBackground(context), + 0f, 0f, context.width, context.height + ) + context.renderContext.translate(insets.toFloat(), insets.toFloat()) + element.render(getChildContext(context)) + context.renderContext.popMatrix() + } +} diff --git a/src/main/kotlin/gui/FirmHoverComponent.kt b/src/main/kotlin/gui/FirmHoverComponent.kt new file mode 100644 index 0000000..e38582a --- /dev/null +++ b/src/main/kotlin/gui/FirmHoverComponent.kt @@ -0,0 +1,59 @@ +package moe.nea.firmament.gui + +import io.github.notenoughupdates.moulconfig.gui.GuiComponent +import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext +import io.github.notenoughupdates.moulconfig.gui.KeyboardEvent +import io.github.notenoughupdates.moulconfig.gui.MouseEvent +import java.util.function.BiFunction +import java.util.function.Supplier +import kotlin.time.Duration +import moe.nea.firmament.util.TimeMark + +class FirmHoverComponent( + val child: GuiComponent, + val hoverLines: Supplier>, + val hoverDelay: Duration, +) : GuiComponent() { + override fun getWidth(): Int { + return child.width + } + + override fun getHeight(): Int { + return child.height + } + + override fun foldChildren( + initial: T, + visitor: BiFunction + ): T { + return visitor.apply(child, initial) + } + + override fun render(context: GuiImmediateContext) { + if (context.isHovered && (permaHover || lastMouseMove.passedTime() > hoverDelay)) { + context.renderContext.scheduleDrawTooltip(context.mouseX, context.mouseY, hoverLines.get()) + permaHover = true + } else { + permaHover = false + } + if (!context.isHovered) { + lastMouseMove = TimeMark.now() + } + child.render(context) + + } + + var permaHover = false + var lastMouseMove = TimeMark.farPast() + + override fun mouseEvent(mouseEvent: MouseEvent, context: GuiImmediateContext): Boolean { + if (mouseEvent is MouseEvent.Move) { + lastMouseMove = TimeMark.now() + } + return child.mouseEvent(mouseEvent, context) + } + + override fun keyboardEvent(event: KeyboardEvent, context: GuiImmediateContext): Boolean { + return child.keyboardEvent(event, context) + } +} diff --git a/src/main/kotlin/gui/FixedComponent.kt b/src/main/kotlin/gui/FixedComponent.kt new file mode 100644 index 0000000..ae1da2d --- /dev/null +++ b/src/main/kotlin/gui/FixedComponent.kt @@ -0,0 +1,38 @@ + +package moe.nea.firmament.gui + +import io.github.notenoughupdates.moulconfig.gui.GuiComponent +import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext +import io.github.notenoughupdates.moulconfig.gui.KeyboardEvent +import io.github.notenoughupdates.moulconfig.gui.MouseEvent +import io.github.notenoughupdates.moulconfig.observer.GetSetter +import java.util.function.BiFunction + +class FixedComponent( + val fixedWidth: GetSetter?, + val fixedHeight: GetSetter?, + val component: GuiComponent, +) : GuiComponent() { + override fun getWidth(): Int = fixedWidth?.get() ?: component.width + + override fun getHeight(): Int = fixedHeight?.get() ?: component.height + + override fun foldChildren(initial: T, visitor: BiFunction): T { + return visitor.apply(component, initial) + } + + fun fixContext(context: GuiImmediateContext): GuiImmediateContext = + context.translated(0, 0, width, height) + + override fun render(context: GuiImmediateContext) { + component.render(fixContext(context)) + } + + override fun mouseEvent(mouseEvent: MouseEvent, context: GuiImmediateContext): Boolean { + return component.mouseEvent(mouseEvent, fixContext(context)) + } + + override fun keyboardEvent(event: KeyboardEvent, context: GuiImmediateContext): Boolean { + return component.keyboardEvent(event, fixContext(context)) + } +} diff --git a/src/main/kotlin/gui/ImageComponent.kt b/src/main/kotlin/gui/ImageComponent.kt new file mode 100644 index 0000000..695c0ed --- /dev/null +++ b/src/main/kotlin/gui/ImageComponent.kt @@ -0,0 +1,35 @@ +package moe.nea.firmament.gui + +import io.github.notenoughupdates.moulconfig.common.MyResourceLocation +import io.github.notenoughupdates.moulconfig.gui.GuiComponent +import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext +import java.util.function.Supplier + +class ImageComponent( + private val width: Int, + private val height: Int, + val resourceLocation: Supplier, + val u1: Float, + val u2: Float, + val v1: Float, + val v2: Float, +) : GuiComponent() { + override fun getWidth(): Int { + return width + } + + override fun getHeight(): Int { + return height + } + + override fun render(context: GuiImmediateContext) { + context.renderContext.drawComplexTexture( + resourceLocation.get(), + 0f, 0f, + context.width.toFloat(), context.height.toFloat(), + { + it.uv(u1, v1, u2, v2) + } + ) + } +} diff --git a/src/main/kotlin/gui/TickComponent.kt b/src/main/kotlin/gui/TickComponent.kt new file mode 100644 index 0000000..d1879b1 --- /dev/null +++ b/src/main/kotlin/gui/TickComponent.kt @@ -0,0 +1,18 @@ +package moe.nea.firmament.gui + +import io.github.notenoughupdates.moulconfig.gui.GuiComponent +import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext + +class TickComponent(val onTick: Runnable) : GuiComponent() { + override fun getWidth(): Int { + return 0 + } + + override fun getHeight(): Int { + return 0 + } + + override fun render(context: GuiImmediateContext) { + onTick.run() + } +} diff --git a/src/main/kotlin/gui/config/AllConfigsGui.kt b/src/main/kotlin/gui/config/AllConfigsGui.kt new file mode 100644 index 0000000..f9ffd2d --- /dev/null +++ b/src/main/kotlin/gui/config/AllConfigsGui.kt @@ -0,0 +1,103 @@ +package moe.nea.firmament.gui.config + +import io.github.notenoughupdates.moulconfig.observer.ObservableList +import io.github.notenoughupdates.moulconfig.xml.Bind +import net.minecraft.client.gui.screen.Screen +import net.minecraft.text.Text +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.RestArgumentType +import moe.nea.firmament.commands.get +import moe.nea.firmament.commands.thenArgument +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.MoulConfigUtils +import moe.nea.firmament.util.ScreenUtil.setScreenLater + +object AllConfigsGui { +// +// val allConfigs +// get() = listOf( +// RepoManager.Config +// ) + FeatureManager.allFeatures.mapNotNull { it.config } + + object ConfigConfig : ManagedConfig("configconfig", Category.META) { + val enableYacl by toggle("enable-yacl") { false } + val enableMoulConfig by toggle("enable-moulconfig") { true } + val enableWideMC by toggle("wide-moulconfig") { false } + } + + fun List.toObservableList(): ObservableList = ObservableList(this) + + class CategoryMapping(val category: ManagedConfig.Category) { + @get:Bind("configs") + val configs = category.configs.map { EntryMapping(it) }.toObservableList() + + @Bind + fun name() = category.labelText.string + + @Bind + fun close() { + MC.screen?.close() + } + + class EntryMapping(val config: ManagedConfig) { + @Bind + fun name() = Text.translatable("firmament.config.${config.name}").string + + @Bind + fun openEditor() { + config.showConfigEditor(MC.screen) + } + } + } + + class CategoryView { + @get:Bind("categories") + val categories = ManagedConfig.Category.entries + .map { CategoryEntry(it) } + .toObservableList() + + class CategoryEntry(val category: ManagedConfig.Category) { + @Bind + fun name() = category.labelText.string + + @Bind + fun open() { + MC.screen = MoulConfigUtils.loadScreen("config/category", CategoryMapping(category), MC.screen) + } + } + } + + fun makeBuiltInScreen(parent: Screen? = null): Screen { + return MoulConfigUtils.loadScreen("config/main", CategoryView(), parent) + } + + fun makeScreen(search: String? = null, parent: Screen? = null): Screen { + val wantedKey = when { + ConfigConfig.enableMoulConfig -> "moulconfig" + ConfigConfig.enableYacl -> "yacl" + else -> "builtin" + } + val provider = FirmamentConfigScreenProvider.providers.find { it.key == wantedKey } + ?: FirmamentConfigScreenProvider.providers.first() + return provider.open(search, parent) + } + + fun showAllGuis() { + setScreenLater(makeScreen()) + } + + @Subscribe + fun registerCommands(event: CommandEvent.SubCommand) { + event.subcommand("search") { + thenArgument("search", RestArgumentType) { search -> + thenExecute { + val search = this[search] + setScreenLater(makeScreen(search = search)) + } + } + } + } + +} diff --git a/src/main/kotlin/gui/config/BooleanHandler.kt b/src/main/kotlin/gui/config/BooleanHandler.kt new file mode 100644 index 0000000..8592777 --- /dev/null +++ b/src/main/kotlin/gui/config/BooleanHandler.kt @@ -0,0 +1,37 @@ + + +package moe.nea.firmament.gui.config + +import io.github.notenoughupdates.moulconfig.gui.component.CenterComponent +import io.github.notenoughupdates.moulconfig.gui.component.SwitchComponent +import io.github.notenoughupdates.moulconfig.observer.GetSetter +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.jsonPrimitive + +class BooleanHandler(val config: ManagedConfig) : ManagedConfig.OptionHandler { + override fun toJson(element: Boolean): JsonElement? { + return JsonPrimitive(element) + } + + override fun fromJson(element: JsonElement): Boolean { + return element.jsonPrimitive.boolean + } + + override fun emitGuiElements(opt: ManagedOption, guiAppender: GuiAppender) { + guiAppender.appendLabeledRow( + opt.labelText, + CenterComponent(SwitchComponent(object : GetSetter { + override fun get(): Boolean { + return opt.get() + } + + override fun set(newValue: Boolean) { + opt.set(newValue) + config.save() + } + }, 200) + )) + } +} diff --git a/src/main/kotlin/gui/config/BuiltInConfigScreenProvider.kt b/src/main/kotlin/gui/config/BuiltInConfigScreenProvider.kt new file mode 100644 index 0000000..8ecdfa2 --- /dev/null +++ b/src/main/kotlin/gui/config/BuiltInConfigScreenProvider.kt @@ -0,0 +1,14 @@ +package moe.nea.firmament.gui.config + +import com.google.auto.service.AutoService +import net.minecraft.client.gui.screen.Screen + +@AutoService(FirmamentConfigScreenProvider::class) +class BuiltInConfigScreenProvider : FirmamentConfigScreenProvider { + override val key: String + get() = "builtin" + + override fun open(search: String?, parent: Screen?): Screen { + return AllConfigsGui.makeBuiltInScreen(parent) + } +} diff --git a/src/main/kotlin/gui/config/ChoiceHandler.kt b/src/main/kotlin/gui/config/ChoiceHandler.kt new file mode 100644 index 0000000..2ea3efc --- /dev/null +++ b/src/main/kotlin/gui/config/ChoiceHandler.kt @@ -0,0 +1,48 @@ +package moe.nea.firmament.gui.config + +import io.github.notenoughupdates.moulconfig.gui.HorizontalAlign +import io.github.notenoughupdates.moulconfig.gui.VerticalAlign +import io.github.notenoughupdates.moulconfig.gui.component.AlignComponent +import io.github.notenoughupdates.moulconfig.gui.component.RowComponent +import io.github.notenoughupdates.moulconfig.gui.component.TextComponent +import kotlinx.serialization.json.JsonElement +import kotlin.jvm.optionals.getOrNull +import net.minecraft.util.StringIdentifiable +import moe.nea.firmament.gui.CheckboxComponent +import moe.nea.firmament.util.ErrorUtil +import moe.nea.firmament.util.json.KJsonOps + +class ChoiceHandler( + val enumClass: Class, + val universe: List, +) : ManagedConfig.OptionHandler where E : Enum, E : StringIdentifiable { + val codec = StringIdentifiable.createCodec { + @Suppress("UNCHECKED_CAST", "PLATFORM_CLASS_MAPPED_TO_KOTLIN") + (universe as java.util.List<*>).toArray(arrayOfNulls>(0)) as Array + } + val renderer = EnumRenderer.default() + + override fun toJson(element: E): JsonElement? { + return codec.encodeStart(KJsonOps.INSTANCE, element) + .promotePartial { ErrorUtil.softError("Failed to encode json element '$element': $it") }.result() + .getOrNull() + } + + override fun fromJson(element: JsonElement): E { + return codec.decode(KJsonOps.INSTANCE, element) + .promotePartial { ErrorUtil.softError("Failed to decode json element '$element': $it") } + .result() + .get() + .first + } + + override fun emitGuiElements(opt: ManagedOption, guiAppender: GuiAppender) { + guiAppender.appendFullRow(TextComponent(opt.labelText.string)) + for (e in universe) { + guiAppender.appendFullRow(RowComponent( + AlignComponent(CheckboxComponent(opt, e), { HorizontalAlign.LEFT }, { VerticalAlign.CENTER }), + TextComponent(renderer.getName(opt, e).string) + )) + } + } +} diff --git a/src/main/kotlin/gui/config/ClickHandler.kt b/src/main/kotlin/gui/config/ClickHandler.kt new file mode 100644 index 0000000..fa1c621 --- /dev/null +++ b/src/main/kotlin/gui/config/ClickHandler.kt @@ -0,0 +1,24 @@ + + +package moe.nea.firmament.gui.config + +import io.github.notenoughupdates.moulconfig.gui.component.TextComponent +import kotlinx.serialization.json.JsonElement +import moe.nea.firmament.gui.FirmButtonComponent + +class ClickHandler(val config: ManagedConfig, val runnable: () -> Unit) : ManagedConfig.OptionHandler { + override fun toJson(element: Unit): JsonElement? { + return null + } + + override fun fromJson(element: JsonElement) {} + + override fun emitGuiElements(opt: ManagedOption, guiAppender: GuiAppender) { + guiAppender.appendLabeledRow( + opt.labelText, + FirmButtonComponent( + TextComponent(opt.labelText.string), + action = runnable), + ) + } +} diff --git a/src/main/kotlin/gui/config/ColourHandler.kt b/src/main/kotlin/gui/config/ColourHandler.kt new file mode 100644 index 0000000..83ce8ac --- /dev/null +++ b/src/main/kotlin/gui/config/ColourHandler.kt @@ -0,0 +1,82 @@ +package moe.nea.firmament.gui.config + +import io.github.notenoughupdates.moulconfig.ChromaColour +import io.github.notenoughupdates.moulconfig.gui.component.ColorSelectComponent +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement + +class ColourHandler(val config: ManagedConfig) : + ManagedConfig.OptionHandler { + @Serializable + data class ChromaDelegate( + @SerialName("h") + val hue: Float, + @SerialName("s") + val saturation: Float, + @SerialName("b") + val brightness: Float, + @SerialName("a") + val alpha: Int, + @SerialName("c") + val timeForFullRotationInMillis: Int, + ) { + constructor(delegate: ChromaColour) : this( + delegate.hue, + delegate.saturation, + delegate.brightness, + delegate.alpha, + delegate.timeForFullRotationInMillis + ) + + fun into(): ChromaColour = ChromaColour(hue, saturation, brightness, timeForFullRotationInMillis, alpha) + } + + object ChromaSerializer : KSerializer { + override val descriptor: SerialDescriptor + get() = SerialDescriptor("FirmChromaColour", ChromaDelegate.serializer().descriptor) + + override fun serialize( + encoder: Encoder, + value: ChromaColour + ) { + encoder.encodeSerializableValue(ChromaDelegate.serializer(), ChromaDelegate(value)) + } + + override fun deserialize(decoder: Decoder): ChromaColour { + return decoder.decodeSerializableValue(ChromaDelegate.serializer()).into() + } + } + + override fun toJson(element: ChromaColour): JsonElement? { + return Json.encodeToJsonElement(ChromaSerializer, element) + } + + override fun fromJson(element: JsonElement): ChromaColour { + return Json.decodeFromJsonElement(ChromaSerializer, element) + } + + override fun emitGuiElements( + opt: ManagedOption, + guiAppender: GuiAppender + ) { + guiAppender.appendLabeledRow( + opt.labelText, + ColorSelectComponent( + 0, + 0, + opt.value.toLegacyString(), + { + opt.value = ChromaColour.forLegacyString(it) + config.save() + }, + { } + ) + ) + } +} diff --git a/src/main/kotlin/gui/config/DurationHandler.kt b/src/main/kotlin/gui/config/DurationHandler.kt new file mode 100644 index 0000000..8d485b1 --- /dev/null +++ b/src/main/kotlin/gui/config/DurationHandler.kt @@ -0,0 +1,58 @@ + + +package moe.nea.firmament.gui.config + +import io.github.notenoughupdates.moulconfig.common.IMinecraft +import io.github.notenoughupdates.moulconfig.gui.component.RowComponent +import io.github.notenoughupdates.moulconfig.gui.component.SliderComponent +import io.github.notenoughupdates.moulconfig.gui.component.TextComponent +import io.github.notenoughupdates.moulconfig.observer.GetSetter +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long +import kotlin.time.Duration +import kotlin.time.DurationUnit +import kotlin.time.toDuration +import net.minecraft.text.Text +import moe.nea.firmament.util.FirmFormatters + +class DurationHandler(val config: ManagedConfig, val min: Duration, val max: Duration) : + ManagedConfig.OptionHandler { + override fun toJson(element: Duration): JsonElement? { + return JsonPrimitive(element.inWholeMilliseconds) + } + + override fun fromJson(element: JsonElement): Duration { + return element.jsonPrimitive.long.toDuration(DurationUnit.MILLISECONDS) + } + + override fun emitGuiElements(opt: ManagedOption, guiAppender: GuiAppender) { + guiAppender.appendLabeledRow( + opt.labelText, + RowComponent( + TextComponent(IMinecraft.instance.defaultFontRenderer, + { FirmFormatters.formatTimespan(opt.value) }, + 40, + TextComponent.TextAlignment.CENTER, + true, + false), + SliderComponent( + object : GetSetter { + override fun get(): Float { + return opt.value.toDouble(DurationUnit.SECONDS).toFloat() + } + + override fun set(newValue: Float) { + opt.value = newValue.toDouble().toDuration(DurationUnit.SECONDS) + } + }, + min.toDouble(DurationUnit.SECONDS).toFloat(), + max.toDouble(DurationUnit.SECONDS).toFloat(), + 0.1F, + 130 + ) + )) + } + +} diff --git a/src/main/kotlin/gui/config/EnumRenderer.kt b/src/main/kotlin/gui/config/EnumRenderer.kt new file mode 100644 index 0000000..3b80b7e --- /dev/null +++ b/src/main/kotlin/gui/config/EnumRenderer.kt @@ -0,0 +1,15 @@ +package moe.nea.firmament.gui.config + +import net.minecraft.text.Text + +interface EnumRenderer { + fun getName(option: ManagedOption, value: E): Text + + companion object { + fun > default() = object : EnumRenderer { + override fun getName(option: ManagedOption, value: E): Text { + return Text.translatable(option.rawLabelText + ".choice." + value.name.lowercase()) + } + } + } +} diff --git a/src/main/kotlin/gui/config/FirmamentConfigScreenProvider.kt b/src/main/kotlin/gui/config/FirmamentConfigScreenProvider.kt new file mode 100644 index 0000000..8700ffa --- /dev/null +++ b/src/main/kotlin/gui/config/FirmamentConfigScreenProvider.kt @@ -0,0 +1,24 @@ +package moe.nea.firmament.gui.config + +import net.minecraft.client.gui.screen.Screen +import moe.nea.firmament.util.compatloader.CompatLoader + +interface FirmamentConfigScreenProvider { + val key: String + val isEnabled: Boolean get() = true + + fun open(search: String?, parent: Screen?): Screen + + companion object : CompatLoader(FirmamentConfigScreenProvider::class) { + val providers by lazy { + allValidInstances + .filter { it.isEnabled } + .sortedWith( + Comparator + .comparing({ it.key == "builtin" }) + .reversed() + .then(Comparator.comparing({ it.key })) + ).toList() + } + } +} diff --git a/src/main/kotlin/gui/config/GuiAppender.kt b/src/main/kotlin/gui/config/GuiAppender.kt new file mode 100644 index 0000000..329319d --- /dev/null +++ b/src/main/kotlin/gui/config/GuiAppender.kt @@ -0,0 +1,40 @@ + + +package moe.nea.firmament.gui.config + +import io.github.notenoughupdates.moulconfig.gui.GuiComponent +import io.github.notenoughupdates.moulconfig.gui.component.RowComponent +import io.github.notenoughupdates.moulconfig.gui.component.TextComponent +import io.github.notenoughupdates.moulconfig.observer.GetSetter +import net.minecraft.client.gui.screen.Screen +import net.minecraft.text.Text +import moe.nea.firmament.gui.FixedComponent + +class GuiAppender(val width: Int, val screenAccessor: () -> Screen) { + val panel = mutableListOf() + internal val reloadables = mutableListOf<(() -> Unit)>() + + fun onReload(reloadable: () -> Unit) { + reloadables.add(reloadable) + } + + fun appendLabeledRow(label: Text, right: GuiComponent) { + appendSplitRow( + TextComponent(label.string), + right + ) + } + + fun appendSplitRow(left: GuiComponent, right: GuiComponent) { + // TODO: make this more dynamic + // i could just make a component that allows for using half the available size + appendFullRow(RowComponent( + FixedComponent(GetSetter.constant(width / 2), null, left), + FixedComponent(GetSetter.constant(width / 2), null, right), + )) + } + + fun appendFullRow(widget: GuiComponent) { + panel.add(widget) + } +} diff --git a/src/main/kotlin/gui/config/HudMetaHandler.kt b/src/main/kotlin/gui/config/HudMetaHandler.kt new file mode 100644 index 0000000..a9659ee --- /dev/null +++ b/src/main/kotlin/gui/config/HudMetaHandler.kt @@ -0,0 +1,42 @@ +package moe.nea.firmament.gui.config + +import io.github.notenoughupdates.moulconfig.gui.component.TextComponent +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.encodeToJsonElement +import net.minecraft.client.gui.screen.Screen +import net.minecraft.text.MutableText +import net.minecraft.text.Text +import moe.nea.firmament.gui.FirmButtonComponent +import moe.nea.firmament.jarvis.JarvisIntegration +import moe.nea.firmament.util.MC + +class HudMetaHandler(val config: ManagedConfig, val label: MutableText, val width: Int, val height: Int) : + ManagedConfig.OptionHandler { + override fun toJson(element: HudMeta): JsonElement? { + return Json.encodeToJsonElement(element.position) + } + + override fun fromJson(element: JsonElement): HudMeta { + return HudMeta(Json.decodeFromJsonElement(element), label, width, height) + } + + fun openEditor(option: ManagedOption, oldScreen: Screen) { + MC.screen = JarvisIntegration.jarvis.getHudEditor( + oldScreen, + listOf(option.value) + ) + } + + override fun emitGuiElements(opt: ManagedOption, guiAppender: GuiAppender) { + guiAppender.appendLabeledRow( + opt.labelText, + FirmButtonComponent( + TextComponent( + Text.stringifiedTranslatable("firmament.hud.edit", label).string), + ) { + openEditor(opt, guiAppender.screenAccessor()) + }) + } +} diff --git a/src/main/kotlin/gui/config/IntegerHandler.kt b/src/main/kotlin/gui/config/IntegerHandler.kt new file mode 100644 index 0000000..31ce90f --- /dev/null +++ b/src/main/kotlin/gui/config/IntegerHandler.kt @@ -0,0 +1,54 @@ + + +package moe.nea.firmament.gui.config + +import io.github.notenoughupdates.moulconfig.common.IMinecraft +import io.github.notenoughupdates.moulconfig.gui.component.RowComponent +import io.github.notenoughupdates.moulconfig.gui.component.SliderComponent +import io.github.notenoughupdates.moulconfig.gui.component.TextComponent +import io.github.notenoughupdates.moulconfig.observer.GetSetter +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonPrimitive +import moe.nea.firmament.util.FirmFormatters + +class IntegerHandler(val config: ManagedConfig, val min: Int, val max: Int) : ManagedConfig.OptionHandler { + override fun toJson(element: Int): JsonElement? { + return JsonPrimitive(element) + } + + override fun fromJson(element: JsonElement): Int { + return element.jsonPrimitive.int + } + + override fun emitGuiElements(opt: ManagedOption, guiAppender: GuiAppender) { + guiAppender.appendLabeledRow( + opt.labelText, + RowComponent( + TextComponent(IMinecraft.instance.defaultFontRenderer, + { FirmFormatters.formatCommas(opt.value, 0) }, + 40, + TextComponent.TextAlignment.CENTER, + true, + false), + SliderComponent( + object : GetSetter { + override fun get(): Float { + return opt.value.toFloat() + } + + override fun set(newValue: Float) { + opt.value = newValue.toInt() + } + }, + min.toFloat(), + max.toFloat(), + 0.1F, + 130 + ) + )) + + } + +} diff --git a/src/main/kotlin/gui/config/JAnyHud.kt b/src/main/kotlin/gui/config/JAnyHud.kt new file mode 100644 index 0000000..35c4eb2 --- /dev/null +++ b/src/main/kotlin/gui/config/JAnyHud.kt @@ -0,0 +1,48 @@ + + +package moe.nea.firmament.gui.config + +import moe.nea.jarvis.api.JarvisHud +import moe.nea.jarvis.api.JarvisScalable +import kotlinx.serialization.Serializable +import net.minecraft.text.Text + +@Serializable +data class HudPosition( + var x: Double, + var y: Double, + var scale: Float, +) + + +data class HudMeta( + val position: HudPosition, + private val label: Text, + private val width: Int, + private val height: Int, +) : JarvisScalable, JarvisHud { + override fun getX(): Double = position.x + + override fun setX(newX: Double) { + position.x = newX + } + + override fun getY(): Double = position.y + + override fun setY(newY: Double) { + position.y = newY + } + + override fun getLabel(): Text = label + + override fun getWidth(): Int = width + + override fun getHeight(): Int = height + + override fun getScale(): Float = position.scale + + override fun setScale(newScale: Float) { + position.scale = newScale + } + +} diff --git a/src/main/kotlin/gui/config/KeyBindingHandler.kt b/src/main/kotlin/gui/config/KeyBindingHandler.kt new file mode 100644 index 0000000..14a4b32 --- /dev/null +++ b/src/main/kotlin/gui/config/KeyBindingHandler.kt @@ -0,0 +1,52 @@ +package moe.nea.firmament.gui.config + +import io.github.notenoughupdates.moulconfig.common.IMinecraft +import io.github.notenoughupdates.moulconfig.common.MyResourceLocation +import io.github.notenoughupdates.moulconfig.deps.libninepatch.NinePatch +import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext +import io.github.notenoughupdates.moulconfig.gui.KeyboardEvent +import io.github.notenoughupdates.moulconfig.gui.component.TextComponent +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.encodeToJsonElement +import moe.nea.firmament.gui.FirmButtonComponent +import moe.nea.firmament.keybindings.FirmamentKeyBindings +import moe.nea.firmament.keybindings.SavedKeyBinding + +class KeyBindingHandler(val name: String, val managedConfig: ManagedConfig) : + ManagedConfig.OptionHandler { + + override fun initOption(opt: ManagedOption) { + FirmamentKeyBindings.registerKeyBinding(name, opt) + } + + override fun toJson(element: SavedKeyBinding): JsonElement? { + return Json.encodeToJsonElement(element) + } + + override fun fromJson(element: JsonElement): SavedKeyBinding { + return Json.decodeFromJsonElement(element) + } + + fun createButtonComponent(opt: ManagedOption): FirmButtonComponent { + lateinit var button: FirmButtonComponent + val sm = KeyBindingStateManager( + { opt.value }, + { + opt.value = it + opt.element.save() + }, + { button.blur() }, + { button.requestFocus() } + ) + button = sm.createButton() + sm.updateLabel() + return button + } + + override fun emitGuiElements(opt: ManagedOption, guiAppender: GuiAppender) { + guiAppender.appendLabeledRow(opt.labelText, createButtonComponent(opt)) + } + +} diff --git a/src/main/kotlin/gui/config/KeyBindingStateManager.kt b/src/main/kotlin/gui/config/KeyBindingStateManager.kt new file mode 100644 index 0000000..1528ac4 --- /dev/null +++ b/src/main/kotlin/gui/config/KeyBindingStateManager.kt @@ -0,0 +1,146 @@ +package moe.nea.firmament.gui.config + +import io.github.notenoughupdates.moulconfig.common.IMinecraft +import io.github.notenoughupdates.moulconfig.common.MyResourceLocation +import io.github.notenoughupdates.moulconfig.deps.libninepatch.NinePatch +import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext +import io.github.notenoughupdates.moulconfig.gui.KeyboardEvent +import io.github.notenoughupdates.moulconfig.gui.component.TextComponent +import org.lwjgl.glfw.GLFW +import net.minecraft.text.Text +import net.minecraft.util.Formatting +import moe.nea.firmament.gui.FirmButtonComponent +import moe.nea.firmament.keybindings.SavedKeyBinding + +class KeyBindingStateManager( + val value: () -> SavedKeyBinding, + val setValue: (key: SavedKeyBinding) -> Unit, + val blur: () -> Unit, + val requestFocus: () -> Unit, +) { + var editing = false + var lastPressed = 0 + var lastPressedNonModifier = 0 + var label: Text = Text.literal("") + + fun onClick() { + if (editing) { + editing = false + blur() + } else { + editing = true + requestFocus() + } + updateLabel() + } + + fun keyboardEvent(keyCode: Int, pressed: Boolean): Boolean { + return if (pressed) onKeyPressed(keyCode, SavedKeyBinding.getModInt()) + else onKeyReleased(keyCode, SavedKeyBinding.getModInt()) + } + + fun onKeyPressed(ch: Int, modifiers: Int): Boolean { + if (!editing) { + return false + } + if (ch == GLFW.GLFW_KEY_ESCAPE) { + lastPressedNonModifier = 0 + editing = false + lastPressed = 0 + setValue(SavedKeyBinding(GLFW.GLFW_KEY_UNKNOWN)) + updateLabel() + blur() + return true + } + if (ch == GLFW.GLFW_KEY_LEFT_SHIFT || ch == GLFW.GLFW_KEY_RIGHT_SHIFT + || ch == GLFW.GLFW_KEY_LEFT_ALT || ch == GLFW.GLFW_KEY_RIGHT_ALT + || ch == GLFW.GLFW_KEY_LEFT_CONTROL || ch == GLFW.GLFW_KEY_RIGHT_CONTROL + ) { + lastPressed = ch + } else { + setValue( + SavedKeyBinding( + ch, modifiers + ) + ) + editing = false + blur() + lastPressed = 0 + lastPressedNonModifier = 0 + } + updateLabel() + return true + } + + fun onLostFocus() { + lastPressedNonModifier = 0 + editing = false + lastPressed = 0 + updateLabel() + } + + fun onKeyReleased(ch: Int, modifiers: Int): Boolean { + if (!editing) + return false + if (lastPressedNonModifier == ch || (lastPressedNonModifier == 0 && ch == lastPressed)) { + setValue(SavedKeyBinding(ch, modifiers)) + editing = false + blur() + lastPressed = 0 + lastPressedNonModifier = 0 + } + updateLabel() + return true + } + + fun updateLabel() { + var stroke = value().format() + if (editing) { + stroke = Text.literal("") + val (shift, ctrl, alt) = SavedKeyBinding.getMods(SavedKeyBinding.getModInt()) + if (shift) { + stroke.append("SHIFT + ") + } + if (alt) { + stroke.append("ALT + ") + } + if (ctrl) { + stroke.append("CTRL + ") + } + stroke.append("???") + stroke.styled { it.withColor(Formatting.YELLOW) } + } + label = stroke + } + + fun createButton(): FirmButtonComponent { + return object : FirmButtonComponent( + TextComponent( + IMinecraft.instance.defaultFontRenderer, + { this@KeyBindingStateManager.label.string }, + 130, + TextComponent.TextAlignment.LEFT, + false, + false + ), action = { + this@KeyBindingStateManager.onClick() + }) { + override fun keyboardEvent(event: KeyboardEvent, context: GuiImmediateContext): Boolean { + if (event is KeyboardEvent.KeyPressed) { + return this@KeyBindingStateManager.keyboardEvent(event.keycode, event.pressed) + } + return super.keyboardEvent(event, context) + } + + override fun getBackground(context: GuiImmediateContext): NinePatch { + if (this@KeyBindingStateManager.editing) return activeBg + return super.getBackground(context) + } + + + override fun onLostFocus() { + this@KeyBindingStateManager.onLostFocus() + } + } + } +} diff --git a/src/main/kotlin/gui/config/ManagedConfig.kt b/src/main/kotlin/gui/config/ManagedConfig.kt new file mode 100644 index 0000000..12b82d6 --- /dev/null +++ b/src/main/kotlin/gui/config/ManagedConfig.kt @@ -0,0 +1,260 @@ +package moe.nea.firmament.gui.config + +import com.mojang.serialization.Codec +import io.github.notenoughupdates.moulconfig.ChromaColour +import io.github.notenoughupdates.moulconfig.gui.CloseEventListener +import io.github.notenoughupdates.moulconfig.gui.GuiComponentWrapper +import io.github.notenoughupdates.moulconfig.gui.GuiContext +import io.github.notenoughupdates.moulconfig.gui.component.CenterComponent +import io.github.notenoughupdates.moulconfig.gui.component.ColumnComponent +import io.github.notenoughupdates.moulconfig.gui.component.PanelComponent +import io.github.notenoughupdates.moulconfig.gui.component.RowComponent +import io.github.notenoughupdates.moulconfig.gui.component.ScrollPanelComponent +import io.github.notenoughupdates.moulconfig.gui.component.TextComponent +import moe.nea.jarvis.api.Point +import org.lwjgl.glfw.GLFW +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlin.io.path.createDirectories +import kotlin.io.path.readText +import kotlin.io.path.writeText +import kotlin.time.Duration +import net.minecraft.client.gui.screen.Screen +import net.minecraft.text.Text +import net.minecraft.util.StringIdentifiable +import moe.nea.firmament.Firmament +import moe.nea.firmament.gui.FirmButtonComponent +import moe.nea.firmament.keybindings.SavedKeyBinding +import moe.nea.firmament.util.ScreenUtil.setScreenLater +import moe.nea.firmament.util.collections.InstanceList + +abstract class ManagedConfig( + override val name: String, + val category: Category, + // TODO: allow vararg secondaryCategories: Category, +) : ManagedConfigElement() { + enum class Category { + // Böse Kategorie, nicht benutzten lol + MISC, + CHAT, + INVENTORY, + ITEMS, + MINING, + GARDEN, + EVENTS, + INTEGRATIONS, + META, + DEV, + ; + + val labelText: Text = Text.translatable("firmament.config.category.${name.lowercase()}") + val description: Text = Text.translatable("firmament.config.category.${name.lowercase()}.description") + val configs: MutableList = mutableListOf() + } + + companion object { + val allManagedConfigs = InstanceList("ManagedConfig") + } + + interface OptionHandler { + fun initOption(opt: ManagedOption) {} + fun toJson(element: T): JsonElement? + fun fromJson(element: JsonElement): T + fun emitGuiElements(opt: ManagedOption, guiAppender: GuiAppender) + } + + init { + allManagedConfigs.getAll().forEach { + require(it.name != name) { "Duplicate name '$name' used for config" } + } + allManagedConfigs.add(this) + category.configs.add(this) + } + + // TODO: warn if two files use the same config file name :( + val file = Firmament.CONFIG_DIR.resolve("$name.json") + val data: JsonObject by lazy { + try { + Firmament.json.decodeFromString( + file.readText() + ) + } catch (e: Exception) { + Firmament.logger.info("Could not read config $name. Loading empty config.") + JsonObject(mutableMapOf()) + } + } + + fun save() { + val data = JsonObject(allOptions.mapNotNull { (key, value) -> + value.toJson()?.let { + key to it + } + }.toMap()) + file.parent.createDirectories() + file.writeText(Firmament.json.encodeToString(data)) + } + + + val allOptions = mutableMapOf>() + val sortedOptions = mutableListOf>() + + private var latestGuiAppender: GuiAppender? = null + + protected fun option( + propertyName: String, + default: () -> T, + handler: OptionHandler + ): ManagedOption { + if (propertyName in allOptions) error("Cannot register the same name twice") + return ManagedOption(this, propertyName, default, handler).also { + it.handler.initOption(it) + it.load(data) + allOptions[propertyName] = it + sortedOptions.add(it) + } + } + + protected fun toggle(propertyName: String, default: () -> Boolean): ManagedOption { + return option(propertyName, default, BooleanHandler(this)) + } + + protected fun colour(propertyName: String, default: ()-> ChromaColour) : ManagedOption { + return option(propertyName, default, ColourHandler(this)) + } + + protected fun choice( + propertyName: String, + enumClass: Class, + default: () -> E + ): ManagedOption where E : Enum, E : StringIdentifiable { + return option(propertyName, default, ChoiceHandler(enumClass, enumClass.enumConstants.toList())) + } + + protected inline fun choice( + propertyName: String, + noinline default: () -> E + ): ManagedOption where E : Enum, E : StringIdentifiable { + return choice(propertyName, E::class.java, default) + } + + private fun createStringIdentifiable(x: () -> Array): Codec where E : Enum, E : StringIdentifiable { + return StringIdentifiable.createCodec { x() } + } + + // TODO: wait on https://youtrack.jetbrains.com/issue/KT-73434 +// protected inline fun choice( +// propertyName: String, +// noinline default: () -> E +// ): ManagedOption where E : Enum, E : StringIdentifiable { +// return choice( +// propertyName, +// enumEntries().toList(), +// StringIdentifiable.createCodec { enumValues() }, +// EnumRenderer.default(), +// default +// ) +// } + open fun onChange(option: ManagedOption<*>) { + } + + protected fun duration( + propertyName: String, + min: Duration, + max: Duration, + default: () -> Duration, + ): ManagedOption { + return option(propertyName, default, DurationHandler(this, min, max)) + } + + + protected fun position( + propertyName: String, + width: Int, + height: Int, + default: () -> Point, + ): ManagedOption { + val label = Text.translatable("firmament.config.${name}.${propertyName}") + return option(propertyName, { + val p = default() + HudMeta(HudPosition(p.x, p.y, 1F), label, width, height) + }, HudMetaHandler(this, label, width, height)) + } + + protected fun keyBinding( + propertyName: String, + default: () -> Int, + ): ManagedOption = keyBindingWithOutDefaultModifiers(propertyName) { SavedKeyBinding(default()) } + + protected fun keyBindingWithOutDefaultModifiers( + propertyName: String, + default: () -> SavedKeyBinding, + ): ManagedOption { + return option(propertyName, default, KeyBindingHandler("firmament.config.${name}.${propertyName}", this)) + } + + protected fun keyBindingWithDefaultUnbound( + propertyName: String, + ): ManagedOption { + return keyBindingWithOutDefaultModifiers(propertyName) { SavedKeyBinding(GLFW.GLFW_KEY_UNKNOWN) } + } + + protected fun integer( + propertyName: String, + min: Int, + max: Int, + default: () -> Int, + ): ManagedOption { + return option(propertyName, default, IntegerHandler(this, min, max)) + } + + protected fun button(propertyName: String, runnable: () -> Unit): ManagedOption { + return option(propertyName, { }, ClickHandler(this, runnable)) + } + + protected fun string(propertyName: String, default: () -> String): ManagedOption { + return option(propertyName, default, StringHandler(this)) + } + + + fun reloadGui() { + latestGuiAppender?.reloadables?.forEach { it() } + } + + val translationKey get() = "firmament.config.${name}" + val labelText: Text = Text.translatable(translationKey) + + fun getConfigEditor(parent: Screen? = null): Screen { + var screen: Screen? = null + val guiapp = GuiAppender(400) { requireNotNull(screen) { "Screen Accessor called too early" } } + latestGuiAppender = guiapp + guiapp.appendFullRow(RowComponent( + FirmButtonComponent(TextComponent("←")) { + if (parent != null) { + save() + setScreenLater(parent) + } else { + AllConfigsGui.showAllGuis() + } + } + )) + sortedOptions.forEach { it.appendToGui(guiapp) } + guiapp.reloadables.forEach { it() } + val component = CenterComponent(PanelComponent(ScrollPanelComponent(400, 300, ColumnComponent(guiapp.panel)), + 10, + PanelComponent.DefaultBackgroundRenderer.VANILLA)) + screen = object : GuiComponentWrapper(GuiContext(component)) { + override fun close() { + if (context.onBeforeClose() == CloseEventListener.CloseAction.NO_OBJECTIONS_TO_CLOSE) { + client!!.setScreen(parent) + } + } + } + return screen + } + + fun showConfigEditor(parent: Screen? = null) { + setScreenLater(getConfigEditor(parent)) + } + +} diff --git a/src/main/kotlin/gui/config/ManagedConfigElement.kt b/src/main/kotlin/gui/config/ManagedConfigElement.kt new file mode 100644 index 0000000..28cd6b8 --- /dev/null +++ b/src/main/kotlin/gui/config/ManagedConfigElement.kt @@ -0,0 +1,8 @@ + + +package moe.nea.firmament.gui.config + +abstract class ManagedConfigElement { + abstract val name: String + +} diff --git a/src/main/kotlin/gui/config/ManagedOption.kt b/src/main/kotlin/gui/config/ManagedOption.kt new file mode 100644 index 0000000..830086c --- /dev/null +++ b/src/main/kotlin/gui/config/ManagedOption.kt @@ -0,0 +1,68 @@ +package moe.nea.firmament.gui.config + +import io.github.notenoughupdates.moulconfig.observer.GetSetter +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty +import net.minecraft.text.Text +import moe.nea.firmament.util.ErrorUtil + +class ManagedOption( + val element: ManagedConfig, + val propertyName: String, + val default: () -> T, + val handler: ManagedConfig.OptionHandler +) : ReadWriteProperty, GetSetter { + override fun set(newValue: T) { + this.value = newValue + } + + override fun get(): T { + return this.value + } + + val rawLabelText = "firmament.config.${element.name}.${propertyName}" + val labelText: Text = Text.translatable(rawLabelText) + val descriptionTranslationKey = "firmament.config.${element.name}.${propertyName}.description" + val labelDescription: Text = Text.translatable(descriptionTranslationKey) + + private var actualValue: T? = null + var value: T + get() = actualValue ?: error("Lateinit variable not initialized") + set(value) { + actualValue = value + element.onChange(this) + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + this.value = value + } + + override fun getValue(thisRef: Any?, property: KProperty<*>): T { + return value + } + + fun load(root: JsonElement) { + if (root is JsonObject && root.containsKey(propertyName)) { + try { + value = handler.fromJson(root[propertyName]!!) + return + } catch (e: Exception) { + ErrorUtil.logError( + "Exception during loading of config file ${element.name}. This will reset this config.", + e + ) + } + } + value = default() + } + + fun toJson(): JsonElement? { + return handler.toJson(value) + } + + fun appendToGui(guiapp: GuiAppender) { + handler.emitGuiElements(this, guiapp) + } +} diff --git a/src/main/kotlin/gui/config/StringHandler.kt b/src/main/kotlin/gui/config/StringHandler.kt new file mode 100644 index 0000000..a326abb --- /dev/null +++ b/src/main/kotlin/gui/config/StringHandler.kt @@ -0,0 +1,36 @@ + + +package moe.nea.firmament.gui.config + +import io.github.notenoughupdates.moulconfig.gui.component.TextFieldComponent +import io.github.notenoughupdates.moulconfig.observer.GetSetter +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.jsonPrimitive +import net.minecraft.text.Text + +class StringHandler(val config: ManagedConfig) : ManagedConfig.OptionHandler { + override fun toJson(element: String): JsonElement? { + return JsonPrimitive(element) + } + + override fun fromJson(element: JsonElement): String { + return element.jsonPrimitive.content + } + + override fun emitGuiElements(opt: ManagedOption, guiAppender: GuiAppender) { + guiAppender.appendLabeledRow( + opt.labelText, + TextFieldComponent( + object : GetSetter by opt { + override fun set(newValue: String) { + opt.set(newValue) + config.save() + } + }, + 130, + suggestion = Text.translatableWithFallback(opt.rawLabelText + ".hint", "").string + ), + ) + } +} diff --git a/src/main/kotlin/gui/entity/EntityModifier.kt b/src/main/kotlin/gui/entity/EntityModifier.kt new file mode 100644 index 0000000..9623070 --- /dev/null +++ b/src/main/kotlin/gui/entity/EntityModifier.kt @@ -0,0 +1,9 @@ + +package moe.nea.firmament.gui.entity + +import com.google.gson.JsonObject +import net.minecraft.entity.LivingEntity + +fun interface EntityModifier { + fun apply(entity: LivingEntity, info: JsonObject): LivingEntity +} diff --git a/src/main/kotlin/gui/entity/EntityRenderer.kt b/src/main/kotlin/gui/entity/EntityRenderer.kt new file mode 100644 index 0000000..a1b2577 --- /dev/null +++ b/src/main/kotlin/gui/entity/EntityRenderer.kt @@ -0,0 +1,237 @@ +package moe.nea.firmament.gui.entity + +import com.google.gson.Gson +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import org.joml.Quaternionf +import org.joml.Vector3f +import kotlin.math.atan +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.gui.screen.ingame.InventoryScreen +import net.minecraft.entity.Entity +import net.minecraft.entity.EntityType +import net.minecraft.entity.LivingEntity +import net.minecraft.entity.SpawnReason +import net.minecraft.util.Identifier +import net.minecraft.world.World +import moe.nea.firmament.util.ErrorUtil +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.iterate +import moe.nea.firmament.util.openFirmamentResource +import moe.nea.firmament.util.render.enableScissorWithTranslation + +object EntityRenderer { + val fakeWorld: World get() = MC.lastWorld!! + private fun t(entityType: EntityType): () -> T { + return { entityType.create(fakeWorld, SpawnReason.LOAD)!! } + } + + val entityIds: Map LivingEntity> = mapOf( + "Armadillo" to t(EntityType.ARMADILLO), + "ArmorStand" to t(EntityType.ARMOR_STAND), + "Axolotl" to t(EntityType.AXOLOTL), + "BREEZE" to t(EntityType.BREEZE), + "Bat" to t(EntityType.BAT), + "Bee" to t(EntityType.BEE), + "Blaze" to t(EntityType.BLAZE), + "CaveSpider" to t(EntityType.CAVE_SPIDER), + "Chicken" to t(EntityType.CHICKEN), + "Cod" to t(EntityType.COD), + "Cow" to t(EntityType.COW), + "Creaking" to t(EntityType.CREAKING), + "Creeper" to t(EntityType.CREEPER), + "Dolphin" to t(EntityType.DOLPHIN), + "Donkey" to t(EntityType.DONKEY), + "Dragon" to t(EntityType.ENDER_DRAGON), + "Drowned" to t(EntityType.DROWNED), + "Eisengolem" to t(EntityType.IRON_GOLEM), + "Enderman" to t(EntityType.ENDERMAN), + "Endermite" to t(EntityType.ENDERMITE), + "Evoker" to t(EntityType.EVOKER), + "Fox" to t(EntityType.FOX), + "Frog" to t(EntityType.FROG), + "Ghast" to t(EntityType.GHAST), + "Giant" to t(EntityType.GIANT), + "GlowSquid" to t(EntityType.GLOW_SQUID), + "Goat" to t(EntityType.GOAT), + "Guardian" to t(EntityType.GUARDIAN), + "Horse" to t(EntityType.HORSE), + "Husk" to t(EntityType.HUSK), + "Illusioner" to t(EntityType.ILLUSIONER), + "LLama" to t(EntityType.LLAMA), + "MagmaCube" to t(EntityType.MAGMA_CUBE), + "Mooshroom" to t(EntityType.MOOSHROOM), + "Mule" to t(EntityType.MULE), + "Ocelot" to t(EntityType.OCELOT), + "Panda" to t(EntityType.PANDA), + "Phantom" to t(EntityType.PHANTOM), + "Pig" to t(EntityType.PIG), + "Piglin" to t(EntityType.PIGLIN), + "PiglinBrute" to t(EntityType.PIGLIN_BRUTE), + "Pigman" to t(EntityType.ZOMBIFIED_PIGLIN), + "Pillager" to t(EntityType.PILLAGER), + "Player" to { makeGuiPlayer(fakeWorld) }, + "PolarBear" to t(EntityType.POLAR_BEAR), + "Pufferfish" to t(EntityType.PUFFERFISH), + "Rabbit" to t(EntityType.RABBIT), + "Salmom" to t(EntityType.SALMON), + "Sheep" to t(EntityType.SHEEP), + "Shulker" to t(EntityType.SHULKER), + "Silverfish" to t(EntityType.SILVERFISH), + "Skeleton" to t(EntityType.SKELETON), + "Slime" to t(EntityType.SLIME), + "Sniffer" to t(EntityType.SNIFFER), + "Snowman" to t(EntityType.SNOW_GOLEM), + "Spider" to t(EntityType.SPIDER), + "Squid" to t(EntityType.SQUID), + "Stray" to t(EntityType.STRAY), + "Strider" to t(EntityType.STRIDER), + "Tadpole" to t(EntityType.TADPOLE), + "TropicalFish" to t(EntityType.TROPICAL_FISH), + "Turtle" to t(EntityType.TURTLE), + "Vex" to t(EntityType.VEX), + "Villager" to t(EntityType.VILLAGER), + "Vindicator" to t(EntityType.VINDICATOR), + "Warden" to t(EntityType.WARDEN), + "Witch" to t(EntityType.WITCH), + "Wither" to t(EntityType.WITHER), + "WitherSkeleton" to t(EntityType.WITHER_SKELETON), + "Wolf" to t(EntityType.WOLF), + "Zoglin" to t(EntityType.ZOGLIN), + "Zombie" to t(EntityType.ZOMBIE), + "ZombieVillager" to t(EntityType.ZOMBIE_VILLAGER) + ) + val entityModifiers: Map = mapOf( + "playerdata" to ModifyPlayerSkin, + "equipment" to ModifyEquipment, + "riding" to ModifyRiding, + "charged" to ModifyCharged, + "witherdata" to ModifyWither, + "invisible" to ModifyInvisible, + "age" to ModifyAge, + "horse" to ModifyHorse, + "name" to ModifyName, + ) + + fun applyModifiers(entityId: String, modifiers: List): LivingEntity? { + val entityType = ErrorUtil.notNullOr(entityIds[entityId], "Could not create entity with id $entityId") { + return null + } + var entity = ErrorUtil.catch("") { entityType() }.or { return null } + for (modifierJson in modifiers) { + val modifier = ErrorUtil.notNullOr( + modifierJson["type"]?.asString?.let(entityModifiers::get), + "Could not create entity with id $entityId. Failed to apply modifier $modifierJson") { return null } + entity = modifier.apply(entity, modifierJson) + } + return entity + } + + fun constructEntity(info: JsonObject): LivingEntity? { + val modifiers = (info["modifiers"] as JsonArray?)?.map { it.asJsonObject } ?: emptyList() + val entityType = ErrorUtil.notNullOr(info["entity"]?.asString, "Missing entity type on entity object") { + return null + } + return applyModifiers(entityType, modifiers) + } + + private val gson = Gson() + fun constructEntity(location: Identifier): LivingEntity? { + return constructEntity( + gson.fromJson( + location.openFirmamentResource().bufferedReader(), JsonObject::class.java + ) + ) + } + + fun renderEntity( + entity: LivingEntity, + renderContext: DrawContext, + posX: Int, + posY: Int, + // TODO: Add width, height properties here + width: Double, + height: Double, + mouseX: Double, + mouseY: Double, + entityScale: Double = (height - 10.0) / 2.0 + ) { + var bottomOffset = 0.0 + var currentEntity = entity + val maxSize = entity.iterate { it.firstPassenger as? LivingEntity } + .map { it.height } + .sum() + while (true) { + currentEntity.age = MC.player?.age ?: 0 + drawEntity( + renderContext, + posX, + posY, + (posX + width).toInt(), + (posY + height).toInt(), + minOf(2F / maxSize, 1F) * entityScale, + -bottomOffset, + mouseX, + mouseY, + currentEntity + ) + val next = currentEntity.firstPassenger as? LivingEntity ?: break + bottomOffset += currentEntity.getPassengerRidingPos(next).y.toFloat() * 0.75F + currentEntity = next + } + } + + + fun drawEntity( + context: DrawContext, + x1: Int, + y1: Int, + x2: Int, + y2: Int, + size: Double, + bottomOffset: Double, + mouseX: Double, + mouseY: Double, + entity: LivingEntity + ) { + context.enableScissorWithTranslation(x1.toFloat(), y1.toFloat(), x2.toFloat(), y2.toFloat()) + val centerX = (x1 + x2) / 2f + val centerY = (y1 + y2) / 2f + val hw = (x2 - x1) / 2 + val hh = (y2 - y1) / 2 + val targetYaw = atan(((centerX - mouseX) / hw)).toFloat() + val targetPitch = atan(((centerY - mouseY) / hh)).toFloat() + val rotateToFaceTheFront = Quaternionf().rotateZ(Math.PI.toFloat()) + val rotateToFaceTheCamera = Quaternionf().rotateX(targetPitch * 20.0f * (Math.PI.toFloat() / 180)) + rotateToFaceTheFront.mul(rotateToFaceTheCamera) + val oldBodyYaw = entity.bodyYaw + val oldYaw = entity.yaw + val oldPitch = entity.pitch + val oldPrevHeadYaw = entity.lastHeadYaw + val oldHeadYaw = entity.headYaw + entity.bodyYaw = 180.0f + targetYaw * 20.0f + entity.yaw = 180.0f + targetYaw * 40.0f + entity.pitch = -targetPitch * 20.0f + entity.headYaw = entity.yaw + entity.lastHeadYaw = entity.yaw + val vector3f = Vector3f(0.0f, (entity.height / 2.0f + bottomOffset).toFloat(), 0.0f) + InventoryScreen.drawEntity( + context, + centerX, + centerY, + size.toFloat(), + vector3f, + rotateToFaceTheFront, + rotateToFaceTheCamera, + entity + ) + entity.bodyYaw = oldBodyYaw + entity.yaw = oldYaw + entity.pitch = oldPitch + entity.lastHeadYaw = oldPrevHeadYaw + entity.headYaw = oldHeadYaw + context.disableScissor() + } + + +} diff --git a/src/main/kotlin/gui/entity/GuiPlayer.kt b/src/main/kotlin/gui/entity/GuiPlayer.kt new file mode 100644 index 0000000..f728dbf --- /dev/null +++ b/src/main/kotlin/gui/entity/GuiPlayer.kt @@ -0,0 +1,62 @@ +package moe.nea.firmament.gui.entity + +import com.mojang.authlib.GameProfile +import java.util.UUID +import net.minecraft.client.network.AbstractClientPlayerEntity +import net.minecraft.client.util.DefaultSkinHelper +import net.minecraft.client.util.SkinTextures +import net.minecraft.client.util.SkinTextures.Model +import net.minecraft.client.world.ClientWorld +import net.minecraft.util.Identifier +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Vec3d +import net.minecraft.world.World + +/** + * @see moe.nea.firmament.init.EarlyRiser + */ +fun makeGuiPlayer(world: World): GuiPlayer { + val constructor = GuiPlayer::class.java.getDeclaredConstructor( + World::class.java, + BlockPos::class.java, + Float::class.javaPrimitiveType, + GameProfile::class.java + ) + val player = constructor.newInstance(world, BlockPos.ORIGIN, 0F, GameProfile(UUID.randomUUID(), "Linnea")) + player.postInit() + return player +} + +class GuiPlayer(world: ClientWorld?, profile: GameProfile?) : AbstractClientPlayerEntity(world, profile) { + override fun isSpectator(): Boolean { + return false + } + + fun postInit() { + skinTexture = DefaultSkinHelper.getSkinTextures(this.getUuid()).texture + lastVelocity = Vec3d.ZERO + model = Model.WIDE + } + + override fun isCreative(): Boolean { + return false + } + + override fun shouldRenderName(): Boolean { + return false + } + + lateinit var skinTexture: Identifier + var capeTexture: Identifier? = null + var model: Model = Model.WIDE + override fun getSkinTextures(): SkinTextures { + return SkinTextures( + skinTexture, + null, + capeTexture, + null, + model, + true + ) + } +} diff --git a/src/main/kotlin/gui/entity/ModifyAge.kt b/src/main/kotlin/gui/entity/ModifyAge.kt new file mode 100644 index 0000000..a65c368 --- /dev/null +++ b/src/main/kotlin/gui/entity/ModifyAge.kt @@ -0,0 +1,25 @@ + +package moe.nea.firmament.gui.entity + +import com.google.gson.JsonObject +import net.minecraft.entity.LivingEntity +import net.minecraft.entity.decoration.ArmorStandEntity +import net.minecraft.entity.mob.ZombieEntity +import net.minecraft.entity.passive.PassiveEntity + +object ModifyAge : EntityModifier { + override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity { + val isBaby = info["baby"]?.asBoolean ?: false + if (entity is PassiveEntity) { + entity.breedingAge = if (isBaby) -1 else 1 + } else if (entity is ZombieEntity) { + entity.isBaby = isBaby + } else if (entity is ArmorStandEntity) { + entity.isSmall = isBaby + } else { + error("Cannot set age for $entity") + } + return entity + } + +} diff --git a/src/main/kotlin/gui/entity/ModifyCharged.kt b/src/main/kotlin/gui/entity/ModifyCharged.kt new file mode 100644 index 0000000..d22f6e3 --- /dev/null +++ b/src/main/kotlin/gui/entity/ModifyCharged.kt @@ -0,0 +1,14 @@ + +package moe.nea.firmament.gui.entity + +import com.google.gson.JsonObject +import net.minecraft.entity.LivingEntity +import net.minecraft.entity.mob.CreeperEntity + +object ModifyCharged : EntityModifier { + override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity { + require(entity is CreeperEntity) + entity.dataTracker.set(CreeperEntity.CHARGED, true) + return entity + } +} diff --git a/src/main/kotlin/gui/entity/ModifyEquipment.kt b/src/main/kotlin/gui/entity/ModifyEquipment.kt new file mode 100644 index 0000000..2ef5007 --- /dev/null +++ b/src/main/kotlin/gui/entity/ModifyEquipment.kt @@ -0,0 +1,55 @@ +package moe.nea.firmament.gui.entity + +import com.google.gson.JsonObject +import net.minecraft.component.DataComponentTypes +import net.minecraft.component.type.DyedColorComponent +import net.minecraft.entity.EquipmentSlot +import net.minecraft.entity.LivingEntity +import net.minecraft.item.Item +import net.minecraft.item.ItemStack +import net.minecraft.item.Items +import moe.nea.firmament.repo.ExpensiveItemCacheApi +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.mc.setEncodedSkullOwner +import moe.nea.firmament.util.mc.arbitraryUUID + +object ModifyEquipment : EntityModifier { + val names = mapOf( + "hand" to EquipmentSlot.MAINHAND, + "helmet" to EquipmentSlot.HEAD, + "chestplate" to EquipmentSlot.CHEST, + "leggings" to EquipmentSlot.LEGS, + "feet" to EquipmentSlot.FEET, + ) + + override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity { + names.forEach { (key, slot) -> + info[key]?.let { + entity.equipStack(slot, createItem(it.asString)) + } + } + return entity + } + + @OptIn(ExpensiveItemCacheApi::class) + private fun createItem(item: String): ItemStack { + val split = item.split("#") + if (split.size != 2) return SBItemStack(SkyblockId(item)).asImmutableItemStack() + val (type, data) = split + return when (type) { + "SKULL" -> ItemStack(Items.PLAYER_HEAD).also { it.setEncodedSkullOwner(arbitraryUUID, data) } + "LEATHER_LEGGINGS" -> coloredLeatherArmor(Items.LEATHER_LEGGINGS, data) + "LEATHER_BOOTS" -> coloredLeatherArmor(Items.LEATHER_BOOTS, data) + "LEATHER_HELMET" -> coloredLeatherArmor(Items.LEATHER_HELMET, data) + "LEATHER_CHESTPLATE" -> coloredLeatherArmor(Items.LEATHER_CHESTPLATE, data) + else -> error("Unknown leather piece: $type") + } + } + + private fun coloredLeatherArmor(leatherArmor: Item, data: String): ItemStack { + val stack = ItemStack(leatherArmor) + stack.set(DataComponentTypes.DYED_COLOR, DyedColorComponent(data.toInt(16))) + return stack + } +} diff --git a/src/main/kotlin/gui/entity/ModifyHorse.kt b/src/main/kotlin/gui/entity/ModifyHorse.kt new file mode 100644 index 0000000..7c8baa7 --- /dev/null +++ b/src/main/kotlin/gui/entity/ModifyHorse.kt @@ -0,0 +1,59 @@ +package moe.nea.firmament.gui.entity + +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import kotlin.experimental.and +import kotlin.experimental.inv +import kotlin.experimental.or +import net.minecraft.entity.EntityType +import net.minecraft.entity.EquipmentSlot +import net.minecraft.entity.LivingEntity +import net.minecraft.entity.SpawnReason +import net.minecraft.entity.passive.AbstractHorseEntity +import net.minecraft.item.ItemStack +import net.minecraft.item.Items +import moe.nea.firmament.gui.entity.EntityRenderer.fakeWorld + +object ModifyHorse : EntityModifier { + override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity { + require(entity is AbstractHorseEntity) + var entity: AbstractHorseEntity = entity + info["kind"]?.let { + entity = when (it.asString) { + "skeleton" -> EntityType.SKELETON_HORSE.create(fakeWorld, SpawnReason.LOAD)!! + "zombie" -> EntityType.ZOMBIE_HORSE.create(fakeWorld, SpawnReason.LOAD)!! + "mule" -> EntityType.MULE.create(fakeWorld, SpawnReason.LOAD)!! + "donkey" -> EntityType.DONKEY.create(fakeWorld, SpawnReason.LOAD)!! + "horse" -> EntityType.HORSE.create(fakeWorld, SpawnReason.LOAD)!! + else -> error("Unknown horse kind $it") + } + } + info["armor"]?.let { + if (it is JsonNull) { + entity.setHorseArmor(ItemStack.EMPTY) + } else { + when (it.asString) { + "iron" -> entity.setHorseArmor(ItemStack(Items.IRON_HORSE_ARMOR)) + "golden" -> entity.setHorseArmor(ItemStack(Items.GOLDEN_HORSE_ARMOR)) + "diamond" -> entity.setHorseArmor(ItemStack(Items.DIAMOND_HORSE_ARMOR)) + else -> error("Unknown horse armor $it") + } + } + } + info["saddled"]?.let { + entity.setIsSaddled(it.asBoolean) + } + return entity + } + +} + +fun AbstractHorseEntity.setIsSaddled(shouldBeSaddled: Boolean) { + this.equipStack(EquipmentSlot.SADDLE, + if (shouldBeSaddled) ItemStack(Items.SADDLE) + else ItemStack.EMPTY) +} + +fun AbstractHorseEntity.setHorseArmor(itemStack: ItemStack) { + this.equipBodyArmor(itemStack) +} diff --git a/src/main/kotlin/gui/entity/ModifyInvisible.kt b/src/main/kotlin/gui/entity/ModifyInvisible.kt new file mode 100644 index 0000000..8d36991 --- /dev/null +++ b/src/main/kotlin/gui/entity/ModifyInvisible.kt @@ -0,0 +1,13 @@ + +package moe.nea.firmament.gui.entity + +import com.google.gson.JsonObject +import net.minecraft.entity.LivingEntity + +object ModifyInvisible : EntityModifier { + override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity { + entity.isInvisible = info.get("invisible")?.asBoolean ?: true + return entity + } + +} diff --git a/src/main/kotlin/gui/entity/ModifyName.kt b/src/main/kotlin/gui/entity/ModifyName.kt new file mode 100644 index 0000000..a03da96 --- /dev/null +++ b/src/main/kotlin/gui/entity/ModifyName.kt @@ -0,0 +1,14 @@ + +package moe.nea.firmament.gui.entity + +import com.google.gson.JsonObject +import net.minecraft.entity.LivingEntity +import net.minecraft.text.Text + +object ModifyName : EntityModifier { + override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity { + entity.customName = Text.literal(info.get("name").asString) + return entity + } + +} diff --git a/src/main/kotlin/gui/entity/ModifyPlayerSkin.kt b/src/main/kotlin/gui/entity/ModifyPlayerSkin.kt new file mode 100644 index 0000000..28f0070 --- /dev/null +++ b/src/main/kotlin/gui/entity/ModifyPlayerSkin.kt @@ -0,0 +1,47 @@ + +package moe.nea.firmament.gui.entity + +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import kotlin.experimental.and +import kotlin.experimental.or +import net.minecraft.client.util.SkinTextures +import net.minecraft.entity.LivingEntity +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.entity.player.PlayerModelPart +import net.minecraft.util.Identifier + +object ModifyPlayerSkin : EntityModifier { + val playerModelPartIndex = PlayerModelPart.entries.associateBy { it.getName() } + override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity { + require(entity is GuiPlayer) + info["cape"]?.let { + entity.capeTexture = Identifier.of(it.asString) + } + info["skin"]?.let { + entity.skinTexture = Identifier.of(it.asString) + } + info["slim"]?.let { + entity.model = if (it.asBoolean) SkinTextures.Model.SLIM else SkinTextures.Model.WIDE + } + info["parts"]?.let { + var trackedData = entity.dataTracker.get(PlayerEntity.PLAYER_MODEL_PARTS) + if (it is JsonPrimitive && it.isBoolean) { + trackedData = (if (it.asBoolean) -1 else 0).toByte() + } else { + val obj = it.asJsonObject + for ((k, v) in obj.entrySet()) { + val part = playerModelPartIndex[k]!! + trackedData = if (v.asBoolean) { + trackedData and (part.bitFlag.inv().toByte()) + } else { + trackedData or (part.bitFlag.toByte()) + } + } + } + entity.dataTracker.set(PlayerEntity.PLAYER_MODEL_PARTS, trackedData) + } + return entity + } + +} diff --git a/src/main/kotlin/gui/entity/ModifyRiding.kt b/src/main/kotlin/gui/entity/ModifyRiding.kt new file mode 100644 index 0000000..5c4c78d --- /dev/null +++ b/src/main/kotlin/gui/entity/ModifyRiding.kt @@ -0,0 +1,15 @@ + +package moe.nea.firmament.gui.entity + +import com.google.gson.JsonObject +import net.minecraft.entity.LivingEntity + +object ModifyRiding : EntityModifier { + override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity { + val newEntity = EntityRenderer.constructEntity(info) + require(newEntity != null) + newEntity.startRiding(entity, true) + return entity + } + +} diff --git a/src/main/kotlin/gui/entity/ModifyWither.kt b/src/main/kotlin/gui/entity/ModifyWither.kt new file mode 100644 index 0000000..6083d88 --- /dev/null +++ b/src/main/kotlin/gui/entity/ModifyWither.kt @@ -0,0 +1,20 @@ + +package moe.nea.firmament.gui.entity + +import com.google.gson.JsonObject +import net.minecraft.entity.LivingEntity +import net.minecraft.entity.boss.WitherEntity + +object ModifyWither : EntityModifier { + override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity { + require(entity is WitherEntity) + info["tiny"]?.let { + entity.setInvulTimer(if (it.asBoolean) 800 else 0) + } + info["armored"]?.let { + entity.health = if (it.asBoolean) 1F else entity.maxHealth + } + return entity + } + +} diff --git a/src/main/kotlin/gui/hud/MoulConfigHud.kt b/src/main/kotlin/gui/hud/MoulConfigHud.kt new file mode 100644 index 0000000..e99b069 --- /dev/null +++ b/src/main/kotlin/gui/hud/MoulConfigHud.kt @@ -0,0 +1,66 @@ + +package moe.nea.firmament.gui.hud + +import io.github.notenoughupdates.moulconfig.gui.GuiComponentWrapper +import io.github.notenoughupdates.moulconfig.gui.GuiContext +import io.github.notenoughupdates.moulconfig.gui.component.TextComponent +import net.minecraft.resource.ResourceManager +import net.minecraft.resource.SynchronousResourceReloader +import moe.nea.firmament.events.FinalizeResourceManagerEvent +import moe.nea.firmament.events.HudRenderEvent +import moe.nea.firmament.gui.config.HudMeta +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.MoulConfigUtils + +abstract class MoulConfigHud( + val name: String, + val hudMeta: HudMeta, +) { + companion object { + private val componentWrapper by lazy { + object : GuiComponentWrapper(GuiContext(TextComponent("§cERROR"))) { + init { + this.client = MC.instance + } + } + } + } + + private var fragment: GuiContext? = null + + fun forceInit() { + } + + open fun shouldRender(): Boolean { + return true + } + + init { + require(name.matches("^[a-z_/]+$".toRegex())) + HudRenderEvent.subscribe("MoulConfigHud:render") { + if (!shouldRender()) return@subscribe + val renderContext = componentWrapper.createContext(it.context) + if (fragment == null) + loadFragment() + it.context.matrices.push() + hudMeta.applyTransformations(it.context.matrices) + val renderContextTranslated = + renderContext.translated(hudMeta.absoluteX, hudMeta.absoluteY, hudMeta.width, hudMeta.height) + .scaled(hudMeta.scale) + fragment!!.root.render(renderContextTranslated) + it.context.matrices.pop() + } + FinalizeResourceManagerEvent.subscribe("MoulConfigHud:finalizeResourceManager") { + MC.resourceManager.registerReloader(object : SynchronousResourceReloader { + override fun reload(manager: ResourceManager?) { + fragment = null + } + }) + } + } + + fun loadFragment() { + fragment = MoulConfigUtils.loadGui(name, this) + } + +} diff --git a/src/main/kotlin/jarvis/JarvisIntegration.kt b/src/main/kotlin/jarvis/JarvisIntegration.kt new file mode 100644 index 0000000..96f47f7 --- /dev/null +++ b/src/main/kotlin/jarvis/JarvisIntegration.kt @@ -0,0 +1,64 @@ + + +package moe.nea.firmament.jarvis + +import moe.nea.jarvis.api.Jarvis +import moe.nea.jarvis.api.JarvisConfigOption +import moe.nea.jarvis.api.JarvisHud +import moe.nea.jarvis.api.JarvisPlugin +import net.minecraft.client.gui.screen.Screen +import net.minecraft.text.Text +import moe.nea.firmament.Firmament +import moe.nea.firmament.features.FeatureManager +import moe.nea.firmament.gui.config.HudMeta +import moe.nea.firmament.gui.config.HudMetaHandler +import moe.nea.firmament.repo.RepoManager + +class JarvisIntegration : JarvisPlugin { + override fun getModId(): String = + Firmament.MOD_ID + + companion object { + lateinit var jarvis: Jarvis + } + + override fun onInitialize(jarvis: Jarvis) { + Companion.jarvis = jarvis + } + + val configs + get() = listOf( + RepoManager.Config + ) + FeatureManager.allFeatures.mapNotNull { it.config } + + + override fun getAllHuds(): List { + return configs.flatMap { config -> + config.sortedOptions.mapNotNull { if (it.handler is HudMetaHandler) it.value as HudMeta else null } + } + } + + override fun onHudEditorClosed() { + configs.forEach { it.save() } + } + + override fun getAllConfigOptions(): List { + return configs.flatMap { config -> + config.sortedOptions.map { + object : JarvisConfigOption { + override fun title(): Text { + return it.labelText + } + + override fun description(): List { + return emptyList() + } + + override fun jumpTo(parentScreen: Screen?): Screen { + return config.getConfigEditor(parentScreen) + } + } + } + } + } +} diff --git a/src/main/kotlin/keybindings/FirmamentKeyBindings.kt b/src/main/kotlin/keybindings/FirmamentKeyBindings.kt new file mode 100644 index 0000000..59b131a --- /dev/null +++ b/src/main/kotlin/keybindings/FirmamentKeyBindings.kt @@ -0,0 +1,25 @@ +package moe.nea.firmament.keybindings + +import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper +import net.minecraft.client.option.KeyBinding +import net.minecraft.client.util.InputUtil +import moe.nea.firmament.gui.config.ManagedOption +import moe.nea.firmament.util.TestUtil + +object FirmamentKeyBindings { + fun registerKeyBinding(name: String, config: ManagedOption) { + val vanillaKeyBinding = KeyBinding( + name, + InputUtil.Type.KEYSYM, + -1, + "firmament.key.category" + ) + if (!TestUtil.isInTest) { + KeyBindingHelper.registerKeyBinding(vanillaKeyBinding) + } + keyBindings[vanillaKeyBinding] = config + } + + val keyBindings = mutableMapOf>() + +} diff --git a/src/main/kotlin/keybindings/IKeyBinding.kt b/src/main/kotlin/keybindings/IKeyBinding.kt new file mode 100644 index 0000000..9d9b106 --- /dev/null +++ b/src/main/kotlin/keybindings/IKeyBinding.kt @@ -0,0 +1,50 @@ + + +package moe.nea.firmament.keybindings + +import net.minecraft.client.option.KeyBinding + +interface IKeyBinding { + fun matches(keyCode: Int, scanCode: Int, modifiers: Int): Boolean + fun matchesAtLeast(keyCode: Int, scanCode: Int, modifiers: Int): Boolean + + fun withModifiers(wantedModifiers: Int): IKeyBinding { + val old = this + return object : IKeyBinding { + override fun matches(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + return old.matchesAtLeast(keyCode, scanCode, modifiers) && (modifiers and wantedModifiers) == wantedModifiers + } + + override fun matchesAtLeast( + keyCode: Int, + scanCode: Int, + modifiers: Int + ): Boolean { + return old.matchesAtLeast(keyCode, scanCode, modifiers) && (modifiers.inv() and wantedModifiers) == 0 + } + } + } + + companion object { + fun minecraft(keyBinding: KeyBinding) = object : IKeyBinding { + override fun matches(keyCode: Int, scanCode: Int, modifiers: Int) = + keyBinding.matchesKey(keyCode, scanCode) + + override fun matchesAtLeast( + keyCode: Int, + scanCode: Int, + modifiers: Int + ): Boolean = + keyBinding.matchesKey(keyCode, scanCode) + } + + fun ofKeyCode(wantedKeyCode: Int) = object : IKeyBinding { + override fun matches(keyCode: Int, scanCode: Int, modifiers: Int): Boolean = keyCode == wantedKeyCode && modifiers == 0 + override fun matchesAtLeast( + keyCode: Int, + scanCode: Int, + modifiers: Int + ): Boolean = keyCode == wantedKeyCode + } + } +} diff --git a/src/main/kotlin/keybindings/SavedKeyBinding.kt b/src/main/kotlin/keybindings/SavedKeyBinding.kt new file mode 100644 index 0000000..01baa8f --- /dev/null +++ b/src/main/kotlin/keybindings/SavedKeyBinding.kt @@ -0,0 +1,125 @@ +package moe.nea.firmament.keybindings + +import org.lwjgl.glfw.GLFW +import kotlinx.serialization.Serializable +import net.minecraft.client.MinecraftClient +import net.minecraft.client.util.InputUtil +import net.minecraft.text.Text +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.mc.InitLevel + +// TODO: add support for mouse keybindings +@Serializable +data class SavedKeyBinding( + val keyCode: Int, + val shift: Boolean = false, + val ctrl: Boolean = false, + val alt: Boolean = false, +) : IKeyBinding { + val isBound: Boolean get() = keyCode != GLFW.GLFW_KEY_UNKNOWN + + constructor(keyCode: Int, mods: Triple) : this( + keyCode, + mods.first && keyCode != GLFW.GLFW_KEY_LEFT_SHIFT && keyCode != GLFW.GLFW_KEY_RIGHT_SHIFT, + mods.second && keyCode != GLFW.GLFW_KEY_LEFT_CONTROL && keyCode != GLFW.GLFW_KEY_RIGHT_CONTROL, + mods.third && keyCode != GLFW.GLFW_KEY_LEFT_ALT && keyCode != GLFW.GLFW_KEY_RIGHT_ALT, + ) + + constructor(keyCode: Int, mods: Int) : this(keyCode, getMods(mods)) + + companion object { + fun getMods(modifiers: Int): Triple { + return Triple( + modifiers and GLFW.GLFW_MOD_SHIFT != 0, + modifiers and GLFW.GLFW_MOD_CONTROL != 0, + modifiers and GLFW.GLFW_MOD_ALT != 0, + ) + } + + fun getModInt(): Int { + val h = MC.window.handle + val ctrl = if (MinecraftClient.IS_SYSTEM_MAC) { + InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_LEFT_SUPER) + || InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_RIGHT_SUPER) + } else InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_LEFT_CONTROL) + || InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_RIGHT_CONTROL) + val shift = isShiftDown() + val alt = InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_LEFT_ALT) + || InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_RIGHT_ALT) + var mods = 0 + if (ctrl) mods = mods or GLFW.GLFW_MOD_CONTROL + if (shift) mods = mods or GLFW.GLFW_MOD_SHIFT + if (alt) mods = mods or GLFW.GLFW_MOD_ALT + return mods + } + + private val h get() = MC.window.handle + fun isShiftDown() = shiftKeys.any { InputUtil.isKeyPressed(h, it) } + + fun unbound(): SavedKeyBinding = + SavedKeyBinding(GLFW.GLFW_KEY_UNKNOWN) + + val controlKeys = if (MinecraftClient.IS_SYSTEM_MAC) { + listOf(GLFW.GLFW_KEY_LEFT_SUPER, GLFW.GLFW_KEY_RIGHT_SUPER) + } else { + listOf(GLFW.GLFW_KEY_LEFT_CONTROL, GLFW.GLFW_KEY_RIGHT_CONTROL) + } + val shiftKeys = listOf(GLFW.GLFW_KEY_LEFT_SHIFT, GLFW.GLFW_KEY_RIGHT_SHIFT) + + val altKeys = listOf(GLFW.GLFW_KEY_LEFT_ALT, GLFW.GLFW_KEY_RIGHT_ALT) + } + + fun isPressed(atLeast: Boolean = false): Boolean { + if (!isBound) return false + val h = MC.window.handle + if (!InputUtil.isKeyPressed(h, keyCode)) return false + + // These are modifiers, so if the searched keyCode is a modifier key, then that key does not count as the modifier + val ctrl = keyCode !in controlKeys && controlKeys.any { InputUtil.isKeyPressed(h, it) } + val shift = keyCode !in shiftKeys && isShiftDown() + val alt = keyCode !in altKeys && altKeys.any { InputUtil.isKeyPressed(h, it) } + if (atLeast) + return (ctrl >= this.ctrl) && + (alt >= this.alt) && + (shift >= this.shift) + + return (ctrl == this.ctrl) && + (alt == this.alt) && + (shift == this.shift) + } + + override fun matchesAtLeast(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + if (this.keyCode == GLFW.GLFW_KEY_UNKNOWN) return false + val (shift, ctrl, alt) = getMods(modifiers) + return keyCode == this.keyCode && this.shift <= shift && this.ctrl <= ctrl && this.alt <= alt + } + + override fun matches(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + if (this.keyCode == GLFW.GLFW_KEY_UNKNOWN) return false + return keyCode == this.keyCode && getMods(modifiers) == Triple(shift, ctrl, alt) + } + + override fun toString(): String { + return format().string + } + + fun format(): Text { + val stroke = Text.literal("") + if (ctrl) { + stroke.append("CTRL + ") + } + if (alt) { + stroke.append("ALT + ") + } + if (shift) { + stroke.append("SHIFT + ") // TODO: translations? + } + if (InitLevel.isAtLeast(InitLevel.RENDER_INIT)) { + stroke.append(InputUtil.Type.KEYSYM.createFromCode(keyCode).localizedText) + } else { + stroke.append(keyCode.toString()) + } + return stroke + } + +} diff --git a/src/main/kotlin/repo/BetterRepoRecipeCache.kt b/src/main/kotlin/repo/BetterRepoRecipeCache.kt new file mode 100644 index 0000000..6d18223 --- /dev/null +++ b/src/main/kotlin/repo/BetterRepoRecipeCache.kt @@ -0,0 +1,31 @@ +package moe.nea.firmament.repo + +import io.github.moulberry.repo.IReloadable +import io.github.moulberry.repo.NEURepository +import io.github.moulberry.repo.data.NEUNpcShopRecipe +import io.github.moulberry.repo.data.NEURecipe +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.skyblockId + +class BetterRepoRecipeCache(vararg val extraProviders: ExtraRecipeProvider) : IReloadable { + var usages: Map> = mapOf() + var recipes: Map> = mapOf() + + override fun reload(repository: NEURepository) { + val usages = mutableMapOf>() + val recipes = mutableMapOf>() + val baseRecipes = repository.items.items.values + .asSequence() + .flatMap { it.recipes } + (baseRecipes + extraProviders.flatMap { it.provideExtraRecipes() }) + .forEach { recipe -> + if (recipe is NEUNpcShopRecipe) { + usages.getOrPut(recipe.isSoldBy.skyblockId, ::mutableSetOf).add(recipe) + } + recipe.allInputs.forEach { usages.getOrPut(SkyblockId(it.itemId), ::mutableSetOf).add(recipe) } + recipe.allOutputs.forEach { recipes.getOrPut(SkyblockId(it.itemId), ::mutableSetOf).add(recipe) } + } + this.usages = usages + this.recipes = recipes + } +} diff --git a/src/main/kotlin/repo/EssenceRecipeProvider.kt b/src/main/kotlin/repo/EssenceRecipeProvider.kt new file mode 100644 index 0000000..38559d5 --- /dev/null +++ b/src/main/kotlin/repo/EssenceRecipeProvider.kt @@ -0,0 +1,51 @@ +package moe.nea.firmament.repo + +import io.github.moulberry.repo.IReloadable +import io.github.moulberry.repo.NEURepository +import io.github.moulberry.repo.data.NEUIngredient +import io.github.moulberry.repo.data.NEURecipe +import moe.nea.firmament.util.SkyblockId + +class EssenceRecipeProvider : IReloadable, ExtraRecipeProvider { + data class EssenceUpgradeRecipe( + val itemId: SkyblockId, + val starCountAfter: Int, + val essenceCost: Int, + val essenceType: String, // TODO: replace with proper type + val extraItems: List, + ) : NEURecipe { + val essenceIngredient = NEUIngredient.fromString("${essenceType}:$essenceCost") + val allUpgradeComponents = listOf(essenceIngredient) + extraItems + + override fun getAllInputs(): Collection { + return listOf(NEUIngredient.fromString(itemId.neuItem + ":1")) + allUpgradeComponents + } + + override fun getAllOutputs(): Collection { + return listOf(NEUIngredient.fromString(itemId.neuItem + ":1")) + } + } + + var recipes = listOf() + private set + + override fun provideExtraRecipes(): Iterable = recipes + + override fun reload(repository: NEURepository) { + val recipes = mutableListOf() + for ((neuId, costs) in repository.constants.essenceCost.costs) { + // TODO: add dungeonization costs. this is in repo, but not in the repo parser. + for ((starCountAfter, essenceCost) in costs.essenceCosts.entries) { + val items = costs.itemCosts[starCountAfter] ?: emptyList() + recipes.add( + EssenceUpgradeRecipe( + SkyblockId(neuId), + starCountAfter, + essenceCost, + "ESSENCE_" + costs.type.uppercase(), // how flimsy + items.map { NEUIngredient.fromString(it) })) + } + } + this.recipes = recipes + } +} diff --git a/src/main/kotlin/repo/ExpLadder.kt b/src/main/kotlin/repo/ExpLadder.kt new file mode 100644 index 0000000..25a74de --- /dev/null +++ b/src/main/kotlin/repo/ExpLadder.kt @@ -0,0 +1,95 @@ + + +package moe.nea.firmament.repo + +import com.google.common.cache.CacheBuilder +import com.google.common.cache.CacheLoader +import io.github.moulberry.repo.IReloadable +import io.github.moulberry.repo.NEURepository +import io.github.moulberry.repo.constants.PetLevelingBehaviourOverride +import io.github.moulberry.repo.data.Rarity + +object ExpLadders : IReloadable { + + data class PetLevel( + val currentLevel: Int, + val maxLevel: Int, + val expRequiredForNextLevel: Long, + val expRequiredForMaxLevel: Long, + val expInCurrentLevel: Float, + var expTotal: Float, + ) { + val percentageToNextLevel: Float = expInCurrentLevel / expRequiredForNextLevel + val percentageToMaxLevel: Float = expTotal / expRequiredForMaxLevel + } + + data class ExpLadder( + val individualLevelCost: List, + ) { + val cumulativeLevelCost = individualLevelCost.runningFold(0F) { a, b -> a + b }.map { it.toLong() } + fun getPetLevel(currentExp: Double): PetLevel { + val currentOneIndexedLevel = cumulativeLevelCost.indexOfLast { it <= currentExp } + 1 + val expForNextLevel = if (currentOneIndexedLevel > individualLevelCost.size) { // Max leveled pet + individualLevelCost.last() + } else { + individualLevelCost[currentOneIndexedLevel - 1] + } + val expInCurrentLevel = + if (currentOneIndexedLevel >= cumulativeLevelCost.size) + currentExp.toFloat() - cumulativeLevelCost.last() + else + (expForNextLevel - (cumulativeLevelCost[currentOneIndexedLevel] - currentExp.toFloat())).coerceAtLeast( + 0F + ) + return PetLevel( + currentLevel = currentOneIndexedLevel, + maxLevel = cumulativeLevelCost.size, + expRequiredForNextLevel = expForNextLevel, + expRequiredForMaxLevel = cumulativeLevelCost.last(), + expInCurrentLevel = expInCurrentLevel, + expTotal = currentExp.toFloat() + ) + } + + fun getPetExpForLevel(level: Int): Long { + if (level < 2) return 0L + if (level >= cumulativeLevelCost.size) return cumulativeLevelCost.last() + return cumulativeLevelCost[level - 1] + } + } + + private data class Key(val petIdWithoutRarity: String, val rarity: Rarity) + + private val expLadders = CacheBuilder.newBuilder() + .build(object : CacheLoader() { + override fun load(key: Key): ExpLadder { + val pld = RepoManager.neuRepo.constants.petLevelingData + var exp = pld.petExpCostForLevel + var offset = pld.petLevelStartOffset[key.rarity]!! + var maxLevel = 100 + val override = pld.petLevelingBehaviourOverrides[key.petIdWithoutRarity] + if (override != null) { + maxLevel = override.maxLevel ?: maxLevel + offset = override.petLevelStartOffset?.get(key.rarity) ?: offset + when (override.petExpCostModifierType) { + PetLevelingBehaviourOverride.PetExpModifierType.APPEND -> + exp = exp + override.petExpCostModifier + + PetLevelingBehaviourOverride.PetExpModifierType.REPLACE -> + exp = override.petExpCostModifier + + null -> {} + } + } + return ExpLadder(exp.drop(offset).take(maxLevel - 1).map { it.toLong() }) + } + }) + + override fun reload(repository: NEURepository?) { + expLadders.invalidateAll() + } + + fun getExpLadder(petId: String, rarity: Rarity): ExpLadder { + return expLadders.get(Key(petId, rarity)) + } +} diff --git a/src/main/kotlin/repo/ExpensiveItemCacheApi.kt b/src/main/kotlin/repo/ExpensiveItemCacheApi.kt new file mode 100644 index 0000000..eef95a6 --- /dev/null +++ b/src/main/kotlin/repo/ExpensiveItemCacheApi.kt @@ -0,0 +1,8 @@ +package moe.nea.firmament.repo + +/** + * Marker for functions that could potentially invoke DFU. Please do not call on a lot of objects at once, or try to make sure the item is cached and fall back to a more gentle function call using [SBItemStack.isWarm] and similar functions. + */ +@RequiresOptIn +@Retention(AnnotationRetention.BINARY) +annotation class ExpensiveItemCacheApi diff --git a/src/main/kotlin/repo/ExtraRecipeProvider.kt b/src/main/kotlin/repo/ExtraRecipeProvider.kt new file mode 100644 index 0000000..9d3b5a0 --- /dev/null +++ b/src/main/kotlin/repo/ExtraRecipeProvider.kt @@ -0,0 +1,7 @@ +package moe.nea.firmament.repo + +import io.github.moulberry.repo.data.NEURecipe + +interface ExtraRecipeProvider { + fun provideExtraRecipes(): Iterable +} diff --git a/src/main/kotlin/repo/HypixelStaticData.kt b/src/main/kotlin/repo/HypixelStaticData.kt new file mode 100644 index 0000000..b0ada77 --- /dev/null +++ b/src/main/kotlin/repo/HypixelStaticData.kt @@ -0,0 +1,122 @@ +package moe.nea.firmament.repo + +import io.ktor.client.call.body +import io.ktor.client.request.get +import org.apache.logging.log4j.LogManager +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlin.time.Duration.Companion.minutes +import moe.nea.firmament.Firmament +import moe.nea.firmament.apis.CollectionResponse +import moe.nea.firmament.apis.CollectionSkillData +import moe.nea.firmament.util.SkyblockId + +object HypixelStaticData { + private val logger = LogManager.getLogger("Firmament.HypixelStaticData") + private val moulberryBaseUrl = "https://moulberry.codes" + private val hypixelApiBaseUrl = "https://api.hypixel.net" + var lowestBin: Map = mapOf() + private set + var avg1dlowestBin: Map = mapOf() + private set + var avg3dlowestBin: Map = mapOf() + private set + var avg7dlowestBin: Map = mapOf() + private set + var bazaarData: Map = mapOf() + private set + var collectionData: Map = mapOf() + private set + + @Serializable + data class BazaarData( + @SerialName("product_id") + val productId: SkyblockId.BazaarStock, + @SerialName("quick_status") + val quickStatus: BazaarStatus, + ) + + @Serializable + data class BazaarStatus( + val sellPrice: Double, + val sellVolume: Long, + val sellMovingWeek: Long, + val sellOrders: Long, + val buyPrice: Double, + val buyVolume: Long, + val buyMovingWeek: Long, + val buyOrders: Long + ) + + @Serializable + private data class BazaarResponse( + val success: Boolean, + val products: Map = mapOf(), + ) + + + fun getPriceOfItem(item: SkyblockId): Double? = bazaarData[SkyblockId.BazaarStock.fromSkyBlockId(item)]?.quickStatus?.buyPrice ?: lowestBin[item] + + fun hasBazaarStock(item: SkyblockId.BazaarStock): Boolean { + return item in bazaarData + } + + fun hasAuctionHouseOffers(item: SkyblockId): Boolean { + return (item in lowestBin) // TODO: || (item in biddableAuctionPrices) + } + + fun spawnDataCollectionLoop() { + Firmament.coroutineScope.launch { + logger.info("Updating collection data") + updateCollectionData() + } + Firmament.coroutineScope.launch { + while (true) { + logger.info("Updating NEU prices") + updatePrices() + delay(10.minutes) + } + } + } + + private suspend fun updatePrices() { + awaitAll( + Firmament.coroutineScope.async { fetchBazaarPrices() }, + Firmament.coroutineScope.async { fetchPricesFromMoulberry() }, + ) + } + + private suspend fun fetchPricesFromMoulberry() { + lowestBin = Firmament.httpClient.get("$moulberryBaseUrl/lowestbin.json") + .body>() + avg1dlowestBin = Firmament.httpClient.get("$moulberryBaseUrl/auction_averages_lbin/1day.json") + .body>() + avg3dlowestBin = Firmament.httpClient.get("$moulberryBaseUrl/auction_averages_lbin/3day.json") + .body>() + avg7dlowestBin = Firmament.httpClient.get("$moulberryBaseUrl/auction_averages_lbin/7day.json") + .body>() + } + + private suspend fun fetchBazaarPrices() { + val response = Firmament.httpClient.get("$hypixelApiBaseUrl/skyblock/bazaar").body() + if (!response.success) { + logger.warn("Retrieved unsuccessful bazaar data") + } + bazaarData = response.products + } + + private suspend fun updateCollectionData() { + val response = + Firmament.httpClient.get("$hypixelApiBaseUrl/resources/skyblock/collections").body() + if (!response.success) { + logger.warn("Retrieved unsuccessful collection data") + } + collectionData = response.collections + logger.info("Downloaded ${collectionData.values.sumOf { it.items.values.size }} collections") + } + +} diff --git a/src/main/kotlin/repo/ItemCache.kt b/src/main/kotlin/repo/ItemCache.kt new file mode 100644 index 0000000..14decd8 --- /dev/null +++ b/src/main/kotlin/repo/ItemCache.kt @@ -0,0 +1,312 @@ +package moe.nea.firmament.repo + +import com.mojang.serialization.Dynamic +import io.github.moulberry.repo.IReloadable +import io.github.moulberry.repo.NEURepository +import io.github.moulberry.repo.data.NEUItem +import java.text.NumberFormat +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap +import org.apache.logging.log4j.LogManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.io.path.readText +import kotlin.jvm.optionals.getOrNull +import net.minecraft.SharedConstants +import net.minecraft.component.DataComponentTypes +import net.minecraft.component.type.NbtComponent +import net.minecraft.datafixer.Schemas +import net.minecraft.datafixer.TypeReferences +import net.minecraft.item.ItemStack +import net.minecraft.item.Items +import net.minecraft.nbt.NbtCompound +import net.minecraft.nbt.NbtElement +import net.minecraft.nbt.NbtOps +import net.minecraft.nbt.NbtString +import net.minecraft.nbt.StringNbtReader +import net.minecraft.text.MutableText +import net.minecraft.text.Style +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import moe.nea.firmament.Firmament +import moe.nea.firmament.features.debug.ExportedTestConstantMeta +import moe.nea.firmament.repo.RepoManager.initialize +import moe.nea.firmament.util.LegacyFormattingCode +import moe.nea.firmament.util.LegacyTagParser +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.MinecraftDispatcher +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.TestUtil +import moe.nea.firmament.util.directLiteralStringContent +import moe.nea.firmament.util.mc.FirmamentDataComponentTypes +import moe.nea.firmament.util.mc.appendLore +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.mc.modifyLore +import moe.nea.firmament.util.mc.setCustomName +import moe.nea.firmament.util.mc.setSkullOwner +import moe.nea.firmament.util.skyblockId +import moe.nea.firmament.util.transformEachRecursively + +object ItemCache : IReloadable { + private val cache: MutableMap = ConcurrentHashMap() + private val df = Schemas.getFixer() + val logger = LogManager.getLogger("${Firmament.logger.name}.ItemCache") + var isFlawless = true + private set + + private fun NEUItem.get10809CompoundTag(): NbtCompound = NbtCompound().apply { + put("tag", LegacyTagParser.parse(nbttag)) + putString("id", minecraftItemId) + putByte("Count", 1) + putShort("Damage", damage.toShort()) + } + + @ExpensiveItemCacheApi + private fun NbtCompound.transformFrom10809ToModern() = convert189ToModern(this@transformFrom10809ToModern) + val currentSaveVersion = SharedConstants.getGameVersion().saveVersion.id + + @ExpensiveItemCacheApi + fun convert189ToModern(nbtComponent: NbtCompound): NbtCompound? = + try { + df.update( + TypeReferences.ITEM_STACK, + Dynamic(NbtOps.INSTANCE, nbtComponent), + -1, + currentSaveVersion + ).value as NbtCompound + } catch (e: Exception) { + isFlawless = false + logger.error("Could not data fix up $this", e) + null + } + + val ItemStack.isBroken + get() = get(FirmamentDataComponentTypes.IS_BROKEN) ?: false + + fun ItemStack.withFallback(fallback: ItemStack?): ItemStack { + if (isBroken && fallback != null) return fallback + return this + } + + fun brokenItemStack(neuItem: NEUItem?, idHint: SkyblockId? = null): ItemStack { + return ItemStack(Items.PAINTING).apply { + setCustomName(Text.literal(neuItem?.displayName ?: idHint?.neuItem ?: "null")) + appendLore( + listOf( + Text.stringifiedTranslatable( + "firmament.repo.brokenitem", + (neuItem?.skyblockItemId ?: idHint) + ) + ) + ) + set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(NbtCompound().apply { + put("ID", NbtString.of(neuItem?.skyblockItemId ?: idHint?.neuItem ?: "null")) + })) + set(FirmamentDataComponentTypes.IS_BROKEN, true) + } + } + + fun un189Lore(lore: String): MutableText { + val base = Text.literal("") + base.setStyle(Style.EMPTY.withItalic(false)) + var lastColorCode = Style.EMPTY + var readOffset = 0 + while (readOffset < lore.length) { + var nextCode = lore.indexOf('§', readOffset) + if (nextCode < 0) { + nextCode = lore.length + } + val text = lore.substring(readOffset, nextCode) + if (text.isNotEmpty()) { + base.append(Text.literal(text).setStyle(lastColorCode)) + } + readOffset = nextCode + 2 + if (nextCode + 1 < lore.length) { + val colorCode = lore[nextCode + 1] + val formatting = LegacyFormattingCode.byCode[colorCode.lowercaseChar()] ?: LegacyFormattingCode.RESET + val modernFormatting = formatting.modern + if (modernFormatting.isColor) { + lastColorCode = Style.EMPTY.withColor(modernFormatting) + } else { + lastColorCode = lastColorCode.withFormatting(modernFormatting) + } + } + } + return base + } + + fun tryFindFromModernFormat(skyblockId: SkyblockId): NbtCompound? { + val overlayFile = + RepoManager.overlayData.getMostModernReadableOverlay(skyblockId, currentSaveVersion) ?: return null + val overlay = StringNbtReader.readCompound(overlayFile.path.readText()) + val result = ExportedTestConstantMeta.SOURCE_CODEC.decode( + NbtOps.INSTANCE, overlay + ).result().getOrNull() ?: return null + val meta = result.first + return df.update( + TypeReferences.ITEM_STACK, + Dynamic(NbtOps.INSTANCE, result.second), + meta.dataVersion, + currentSaveVersion + ).value as NbtCompound + } + + @ExpensiveItemCacheApi + private fun NEUItem.asItemStackNow(): ItemStack { + + try { + var modernItemTag = tryFindFromModernFormat(this.skyblockId) + val oldItemTag = get10809CompoundTag() + var usedOldNbt = false + if (modernItemTag == null) { + usedOldNbt = true + modernItemTag = oldItemTag.transformFrom10809ToModern() + ?: return brokenItemStack(this) + } + val itemInstance = + ItemStack.fromNbt(MC.defaultRegistries, modernItemTag).getOrNull() ?: return brokenItemStack(this) + if (usedOldNbt) { + val tag = oldItemTag.getCompound("tag") + val extraAttributes = tag.flatMap { it.getCompound("ExtraAttributes") } + .getOrNull() + if (extraAttributes != null) + itemInstance.set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(extraAttributes)) + val itemModel = tag.flatMap { it.getString("ItemModel") }.getOrNull() + if (itemModel != null) + itemInstance.set(DataComponentTypes.ITEM_MODEL, Identifier.of(itemModel)) + } + itemInstance.loreAccordingToNbt = lore.map { un189Lore(it) } + itemInstance.displayNameAccordingToNbt = un189Lore(displayName) + return itemInstance + } catch (e: Exception) { + e.printStackTrace() + return brokenItemStack(this) + } + } + + fun hasCacheFor(skyblockId: SkyblockId): Boolean { + return skyblockId.neuItem in cache + } + + @ExpensiveItemCacheApi + fun NEUItem?.asItemStack(idHint: SkyblockId? = null, loreReplacements: Map? = null): ItemStack { + if (this == null) return brokenItemStack(null, idHint) + var s = cache[this.skyblockItemId] + if (s == null) { + s = asItemStackNow() + cache[this.skyblockItemId] = s + } + if (!loreReplacements.isNullOrEmpty()) { + s = s.copy()!! + s.applyLoreReplacements(loreReplacements) + s.setCustomName(s.name.applyLoreReplacements(loreReplacements)) + } + return s + } + + fun ItemStack.applyLoreReplacements(loreReplacements: Map) { + modifyLore { lore -> + lore.map { + it.applyLoreReplacements(loreReplacements) + } + } + } + + fun Text.applyLoreReplacements(loreReplacements: Map): Text { + return this.transformEachRecursively { + var string = it.directLiteralStringContent ?: return@transformEachRecursively it + loreReplacements.forEach { (find, replace) -> + string = string.replace("{$find}", replace) + } + Text.literal(string).setStyle(it.style) + } + } + + var itemRecacheScope: CoroutineScope? = null + + private var recacheSoonSubmitted = mutableSetOf() + + @OptIn(ExpensiveItemCacheApi::class) + fun recacheSoon(neuItem: NEUItem) { + itemRecacheScope?.launch { + if (!withContext(MinecraftDispatcher) { + recacheSoonSubmitted.add(neuItem.skyblockId) + }) { + return@launch + } + neuItem.asItemStack() + } + } + + @OptIn(ExpensiveItemCacheApi::class) + override fun reload(repository: NEURepository) { + val j = itemRecacheScope + j?.cancel("New reload invoked") + cache.clear() + isFlawless = true + if (TestUtil.isInTest) return + val newScope = + CoroutineScope( + Firmament.coroutineScope.coroutineContext + + SupervisorJob(Firmament.globalJob) + + Dispatchers.Default.limitedParallelism( + (Runtime.getRuntime().availableProcessors() / 4).coerceAtLeast(1) + ) + ) + val items = repository.items?.items + newScope.launch { + val items = items ?: return@launch + items.values.chunked(500).map { chunk -> + async { + chunk.forEach { + it.asItemStack() // Rebuild cache + } + } + }.awaitAll() + } + itemRecacheScope = newScope + } + + fun coinItem(coinAmount: Int): ItemStack { + var uuid = UUID.fromString("2070f6cb-f5db-367a-acd0-64d39a7e5d1b") + var texture = + "http://textures.minecraft.net/texture/538071721cc5b4cd406ce431a13f86083a8973e1064d2f8897869930ee6e5237" + if (coinAmount >= 100000) { + uuid = UUID.fromString("94fa2455-2881-31fe-bb4e-e3e24d58dbe3") + texture = + "http://textures.minecraft.net/texture/c9b77999fed3a2758bfeaf0793e52283817bea64044bf43ef29433f954bb52f6" + } + if (coinAmount >= 10000000) { + uuid = UUID.fromString("0af8df1f-098c-3b72-ac6b-65d65fd0b668") + texture = + "http://textures.minecraft.net/texture/7b951fed6a7b2cbc2036916dec7a46c4a56481564d14f945b6ebc03382766d3b" + } + val itemStack = ItemStack(Items.PLAYER_HEAD) + itemStack.setCustomName(Text.literal("§r§6" + NumberFormat.getInstance().format(coinAmount) + " Coins")) + itemStack.setSkullOwner(uuid, texture) + return itemStack + } + + init { + if (TestUtil.isInTest) { + initialize() + } + } + +} + + +operator fun NbtCompound.set(key: String, value: String) { + putString(key, value) +} + +operator fun NbtCompound.set(key: String, value: NbtElement) { + put(key, value) +} diff --git a/src/main/kotlin/repo/ItemNameLookup.kt b/src/main/kotlin/repo/ItemNameLookup.kt new file mode 100644 index 0000000..1250730 --- /dev/null +++ b/src/main/kotlin/repo/ItemNameLookup.kt @@ -0,0 +1,101 @@ +package moe.nea.firmament.repo + +import io.github.moulberry.repo.IReloadable +import io.github.moulberry.repo.NEURepository +import io.github.moulberry.repo.data.NEUItem +import java.util.NavigableMap +import java.util.TreeMap +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.removeColorCodes +import moe.nea.firmament.util.skyblockId + +object ItemNameLookup : IReloadable { + + fun getItemNameChunks(name: String): Set { + return name.removeColorCodes().split(" ").filterTo(mutableSetOf()) { it.isNotBlank() } + } + + var nameMap: NavigableMap> = TreeMap() + + override fun reload(repository: NEURepository) { + val nameMap = TreeMap>() + repository.items.items.values.forEach { item -> + getAllNamesForItem(item).forEach { name -> + val chunks = getItemNameChunks(name) + chunks.forEach { chunk -> + val set = nameMap.getOrPut(chunk, ::mutableSetOf) + set.add(item.skyblockId) + } + } + } + this.nameMap = nameMap + } + + fun getAllNamesForItem(item: NEUItem): Set { + val names = mutableSetOf() + names.add(item.displayName) + if (item.displayName.contains("Enchanted Book")) { + val enchantName = item.lore.firstOrNull() + if (enchantName != null) { + names.add(enchantName) + } + } + return names + } + + fun findItemCandidatesByName(name: String): MutableSet { + val candidates = mutableSetOf() + for (chunk in getItemNameChunks(name)) { + val set = nameMap[chunk] ?: emptySet() + candidates.addAll(set) + } + return candidates + } + + + fun guessItemByName( + /** + * The display name of the item. Color codes will be ignored. + */ + name: String, + /** + * Whether the [name] may contain other text, such as reforges, master stars and such. + */ + mayBeMangled: Boolean + ): SkyblockId? { + val cleanName = name.removeColorCodes() + return findBestItemFromCandidates( + findItemCandidatesByName(cleanName), + cleanName, + true + ) + } + + fun findBestItemFromCandidates( + candidates: Iterable, + name: String, mayBeMangled: Boolean + ): SkyblockId? { + val expectedClean = name.removeColorCodes() + var bestMatch: SkyblockId? = null + var bestMatchLength = -1 + for (candidate in candidates) { + val item = RepoManager.getNEUItem(candidate) ?: continue + for (name in getAllNamesForItem(item)) { + val actualClean = name.removeColorCodes() + val matches = if (mayBeMangled) expectedClean == actualClean + else expectedClean.contains(actualClean) + if (!matches) continue + if (actualClean.length > bestMatchLength) { + bestMatch = candidate + bestMatchLength = actualClean.length + } + } + } + return bestMatch + } + + init { + RepoManager.initialize() + } + +} diff --git a/src/main/kotlin/repo/MiningRepoData.kt b/src/main/kotlin/repo/MiningRepoData.kt new file mode 100644 index 0000000..e96a241 --- /dev/null +++ b/src/main/kotlin/repo/MiningRepoData.kt @@ -0,0 +1,133 @@ +package moe.nea.firmament.repo + +import io.github.moulberry.repo.IReloadable +import io.github.moulberry.repo.NEURepository +import io.github.moulberry.repo.data.NEUItem +import java.util.Collections +import java.util.NavigableMap +import java.util.TreeMap +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlinx.serialization.serializer +import kotlin.jvm.optionals.getOrNull +import kotlin.streams.asSequence +import net.minecraft.block.Block +import net.minecraft.item.BlockItem +import net.minecraft.item.ItemStack +import net.minecraft.nbt.NbtCompound +import net.minecraft.text.Text +import moe.nea.firmament.repo.ReforgeStore.kJson +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.SkyBlockIsland +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.mc.FirmamentDataComponentTypes +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.skyblockId + +class MiningRepoData : IReloadable { + var customMiningAreas: Map = mapOf() + private set + var customMiningBlocks: List = listOf() + private set + var toolsByBreakingPower: NavigableMap = Collections.emptyNavigableMap() + private set + + + data class BreakingPowerKey( + val breakingPower: Int, + val itemId: SkyblockId? = null + ) { + companion object { + val COMPARATOR: Comparator = + Comparator + .comparingInt { it.breakingPower } + .thenComparing(Comparator.comparing( + { it.itemId }, + nullsFirst(Comparator.comparing { "PICK" in it.neuItem || "BING" in it.neuItem }.thenComparing(Comparator.naturalOrder())))) + } + } + + override fun reload(repo: NEURepository) { + customMiningAreas = repo.file("mining/custom_mining_areas.json") + ?.kJson(serializer()) ?: mapOf() + customMiningBlocks = repo.tree("mining/blocks") + .asSequence() + .filter { it.path.endsWith(".json") } + .map { it.kJson(serializer()) } + .toList() + toolsByBreakingPower = Collections.unmodifiableNavigableMap( + repo.items.items + .values + .asSequence() + .map { SBItemStack(it.skyblockId) } + .filter { it.breakingPower > 0 } + .associateTo(TreeMap(BreakingPowerKey.COMPARATOR)) { + BreakingPowerKey(it.breakingPower, it.skyblockId) to it + }) + } + + fun getToolsThatCanBreak(breakingPower: Int): Collection { + return toolsByBreakingPower.tailMap(BreakingPowerKey(breakingPower, null), true).values + } + + @Serializable + data class CustomMiningBlock( + val breakingPower: Int = 0, + val blockStrength: Int = 0, + val name: String? = null, + val baseDrop: SkyblockId? = null, + val blocks189: List = emptyList() + ) { + @Transient + val dropItem = baseDrop?.let(::SBItemStack) + @OptIn(ExpensiveItemCacheApi::class) + private val labeledStack by lazy { + dropItem?.asCopiedItemStack()?.also(::markItemStack) + } + + private fun markItemStack(itemStack: ItemStack) { + itemStack.set(FirmamentDataComponentTypes.CUSTOM_MINING_BLOCK_DATA, this) + if (name != null) + itemStack.displayNameAccordingToNbt = Text.literal(name) + } + + fun getDisplayItem(block: Block): ItemStack { + return labeledStack ?: ItemStack(block).also(::markItemStack) + } + } + + @Serializable + data class Block189( + val itemId: String, + val damage: Short = 0, + val onlyIn: List? = null, + ) { + @Transient + val block = convertToModernBlock() + + val isCurrentlyActive: Boolean + get() = isActiveIn(SBData.skyblockLocation ?: SkyBlockIsland.NIL) + + fun isActiveIn(location: SkyBlockIsland) = onlyIn == null || location in onlyIn + + @OptIn(ExpensiveItemCacheApi::class) + private fun convertToModernBlock(): Block? { + // TODO: this should be in a shared util, really + val newCompound = ItemCache.convert189ToModern(NbtCompound().apply { + putString("id", itemId) + putShort("Damage", damage) + }) ?: return null + val itemStack = ItemStack.fromNbt(MC.defaultRegistries, newCompound).getOrNull() ?: return null + val blockItem = itemStack.item as? BlockItem ?: return null + return blockItem.block + } + } + + @Serializable + data class CustomMiningArea( + val isSpecialMining: Boolean = true + ) + + +} diff --git a/src/main/kotlin/repo/ModernOverlaysData.kt b/src/main/kotlin/repo/ModernOverlaysData.kt new file mode 100644 index 0000000..543b800 --- /dev/null +++ b/src/main/kotlin/repo/ModernOverlaysData.kt @@ -0,0 +1,41 @@ +package moe.nea.firmament.repo + +import io.github.moulberry.repo.IReloadable +import io.github.moulberry.repo.NEURepository +import java.nio.file.Path +import kotlin.io.path.extension +import kotlin.io.path.isDirectory +import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.nameWithoutExtension +import moe.nea.firmament.util.SkyblockId + +// TODO: move this over to the repo parser +class ModernOverlaysData : IReloadable { + data class OverlayFile( + val version: Int, + val path: Path, + ) + + var overlays: Map> = mapOf() + override fun reload(repo: NEURepository) { + val items = mutableMapOf>() + repo.baseFolder.resolve("itemsOverlay") + .takeIf { it.isDirectory() } + ?.listDirectoryEntries() + ?.forEach { versionFolder -> + val version = versionFolder.fileName.toString().toIntOrNull() ?: return@forEach + versionFolder.listDirectoryEntries() + .forEach { item -> + if (item.extension != "snbt") return@forEach + val itemId = item.nameWithoutExtension + items.getOrPut(SkyblockId(itemId)) { mutableListOf() }.add(OverlayFile(version, item)) + } + } + this.overlays = items + } + + fun getOverlayFiles(skyblockId: SkyblockId) = overlays[skyblockId] ?: listOf() + fun getMostModernReadableOverlay(skyblockId: SkyblockId, version: Int) = getOverlayFiles(skyblockId) + .filter { it.version <= version } + .maxByOrNull { it.version } +} diff --git a/src/main/kotlin/repo/PetData.kt b/src/main/kotlin/repo/PetData.kt new file mode 100644 index 0000000..2ce3402 --- /dev/null +++ b/src/main/kotlin/repo/PetData.kt @@ -0,0 +1,24 @@ +package moe.nea.firmament.repo + +import io.github.moulberry.repo.data.Rarity +import moe.nea.firmament.util.HypixelPetInfo + +// TODO: add in extra data like pet info, into this structure +data class PetData( + val rarity: Rarity, + val petId: String, + val exp: Double, + val isStub: Boolean = false, +) { + companion object { + fun fromHypixel(petInfo: HypixelPetInfo) = PetData( + petInfo.tier, petInfo.type, petInfo.exp, + ) + + fun forLevel(petId: String, rarity: Rarity, level: Int) = PetData( + rarity, petId, ExpLadders.getExpLadder(petId, rarity).getPetExpForLevel(level).toDouble() + ) + } + + val levelData by lazy { ExpLadders.getExpLadder(petId, rarity).getPetLevel(exp) } +} diff --git a/src/main/kotlin/repo/Reforge.kt b/src/main/kotlin/repo/Reforge.kt new file mode 100644 index 0000000..dc0d93d --- /dev/null +++ b/src/main/kotlin/repo/Reforge.kt @@ -0,0 +1,160 @@ +package moe.nea.firmament.repo + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.serializer +import net.minecraft.item.Item +import net.minecraft.registry.RegistryKey +import net.minecraft.registry.RegistryKeys +import net.minecraft.util.Identifier +import moe.nea.firmament.util.ReforgeId +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.skyblock.ItemType +import moe.nea.firmament.util.skyblock.Rarity + +@Serializable +data class Reforge( + val reforgeName: String, + @SerialName("internalName") val reforgeStone: SkyblockId? = null, + val nbtModifier: ReforgeId? = null, + val requiredRarities: List? = null, + val itemTypes: @Serializable(with = ReforgeEligibilityFilter.ItemTypesSerializer::class) List? = null, + val allowOn: List? = null, + val reforgeCosts: RarityMapped? = null, + val reforgeAbility: RarityMapped? = null, + val reforgeStats: RarityMapped>? = null, +) { + val eligibleItems get() = allowOn ?: itemTypes ?: listOf() + + val statUniverse: Set = Rarity.entries.flatMapTo(mutableSetOf()) { + reforgeStats?.get(it)?.keys ?: emptySet() + } + + @Serializable(with = ReforgeEligibilityFilter.Serializer::class) + sealed interface ReforgeEligibilityFilter { + object ItemTypesSerializer : KSerializer> { + override val descriptor: SerialDescriptor + get() = JsonElement.serializer().descriptor + + override fun deserialize(decoder: Decoder): List { + decoder as JsonDecoder + val jsonElement = decoder.decodeJsonElement() + if (jsonElement is JsonPrimitive && jsonElement.isString) { + return jsonElement.content.split("/").map { AllowsItemType(ItemType.ofName(it)) } + } + if (jsonElement is JsonArray) { + return decoder.json.decodeFromJsonElement(serializer>(), jsonElement) + } + jsonElement as JsonObject + val filters = mutableListOf() + jsonElement["internalName"]?.let { + decoder.json.decodeFromJsonElement(serializer>(), it).forEach { + filters.add(AllowsInternalName(it)) + } + } + jsonElement["itemId"]?.let { + decoder.json.decodeFromJsonElement(serializer>(), it).forEach { + val ident = Identifier.tryParse(it) + if (ident != null) + filters.add(AllowsVanillaItemType(RegistryKey.of(RegistryKeys.ITEM, ident))) + } + } + return filters + } + + override fun serialize(encoder: Encoder, value: List) { + TODO("Not yet implemented") + } + } + + object Serializer : KSerializer { + override val descriptor: SerialDescriptor + get() = serializer().descriptor + + override fun deserialize(decoder: Decoder): ReforgeEligibilityFilter { + val jsonObject = serializer().deserialize(decoder) + jsonObject["internalName"]?.let { + return AllowsInternalName(SkyblockId((it as JsonPrimitive).content)) + } + jsonObject["itemType"]?.let { + return AllowsItemType(ItemType.ofName((it as JsonPrimitive).content)) + } + jsonObject["minecraftId"]?.let { + return AllowsVanillaItemType(RegistryKey.of(RegistryKeys.ITEM, + Identifier.of((it as JsonPrimitive).content))) + } + error("Unknown item type") + } + + override fun serialize(encoder: Encoder, value: ReforgeEligibilityFilter) { + TODO("Not yet implemented") + } + + } + + data class AllowsItemType(val itemType: ItemType) : ReforgeEligibilityFilter + data class AllowsInternalName(val internalName: SkyblockId) : ReforgeEligibilityFilter + data class AllowsVanillaItemType(val minecraftId: RegistryKey) : ReforgeEligibilityFilter + } + + + val reforgeId get() = nbtModifier ?: ReforgeId(reforgeName.lowercase()) + + @Serializable(with = RarityMapped.Serializer::class) + sealed interface RarityMapped { + fun get(rarity: Rarity?): T? + + class Serializer( + val values: KSerializer + ) : KSerializer> { + override val descriptor: SerialDescriptor + get() = JsonElement.serializer().descriptor + + val indirect = MapSerializer(Rarity.serializer(), values) + override fun deserialize(decoder: Decoder): RarityMapped { + decoder as JsonDecoder + val element = decoder.decodeJsonElement() + if (element is JsonObject) { + return PerRarity(decoder.json.decodeFromJsonElement(indirect, element)) + } else { + return Direct(decoder.json.decodeFromJsonElement(values, element)) + } + } + + override fun serialize(encoder: Encoder, value: RarityMapped) { + when (value) { + is Direct -> + values.serialize(encoder, value.value) + + is PerRarity -> + indirect.serialize(encoder, value.values) + } + } + } + + @Serializable + data class Direct(val value: T) : RarityMapped { + override fun get(rarity: Rarity?): T { + return value + } + } + + @Serializable + data class PerRarity(val values: Map) : RarityMapped { + override fun get(rarity: Rarity?): T? { + return values[rarity] + } + } + } + +} diff --git a/src/main/kotlin/repo/ReforgeStore.kt b/src/main/kotlin/repo/ReforgeStore.kt new file mode 100644 index 0000000..4c01974 --- /dev/null +++ b/src/main/kotlin/repo/ReforgeStore.kt @@ -0,0 +1,125 @@ +package moe.nea.firmament.repo + +import com.google.gson.JsonElement +import com.mojang.serialization.JsonOps +import io.github.moulberry.repo.IReloadable +import io.github.moulberry.repo.NEURepoFile +import io.github.moulberry.repo.NEURepository +import io.github.moulberry.repo.NEURepositoryException +import io.github.moulberry.repo.data.NEURecipe +import kotlinx.serialization.KSerializer +import kotlinx.serialization.serializer +import net.minecraft.item.Item +import net.minecraft.registry.RegistryKey +import moe.nea.firmament.Firmament +import moe.nea.firmament.util.ReforgeId +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.json.KJsonOps +import moe.nea.firmament.util.skyblock.ItemType + +object ReforgeStore : ExtraRecipeProvider, IReloadable { + override fun provideExtraRecipes(): Iterable { + return emptyList() + } + + var byType: Map> = mapOf() + var byVanilla: Map, List> = mapOf() + var byInternalName: Map> = mapOf() + var modifierLut = mapOf() + var byReforgeStone = mapOf() + var allReforges = listOf() + + fun findEligibleForItem(itemType: ItemType): List { + return byType[itemType] ?: listOf() + } + + fun findEligibleForInternalName(internalName: SkyblockId): List { + return byInternalName[internalName] ?: listOf() + } + + //TODO: return byVanillla + override fun reload(repo: NEURepository) { + val basicReforges = + repo.file("constants/reforges.json") + ?.kJson(serializer>()) + ?.values ?: emptyList() + val advancedReforges = + repo.file("constants/reforgestones.json") + ?.kJson(serializer>()) + ?.values ?: emptyList() + val allReforges = (basicReforges + advancedReforges) + modifierLut = allReforges.associateBy { it.reforgeId } + byReforgeStone = allReforges.filter { it.reforgeStone != null } + .associateBy { it.reforgeStone!! } + val byType = mutableMapOf>() + val byVanilla = mutableMapOf, MutableList>() + val byInternalName = mutableMapOf>() + this.byType = byType + this.byVanilla = byVanilla + this.byInternalName = byInternalName + for (reforge in allReforges) { + for (eligibleItem in reforge.eligibleItems) { + when (eligibleItem) { + is Reforge.ReforgeEligibilityFilter.AllowsInternalName -> { + byInternalName.getOrPut(eligibleItem.internalName, ::mutableListOf).add(reforge) + } + + is Reforge.ReforgeEligibilityFilter.AllowsItemType -> { + val actualItemTypes = resolveItemType(eligibleItem.itemType) + for (itemType in actualItemTypes) { + byType.getOrPut(itemType, ::mutableListOf).add(reforge) + byType.getOrPut(itemType.dungeonVariant, ::mutableListOf).add(reforge) + } + } + + is Reforge.ReforgeEligibilityFilter.AllowsVanillaItemType -> { + byVanilla.getOrPut(eligibleItem.minecraftId, ::mutableListOf).add(reforge) + } + } + } + } + this.allReforges = allReforges + } + + fun resolveItemType(itemType: ItemType): List { + if (ItemType.SWORD == itemType) { + return listOf( + ItemType.SWORD, + ItemType.GAUNTLET, + ItemType.LONGSWORD,// TODO: check name + ItemType.FISHING_WEAPON,// TODO: check name + ) + } + if (itemType == ItemType.ofName("ARMOR")) { + return listOf( + ItemType.CHESTPLATE, + ItemType.LEGGINGS, + ItemType.HELMET, + ItemType.BOOTS, + ) + } + if (itemType == ItemType.EQUIPMENT) { + return listOf( + ItemType.CLOAK, + ItemType.BRACELET, + ItemType.NECKLACE, + ItemType.BELT, + ItemType.GLOVES, + ) + } + if (itemType == ItemType.ROD) { + return listOf(ItemType.FISHING_ROD, ItemType.FISHING_WEAPON) + } + return listOf(itemType) + } + + fun NEURepoFile.kJson(serializer: KSerializer): T { + val rawJson = json(JsonElement::class.java) + try { + val kJsonElement = JsonOps.INSTANCE.convertTo(KJsonOps.INSTANCE, rawJson) + return Firmament.json.decodeFromJsonElement(serializer, kJsonElement) + } catch (ex: Exception) { + throw NEURepositoryException(path, "Could not decode kotlin JSON element", ex) + } + } +} diff --git a/src/main/kotlin/repo/RepoDownloadManager.kt b/src/main/kotlin/repo/RepoDownloadManager.kt new file mode 100644 index 0000000..888248d --- /dev/null +++ b/src/main/kotlin/repo/RepoDownloadManager.kt @@ -0,0 +1,127 @@ +package moe.nea.firmament.repo + +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsChannel +import io.ktor.utils.io.copyTo +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardOpenOption +import java.util.zip.ZipInputStream +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.withContext +import kotlinx.serialization.Serializable +import kotlin.io.path.createDirectories +import kotlin.io.path.exists +import kotlin.io.path.inputStream +import kotlin.io.path.outputStream +import kotlin.io.path.readText +import kotlin.io.path.writeText +import moe.nea.firmament.Firmament +import moe.nea.firmament.Firmament.logger +import moe.nea.firmament.util.iterate + + +object RepoDownloadManager { + + val repoSavedLocation = Firmament.DATA_DIR.resolve("repo-extracted") + val repoMetadataLocation = Firmament.DATA_DIR.resolve("loaded-repo-sha.txt") + + private fun loadSavedVersionHash(): String? = + if (repoSavedLocation.exists()) { + if (repoMetadataLocation.exists()) { + try { + repoMetadataLocation.readText().trim() + } catch (e: IOException) { + null + } + } else { + null + } + } else null + + private fun saveVersionHash(versionHash: String) { + latestSavedVersionHash = versionHash + repoMetadataLocation.writeText(versionHash) + } + + var latestSavedVersionHash: String? = loadSavedVersionHash() + private set + + @Serializable + private class GithubCommitsResponse(val sha: String) + + private suspend fun requestLatestGithubSha(branchOverride: String?): String? { + if (RepoManager.Config.branch == "prerelease") { + RepoManager.Config.branch = "master" + } + val response = + Firmament.httpClient.get("https://api.github.com/repos/${RepoManager.Config.username}/${RepoManager.Config.reponame}/commits/${branchOverride ?: RepoManager.Config.branch}") + if (response.status.value != 200) { + return null + } + return response.body().sha + } + + private suspend fun downloadGithubArchive(url: String): Path = withContext(IO) { + val response = Firmament.httpClient.get(url) + val targetFile = Files.createTempFile("firmament-repo", ".zip") + val outputChannel = Files.newByteChannel(targetFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE) + response.bodyAsChannel().copyTo(outputChannel) + targetFile + } + + /** + * Downloads the latest repository from github, setting [latestSavedVersionHash]. + * @return true, if an update was performed, false, otherwise (no update needed, or wasn't able to complete update) + */ + suspend fun downloadUpdate(force: Boolean, branch: String? = null): Boolean = + withContext(CoroutineName("Repo Update Check")) { + val latestSha = requestLatestGithubSha(branch) + if (latestSha == null) { + logger.warn("Could not request github API to retrieve latest REPO sha.") + return@withContext false + } + val currentSha = loadSavedVersionHash() + if (latestSha != currentSha || force) { + val requestUrl = + "https://github.com/${RepoManager.Config.username}/${RepoManager.Config.reponame}/archive/$latestSha.zip" + logger.info("Planning to upgrade repository from $currentSha to $latestSha from $requestUrl") + val zipFile = downloadGithubArchive(requestUrl) + logger.info("Download repository zip file to $zipFile. Deleting old repository") + withContext(IO) { repoSavedLocation.toFile().deleteRecursively() } + logger.info("Extracting new repository") + withContext(IO) { extractNewRepository(zipFile) } + logger.info("Repository loaded on disk.") + saveVersionHash(latestSha) + return@withContext true + } else { + logger.debug("Repository on latest sha $currentSha. Not performing update") + return@withContext false + } + } + + private fun extractNewRepository(zipFile: Path) { + repoSavedLocation.createDirectories() + ZipInputStream(zipFile.inputStream()).use { cis -> + while (true) { + val entry = cis.nextEntry ?: break + if (entry.isDirectory) continue + val extractedLocation = + repoSavedLocation.resolve( + entry.name.substringAfter('/', missingDelimiterValue = "") + ) + if (repoSavedLocation !in extractedLocation.iterate { it.parent }) { + logger.error("Firmament detected an invalid zip file. This is a potential security risk, please report this in the Firmament discord.") + throw RuntimeException("Firmament detected an invalid zip file. This is a potential security risk, please report this in the Firmament discord.") + } + extractedLocation.parent.createDirectories() + extractedLocation.outputStream().use { cis.copyTo(it) } + } + } + } + + +} diff --git a/src/main/kotlin/repo/RepoItemTypeCache.kt b/src/main/kotlin/repo/RepoItemTypeCache.kt new file mode 100644 index 0000000..414ec09 --- /dev/null +++ b/src/main/kotlin/repo/RepoItemTypeCache.kt @@ -0,0 +1,15 @@ +package moe.nea.firmament.repo + +import io.github.moulberry.repo.IReloadable +import io.github.moulberry.repo.NEURepository +import io.github.moulberry.repo.data.NEUItem +import moe.nea.firmament.util.skyblock.ItemType + +object RepoItemTypeCache : IReloadable { + + var byItemType: Map> = mapOf() + + override fun reload(repository: NEURepository) { + byItemType = repository.items.items.values.groupBy { ItemType.fromEscapeCodeLore(it.lore.lastOrNull() ?: "") } + } +} diff --git a/src/main/kotlin/repo/RepoManager.kt b/src/main/kotlin/repo/RepoManager.kt new file mode 100644 index 0000000..c3d1c52 --- /dev/null +++ b/src/main/kotlin/repo/RepoManager.kt @@ -0,0 +1,211 @@ +package moe.nea.firmament.repo + +import io.github.moulberry.repo.NEURepository +import io.github.moulberry.repo.NEURepositoryException +import io.github.moulberry.repo.data.NEUItem +import io.github.moulberry.repo.data.NEURecipe +import io.github.moulberry.repo.data.Rarity +import java.nio.file.Path +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import net.minecraft.client.MinecraftClient +import net.minecraft.network.packet.s2c.play.SynchronizeRecipesS2CPacket +import net.minecraft.recipe.display.CuttingRecipeDisplay +import net.minecraft.util.StringIdentifiable +import moe.nea.firmament.Firmament +import moe.nea.firmament.Firmament.logger +import moe.nea.firmament.events.ReloadRegistrationEvent +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.ErrorUtil +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.MinecraftDispatcher +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.TestUtil +import moe.nea.firmament.util.tr + +object RepoManager { + object Config : ManagedConfig("repo", Category.META) { + var username by string("username") { "NotEnoughUpdates" } + var reponame by string("reponame") { "NotEnoughUpdates-REPO" } + var branch by string("branch") { "master" } + val autoUpdate by toggle("autoUpdate") { true } + val reset by button("reset") { + username = "NotEnoughUpdates" + reponame = "NotEnoughUpdates-REPO" + branch = "master" + save() + } + val enableREI by toggle("enable-rei") { true } + val disableItemGroups by toggle("disable-item-groups") { true } + val reload by button("reload") { + save() + Firmament.coroutineScope.launch { + RepoManager.reload() + } + } + val redownload by button("redownload") { + save() + RepoManager.launchAsyncUpdate(true) + } + val alwaysSuperCraft by toggle("enable-super-craft") { true } + var warnForMissingItemListMod by toggle("warn-for-missing-item-list-mod") { true } + val perfectRenders by choice("perfect-renders") { PerfectRender.RENDER } + } + + enum class PerfectRender(val label: String) : StringIdentifiable { + NOTHING("nothing"), + RENDER("render"), + RENDER_AND_TEXT("text"), + ; + + fun rendersPerfectText() = this == RENDER_AND_TEXT + fun rendersPerfectVisuals() = this == RENDER || this == RENDER_AND_TEXT + + override fun asString(): String? = label + } + + val currentDownloadedSha by RepoDownloadManager::latestSavedVersionHash + + var recentlyFailedToUpdateItemList = false + + val essenceRecipeProvider = EssenceRecipeProvider() + val recipeCache = BetterRepoRecipeCache(essenceRecipeProvider, ReforgeStore) + val miningData = MiningRepoData() + val overlayData = ModernOverlaysData() + + fun makeNEURepository(path: Path): NEURepository { + return NEURepository.of(path).apply { + registerReloadListener(overlayData) + registerReloadListener(ItemCache) + registerReloadListener(RepoItemTypeCache) + registerReloadListener(ExpLadders) + registerReloadListener(ItemNameLookup) + registerReloadListener(ReforgeStore) + registerReloadListener(essenceRecipeProvider) + registerReloadListener(recipeCache) + registerReloadListener(miningData) + ReloadRegistrationEvent.publish(ReloadRegistrationEvent(this)) + registerReloadListener { + if (TestUtil.isInTest) return@registerReloadListener + Firmament.coroutineScope.launch(MinecraftDispatcher) { + if (!trySendClientboundUpdateRecipesPacket()) { + logger.warn("Failed to issue a ClientboundUpdateRecipesPacket (to reload REI). This may lead to an outdated item list.") + recentlyFailedToUpdateItemList = true + } + } + } + } + } + + lateinit var neuRepo: NEURepository + private set + + fun getAllRecipes() = neuRepo.items.items.values.asSequence().flatMap { it.recipes } + + fun getRecipesFor(skyblockId: SkyblockId): Set = recipeCache.recipes[skyblockId] ?: setOf() + fun getUsagesFor(skyblockId: SkyblockId): Set = recipeCache.usages[skyblockId] ?: setOf() + + private fun trySendClientboundUpdateRecipesPacket(): Boolean { + return MinecraftClient.getInstance().world != null && MinecraftClient.getInstance().networkHandler?.onSynchronizeRecipes( + SynchronizeRecipesS2CPacket(mutableMapOf(), CuttingRecipeDisplay.Grouping.empty()) + ) != null + } + + init { + ClientTickEvents.START_WORLD_TICK.register(ClientTickEvents.StartWorldTick { + if (recentlyFailedToUpdateItemList && trySendClientboundUpdateRecipesPacket()) + recentlyFailedToUpdateItemList = false + }) + } + + fun getNEUItem(skyblockId: SkyblockId): NEUItem? = neuRepo.items.getItemBySkyblockId(skyblockId.neuItem) + + fun downloadOverridenBranch(branch: String) { + Firmament.coroutineScope.launch { + RepoDownloadManager.downloadUpdate(true, branch) + reload() + } + } + + fun launchAsyncUpdate(force: Boolean = false) { + Firmament.coroutineScope.launch { + RepoDownloadManager.downloadUpdate(force) + reload() + } + } + + fun reloadForTest(from: Path) { + neuRepo = makeNEURepository(from) + reloadSync() + } + + + suspend fun reload() { + withContext(Dispatchers.IO) { + reloadSync() + } + } + + fun reloadSync() { + try { + logger.info("Repo reload started.") + neuRepo.reload() + logger.info("Repo reload completed.") + } catch (exc: NEURepositoryException) { + ErrorUtil.softError("Failed to reload repository", exc) + MC.sendChat( + tr( + "firmament.repo.reloadfail", + "Failed to reload repository. This will result in some mod features not working." + ) + ) + } + } + + private var wasInitialized = false + fun initialize() { + if (wasInitialized) return + wasInitialized = true + System.getProperty("firmament.testrepo")?.let { compTimeRepo -> + reloadForTest(Path.of(compTimeRepo)) + return + } + neuRepo = makeNEURepository(RepoDownloadManager.repoSavedLocation) + if (Config.autoUpdate) { + launchAsyncUpdate() + } else { + Firmament.coroutineScope.launch { + reload() + } + } + } + + init { + if (TestUtil.isInTest) { + initialize() + } + } + + fun getPotentialStubPetData(skyblockId: SkyblockId): PetData? { + val parts = skyblockId.neuItem.split(";") + if (parts.size != 2) { + return null + } + val (petId, rarityIndex) = parts + if (!rarityIndex.all { it.isDigit() }) { + return null + } + val intIndex = rarityIndex.toInt() + if (intIndex !in Rarity.entries.indices) return null + if (petId !in neuRepo.constants.petNumbers) return null + return PetData(Rarity.entries[intIndex], petId, 0.0, true) + } + + fun getRepoRef(): String { + return "${Config.username}/${Config.reponame}#${Config.branch}" + } + + fun shouldLoadREI(): Boolean = Config.enableREI +} diff --git a/src/main/kotlin/repo/RepoModResourcePack.kt b/src/main/kotlin/repo/RepoModResourcePack.kt new file mode 100644 index 0000000..4fec14a --- /dev/null +++ b/src/main/kotlin/repo/RepoModResourcePack.kt @@ -0,0 +1,127 @@ +package moe.nea.firmament.repo + +import java.io.InputStream +import java.nio.file.Files +import java.nio.file.Path +import java.util.* +import net.fabricmc.fabric.api.resource.ModResourcePack +import net.fabricmc.fabric.impl.resource.loader.ModResourcePackSorter +import net.fabricmc.loader.api.FabricLoader +import net.fabricmc.loader.api.metadata.ModMetadata +import kotlin.io.path.exists +import kotlin.io.path.isRegularFile +import kotlin.io.path.relativeTo +import kotlin.streams.asSequence +import net.minecraft.resource.AbstractFileResourcePack +import net.minecraft.resource.InputSupplier +import net.minecraft.resource.NamespaceResourceManager +import net.minecraft.resource.Resource +import net.minecraft.resource.ResourcePack +import net.minecraft.resource.ResourcePackInfo +import net.minecraft.resource.ResourcePackSource +import net.minecraft.resource.ResourceType +import net.minecraft.resource.metadata.ResourceMetadata +import net.minecraft.resource.metadata.ResourceMetadataSerializer +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import net.minecraft.util.path.PathUtil +import moe.nea.firmament.Firmament + +class RepoModResourcePack(val basePath: Path) : ModResourcePack { + companion object { + fun append(packs: ModResourcePackSorter) { + Firmament.logger.info("Registering mod resource pack") + packs.addPack(RepoModResourcePack(RepoDownloadManager.repoSavedLocation)) + } + + fun createResourceDirectly(identifier: Identifier): Optional { + val pack = RepoModResourcePack(RepoDownloadManager.repoSavedLocation) + return Optional.of( + Resource( + pack, + pack.open(ResourceType.CLIENT_RESOURCES, identifier) ?: return Optional.empty() + ) { + val base = + pack.open(ResourceType.CLIENT_RESOURCES, identifier.withPath(identifier.path + ".mcmeta")) + if (base == null) + ResourceMetadata.NONE + else + NamespaceResourceManager.loadMetadata(base) + } + ) + } + } + + override fun close() { + } + + override fun openRoot(vararg segments: String): InputSupplier? { + return getFile(segments)?.let { InputSupplier.create(it) } + } + + fun getFile(segments: Array): Path? { + PathUtil.validatePath(*segments) + val path = segments.fold(basePath, Path::resolve) + if (!path.isRegularFile()) return null + return path + } + + override fun open(type: ResourceType?, id: Identifier): InputSupplier? { + if (type != ResourceType.CLIENT_RESOURCES) return null + if (id.namespace != "neurepo") return null + val file = getFile(id.path.split("/").toTypedArray()) + return file?.let { InputSupplier.create(it) } + } + + override fun findResources( + type: ResourceType?, + namespace: String, + prefix: String, + consumer: ResourcePack.ResultConsumer + ) { + if (namespace != "neurepo") return + if (type != ResourceType.CLIENT_RESOURCES) return + + val prefixPath = basePath.resolve(prefix) + if (!prefixPath.exists()) + return + Files.walk(prefixPath) + .asSequence() + .map { it.relativeTo(basePath) } + .forEach { + consumer.accept(Identifier.of("neurepo", it.toString()), InputSupplier.create(it)) + } + } + + override fun getNamespaces(type: ResourceType?): Set { + if (type != ResourceType.CLIENT_RESOURCES) return emptySet() + return setOf("neurepo") + } + + override fun parseMetadata(metadataSerializer: ResourceMetadataSerializer?): T? { + return AbstractFileResourcePack.parseMetadata( + metadataSerializer, """ +{ + "pack": { + "pack_format": 12, + "description": "NEU Repo Resources" + } +} +""".trimIndent().byteInputStream() + ) + } + + + override fun getInfo(): ResourcePackInfo { + return ResourcePackInfo("neurepo", Text.literal("NEU Repo"), ResourcePackSource.BUILTIN, Optional.empty()) + } + + override fun getFabricModMetadata(): ModMetadata { + return FabricLoader.getInstance().getModContainer("firmament") + .get().metadata + } + + override fun createOverlay(overlay: String): ModResourcePack { + return RepoModResourcePack(basePath.resolve(overlay)) + } +} diff --git a/src/main/kotlin/repo/SBItemStack.kt b/src/main/kotlin/repo/SBItemStack.kt new file mode 100644 index 0000000..01d1c4d --- /dev/null +++ b/src/main/kotlin/repo/SBItemStack.kt @@ -0,0 +1,458 @@ +package moe.nea.firmament.repo + +import com.mojang.serialization.Codec +import com.mojang.serialization.codecs.RecordCodecBuilder +import io.github.moulberry.repo.constants.PetNumbers +import io.github.moulberry.repo.data.NEUIngredient +import io.github.moulberry.repo.data.NEUItem +import net.minecraft.item.ItemStack +import net.minecraft.network.RegistryByteBuf +import net.minecraft.network.codec.PacketCodec +import net.minecraft.network.codec.PacketCodecs +import net.minecraft.text.Style +import net.minecraft.text.Text +import net.minecraft.text.TextColor +import net.minecraft.util.Formatting +import moe.nea.firmament.repo.ItemCache.asItemStack +import moe.nea.firmament.repo.ItemCache.withFallback +import moe.nea.firmament.util.FirmFormatters +import moe.nea.firmament.util.LegacyFormattingCode +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.ReforgeId +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.blue +import moe.nea.firmament.util.directLiteralStringContent +import moe.nea.firmament.util.extraAttributes +import moe.nea.firmament.util.getReforgeId +import moe.nea.firmament.util.getUpgradeStars +import moe.nea.firmament.util.grey +import moe.nea.firmament.util.mc.appendLore +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.mc.modifyLore +import moe.nea.firmament.util.modifyExtraAttributes +import moe.nea.firmament.util.petData +import moe.nea.firmament.util.prepend +import moe.nea.firmament.util.reconstitute +import moe.nea.firmament.util.removeColorCodes +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.skyblock.ItemType +import moe.nea.firmament.util.skyblock.Rarity +import moe.nea.firmament.util.skyblockId +import moe.nea.firmament.util.unformattedString +import moe.nea.firmament.util.useMatch +import moe.nea.firmament.util.withColor + +data class SBItemStack constructor( + val skyblockId: SkyblockId, + val neuItem: NEUItem?, + private var stackSize: Int, + private var petData: PetData?, + val extraLore: List = emptyList(), + val stars: Int = 0, + val fallback: ItemStack? = null, + val reforge: ReforgeId? = null, +) { + + fun getStackSize() = stackSize + fun setStackSize(newSize: Int) { + this.stackSize = newSize + this.itemStack_ = null + } + + fun getPetData() = petData + fun setPetData(petData: PetData?) { + this.petData = petData + this.itemStack_ = null + } + + companion object { + val PACKET_CODEC: PacketCodec = PacketCodec.tuple( + SkyblockId.PACKET_CODEC, { it.skyblockId }, + PacketCodecs.VAR_INT, { it.stackSize }, + { id, count -> SBItemStack(id, count) } + ) + val CODEC: Codec = RecordCodecBuilder.create { + it.group( + SkyblockId.CODEC.fieldOf("skyblockId").forGetter { it.skyblockId }, + Codec.INT.fieldOf("count").forGetter { it.stackSize }, + ).apply(it) { id, count -> + SBItemStack(id, count) + } + } + val EMPTY = SBItemStack(SkyblockId.NULL, 0) + + private val BREAKING_POWER_REGEX = "Breaking Power (?[0-9]+)".toPattern() + operator fun invoke(itemStack: ItemStack): SBItemStack { + val skyblockId = itemStack.skyBlockId ?: SkyblockId.NULL + return SBItemStack( + skyblockId, + RepoManager.getNEUItem(skyblockId), + itemStack.count, + petData = itemStack.petData?.let { PetData.fromHypixel(it) }, + stars = itemStack.getUpgradeStars(), + reforge = itemStack.getReforgeId() + ) + } + + operator fun invoke(neuIngredient: NEUIngredient): SBItemStack? { + if (neuIngredient.skyblockId == SkyblockId.SENTINEL_EMPTY) return null // TODO: better fallback, maybe? + if (neuIngredient.skyblockId == SkyblockId.COINS) { + // TODO: specially handle coins to include the decimals + } + return SBItemStack(neuIngredient.skyblockId, neuIngredient.amount.toInt()) + } + + fun passthrough(itemStack: ItemStack): SBItemStack { + return SBItemStack(SkyblockId.NULL, null, itemStack.count, null, fallback = itemStack) + } + + fun parseStatBlock(itemStack: ItemStack): List { + return itemStack.loreAccordingToNbt + .map { parseStatLine(it) } + .takeWhile { it != null } + .filterNotNull() + } + + fun appendEnhancedStats( + itemStack: ItemStack, + reforgeStats: Map, + buffKind: BuffKind, + ) { + val namedReforgeStats = reforgeStats + .mapKeysTo(mutableMapOf()) { statIdToName(it.key) } + val loreMut = itemStack.loreAccordingToNbt.toMutableList() + var statBlockLastIndex = -1 + for (i in loreMut.indices) { + val statLine = parseStatLine(loreMut[i]) + if (statLine == null && statBlockLastIndex >= 0) { + break + } + if (statLine == null) { + continue + } + statBlockLastIndex = i + val statBuff = namedReforgeStats.remove(statLine.statName) ?: continue + loreMut[i] = statLine.addStat(statBuff, buffKind).reconstitute() + } + if (namedReforgeStats.isNotEmpty() && statBlockLastIndex == -1) { + loreMut.add(0, Text.literal("")) + } + // If there is no stat block the statBlockLastIndex falls through to -1 + // TODO: this is good enough for some items. some other items might have their stats at a different place. + for ((statName, statBuff) in namedReforgeStats) { + val statLine = StatLine(statName, null).addStat(statBuff, buffKind) + loreMut.add(statBlockLastIndex + 1, statLine.reconstitute()) + } + itemStack.loreAccordingToNbt = loreMut + } + + data class StatFormatting( + val postFix: String, + val color: Formatting, + val isStarAffected: Boolean = true, + ) + + val formattingOverrides = mapOf( + "Sea Creature Chance" to StatFormatting("%", Formatting.RED), + "Strength" to StatFormatting("", Formatting.RED), + "Damage" to StatFormatting("", Formatting.RED), + "Bonus Attack Speed" to StatFormatting("%", Formatting.RED), + "Shot Cooldown" to StatFormatting("s", Formatting.GREEN, false), + "Ability Damage" to StatFormatting("%", Formatting.RED), + "Crit Damage" to StatFormatting("%", Formatting.RED), + "Crit Chance" to StatFormatting("%", Formatting.RED), + "Ability Damage" to StatFormatting("%", Formatting.RED), + "Trophy Fish Chance" to StatFormatting("%", Formatting.GREEN), + "Health" to StatFormatting("", Formatting.GREEN), + "Defense" to StatFormatting("", Formatting.GREEN), + "Fishing Speed" to StatFormatting("", Formatting.GREEN), + "Double Hook Chance" to StatFormatting("%", Formatting.GREEN), + "Mining Speed" to StatFormatting("", Formatting.GREEN), + "Mining Fortune" to StatFormatting("", Formatting.GREEN), + "Heat Resistance" to StatFormatting("", Formatting.GREEN), + "Swing Range" to StatFormatting("", Formatting.GREEN), + "Rift Time" to StatFormatting("", Formatting.GREEN), + "Speed" to StatFormatting("", Formatting.GREEN), + "Farming Fortune" to StatFormatting("", Formatting.GREEN), + "True Defense" to StatFormatting("", Formatting.GREEN), + "Mending" to StatFormatting("", Formatting.GREEN), + "Foraging Wisdom" to StatFormatting("", Formatting.GREEN), + "Farming Wisdom" to StatFormatting("", Formatting.GREEN), + "Foraging Fortune" to StatFormatting("", Formatting.GREEN), + "Magic Find" to StatFormatting("", Formatting.GREEN), + "Ferocity" to StatFormatting("", Formatting.GREEN), + "Bonus Pest Chance" to StatFormatting("%", Formatting.GREEN), + "Cold Resistance" to StatFormatting("", Formatting.GREEN), + "Pet Luck" to StatFormatting("", Formatting.GREEN), + "Fear" to StatFormatting("", Formatting.GREEN), + "Mana Regen" to StatFormatting("%", Formatting.GREEN), + "Rift Damage" to StatFormatting("", Formatting.GREEN), + "Hearts" to StatFormatting("", Formatting.GREEN), + "Vitality" to StatFormatting("", Formatting.GREEN), + // TODO: make this a repo json + ) + + + private val statLabelRegex = "(?.*): ".toPattern() + + enum class BuffKind( + val color: Formatting, + val prefix: String, + val postFix: String, + val isHidden: Boolean, + ) { + REFORGE(Formatting.BLUE, "(", ")", false), + STAR_BUFF(Formatting.RESET, "", "", true), + CATA_STAR_BUFF(Formatting.DARK_GRAY, "(", ")", false), + ; + } + + data class StatLine( + val statName: String, + val value: Text?, + val rest: List = listOf(), + val valueNum: Double? = value?.directLiteralStringContent?.trim(' ', 's', '%', '+')?.toDoubleOrNull() + ) { + fun addStat(amount: Double, buffKind: BuffKind): StatLine { + val formattedAmount = FirmFormatters.formatCommas(amount, 1, includeSign = true) + return copy( + valueNum = (valueNum ?: 0.0) + amount, + value = null, + rest = rest + + if (buffKind.isHidden) emptyList() + else listOf( + Text.literal( + buffKind.prefix + formattedAmount + + statFormatting.postFix + + buffKind.postFix + " " + ) + .withColor(buffKind.color) + ) + ) + } + + fun formatValue() = + Text.literal( + FirmFormatters.formatCommas( + valueNum ?: 0.0, + 1, + includeSign = true + ) + statFormatting.postFix + " " + ) + .setStyle(Style.EMPTY.withColor(statFormatting.color)) + + val statFormatting = formattingOverrides[statName] ?: StatFormatting("", Formatting.GREEN) + private fun abbreviate(abbreviateTo: Int): String { + if (abbreviateTo >= statName.length) return statName + val segments = statName.split(" ") + return segments.joinToString(" ") { + it.substring(0, maxOf(1, abbreviateTo / segments.size)) + } + } + + fun reconstitute(abbreviateTo: Int = Int.MAX_VALUE): Text = + Text.literal("").setStyle(Style.EMPTY.withItalic(false)) + .append(Text.literal("${abbreviate(abbreviateTo)}: ").grey()) + .append(value ?: formatValue()) + .also { rest.forEach(it::append) } + } + + fun statIdToName(statId: String): String { + val segments = statId.split("_") + return segments.joinToString(" ") { it.replaceFirstChar { it.uppercaseChar() } } + } + + fun parseStatLine(line: Text): StatLine? { + val sibs = line.siblings + val stat = sibs.firstOrNull() ?: return null + if (stat.style.color != TextColor.fromFormatting(Formatting.GRAY)) return null + val statLabel = stat.directLiteralStringContent ?: return null + val statName = statLabelRegex.useMatch(statLabel) { group("statName") } ?: return null + return StatLine(statName, sibs[1], sibs.subList(2, sibs.size)) + } + } + + constructor(skyblockId: SkyblockId, petData: PetData) : this( + skyblockId, + RepoManager.getNEUItem(skyblockId), + 1, + petData + ) + + constructor(skyblockId: SkyblockId, stackSize: Int = 1) : this( + skyblockId, + RepoManager.getNEUItem(skyblockId), + stackSize, + RepoManager.getPotentialStubPetData(skyblockId) + ) + + private fun injectReplacementDataForPetLevel( + petInfo: PetNumbers, + level: Int, + replacementData: MutableMap + ) { + val stats = petInfo.interpolatedStatsAtLevel(level) ?: return + stats.otherNumbers.forEachIndexed { index, it -> + replacementData[index.toString()] = FirmFormatters.formatCommas(it, 1) + } + stats.statNumbers.forEach { (t, u) -> + replacementData[t] = FirmFormatters.formatCommas(u, 1) + } + } + + private fun injectReplacementDataForPets(replacementData: MutableMap) { + val petData = this.petData ?: return + val petInfo = RepoManager.neuRepo.constants.petNumbers[petData.petId]?.get(petData.rarity) ?: return + if (petData.isStub) { + val mapLow = mutableMapOf() + injectReplacementDataForPetLevel(petInfo, petInfo.lowLevel, mapLow) + val mapHigh = mutableMapOf() + injectReplacementDataForPetLevel(petInfo, petInfo.highLevel, mapHigh) + mapHigh.forEach { (key, highValue) -> + mapLow.merge(key, highValue) { a, b -> "$a → $b" } + } + replacementData.putAll(mapLow) + replacementData["LVL"] = "${petInfo.lowLevel} → ${petInfo.highLevel}" + } else { + injectReplacementDataForPetLevel(petInfo, petData.levelData.currentLevel, replacementData) + replacementData["LVL"] = petData.levelData.currentLevel.toString() + } + } + + + private fun appendReforgeInfo( + itemStack: ItemStack, + ) { + val rarity = Rarity.fromItem(itemStack) ?: return + val reforgeId = this.reforge ?: return + val reforge = ReforgeStore.modifierLut[reforgeId] ?: return + val reforgeStats = reforge.reforgeStats?.get(rarity) ?: mapOf() + itemStack.displayNameAccordingToNbt = itemStack.displayNameAccordingToNbt.copy() + .prepend(Text.literal(reforge.reforgeName + " ").formatted(Rarity.colourMap[rarity] ?: Formatting.WHITE)) + val data = itemStack.extraAttributes.copy() + data.putString("modifier", reforgeId.id) + itemStack.extraAttributes = data + appendEnhancedStats(itemStack, reforgeStats, BuffKind.REFORGE) + reforge.reforgeAbility?.get(rarity)?.let { reforgeAbility -> + val formattedReforgeAbility = ItemCache.un189Lore(reforgeAbility) + .grey() + itemStack.modifyLore { + val lastBlank = it.indexOfLast { it.unformattedString.isBlank() } + val newList = mutableListOf() + newList.addAll(it.subList(0, lastBlank)) + newList.add(Text.literal("")) + newList.add(Text.literal("${reforge.reforgeName} Bonus").blue()) + MC.font.textHandler.wrapLines(formattedReforgeAbility, 180, Style.EMPTY).mapTo(newList) { + it.reconstitute() + } + newList.addAll(it.subList(lastBlank, it.size)) + return@modifyLore newList + } + } + } + + // TODO: avoid instantiating the item stack here + @ExpensiveItemCacheApi + val itemType: ItemType? get() = ItemType.fromItemStack(asImmutableItemStack()) + @ExpensiveItemCacheApi + val rarity: Rarity? get() = Rarity.fromItem(asImmutableItemStack()) + + private var itemStack_: ItemStack? = null + + val breakingPower: Int + get() = + BREAKING_POWER_REGEX.useMatch(neuItem?.lore?.firstOrNull()?.removeColorCodes()) { + group("power").toInt() + } ?: 0 + + @ExpensiveItemCacheApi + private val itemStack: ItemStack + get() { + val itemStack = itemStack_ ?: run { + if (skyblockId == SkyblockId.COINS) + return@run ItemCache.coinItem(stackSize).also { it.appendLore(extraLore) } + if (stackSize == 0) + return@run ItemStack.EMPTY + val replacementData = mutableMapOf() + injectReplacementDataForPets(replacementData) + val baseItem = neuItem.asItemStack(idHint = skyblockId, replacementData) + .withFallback(fallback) + .copyWithCount(stackSize) + val baseStats = parseStatBlock(baseItem) + appendReforgeInfo(baseItem) + baseItem.appendLore(extraLore) + enhanceStatsByStars(baseItem, stars, baseStats) + return@run baseItem + } + if (itemStack_ == null) + itemStack_ = itemStack + return itemStack + } + + + private fun starString(stars: Int): Text { + if (stars <= 0) return Text.empty() + // TODO: idk master stars + val tiers = listOf( + LegacyFormattingCode.GOLD, + LegacyFormattingCode.LIGHT_PURPLE, + LegacyFormattingCode.AQUA, + ) + val maxStars = 5 + if (stars > tiers.size * maxStars) return Text.literal(" ${stars}✪").withColor(Formatting.RED) + val starBaseTier = (stars - 1) / maxStars + val starBaseColor = tiers[starBaseTier] + val starsInCurrentTier = stars - starBaseTier * maxStars + val starString = Text.literal(" " + "✪".repeat(starsInCurrentTier)).withColor(starBaseColor.modern) + if (starBaseTier > 0) { + val starLastTier = tiers[starBaseTier - 1] + val starsInLastTier = 5 - starsInCurrentTier + starString.append(Text.literal("✪".repeat(starsInLastTier)).withColor(starLastTier.modern)) + } + return starString + } + + private fun enhanceStatsByStars(itemStack: ItemStack, stars: Int, baseStats: List) { + if (stars == 0) return + // TODO: increase stats and add the star level into the nbt data so star displays work + itemStack.modifyExtraAttributes { + it.putInt("upgrade_level", stars) + } + itemStack.displayNameAccordingToNbt = itemStack.displayNameAccordingToNbt.copy() + .append(starString(stars)) + val isDungeon = ItemType.fromItemStack(itemStack)?.isDungeon ?: true + val truncatedStarCount = if (isDungeon) minOf(5, stars) else stars + appendEnhancedStats( + itemStack, + baseStats + .filter { it.statFormatting.isStarAffected } + .associate { + it.statName to ((it.valueNum ?: 0.0) * (truncatedStarCount * 0.02)) + }, + BuffKind.STAR_BUFF + ) + } + + fun isWarm(): Boolean { + if (itemStack_ != null) return true + if (ItemCache.hasCacheFor(skyblockId)) return true + return false + } + + @OptIn(ExpensiveItemCacheApi::class) + fun asLazyImmutableItemStack(): ItemStack? { + if (isWarm()) return asImmutableItemStack() + return null + } + + @ExpensiveItemCacheApi + fun asImmutableItemStack(): ItemStack { // TODO: add a "or fallback to painting" option to asLazyImmutableItemStack to be used in more places. + return itemStack + } + + @ExpensiveItemCacheApi + fun asCopiedItemStack(): ItemStack { + return itemStack.copy() + } +} diff --git a/src/main/kotlin/repo/recipes/GenericRecipeRenderer.kt b/src/main/kotlin/repo/recipes/GenericRecipeRenderer.kt new file mode 100644 index 0000000..3774f26 --- /dev/null +++ b/src/main/kotlin/repo/recipes/GenericRecipeRenderer.kt @@ -0,0 +1,21 @@ +package moe.nea.firmament.repo.recipes + +import io.github.moulberry.repo.NEURepository +import io.github.moulberry.repo.data.NEURecipe +import me.shedaniel.math.Rectangle +import net.minecraft.item.ItemStack +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import moe.nea.firmament.repo.SBItemStack + +interface GenericRecipeRenderer { + fun render(recipe: T, bounds: Rectangle, layouter: RecipeLayouter, mainItem: SBItemStack?) + fun getInputs(recipe: T): Collection + fun getOutputs(recipe: T): Collection + val icon: ItemStack + val title: Text + val identifier: Identifier + fun findAllRecipes(neuRepository: NEURepository): Iterable + val displayHeight: Int get() = 66 + val typ: Class +} diff --git a/src/main/kotlin/repo/recipes/RecipeLayouter.kt b/src/main/kotlin/repo/recipes/RecipeLayouter.kt new file mode 100644 index 0000000..ed0dca2 --- /dev/null +++ b/src/main/kotlin/repo/recipes/RecipeLayouter.kt @@ -0,0 +1,38 @@ +package moe.nea.firmament.repo.recipes + +import io.github.notenoughupdates.moulconfig.gui.GuiComponent +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.text.Text +import moe.nea.firmament.repo.SBItemStack + +interface RecipeLayouter { + enum class SlotKind { + SMALL_INPUT, + SMALL_OUTPUT, + + /** + * Create a bigger background and mark the slot as output. The coordinates should still refer the upper left corner of the item stack, not of the bigger background. + */ + BIG_OUTPUT, + } + + fun createItemSlot( + x: Int, y: Int, + content: SBItemStack?, + slotKind: SlotKind, + ) + + fun createTooltip(rectangle: Rectangle, label: Text) + + fun createLabel( + x: Int, y: Int, + text: Text + ) + + fun createArrow(x: Int, y: Int): Rectangle + + fun createMoulConfig(x: Int, y: Int, w: Int, h: Int, component: GuiComponent) + fun createFire(ingredientsCenter: Point, animationTicks: Int) +} + diff --git a/src/main/kotlin/repo/recipes/SBCraftingRecipeRenderer.kt b/src/main/kotlin/repo/recipes/SBCraftingRecipeRenderer.kt new file mode 100644 index 0000000..e38380c --- /dev/null +++ b/src/main/kotlin/repo/recipes/SBCraftingRecipeRenderer.kt @@ -0,0 +1,60 @@ +package moe.nea.firmament.repo.recipes + +import io.github.moulberry.repo.NEURepository +import io.github.moulberry.repo.data.NEUCraftingRecipe +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.block.Blocks +import net.minecraft.item.ItemStack +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import moe.nea.firmament.Firmament +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.util.tr + +object SBCraftingRecipeRenderer : GenericRecipeRenderer { + override fun render( + recipe: NEUCraftingRecipe, + bounds: Rectangle, + layouter: RecipeLayouter, + mainItem: SBItemStack?, + ) { + val point = Point(bounds.centerX - 58, bounds.centerY - 27) + layouter.createArrow(point.x + 60, point.y + 18) + for (i in 0 until 3) { + for (j in 0 until 3) { + val item = recipe.inputs[i + j * 3] + layouter.createItemSlot( + point.x + 1 + i * 18, + point.y + 1 + j * 18, + SBItemStack(item), + RecipeLayouter.SlotKind.SMALL_INPUT + ) + } + } + layouter.createItemSlot( + point.x + 95, point.y + 19, + SBItemStack(recipe.output), + RecipeLayouter.SlotKind.BIG_OUTPUT + ) + } + + override val typ: Class + get() = NEUCraftingRecipe::class.java + + override fun getInputs(recipe: NEUCraftingRecipe): Collection { + return recipe.allInputs.mapNotNull { SBItemStack(it) } + } + + override fun getOutputs(recipe: NEUCraftingRecipe): Collection { + return SBItemStack(recipe.output)?.let(::listOf) ?: emptyList() + } + + override fun findAllRecipes(neuRepository: NEURepository): Iterable { + return neuRepository.items.items.values.flatMap { it.recipes }.filterIsInstance() + } + + override val icon: ItemStack = ItemStack(Blocks.CRAFTING_TABLE) + override val title: Text = tr("firmament.category.crafting", "SkyBlock Crafting") + override val identifier: Identifier = Firmament.identifier("crafting_recipe") +} diff --git a/src/main/kotlin/repo/recipes/SBEssenceUpgradeRecipeRenderer.kt b/src/main/kotlin/repo/recipes/SBEssenceUpgradeRecipeRenderer.kt new file mode 100644 index 0000000..d358e6a --- /dev/null +++ b/src/main/kotlin/repo/recipes/SBEssenceUpgradeRecipeRenderer.kt @@ -0,0 +1,76 @@ +package moe.nea.firmament.repo.recipes + +import io.github.moulberry.repo.NEURepository +import io.github.moulberry.repo.data.NEUForgeRecipe +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.item.ItemStack +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import moe.nea.firmament.Firmament +import moe.nea.firmament.repo.EssenceRecipeProvider +import moe.nea.firmament.repo.ExpensiveItemCacheApi +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.tr + +object SBEssenceUpgradeRecipeRenderer : GenericRecipeRenderer { + override fun render( + recipe: EssenceRecipeProvider.EssenceUpgradeRecipe, + bounds: Rectangle, + layouter: RecipeLayouter, + mainItem: SBItemStack? + ) { + val sourceItem = mainItem ?: SBItemStack(recipe.itemId) + layouter.createItemSlot( + bounds.minX + 12, + bounds.centerY - 8 - 18 / 2, + sourceItem.copy(stars = recipe.starCountAfter - 1), + RecipeLayouter.SlotKind.SMALL_INPUT + ) + layouter.createItemSlot( + bounds.minX + 12, bounds.centerY - 8 + 18 / 2, + SBItemStack(recipe.essenceIngredient), + RecipeLayouter.SlotKind.SMALL_INPUT + ) + layouter.createItemSlot( + bounds.maxX - 12 - 16, bounds.centerY - 8, + sourceItem.copy(stars = recipe.starCountAfter), + RecipeLayouter.SlotKind.SMALL_OUTPUT + ) + val extraItems = recipe.extraItems + layouter.createArrow( + bounds.centerX - 24 / 2, + if (extraItems.isEmpty()) bounds.centerY - 17 / 2 + else bounds.centerY + 18 / 2 + ) + for ((index, item) in extraItems.withIndex()) { + layouter.createItemSlot( + bounds.centerX - extraItems.size * 16 / 2 - 2 / 2 + index * 18, + bounds.centerY - 18 / 2, + SBItemStack(item), + RecipeLayouter.SlotKind.SMALL_INPUT, + ) + } + } + + override fun getInputs(recipe: EssenceRecipeProvider.EssenceUpgradeRecipe): Collection { + return recipe.allInputs.mapNotNull { SBItemStack(it) } + } + + override fun getOutputs(recipe: EssenceRecipeProvider.EssenceUpgradeRecipe): Collection { + return listOfNotNull(SBItemStack(recipe.itemId)) + } + + @OptIn(ExpensiveItemCacheApi::class) + override val icon: ItemStack get() = SBItemStack(SkyblockId("ESSENCE_WITHER")).asImmutableItemStack() + override val title: Text = tr("firmament.category.essence", "Essence Upgrades") + override val identifier: Identifier = Firmament.identifier("essence_upgrade") + override fun findAllRecipes(neuRepository: NEURepository): Iterable { + return RepoManager.essenceRecipeProvider.recipes + } + + override val typ: Class + get() = EssenceRecipeProvider.EssenceUpgradeRecipe::class.java +} diff --git a/src/main/kotlin/repo/recipes/SBForgeRecipeRenderer.kt b/src/main/kotlin/repo/recipes/SBForgeRecipeRenderer.kt new file mode 100644 index 0000000..9fdb756 --- /dev/null +++ b/src/main/kotlin/repo/recipes/SBForgeRecipeRenderer.kt @@ -0,0 +1,83 @@ +package moe.nea.firmament.repo.recipes + +import io.github.moulberry.repo.NEURepository +import io.github.moulberry.repo.data.NEUCraftingRecipe +import io.github.moulberry.repo.data.NEUForgeRecipe +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import kotlin.math.cos +import kotlin.math.sin +import kotlin.time.Duration.Companion.seconds +import net.minecraft.block.Blocks +import net.minecraft.item.ItemStack +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import moe.nea.firmament.Firmament +import moe.nea.firmament.repo.SBItemStack +import moe.nea.firmament.util.tr + +object SBForgeRecipeRenderer : GenericRecipeRenderer { + override fun render( + recipe: NEUForgeRecipe, + bounds: Rectangle, + layouter: RecipeLayouter, + mainItem: SBItemStack?, + ) { + val arrow = layouter.createArrow(bounds.minX + 90, bounds.minY + 54 - 18 / 2) + layouter.createTooltip( + arrow, + Text.stringifiedTranslatable( + "firmament.recipe.forge.time", + recipe.duration.seconds + ) + ) + + val ingredientsCenter = Point(bounds.minX + 49 - 8, bounds.minY + 54 - 8) + layouter.createFire(ingredientsCenter, 25) + val count = recipe.inputs.size + if (count == 1) { + layouter.createItemSlot( + ingredientsCenter.x, ingredientsCenter.y - 18, + SBItemStack(recipe.inputs.single()), + RecipeLayouter.SlotKind.SMALL_INPUT, + ) + } else { + recipe.inputs.forEachIndexed { idx, ingredient -> + val rad = Math.PI * 2 * idx / count + layouter.createItemSlot( + (ingredientsCenter.x + cos(rad) * 30).toInt(), (ingredientsCenter.y + sin(rad) * 30).toInt(), + SBItemStack(ingredient), + RecipeLayouter.SlotKind.SMALL_INPUT, + ) + } + } + layouter.createItemSlot( + bounds.minX + 124, bounds.minY + 46, + SBItemStack(recipe.outputStack), + RecipeLayouter.SlotKind.BIG_OUTPUT + ) + } + + override val displayHeight: Int + get() = 104 + + override fun getInputs(recipe: NEUForgeRecipe): Collection { + return recipe.inputs.mapNotNull { SBItemStack(it) } + } + + override fun getOutputs(recipe: NEUForgeRecipe): Collection { + return listOfNotNull(SBItemStack(recipe.outputStack)) + } + + override val icon: ItemStack = ItemStack(Blocks.ANVIL) + override val title: Text = tr("firmament.category.forge", "Forge Recipes") + override val identifier: Identifier = Firmament.identifier("forge_recipe") + + override fun findAllRecipes(neuRepository: NEURepository): Iterable { + // TODO: theres gotta be an index for these tbh. + return neuRepository.items.items.values.flatMap { it.recipes }.filterIsInstance() + } + + override val typ: Class + get() = NEUForgeRecipe::class.java +} diff --git a/src/main/kotlin/util/AprilFoolsUtil.kt b/src/main/kotlin/util/AprilFoolsUtil.kt new file mode 100644 index 0000000..a940fa1 --- /dev/null +++ b/src/main/kotlin/util/AprilFoolsUtil.kt @@ -0,0 +1,10 @@ +package moe.nea.firmament.util + +import java.time.LocalDateTime +import java.time.Month + +object AprilFoolsUtil { + val isAprilFoolsDay = LocalDateTime.now().let { + it.dayOfMonth == 1 && it.month == Month.APRIL + } +} diff --git a/src/main/kotlin/util/Base64Util.kt b/src/main/kotlin/util/Base64Util.kt new file mode 100644 index 0000000..c39c601 --- /dev/null +++ b/src/main/kotlin/util/Base64Util.kt @@ -0,0 +1,17 @@ + +package moe.nea.firmament.util + +import java.util.Base64 + +object Base64Util { + fun decodeString(str: String): String { + return Base64.getDecoder().decode(str.padToValidBase64()) + .decodeToString() + } + + fun String.padToValidBase64(): String { + val align = this.length % 4 + if (align == 0) return this + return this + "=".repeat(4 - align) + } +} diff --git a/src/main/kotlin/util/BazaarPriceStrategy.kt b/src/main/kotlin/util/BazaarPriceStrategy.kt new file mode 100644 index 0000000..13b6d95 --- /dev/null +++ b/src/main/kotlin/util/BazaarPriceStrategy.kt @@ -0,0 +1,19 @@ + +package moe.nea.firmament.util + +import moe.nea.firmament.repo.HypixelStaticData + +enum class BazaarPriceStrategy { + BUY_ORDER, + SELL_ORDER, + NPC_SELL; + + fun getSellPrice(skyblockId: SkyblockId): Double { + val bazaarEntry = HypixelStaticData.bazaarData[skyblockId.asBazaarStock] ?: return 0.0 + return when (this) { + BUY_ORDER -> bazaarEntry.quickStatus.sellPrice + SELL_ORDER -> bazaarEntry.quickStatus.buyPrice + NPC_SELL -> TODO() + } + } +} diff --git a/src/main/kotlin/util/ChromaColourUtil.kt b/src/main/kotlin/util/ChromaColourUtil.kt new file mode 100644 index 0000000..0130326 --- /dev/null +++ b/src/main/kotlin/util/ChromaColourUtil.kt @@ -0,0 +1,10 @@ +package moe.nea.firmament.util + +import io.github.notenoughupdates.moulconfig.ChromaColour +import java.awt.Color + +fun ChromaColour.getRGBAWithoutAnimation() = + Color(ChromaColour.specialToSimpleRGB(toLegacyString()), true) + +fun Color.toChromaWithoutAnimation(timeForFullRotationInMillis: Int = 0) = + ChromaColour.fromRGB(red, green, blue, timeForFullRotationInMillis, alpha) diff --git a/src/main/kotlin/util/ClipboardUtils.kt b/src/main/kotlin/util/ClipboardUtils.kt new file mode 100644 index 0000000..7b9b836 --- /dev/null +++ b/src/main/kotlin/util/ClipboardUtils.kt @@ -0,0 +1,24 @@ + + +package moe.nea.firmament.util + +import moe.nea.firmament.Firmament + +object ClipboardUtils { + fun setTextContent(string: String) { + try { + MC.keyboard.clipboard = string.ifEmpty { " " } + } catch (e: Exception) { + Firmament.logger.error("Could not write clipboard", e) + } + } + + fun getTextContents(): String { + try { + return MC.keyboard.clipboard ?: "" + } catch (e: Exception) { + Firmament.logger.error("Could not read clipboard", e) + return "" + } + } +} diff --git a/src/main/kotlin/util/CommonSoundEffects.kt b/src/main/kotlin/util/CommonSoundEffects.kt new file mode 100644 index 0000000..a97a2cb --- /dev/null +++ b/src/main/kotlin/util/CommonSoundEffects.kt @@ -0,0 +1,26 @@ + + +package moe.nea.firmament.util + +import net.minecraft.client.sound.PositionedSoundInstance +import net.minecraft.sound.SoundEvent +import net.minecraft.util.Identifier + +// TODO: Replace these with custom sound events that just re use the vanilla ogg s +object CommonSoundEffects { + fun playSound(identifier: Identifier) { + MC.soundManager.play(PositionedSoundInstance.master(SoundEvent.of(identifier), 1F)) + } + + fun playFailure() { + playSound(Identifier.of("minecraft", "block.anvil.place")) + } + + fun playSuccess() { + playDing() + } + + fun playDing() { + playSound(Identifier.of("minecraft", "entity.arrow.hit_player")) + } +} diff --git a/src/main/kotlin/util/DurabilityBarEvent.kt b/src/main/kotlin/util/DurabilityBarEvent.kt new file mode 100644 index 0000000..993462c --- /dev/null +++ b/src/main/kotlin/util/DurabilityBarEvent.kt @@ -0,0 +1,20 @@ + +package moe.nea.firmament.util + +import me.shedaniel.math.Color +import net.minecraft.item.ItemStack +import moe.nea.firmament.events.FirmamentEvent +import moe.nea.firmament.events.FirmamentEventBus + +data class DurabilityBarEvent( + val item: ItemStack, +) : FirmamentEvent() { + data class DurabilityBar( + val color: Color, + val percentage: Float, + ) + + var barOverride: DurabilityBar? = null + + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/util/ErrorBoundary.kt b/src/main/kotlin/util/ErrorBoundary.kt new file mode 100644 index 0000000..fbc5b37 --- /dev/null +++ b/src/main/kotlin/util/ErrorBoundary.kt @@ -0,0 +1,10 @@ + + +package moe.nea.firmament.util + + +fun errorBoundary(block: () -> T): T? { + // TODO: implement a proper error boundary here to avoid crashing minecraft code + return block() +} + diff --git a/src/main/kotlin/util/ErrorUtil.kt b/src/main/kotlin/util/ErrorUtil.kt new file mode 100644 index 0000000..3db4ecd --- /dev/null +++ b/src/main/kotlin/util/ErrorUtil.kt @@ -0,0 +1,97 @@ +@file:OptIn(ExperimentalContracts::class) + +package moe.nea.firmament.util + +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract +import moe.nea.firmament.Firmament + +@Suppress("NOTHING_TO_INLINE") // Suppressed since i want the logger to not pick up the ErrorUtil stack-frame +object ErrorUtil { + var aggressiveErrors = run { + TestUtil.isInTest || Firmament.DEBUG + || ErrorUtil::class.java.desiredAssertionStatus() + } + + inline fun softCheck(message: String, check: Boolean) { + if (!check) softError(message) + } + + inline fun lazyCheck(message: String, func: () -> Boolean) { + contract { + callsInPlace(func, InvocationKind.AT_MOST_ONCE) + } + if (!aggressiveErrors) return + if (func()) return + error(message) + } + + inline fun softError(message: String, exception: Throwable) { + if (aggressiveErrors) throw IllegalStateException(message, exception) + else logError(message, exception) + } + + fun logError(message: String, exception: Throwable) { + Firmament.logger.error(message, exception) + } + fun logError(message: String) { + Firmament.logger.error(message) + } + + inline fun softError(message: String) { + if (aggressiveErrors) error(message) + else logError(message) + } + + fun Result.intoCatch(message: String): Catch { + return this.map { Catch.succeed(it) }.getOrElse { + softError(message, it) + Catch.fail(it) + } + } + + class Catch private constructor(val value: T?, val exc: Throwable?) { + fun orNull(): T? = value + + inline fun or(block: (exc: Throwable) -> T): T { + contract { + callsInPlace(block, InvocationKind.AT_MOST_ONCE) + } + if (exc != null) return block(exc) + @Suppress("UNCHECKED_CAST") + return value as T + } + + companion object { + fun fail(exception: Throwable): Catch = Catch(null, exception) + fun succeed(value: T): Catch = Catch(value, null) + } + } + + inline fun catch(message: String, block: () -> T): Catch { + try { + return Catch.succeed(block()) + } catch (exc: Throwable) { + softError(message, exc) + return Catch.fail(exc) + } + } + + inline fun notNullOr(nullable: T?, message: String, orElse: () -> T): T { + contract { + callsInPlace(orElse, InvocationKind.AT_MOST_ONCE) + } + if (nullable == null) { + softError(message) + return orElse() + } + return nullable + } + + fun softUserError(string: String) { + if (TestUtil.isInTest) + error(string) + MC.sendChat(tr("firmament.usererror", "Firmament encountered a user caused error: $string")) + } +} diff --git a/src/main/kotlin/util/FirmFormatters.kt b/src/main/kotlin/util/FirmFormatters.kt new file mode 100644 index 0000000..03dafc5 --- /dev/null +++ b/src/main/kotlin/util/FirmFormatters.kt @@ -0,0 +1,142 @@ +package moe.nea.firmament.util + +import com.google.common.math.IntMath.pow +import java.nio.file.Path +import kotlin.io.path.exists +import kotlin.io.path.fileSize +import kotlin.io.path.isDirectory +import kotlin.io.path.isReadable +import kotlin.io.path.isRegularFile +import kotlin.io.path.listDirectoryEntries +import kotlin.math.absoluteValue +import kotlin.math.roundToInt +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import net.minecraft.text.Text +import net.minecraft.util.math.BlockPos + +object FirmFormatters { + + private inline fun shortIf( + value: Double, breakpoint: Double, char: String, + return_: (String) -> Nothing + ) { + if (value >= breakpoint) { + val broken = (value / breakpoint * 10).roundToInt() + if (broken > 99) + return_((broken / 10).toString() + char) + val decimals = broken.toString() + decimals.singleOrNull()?.let { + return_("0.$it$char") + } + return_("${decimals[0]}.${decimals[1]}$char") + } + } + + fun shortFormat(double: Double): String { + if (double < 0) return "-" + shortFormat(-double) + shortIf(double, 1_000_000_000_000.0, "t") { return it } + shortIf(double, 1_000_000_000.0, "b") { return it } + shortIf(double, 1_000_000.0, "m") { return it } + shortIf(double, 1_000.0, "k") { return it } + shortIf(double, 1.0, "") { return it } + return double.toString() + } + + fun formatCommas(int: Int, segments: Int = 3): String = formatCommas(int.toLong(), segments) + fun formatCommas(long: Long, segments: Int = 3, includeSign: Boolean = false): String { + if (long < 0 && long != Long.MIN_VALUE) { + return "-" + formatCommas(-long, segments, false) + } + val prefix = if (includeSign) "+" else "" + val α = long / 1000 + if (α != 0L) { + return prefix + formatCommas(α, segments) + "," + (long - α * 1000).toString().padStart(3, '0') + } + return prefix + long.toString() + } + + fun formatCommas(float: Float, fractionalDigits: Int): String = formatCommas(float.toDouble(), fractionalDigits) + fun formatCommas(double: Double, fractionalDigits: Int, includeSign: Boolean = false): String { + val long = double.toLong() + val δ = (double - long).absoluteValue + val μ = pow(10, fractionalDigits) + val digits = (μ * δ).toInt().toString().padStart(fractionalDigits, '0').trimEnd('0') + return formatCommas(long, includeSign = includeSign) + (if (digits.isEmpty()) "" else ".$digits") + } + + fun formatDistance(distance: Double): String { + if (distance < 10) + return "%.1fm".format(distance) + return "%dm".format(distance.toInt()) + } + + fun formatTimespan(duration: Duration, millis: Boolean = false): String { + if (duration.isInfinite()) { + return if (duration.isPositive()) "∞" + else "-∞" + } + val sb = StringBuilder() + if (duration.isNegative()) sb.append("-") + duration.toComponents { days, hours, minutes, seconds, nanoseconds -> + if (days > 0) { + sb.append(days).append("d") + } + if (hours > 0) { + sb.append(hours).append("h") + } + if (minutes > 0) { + sb.append(minutes).append("m") + } + val milliTime = nanoseconds / 1_000_000 + val deciseconds = milliTime / 100 + if (millis) { + sb.append(seconds).append("s") + sb.append(milliTime).append("ms") + } else if (duration.absoluteValue < 5.seconds && deciseconds != 0) { + sb.append(seconds).append('.').append(deciseconds.digitToChar()).append("s") + } else { + sb.append(seconds).append("s") + } + Unit + } + return sb.toString() + } + + fun debugPath(path: Path): Text { + if (!path.exists()) { + return tr("firmament.path.missing", "$path (missing)").red() + } + if (!path.isReadable()) { + return tr("firmament.path.unreadable", "$path (unreadable)").red() + } + if (path.isRegularFile()) { + return tr("firmament.path.regular", + "$path (exists ${formatFileSize(path.fileSize())})").lime() + } + if (path.isDirectory()) { + return tr("firmament.path.directory", "$path (${path.listDirectoryEntries().size} entries)").darkGreen() + } + return tr("firmament.path.unknown", "$path (unknown)").purple() + } + + fun formatFileSize(fileSizeInBytes: Long): String { + return "${fileSizeInBytes / 1024} KiB" + } + + fun formatBool( + boolean: Boolean, + trueIsGood: Boolean = true, + ): Text { + val text = Text.literal(boolean.toString()) + return if (boolean == trueIsGood) text.lime() else text.red() + } + + fun formatPosition(position: BlockPos): Text { + return Text.literal("x: ${position.x}, y: ${position.y}, z: ${position.z}") + } + + fun formatPercent(value: Double, decimals: Int = 1): String { + return "%.${decimals}f%%".format(value * 100) + } +} diff --git a/src/main/kotlin/util/FragmentGuiScreen.kt b/src/main/kotlin/util/FragmentGuiScreen.kt new file mode 100644 index 0000000..5e13d51 --- /dev/null +++ b/src/main/kotlin/util/FragmentGuiScreen.kt @@ -0,0 +1,93 @@ + + +package moe.nea.firmament.util + +import io.github.notenoughupdates.moulconfig.gui.GuiContext +import me.shedaniel.math.Dimension +import me.shedaniel.math.Point +import me.shedaniel.math.Rectangle +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.gui.screen.Screen +import net.minecraft.text.Text + +abstract class FragmentGuiScreen( + val dismissOnOutOfBounds: Boolean = true +) : Screen(Text.literal("")) { + var popup: MoulConfigFragment? = null + + fun createPopup(context: GuiContext, position: Point) { + popup = MoulConfigFragment(context, position) { popup = null } + } + + override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { + super.render(context, mouseX, mouseY, delta) + context.matrices.push() + context.matrices.translate(0f, 0f, 1000f) + popup?.render(context, mouseX, mouseY, delta) + context.matrices.pop() + } + + private inline fun ifPopup(ifYes: (MoulConfigFragment) -> Unit): Boolean { + val p = popup ?: return false + ifYes(p) + return true + } + + override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + return ifPopup { + it.keyPressed(keyCode, scanCode, modifiers) + } + } + + override fun keyReleased(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + return ifPopup { + it.keyReleased(keyCode, scanCode, modifiers) + } + } + + override fun mouseMoved(mouseX: Double, mouseY: Double) { + ifPopup { it.mouseMoved(mouseX, mouseY) } + } + + override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { + return ifPopup { + it.mouseReleased(mouseX, mouseY, button) + } + } + + override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean { + return ifPopup { + it.mouseDragged(mouseX, mouseY, button, deltaX, deltaY) + } + } + + override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { + return ifPopup { + if (!Rectangle( + it.position, + Dimension(it.context.root.width, it.context.root.height) + ).contains(Point(mouseX, mouseY)) + && dismissOnOutOfBounds + ) { + popup = null + } else { + it.mouseClicked(mouseX, mouseY, button) + } + }|| super.mouseClicked(mouseX, mouseY, button) + } + + override fun charTyped(chr: Char, modifiers: Int): Boolean { + return ifPopup { it.charTyped(chr, modifiers) } + } + + override fun mouseScrolled( + mouseX: Double, + mouseY: Double, + horizontalAmount: Double, + verticalAmount: Double + ): Boolean { + return ifPopup { + it.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount) + } + } +} diff --git a/src/main/kotlin/util/HoveredItemStack.kt b/src/main/kotlin/util/HoveredItemStack.kt new file mode 100644 index 0000000..526820a --- /dev/null +++ b/src/main/kotlin/util/HoveredItemStack.kt @@ -0,0 +1,27 @@ +package moe.nea.firmament.util + +import com.google.auto.service.AutoService +import net.minecraft.client.gui.screen.ingame.HandledScreen +import net.minecraft.item.ItemStack +import moe.nea.firmament.mixins.accessor.AccessorHandledScreen +import moe.nea.firmament.util.compatloader.CompatLoader + +interface HoveredItemStackProvider { + fun provideHoveredItemStack(screen: HandledScreen<*>): ItemStack? + + companion object : CompatLoader(HoveredItemStackProvider::class) +} + +@AutoService(HoveredItemStackProvider::class) +class VanillaScreenProvider : HoveredItemStackProvider { + override fun provideHoveredItemStack(screen: HandledScreen<*>): ItemStack? { + screen as AccessorHandledScreen + val vanillaSlot = screen.focusedSlot_Firmament?.stack + return vanillaSlot + } +} + +val HandledScreen<*>.focusedItemStack: ItemStack? + get() = + HoveredItemStackProvider.allValidInstances + .firstNotNullOfOrNull { it.provideHoveredItemStack(this)?.takeIf { !it.isEmpty } } diff --git a/src/main/kotlin/util/IdentifierSerializer.kt b/src/main/kotlin/util/IdentifierSerializer.kt new file mode 100644 index 0000000..65c5b1c --- /dev/null +++ b/src/main/kotlin/util/IdentifierSerializer.kt @@ -0,0 +1,25 @@ + +package moe.nea.firmament.util + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import net.minecraft.util.Identifier + +object IdentifierSerializer : KSerializer { + val delegateSerializer = String.serializer() + override val descriptor: SerialDescriptor + get() = PrimitiveSerialDescriptor("Identifier", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): Identifier { + return Identifier.of(decoder.decodeSerializableValue(delegateSerializer)) + } + + override fun serialize(encoder: Encoder, value: Identifier) { + encoder.encodeSerializableValue(delegateSerializer, value.toString()) + } +} diff --git a/src/main/kotlin/util/IdentityCharacteristics.kt b/src/main/kotlin/util/IdentityCharacteristics.kt new file mode 100644 index 0000000..f6054c4 --- /dev/null +++ b/src/main/kotlin/util/IdentityCharacteristics.kt @@ -0,0 +1,15 @@ + + +package moe.nea.firmament.util + +class IdentityCharacteristics(val value: T) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is IdentityCharacteristics<*>) return false + return value === other.value + } + + override fun hashCode(): Int { + return System.identityHashCode(value) + } +} diff --git a/src/main/kotlin/util/IntUtil.kt b/src/main/kotlin/util/IntUtil.kt new file mode 100644 index 0000000..2695906 --- /dev/null +++ b/src/main/kotlin/util/IntUtil.kt @@ -0,0 +1,12 @@ +package moe.nea.firmament.util + +object IntUtil { + data class RGBA(val r: Int, val g: Int, val b: Int, val a: Int) + + fun Int.toRGBA(): RGBA { + return RGBA( + r = (this shr 16) and 0xFF, g = (this shr 8) and 0xFF, b = this and 0xFF, a = (this shr 24) and 0xFF + ) + } + +} diff --git a/src/main/kotlin/util/JvmUtil.kt b/src/main/kotlin/util/JvmUtil.kt new file mode 100644 index 0000000..5be5ebd --- /dev/null +++ b/src/main/kotlin/util/JvmUtil.kt @@ -0,0 +1,32 @@ +package moe.nea.firmament.util + +import com.sun.tools.attach.VirtualMachine +import java.lang.management.ManagementFactory +import java.nio.file.Path +import kotlin.io.path.absolutePathString + +object JvmUtil { + fun guessJVMPid(): String { + val name = ManagementFactory.getRuntimeMXBean().name + val pid = name.substringBefore('@') + ErrorUtil.softCheck("Not a valid PID: $pid", pid.toIntOrNull() != null) + return pid + } + + fun getVM(): VirtualMachine { + return VirtualMachine.attach(guessJVMPid()) + } + + fun useVM(block: (VirtualMachine) -> Unit) { + val vm = getVM() + block(vm) + vm.detach() + } + + fun loadAgent(jarPath: Path, options: String? = null) { + useVM { + it.loadAgent(jarPath.absolutePathString(), options) + } + } + +} diff --git a/src/main/kotlin/util/LegacyFormattingCode.kt b/src/main/kotlin/util/LegacyFormattingCode.kt new file mode 100644 index 0000000..1a5d1dd --- /dev/null +++ b/src/main/kotlin/util/LegacyFormattingCode.kt @@ -0,0 +1,37 @@ +package moe.nea.firmament.util + +import net.minecraft.util.Formatting + +enum class LegacyFormattingCode(val label: String, val char: Char, val index: Int) { + BLACK("BLACK", '0', 0), + DARK_BLUE("DARK_BLUE", '1', 1), + DARK_GREEN("DARK_GREEN", '2', 2), + DARK_AQUA("DARK_AQUA", '3', 3), + DARK_RED("DARK_RED", '4', 4), + DARK_PURPLE("DARK_PURPLE", '5', 5), + GOLD("GOLD", '6', 6), + GRAY("GRAY", '7', 7), + DARK_GRAY("DARK_GRAY", '8', 8), + BLUE("BLUE", '9', 9), + GREEN("GREEN", 'a', 10), + AQUA("AQUA", 'b', 11), + RED("RED", 'c', 12), + LIGHT_PURPLE("LIGHT_PURPLE", 'd', 13), + YELLOW("YELLOW", 'e', 14), + WHITE("WHITE", 'f', 15), + OBFUSCATED("OBFUSCATED", 'k', -1), + BOLD("BOLD", 'l', -1), + STRIKETHROUGH("STRIKETHROUGH", 'm', -1), + UNDERLINE("UNDERLINE", 'n', -1), + ITALIC("ITALIC", 'o', -1), + RESET("RESET", 'r', -1); + + companion object { + val byCode = entries.associateBy { it.char } + } + + val modern = Formatting.byCode(char)!! + + val formattingCode = "§$char" + +} diff --git a/src/main/kotlin/util/LegacyTagParser.kt b/src/main/kotlin/util/LegacyTagParser.kt new file mode 100644 index 0000000..4e08da1 --- /dev/null +++ b/src/main/kotlin/util/LegacyTagParser.kt @@ -0,0 +1,245 @@ + + +package moe.nea.firmament.util + +import java.util.* +import net.minecraft.nbt.AbstractNbtNumber +import net.minecraft.nbt.NbtByte +import net.minecraft.nbt.NbtCompound +import net.minecraft.nbt.NbtDouble +import net.minecraft.nbt.NbtElement +import net.minecraft.nbt.NbtFloat +import net.minecraft.nbt.NbtInt +import net.minecraft.nbt.NbtList +import net.minecraft.nbt.NbtLong +import net.minecraft.nbt.NbtShort +import net.minecraft.nbt.NbtString + +class LegacyTagParser private constructor(string: String) { + data class TagParsingException(val baseString: String, val offset: Int, val mes0: String) : + Exception("$mes0 at $offset in `$baseString`.") + + class StringRacer(val backing: String) { + var idx = 0 + val stack = Stack() + + fun pushState() { + stack.push(idx) + } + + fun popState() { + idx = stack.pop() + } + + fun discardState() { + stack.pop() + } + + fun peek(count: Int): String { + return backing.substring(minOf(idx, backing.length), minOf(idx + count, backing.length)) + } + + fun finished(): Boolean { + return peek(1).isEmpty() + } + + fun peekReq(count: Int): String? { + val p = peek(count) + if (p.length != count) + return null + return p + } + + fun consumeCountReq(count: Int): String? { + val p = peekReq(count) + if (p != null) + idx += count + return p + } + + fun tryConsume(string: String): Boolean { + val p = peek(string.length) + if (p != string) + return false + idx += p.length + return true + } + + fun consumeWhile(shouldConsumeThisString: (String) -> Boolean): String { + var lastString: String = "" + while (true) { + val nextString = lastString + peek(1) + if (!shouldConsumeThisString(nextString)) { + return lastString + } + idx++ + lastString = nextString + } + } + + fun expect(search: String, errorMessage: String) { + if (!tryConsume(search)) + error(errorMessage) + } + + fun error(errorMessage: String): Nothing { + throw TagParsingException(backing, idx, errorMessage) + } + + } + + val racer = StringRacer(string) + val baseTag = parseTag() + + companion object { + val digitRange = "0123456789-" + fun parse(string: String): NbtCompound { + return LegacyTagParser(string).baseTag + } + } + + fun skipWhitespace() { + racer.consumeWhile { Character.isWhitespace(it.last()) } // Only check last since other chars are always checked before. + } + + fun parseTag(): NbtCompound { + skipWhitespace() + racer.expect("{", "Expected '{’ at start of tag") + skipWhitespace() + val tag = NbtCompound() + while (!racer.tryConsume("}")) { + skipWhitespace() + val lhs = parseIdentifier() + skipWhitespace() + racer.expect(":", "Expected ':' after identifier in tag") + skipWhitespace() + val rhs = parseAny() + tag.put(lhs, rhs) + racer.tryConsume(",") + skipWhitespace() + } + return tag + } + + private fun parseAny(): NbtElement { + skipWhitespace() + val nextChar = racer.peekReq(1) ?: racer.error("Expected new object, found EOF") + return when { + nextChar == "{" -> parseTag() + nextChar == "[" -> parseList() + nextChar == "\"" -> parseStringTag() + nextChar.first() in (digitRange) -> parseNumericTag() + else -> racer.error("Unexpected token found. Expected start of new element") + } + } + + fun parseList(): NbtList { + skipWhitespace() + racer.expect("[", "Expected '[' at start of tag") + skipWhitespace() + val list = NbtList() + while (!racer.tryConsume("]")) { + skipWhitespace() + racer.pushState() + val lhs = racer.consumeWhile { it.all { it in digitRange } } + skipWhitespace() + if (!racer.tryConsume(":") || lhs.isEmpty()) { // No prefixed 0: + racer.popState() + list.add(parseAny()) // Reparse our number (or not a number) as actual tag + } else { + racer.discardState() + skipWhitespace() + list.add(parseAny()) // Ignore prefix indexes. They should not be generated out of order by any vanilla implementation (which is what NEU should export). Instead append where it appears in order. + } + skipWhitespace() + racer.tryConsume(",") + } + return list + } + + fun parseQuotedString(): String { + skipWhitespace() + racer.expect("\"", "Expected '\"' at string start") + val sb = StringBuilder() + while (true) { + when (val peek = racer.consumeCountReq(1)) { + "\"" -> break + "\\" -> { + val escaped = racer.consumeCountReq(1) ?: racer.error("Unfinished backslash escape") + if (escaped != "\"" && escaped != "\\") { + // Surprisingly i couldn't find unicode escapes to be generated by the original minecraft 1.8.9 implementation + racer.idx-- + racer.error("Invalid backslash escape '$escaped'") + } + sb.append(escaped) + } + + null -> racer.error("Unfinished string") + else -> { + sb.append(peek) + } + } + } + return sb.toString() + } + + fun parseStringTag(): NbtString { + return NbtString.of(parseQuotedString()) + } + + object Patterns { + val DOUBLE = "([-+]?[0-9]*\\.?[0-9]+)[d|D]".toRegex() + val FLOAT = "([-+]?[0-9]*\\.?[0-9]+)[f|F]".toRegex() + val BYTE = "([-+]?[0-9]+)[b|B]".toRegex() + val LONG = "([-+]?[0-9]+)[l|L]".toRegex() + val SHORT = "([-+]?[0-9]+)[s|S]".toRegex() + val INTEGER = "([-+]?[0-9]+)".toRegex() + val DOUBLE_UNTYPED = "([-+]?[0-9]*\\.?[0-9]+)".toRegex() + val ROUGH_PATTERN = "[-+]?[0-9]*\\.?[0-9]*[dDbBfFlLsS]?".toRegex() + } + + fun parseNumericTag(): AbstractNbtNumber { + skipWhitespace() + val textForm = racer.consumeWhile { Patterns.ROUGH_PATTERN.matchEntire(it) != null } + if (textForm.isEmpty()) { + racer.error("Expected numeric tag (starting with either -, +, . or a digit") + } + val floatMatch = Patterns.FLOAT.matchEntire(textForm) + if (floatMatch != null) { + return NbtFloat.of(floatMatch.groups[1]!!.value.toFloat()) + } + val byteMatch = Patterns.BYTE.matchEntire(textForm) + if (byteMatch != null) { + return NbtByte.of(byteMatch.groups[1]!!.value.toByte()) + } + val longMatch = Patterns.LONG.matchEntire(textForm) + if (longMatch != null) { + return NbtLong.of(longMatch.groups[1]!!.value.toLong()) + } + val shortMatch = Patterns.SHORT.matchEntire(textForm) + if (shortMatch != null) { + return NbtShort.of(shortMatch.groups[1]!!.value.toShort()) + } + val integerMatch = Patterns.INTEGER.matchEntire(textForm) + if (integerMatch != null) { + return NbtInt.of(integerMatch.groups[1]!!.value.toInt()) + } + val doubleMatch = Patterns.DOUBLE.matchEntire(textForm) ?: Patterns.DOUBLE_UNTYPED.matchEntire(textForm) + if (doubleMatch != null) { + return NbtDouble.of(doubleMatch.groups[1]!!.value.toDouble()) + } + throw IllegalStateException("Could not properly parse numeric tag '$textForm', despite passing rough verification. This is a bug in the LegacyTagParser") + } + + private fun parseIdentifier(): String { + skipWhitespace() + if (racer.peek(1) == "\"") { + return parseQuotedString() + } + return racer.consumeWhile { + val x = it.last() + x != ':' && !Character.isWhitespace(x) + } + } + +} diff --git a/src/main/kotlin/util/LegacyTagWriter.kt b/src/main/kotlin/util/LegacyTagWriter.kt new file mode 100644 index 0000000..9889b2c --- /dev/null +++ b/src/main/kotlin/util/LegacyTagWriter.kt @@ -0,0 +1,103 @@ +package moe.nea.firmament.util + +import kotlinx.serialization.json.JsonPrimitive +import net.minecraft.nbt.AbstractNbtList +import net.minecraft.nbt.NbtByte +import net.minecraft.nbt.NbtCompound +import net.minecraft.nbt.NbtDouble +import net.minecraft.nbt.NbtElement +import net.minecraft.nbt.NbtEnd +import net.minecraft.nbt.NbtFloat +import net.minecraft.nbt.NbtInt +import net.minecraft.nbt.NbtLong +import net.minecraft.nbt.NbtShort +import net.minecraft.nbt.NbtString +import moe.nea.firmament.util.mc.SNbtFormatter.Companion.SIMPLE_NAME + +class LegacyTagWriter(val compact: Boolean) { + companion object { + fun stringify(nbt: NbtElement, compact: Boolean): String { + return LegacyTagWriter(compact).also { it.writeElement(nbt) } + .stringWriter.toString() + } + + fun NbtElement.toLegacyString(pretty: Boolean = false): String { + return stringify(this, !pretty) + } + } + + val stringWriter = StringBuilder() + var indent = 0 + fun newLine() { + if (compact) return + stringWriter.append('\n') + repeat(indent) { + stringWriter.append(" ") + } + } + + fun writeElement(nbt: NbtElement) { + when (nbt) { + is NbtInt -> stringWriter.append(nbt.value.toString()) + is NbtString -> stringWriter.append(escapeString(nbt.value)) + is NbtFloat -> stringWriter.append(nbt.value).append('F') + is NbtDouble -> stringWriter.append(nbt.value).append('D') + is NbtByte -> stringWriter.append(nbt.value).append('B') + is NbtLong -> stringWriter.append(nbt.value).append('L') + is NbtShort -> stringWriter.append(nbt.value).append('S') + is NbtCompound -> writeCompound(nbt) + is NbtEnd -> {} + is AbstractNbtList -> writeArray(nbt) + } + } + + fun writeArray(nbt: AbstractNbtList) { + stringWriter.append('[') + indent++ + newLine() + nbt.forEachIndexed { index, element -> + writeName(index.toString()) + writeElement(element) + if (index != nbt.size() - 1) { + stringWriter.append(',') + newLine() + } + } + indent-- + if (nbt.size() != 0) + newLine() + stringWriter.append(']') + } + + fun writeCompound(nbt: NbtCompound) { + stringWriter.append('{') + indent++ + newLine() + val entries = nbt.entrySet().sortedBy { it.key } + entries.forEachIndexed { index, it -> + writeName(it.key) + writeElement(it.value) + if (index != entries.lastIndex) { + stringWriter.append(',') + newLine() + } + } + indent-- + if (nbt.size != 0) + newLine() + stringWriter.append('}') + } + + fun escapeString(string: String): String { + return JsonPrimitive(string).toString() + } + + fun escapeName(key: String): String = + if (key.matches(SIMPLE_NAME)) key else escapeString(key) + + fun writeName(key: String) { + stringWriter.append(escapeName(key)) + stringWriter.append(':') + if (!compact) stringWriter.append(' ') + } +} diff --git a/src/main/kotlin/util/LoadResource.kt b/src/main/kotlin/util/LoadResource.kt new file mode 100644 index 0000000..4bc8704 --- /dev/null +++ b/src/main/kotlin/util/LoadResource.kt @@ -0,0 +1,20 @@ + +package moe.nea.firmament.util + +import java.io.InputStream +import kotlin.io.path.inputStream +import kotlin.jvm.optionals.getOrNull +import net.minecraft.util.Identifier +import moe.nea.firmament.repo.RepoDownloadManager + + +fun Identifier.openFirmamentResource(): InputStream { + val resource = MC.resourceManager.getResource(this).getOrNull() + if (resource == null) { + if (namespace == "neurepo") + return RepoDownloadManager.repoSavedLocation.resolve(path).inputStream() + error("Could not read resource $this") + } + return resource.inputStream +} + diff --git a/src/main/kotlin/util/Locraw.kt b/src/main/kotlin/util/Locraw.kt new file mode 100644 index 0000000..9778bc7 --- /dev/null +++ b/src/main/kotlin/util/Locraw.kt @@ -0,0 +1,12 @@ + + +package moe.nea.firmament.util + +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient + +@Serializable +data class Locraw(val server: String, val gametype: String? = null, val mode: String? = null, val map: String? = null) { + @Transient + val skyblockLocation = if (gametype == "SKYBLOCK") mode?.let(SkyBlockIsland::forMode) else null +} diff --git a/src/main/kotlin/util/LogIfNull.kt b/src/main/kotlin/util/LogIfNull.kt new file mode 100644 index 0000000..600c5e6 --- /dev/null +++ b/src/main/kotlin/util/LogIfNull.kt @@ -0,0 +1,8 @@ + +package moe.nea.firmament.util + + +fun runNull(block: () -> Unit): Nothing? { + block() + return null +} diff --git a/src/main/kotlin/util/MC.kt b/src/main/kotlin/util/MC.kt new file mode 100644 index 0000000..e85b119 --- /dev/null +++ b/src/main/kotlin/util/MC.kt @@ -0,0 +1,152 @@ +package moe.nea.firmament.util + +import io.github.moulberry.repo.data.Coordinate +import io.github.notenoughupdates.moulconfig.gui.GuiComponentWrapper +import java.util.concurrent.ConcurrentLinkedQueue +import kotlin.jvm.optionals.getOrNull +import net.minecraft.client.MinecraftClient +import net.minecraft.client.gui.hud.InGameHud +import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.gui.screen.ingame.HandledScreen +import net.minecraft.client.network.ClientPlayerEntity +import net.minecraft.client.render.GameRenderer +import net.minecraft.client.render.WorldRenderer +import net.minecraft.client.render.item.ItemRenderer +import net.minecraft.client.world.ClientWorld +import net.minecraft.entity.Entity +import net.minecraft.item.Item +import net.minecraft.item.ItemStack +import net.minecraft.network.packet.c2s.play.CommandExecutionC2SPacket +import net.minecraft.registry.BuiltinRegistries +import net.minecraft.registry.Registry +import net.minecraft.registry.RegistryKey +import net.minecraft.registry.RegistryKeys +import net.minecraft.registry.RegistryWrapper +import net.minecraft.resource.ReloadableResourceManagerImpl +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import net.minecraft.util.Util +import net.minecraft.util.math.BlockPos +import net.minecraft.world.World +import moe.nea.firmament.events.TickEvent +import moe.nea.firmament.events.WorldReadyEvent + +object MC { + + private val messageQueue = ConcurrentLinkedQueue() + + init { + TickEvent.subscribe("MC:push") { + if (inGameHud.chatHud != null && world != null) + while (true) { + inGameHud.chatHud.addMessage(messageQueue.poll() ?: break) + } + while (true) { + (nextTickTodos.poll() ?: break).invoke() + } + } + WorldReadyEvent.subscribe("MC:ready") { + this.lastWorld + } + } + + fun sendChat(text: Text) { + if (instance.isOnThread && inGameHud.chatHud != null && world != null) + inGameHud.chatHud.addMessage(text) + else + messageQueue.add(text) + } + + @Deprecated("Use checked method instead", replaceWith = ReplaceWith("sendCommand(command)")) + fun sendServerCommand(command: String) { + val nh = player?.networkHandler ?: return + nh.sendPacket( + CommandExecutionC2SPacket( + command, + ) + ) + } + + fun sendServerChat(text: String) { + player?.networkHandler?.sendChatMessage(text) + } + + fun sendCommand(command: String) { + // TODO: add a queue to this and sendServerChat + ErrorUtil.softCheck("Server commands have an implied /", !command.startsWith("/")) + player?.networkHandler?.sendCommand(command) + } + + fun onMainThread(block: () -> Unit) { + if (instance.isOnThread) + block() + else + instance.send(block) + } + + private val nextTickTodos = ConcurrentLinkedQueue<() -> Unit>() + fun nextTick(function: () -> Unit) { + nextTickTodos.add(function) + } + + + inline val resourceManager get() = (instance.resourceManager as ReloadableResourceManagerImpl) + inline val itemRenderer: ItemRenderer get() = instance.itemRenderer + inline val worldRenderer: WorldRenderer get() = instance.worldRenderer + inline val gameRenderer: GameRenderer get() = instance.gameRenderer + inline val networkHandler get() = player?.networkHandler + inline val instance get() = MinecraftClient.getInstance() + inline val keyboard get() = instance.keyboard + inline val interactionManager get() = instance.interactionManager + inline val textureManager get() = instance.textureManager + inline val options get() = instance.options + inline val inGameHud: InGameHud get() = instance.inGameHud + inline val font get() = instance.textRenderer + inline val soundManager get() = instance.soundManager + inline val player: ClientPlayerEntity? get() = TestUtil.unlessTesting { instance.player } + inline val camera: Entity? get() = instance.cameraEntity + inline val stackInHand: ItemStack get() = player?.mainHandStack ?: ItemStack.EMPTY + inline val guiAtlasManager get() = instance.guiAtlasManager + inline val world: ClientWorld? get() = TestUtil.unlessTesting { instance.world } + inline val playerName: String? get() = player?.name?.unformattedString + inline var screen: Screen? + get() = TestUtil.unlessTesting { instance.currentScreen } + set(value) = instance.setScreen(value) + val screenName get() = screen?.title?.unformattedString?.trim() + inline val handledScreen: HandledScreen<*>? get() = instance.currentScreen as? HandledScreen<*> + inline val window get() = instance.window + inline val currentRegistries: RegistryWrapper.WrapperLookup? get() = world?.registryManager + val defaultRegistries: RegistryWrapper.WrapperLookup by lazy { BuiltinRegistries.createWrapperLookup() } + inline val currentOrDefaultRegistries get() = currentRegistries ?: defaultRegistries + val defaultItems: RegistryWrapper.Impl by lazy { defaultRegistries.getOrThrow(RegistryKeys.ITEM) } + var currentTick = 0 + var lastWorld: World? = null + get() { + field = world ?: field + return field + } + private set + + val currentMoulConfigContext + get() = (screen as? GuiComponentWrapper)?.context + + fun openUrl(uri: String) { + Util.getOperatingSystem().open(uri) + } + + fun unsafeGetRegistryEntry(registry: RegistryKey>, identifier: Identifier) = + unsafeGetRegistryEntry(RegistryKey.of(registry, identifier)) + + + fun unsafeGetRegistryEntry(registryKey: RegistryKey): T? { + return currentOrDefaultRegistries + .getOrThrow(registryKey.registryRef) + .getOptional(registryKey) + .getOrNull() + ?.value() + } +} + + +val Coordinate.blockPos: BlockPos + get() = BlockPos(x, y, z) diff --git a/src/main/kotlin/util/MinecraftDispatcher.kt b/src/main/kotlin/util/MinecraftDispatcher.kt new file mode 100644 index 0000000..d1f22a9 --- /dev/null +++ b/src/main/kotlin/util/MinecraftDispatcher.kt @@ -0,0 +1,8 @@ + + +package moe.nea.firmament.util + +import kotlinx.coroutines.asCoroutineDispatcher +import net.minecraft.client.MinecraftClient + +val MinecraftDispatcher by lazy { MinecraftClient.getInstance().asCoroutineDispatcher() } diff --git a/src/main/kotlin/util/MoulConfigFragment.kt b/src/main/kotlin/util/MoulConfigFragment.kt new file mode 100644 index 0000000..28ccfd0 --- /dev/null +++ b/src/main/kotlin/util/MoulConfigFragment.kt @@ -0,0 +1,44 @@ + + +package moe.nea.firmament.util + +import io.github.notenoughupdates.moulconfig.gui.GuiComponentWrapper +import io.github.notenoughupdates.moulconfig.gui.GuiContext +import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext +import me.shedaniel.math.Point +import net.minecraft.client.gui.DrawContext + +class MoulConfigFragment( + context: GuiContext, + val position: Point, + val dismiss: () -> Unit +) : GuiComponentWrapper(context) { + init { + this.init(MC.instance, MC.screen!!.width, MC.screen!!.height) + } + + override fun createContext(drawContext: DrawContext?): GuiImmediateContext { + val oldContext = super.createContext(drawContext) + return oldContext.translated( + position.x, + position.y, + context.root.width, + context.root.height, + ) + } + + + override fun render(drawContext: DrawContext?, i: Int, j: Int, f: Float) { + val ctx = createContext(drawContext) + val m = drawContext!!.matrices + m.push() + m.translate(position.x.toFloat(), position.y.toFloat(), 0F) + context.root.render(ctx) + m.pop() + ctx.renderContext.renderExtraLayers() + } + + override fun close() { + dismiss() + } +} diff --git a/src/main/kotlin/util/MoulConfigUtils.kt b/src/main/kotlin/util/MoulConfigUtils.kt new file mode 100644 index 0000000..51ff340 --- /dev/null +++ b/src/main/kotlin/util/MoulConfigUtils.kt @@ -0,0 +1,329 @@ +package moe.nea.firmament.util + +import io.github.notenoughupdates.moulconfig.common.IMinecraft +import io.github.notenoughupdates.moulconfig.common.MyResourceLocation +import io.github.notenoughupdates.moulconfig.gui.CloseEventListener +import io.github.notenoughupdates.moulconfig.gui.GuiComponent +import io.github.notenoughupdates.moulconfig.gui.GuiComponentWrapper +import io.github.notenoughupdates.moulconfig.gui.GuiContext +import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext +import io.github.notenoughupdates.moulconfig.gui.KeyboardEvent +import io.github.notenoughupdates.moulconfig.gui.MouseEvent +import io.github.notenoughupdates.moulconfig.observer.GetSetter +import io.github.notenoughupdates.moulconfig.platform.ModernRenderContext +import io.github.notenoughupdates.moulconfig.xml.ChildCount +import io.github.notenoughupdates.moulconfig.xml.XMLContext +import io.github.notenoughupdates.moulconfig.xml.XMLGuiLoader +import io.github.notenoughupdates.moulconfig.xml.XMLUniverse +import io.github.notenoughupdates.moulconfig.xml.XSDGenerator +import java.io.File +import java.util.function.Supplier +import javax.xml.namespace.QName +import me.shedaniel.math.Color +import org.w3c.dom.Element +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.util.InputUtil +import moe.nea.firmament.gui.BarComponent +import moe.nea.firmament.gui.FirmButtonComponent +import moe.nea.firmament.gui.FirmHoverComponent +import moe.nea.firmament.gui.FixedComponent +import moe.nea.firmament.gui.ImageComponent +import moe.nea.firmament.gui.TickComponent +import moe.nea.firmament.util.render.isUntranslatedGuiDrawContext + +object MoulConfigUtils { + @JvmStatic + fun main(args: Array) { + generateXSD(File("MoulConfig.xsd"), XMLUniverse.MOULCONFIG_XML_NS) + generateXSD(File("MoulConfig.Firmament.xsd"), firmUrl) + File("wrapper.xsd").writeText( + """ + + + + + + """.trimIndent() + ) + } + + val firmUrl = "http://firmament.nea.moe/moulconfig" + val universe = XMLUniverse.getDefaultUniverse().also { uni -> + uni.registerMapper(java.awt.Color::class.java) { + if (it.startsWith("#")) { + val hexString = it.substring(1) + val hex = hexString.toInt(16) + if (hexString.length == 6) { + return@registerMapper java.awt.Color(hex) + } + if (hexString.length == 8) { + return@registerMapper java.awt.Color(hex, true) + } + error("Hexcolor $it needs to be exactly 6 or 8 hex digits long") + } + return@registerMapper java.awt.Color(it.toInt(), true) + } + uni.registerMapper(Color::class.java) { + val color = uni.mapXMLObject(it, java.awt.Color::class.java) + Color.ofRGBA(color.red, color.green, color.blue, color.alpha) + } + uni.registerLoader(object : XMLGuiLoader.Basic { + override fun getName(): QName { + return QName(firmUrl, "Bar") + } + + override fun createInstance(context: XMLContext<*>, element: Element): BarComponent { + return BarComponent( + context.getPropertyFromAttribute(element, QName("progress"), Double::class.java)!!, + context.getPropertyFromAttribute(element, QName("total"), Double::class.java)!!, + context.getPropertyFromAttribute(element, QName("fillColor"), Color::class.java)!!.get(), + context.getPropertyFromAttribute(element, QName("emptyColor"), Color::class.java)!!.get(), + ) + } + + override fun getChildCount(): ChildCount { + return ChildCount.NONE + } + + override fun getAttributeNames(): Map { + return mapOf("progress" to true, "total" to true, "emptyColor" to true, "fillColor" to true) + } + }) + uni.registerLoader(object : XMLGuiLoader.Basic { + override fun createInstance(context: XMLContext<*>, element: Element): FirmHoverComponent { + return FirmHoverComponent( + context.getChildFragment(element), + context.getPropertyFromAttribute( + element, + QName("lines"), + List::class.java + ) as Supplier>, + context.getPropertyFromAttribute(element, QName("delay"), Duration::class.java, 0.6.seconds), + ) + } + + override fun getName(): QName { + return QName(firmUrl, "Hover") + } + + override fun getChildCount(): ChildCount { + return ChildCount.ONE + } + + override fun getAttributeNames(): Map { + return mapOf( + "lines" to true, + "delay" to false, + ) + } + + }) + uni.registerLoader(object : XMLGuiLoader.Basic { + override fun getName(): QName { + return QName(firmUrl, "Button") + } + + override fun createInstance(context: XMLContext<*>, element: Element): FirmButtonComponent { + return FirmButtonComponent( + context.getChildFragment(element), + context.getPropertyFromAttribute(element, QName("enabled"), Boolean::class.java) + ?: GetSetter.constant(true), + context.getPropertyFromAttribute(element, QName("noBackground"), Boolean::class.java, false), + context.getMethodFromAttribute(element, QName("onClick")), + ) + } + + override fun getChildCount(): ChildCount { + return ChildCount.ONE + } + + override fun getAttributeNames(): Map { + return mapOf("onClick" to true, "enabled" to false, "noBackground" to false) + } + }) + uni.registerLoader(object : XMLGuiLoader.Basic { + override fun createInstance(context: XMLContext<*>, element: Element): ImageComponent { + return ImageComponent( + context.getPropertyFromAttribute(element, QName("width"), Int::class.java)!!.get(), + context.getPropertyFromAttribute(element, QName("height"), Int::class.java)!!.get(), + context.getPropertyFromAttribute(element, QName("resource"), MyResourceLocation::class.java)!!, + context.getPropertyFromAttribute(element, QName("u1"), Float::class.java, 0f), + context.getPropertyFromAttribute(element, QName("u2"), Float::class.java, 1f), + context.getPropertyFromAttribute(element, QName("v1"), Float::class.java, 0f), + context.getPropertyFromAttribute(element, QName("v2"), Float::class.java, 1f), + ) + } + + override fun getName(): QName { + return QName(firmUrl, "Image") + } + + override fun getChildCount(): ChildCount { + return ChildCount.NONE + } + + override fun getAttributeNames(): Map { + return mapOf( + "width" to true, "height" to true, + "resource" to true, + "u1" to false, + "u2" to false, + "v1" to false, + "v2" to false, + ) + } + }) + uni.registerLoader(object : XMLGuiLoader.Basic { + override fun createInstance(context: XMLContext<*>, element: Element): TickComponent { + return TickComponent(context.getMethodFromAttribute(element, QName("tick"))) + } + + override fun getName(): QName { + return QName(firmUrl, "Tick") + } + + override fun getChildCount(): ChildCount { + return ChildCount.NONE + } + + override fun getAttributeNames(): Map { + return mapOf("tick" to true) + } + }) + uni.registerLoader(object : XMLGuiLoader.Basic { + override fun createInstance(context: XMLContext<*>, element: Element): FixedComponent { + return FixedComponent( + context.getPropertyFromAttribute(element, QName("width"), Int::class.java), + context.getPropertyFromAttribute(element, QName("height"), Int::class.java), + context.getChildFragment(element) + ) + } + + override fun getName(): QName { + return QName(firmUrl, "Fixed") + } + + override fun getChildCount(): ChildCount { + return ChildCount.ONE + } + + override fun getAttributeNames(): Map { + return mapOf("width" to false, "height" to false) + } + }) + } + + fun generateXSD( + file: File, + namespace: String + ) { + val generator = XSDGenerator(universe, namespace) + generator.writeAll() + generator.dumpToFile(file) + } + + fun wrapScreen(guiContext: GuiContext, parent: Screen?, onClose: () -> Unit = {}): Screen { + return object : GuiComponentWrapper(guiContext) { + override fun close() { + if (context.onBeforeClose() == CloseEventListener.CloseAction.NO_OBJECTIONS_TO_CLOSE) { + client!!.setScreen(parent) + onClose() + } + } + } + } + + fun loadScreen(name: String, bindTo: Any, parent: Screen?): Screen { + return wrapScreen(loadGui(name, bindTo), parent) + } + + // TODO: move this utility into moulconfig (also rework guicontext into an interface so i can make this mesh better into vanilla) + fun GuiContext.adopt(element: GuiComponent) = element.foldRecursive(Unit, { comp, unit -> comp.context = this }) + + inline fun GetSetter.xmap(crossinline fromT: (T) -> R, crossinline toT: (R) -> T): GetSetter { + val outer = this + return object : GetSetter { + override fun get(): R { + return fromT(outer.get()) + } + + override fun set(newValue: R) { + outer.set(toT(newValue)) + } + } + } + + fun typeMCComponentInPlace( + component: GuiComponent, + x: Int, + y: Int, + w: Int, + h: Int, + keyboardEvent: KeyboardEvent + ): Boolean { + val immContext = createInPlaceFullContext(null, IMinecraft.instance.mouseX, IMinecraft.instance.mouseY) + if (component.keyboardEvent(keyboardEvent, immContext.translated(x, y, w, h))) + return true + if (component.context.getFocusedElement() != null) { + if (keyboardEvent is KeyboardEvent.KeyPressed + && keyboardEvent.pressed && keyboardEvent.keycode == InputUtil.GLFW_KEY_ESCAPE + ) { + component.context.setFocusedElement(null) + } + return true + } + return false + } + + fun clickMCComponentInPlace( + component: GuiComponent, + x: Int, + y: Int, + w: Int, + h: Int, + mouseX: Int, mouseY: Int, + mouseEvent: MouseEvent + ): Boolean { + val immContext = createInPlaceFullContext(null, mouseX, mouseY) + return component.mouseEvent(mouseEvent, immContext.translated(x, y, w, h)) + } + + fun createInPlaceFullContext(drawContext: DrawContext?, mouseX: Int, mouseY: Int): GuiImmediateContext { + assert(drawContext?.isUntranslatedGuiDrawContext() != false) + val context = drawContext?.let(::ModernRenderContext) + ?: IMinecraft.instance.provideTopLevelRenderContext() + val immContext = GuiImmediateContext( + context, + 0, 0, 0, 0, + mouseX, mouseY, + mouseX, mouseY, + mouseX.toFloat(), + mouseY.toFloat() + ) + return immContext + } + + fun DrawContext.drawMCComponentInPlace( + component: GuiComponent, + x: Int, + y: Int, + w: Int, + h: Int, + mouseX: Int, + mouseY: Int + ) { + val immContext = createInPlaceFullContext(this, mouseX, mouseY) + matrices.push() + matrices.translate(x.toFloat(), y.toFloat(), 0F) + component.render(immContext.translated(x, y, w, h)) + matrices.pop() + } + + + fun loadGui(name: String, bindTo: Any): GuiContext { + return GuiContext(universe.load(bindTo, MyResourceLocation("firmament", "gui/$name.xml"))) + } +} diff --git a/src/main/kotlin/util/Optionalutil.kt b/src/main/kotlin/util/Optionalutil.kt new file mode 100644 index 0000000..1cef2fe --- /dev/null +++ b/src/main/kotlin/util/Optionalutil.kt @@ -0,0 +1,5 @@ +package moe.nea.firmament.util + +import java.util.Optional + +fun T?.intoOptional(): Optional = Optional.ofNullable(this) diff --git a/src/main/kotlin/util/SBData.kt b/src/main/kotlin/util/SBData.kt new file mode 100644 index 0000000..1a4734c --- /dev/null +++ b/src/main/kotlin/util/SBData.kt @@ -0,0 +1,89 @@ +package moe.nea.firmament.util + +import java.time.ZoneId +import java.util.UUID +import net.hypixel.modapi.HypixelModAPI +import net.hypixel.modapi.packet.impl.clientbound.event.ClientboundLocationPacket +import kotlin.jvm.optionals.getOrNull +import kotlin.time.Duration.Companion.seconds +import moe.nea.firmament.events.AllowChatEvent +import moe.nea.firmament.events.ProcessChatEvent +import moe.nea.firmament.events.ProfileSwitchEvent +import moe.nea.firmament.events.ServerConnectedEvent +import moe.nea.firmament.events.SkyblockServerUpdateEvent + +object SBData { + private val profileRegex = "Profile ID: ([a-z0-9\\-]+)".toRegex() + val profileSuggestTexts = listOf( + "CLICK THIS TO SUGGEST IT IN CHAT [DASHES]", + "CLICK THIS TO SUGGEST IT IN CHAT [NO DASHES]", + ) + var profileId: UUID? = null + get() { + // TODO: allow unfiltered access to this somehow + if (!isOnSkyblock) return null + return field + } + + /** + * Source: https://hypixel-skyblock.fandom.com/wiki/Time_Systems + */ + val hypixelTimeZone = ZoneId.of("US/Eastern") + private var hasReceivedProfile = false + var locraw: Locraw? = null + + /** + * The current server location the player is in. This will be null outside of SkyBlock. + */ + val skyblockLocation: SkyBlockIsland? get() = locraw?.skyblockLocation + val hasValidLocraw get() = locraw?.server !in listOf("limbo", null) + val isOnSkyblock get() = locraw?.gametype == "SKYBLOCK" + var profileIdCommandDebounce = TimeMark.farPast() + fun init() { + ServerConnectedEvent.subscribe("SBData:onServerConnected") { + HypixelModAPI.getInstance().subscribeToEventPacket(ClientboundLocationPacket::class.java) + } + HypixelModAPI.getInstance().createHandler(ClientboundLocationPacket::class.java) { + MC.onMainThread { + val lastLocraw = locraw + val oldProfileId = profileId + locraw = Locraw(it.serverName, + it.serverType.getOrNull()?.name?.uppercase(), + it.mode.getOrNull(), + it.map.getOrNull()) + SkyblockServerUpdateEvent.publish(SkyblockServerUpdateEvent(lastLocraw, locraw)) + if(oldProfileId != profileId) { + ProfileSwitchEvent.publish(ProfileSwitchEvent(oldProfileId, profileId)) + } + profileIdCommandDebounce = TimeMark.now() + } + } + SkyblockServerUpdateEvent.subscribe("SBData:sendProfileId") { + if (!hasReceivedProfile && isOnSkyblock && profileIdCommandDebounce.passedTime() > 10.seconds) { + profileIdCommandDebounce = TimeMark.now() + MC.sendServerCommand("profileid") + } + } + AllowChatEvent.subscribe("SBData:hideProfileSuggest") { event -> + if (event.unformattedString in profileSuggestTexts && profileIdCommandDebounce.passedTime() < 5.seconds) { + event.cancel() + } + } + ProcessChatEvent.subscribe(receivesCancelled = true, "SBData:loadProfile") { event -> + val profileMatch = profileRegex.matchEntire(event.unformattedString) + if (profileMatch != null) { + val oldProfile = profileId + try { + profileId = UUID.fromString(profileMatch.groupValues[1]) + hasReceivedProfile = true + } catch (e: IllegalArgumentException) { + profileId = null + e.printStackTrace() + } + if (oldProfile != profileId) { + ProfileSwitchEvent.publish(ProfileSwitchEvent(oldProfile, profileId)) + } + } + } + } +} diff --git a/src/main/kotlin/util/ScoreboardUtil.kt b/src/main/kotlin/util/ScoreboardUtil.kt new file mode 100644 index 0000000..0970892 --- /dev/null +++ b/src/main/kotlin/util/ScoreboardUtil.kt @@ -0,0 +1,55 @@ +package moe.nea.firmament.util + +import java.util.Optional +import net.minecraft.client.gui.hud.InGameHud +import net.minecraft.scoreboard.ScoreboardDisplaySlot +import net.minecraft.scoreboard.Team +import net.minecraft.text.StringVisitable +import net.minecraft.text.Style +import net.minecraft.text.Text +import net.minecraft.util.Formatting +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.TickEvent + +object ScoreboardUtil { + var scoreboardLines: List = listOf() + var simplifiedScoreboardLines: List = listOf() + + @Subscribe + fun onTick(event: TickEvent) { + scoreboardLines = getScoreboardLinesUncached() + simplifiedScoreboardLines = scoreboardLines.map { it.unformattedString } + } + + private fun getScoreboardLinesUncached(): List { + val scoreboard = MC.player?.scoreboard ?: return listOf() + val activeObjective = scoreboard.getObjectiveForSlot(ScoreboardDisplaySlot.SIDEBAR) ?: return listOf() + return scoreboard.getScoreboardEntries(activeObjective) + .filter { !it.hidden() } + .sortedWith(InGameHud.SCOREBOARD_ENTRY_COMPARATOR) + .take(15).map { + val team = scoreboard.getScoreHolderTeam(it.owner) + val text = it.name() + Team.decorateName(team, text) + } + } +} + +fun Text.formattedString(): String { + val sb = StringBuilder() + visit(StringVisitable.StyledVisitor { style, string -> + val c = Formatting.byName(style.color?.name) + if (c != null) { + sb.append("§${c.code}") + } + if (style.isUnderlined) { + sb.append("§n") + } + if (style.isBold) { + sb.append("§l") + } + sb.append(string) + Optional.empty() + }, Style.EMPTY) + return sb.toString().replace("§[^a-f0-9]".toRegex(), "") +} diff --git a/src/main/kotlin/util/ScreenUtil.kt b/src/main/kotlin/util/ScreenUtil.kt new file mode 100644 index 0000000..99d77fb --- /dev/null +++ b/src/main/kotlin/util/ScreenUtil.kt @@ -0,0 +1,38 @@ + + +package moe.nea.firmament.util + +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents +import net.minecraft.client.MinecraftClient +import net.minecraft.client.gui.screen.Screen +import moe.nea.firmament.Firmament + +object ScreenUtil { + init { + ClientTickEvents.START_CLIENT_TICK.register(::onTick) + } + + private fun onTick(minecraft: MinecraftClient) { + if (nextOpenedGui != null) { + val p = minecraft.player + if (p?.currentScreenHandler != null) { + p.closeHandledScreen() + } + minecraft.setScreen(nextOpenedGui) + nextOpenedGui = null + } + } + + private var nextOpenedGui: Screen? = null + + fun setScreenLater(nextScreen: Screen?) { + val nog = nextOpenedGui + if (nog != null) { + Firmament.logger.warn("Setting screen ${if (nextScreen == null) "null" else nextScreen::class.qualifiedName} to be opened later, but ${nog::class.qualifiedName} is already queued.") + return + } + nextOpenedGui = nextScreen + } + + +} diff --git a/src/main/kotlin/util/SequenceUtil.kt b/src/main/kotlin/util/SequenceUtil.kt new file mode 100644 index 0000000..7b5bad0 --- /dev/null +++ b/src/main/kotlin/util/SequenceUtil.kt @@ -0,0 +1,11 @@ + + +package moe.nea.firmament.util + +fun T.iterate(iterator: (T) -> T?): Sequence = sequence { + var x: T? = this@iterate + while (x != null) { + yield(x) + x = iterator(x) + } +} diff --git a/src/main/kotlin/util/SkyBlockIsland.kt b/src/main/kotlin/util/SkyBlockIsland.kt new file mode 100644 index 0000000..e7f955a --- /dev/null +++ b/src/main/kotlin/util/SkyBlockIsland.kt @@ -0,0 +1,52 @@ +package moe.nea.firmament.util + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import moe.nea.firmament.repo.RepoManager + +@Serializable(with = SkyBlockIsland.Serializer::class) +class SkyBlockIsland +private constructor( + val locrawMode: String, +) { + + object Serializer : KSerializer { + override val descriptor: SerialDescriptor + get() = PrimitiveSerialDescriptor("SkyBlockIsland", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): SkyBlockIsland { + return forMode(decoder.decodeString()) + } + + override fun serialize(encoder: Encoder, value: SkyBlockIsland) { + encoder.encodeString(value.locrawMode) + } + } + + companion object { + private val allIslands = mutableMapOf() + fun forMode(mode: String): SkyBlockIsland = allIslands.computeIfAbsent(mode, ::SkyBlockIsland) + val HUB = forMode("hub") + val DWARVEN_MINES = forMode("dwarven_mines") + val CRYSTAL_HOLLOWS = forMode("crystal_hollows") + val CRIMSON_ISLE = forMode("crimson_isle") + val PRIVATE_ISLAND = forMode("dynamic") + val RIFT = forMode("rift") + val MINESHAFT = forMode("mineshaft") + val GARDEN = forMode("garden") + val DUNGEON = forMode("dungeon") + val NIL = forMode("_") + } + + val hasCustomMining + get() = RepoManager.miningData.customMiningAreas[this]?.isSpecialMining ?: false + + val userFriendlyName + get() = RepoManager.neuRepo.constants.islands.areaNames + .getOrDefault(locrawMode, locrawMode) +} diff --git a/src/main/kotlin/util/SkyblockId.kt b/src/main/kotlin/util/SkyblockId.kt new file mode 100644 index 0000000..051ca86 --- /dev/null +++ b/src/main/kotlin/util/SkyblockId.kt @@ -0,0 +1,285 @@ +@file:UseSerializers(DashlessUUIDSerializer::class) + +package moe.nea.firmament.util + +import com.mojang.serialization.Codec +import io.github.moulberry.repo.data.NEUIngredient +import io.github.moulberry.repo.data.NEUItem +import io.github.moulberry.repo.data.Rarity +import java.time.Instant +import java.time.LocalDateTime +import java.time.format.DateTimeFormatterBuilder +import java.time.format.SignStyle +import java.time.temporal.ChronoField +import java.util.Optional +import java.util.UUID +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers +import kotlinx.serialization.json.Json +import kotlin.jvm.optionals.getOrNull +import net.minecraft.component.DataComponentTypes +import net.minecraft.component.type.NbtComponent +import net.minecraft.item.ItemStack +import net.minecraft.item.Items +import net.minecraft.nbt.NbtCompound +import net.minecraft.network.RegistryByteBuf +import net.minecraft.network.codec.PacketCodec +import net.minecraft.network.codec.PacketCodecs +import net.minecraft.util.Identifier +import moe.nea.firmament.repo.ExpLadders +import moe.nea.firmament.repo.ExpensiveItemCacheApi +import moe.nea.firmament.repo.ItemCache.asItemStack +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.repo.set +import moe.nea.firmament.util.collections.WeakCache +import moe.nea.firmament.util.json.DashlessUUIDSerializer + +/** + * A SkyBlock item id, as used by the NEU repo. + * This is not exactly the format used by Hypixel, but is mostly the same. + * Usually this id splits an id used by Hypixel into more sub items. For example `PET` becomes `$PET_ID;$PET_RARITY`, + * with those values extracted from other metadata. + */ +@JvmInline +@Serializable +value class SkyblockId(val neuItem: String) : Comparable { + val identifier + get() = Identifier.of( + "skyblockitem", + neuItem.lowercase().replace(";", "__") + .replace(":", "___") + .replace(illlegalPathRegex) { + it.value.toCharArray() + .joinToString("") { "__" + it.code.toString(16).padStart(4, '0') } + }) + + override fun toString(): String { + return neuItem + } + + override fun compareTo(other: SkyblockId): Int { + return neuItem.compareTo(other.neuItem) + } + + /** + * A bazaar stock item id, as returned by the Hypixel bazaar api endpoint. + * These are not equivalent to the in-game ids, or the NEU repo ids, and in fact, do not refer to items, but instead + * to bazaar stocks. The main difference from [SkyblockId]s is concerning enchanted books. There are probably more, + * but for now this holds. + */ + @JvmInline + @Serializable + value class BazaarStock(val bazaarId: String) { + companion object { + fun fromSkyBlockId(skyblockId: SkyblockId): BazaarStock { + return BazaarStock(RepoManager.neuRepo.constants.bazaarStocks.getBazaarStockOrDefault(skyblockId.neuItem)) + } + } + } + + companion object { + val COINS: SkyblockId = SkyblockId(NEUIngredient.NEU_SENTINEL_COINS) + val SENTINEL_EMPTY: SkyblockId = SkyblockId(NEUIngredient.NEU_SENTINEL_EMPTY) + private val bazaarEnchantmentRegex = "ENCHANTMENT_(\\D*)_(\\d+)".toRegex() + val NULL: SkyblockId = SkyblockId("null") + val PET_NULL: SkyblockId = SkyblockId("null_pet") + private val illlegalPathRegex = "[^a-z0-9_.-/]".toRegex() + val CODEC = Codec.STRING.xmap({ SkyblockId(it) }, { it.neuItem }) + val PACKET_CODEC: PacketCodec = + PacketCodecs.STRING.xmap({ SkyblockId(it) }, { it.neuItem }) + } +} + +val NEUItem.skyblockId get() = SkyblockId(skyblockItemId) +val NEUIngredient.skyblockId get() = SkyblockId(itemId) +val SkyblockId.asBazaarStock get() = SkyblockId.BazaarStock.fromSkyBlockId(this) + +@ExpensiveItemCacheApi +fun NEUItem.guessRecipeId(): String? { + if (!skyblockItemId.contains(";")) return skyblockItemId + val item = this.asItemStack() + val (id, extraId) = skyblockItemId.split(";") + if (item.item == Items.ENCHANTED_BOOK) { + return "ENCHANTED_BOOK_${id}_${extraId}" + } + if (item.petData != null) return id + return null +} + +@Serializable +data class HypixelPetInfo( + val type: String, + val tier: Rarity, + val exp: Double = 0.0, + val candyUsed: Int = 0, + val uuid: UUID? = null, + val active: Boolean? = false, + val heldItem: String? = null, +) { + val skyblockId get() = SkyblockId("${type.uppercase()};${tier.ordinal}") // TODO: is this ordinal set up correctly? + val level get() = ExpLadders.getExpLadder(type, tier).getPetLevel(exp) +} + +private val jsonparser = Json { ignoreUnknownKeys = true } + +var ItemStack.extraAttributes: NbtCompound + set(value) { + set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(value)) + } + get() { + val customData = get(DataComponentTypes.CUSTOM_DATA) ?: run { + val component = NbtComponent.of(NbtCompound()) + set(DataComponentTypes.CUSTOM_DATA, component) + component + } + return customData.nbt + } + +fun ItemStack.modifyExtraAttributes(block: (NbtCompound) -> Unit) { + val baseNbt = get(DataComponentTypes.CUSTOM_DATA)?.copyNbt() ?: NbtCompound() + block(baseNbt) + set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(baseNbt)) +} + +val ItemStack.skyblockUUIDString: String? + get() = extraAttributes.getString("uuid").getOrNull()?.takeIf { it.isNotBlank() } + +private val timestampFormat = //"10/11/21 3:39 PM" + DateTimeFormatterBuilder().apply { + appendValue(ChronoField.MONTH_OF_YEAR, 2) + appendLiteral("/") + appendValue(ChronoField.DAY_OF_MONTH, 2) + appendLiteral("/") + appendValueReduced(ChronoField.YEAR, 2, 2, 1950) + appendLiteral(" ") + appendValue(ChronoField.HOUR_OF_AMPM, 1, 2, SignStyle.NEVER) + appendLiteral(":") + appendValue(ChronoField.MINUTE_OF_HOUR, 2) + appendLiteral(" ") + appendText(ChronoField.AMPM_OF_DAY) + }.toFormatter() +val ItemStack.timestamp + get() = + extraAttributes.getLong("timestamp").getOrNull()?.let { Instant.ofEpochMilli(it) } + ?: extraAttributes.getString("timestamp").getOrNull()?.let { + ErrorUtil.catch("Could not parse timestamp $it") { + LocalDateTime.from(timestampFormat.parse(it)).atZone(SBData.hypixelTimeZone) + .toInstant() + }.orNull() + } + +val ItemStack.skyblockUUID: UUID? + get() = skyblockUUIDString?.let { UUID.fromString(it) } + +private val petDataCache = WeakCache.memoize>("PetInfo") { + val jsonString = it.extraAttributes.getString("petInfo") + .getOrNull() + if (jsonString.isNullOrBlank()) return@memoize Optional.empty() + ErrorUtil.catch("Could not decode hypixel pet info") { + jsonparser.decodeFromString(jsonString) + } + .or { null }.intoOptional() +} + +fun ItemStack.getUpgradeStars(): Int { + return extraAttributes.getInt("upgrade_level").getOrNull()?.takeIf { it > 0 } + ?: extraAttributes.getInt("dungeon_item_level").getOrNull()?.takeIf { it > 0 } + ?: 0 +} + +@Serializable +@JvmInline +value class ReforgeId(val id: String) + +fun ItemStack.getReforgeId(): ReforgeId? { + return extraAttributes.getString("modifier").getOrNull()?.takeIf { it.isNotBlank() }?.let(::ReforgeId) +} + +val ItemStack.petData: HypixelPetInfo? + get() = petDataCache(this).getOrNull() + +fun ItemStack.setSkyBlockFirmamentUiId(uiId: String) = setSkyBlockId(SkyblockId("FIRMAMENT_UI_$uiId")) +fun ItemStack.setSkyBlockId(skyblockId: SkyblockId): ItemStack { + this.extraAttributes["id"] = skyblockId.neuItem + return this +} + +val ItemStack.skyBlockId: SkyblockId? + get() { + return when (val id = extraAttributes.getString("id").getOrNull()) { + "", null -> { + null + } + + "PET" -> { + petData?.skyblockId ?: SkyblockId.PET_NULL + } + + "RUNE", "UNIQUE_RUNE" -> { + val runeData = extraAttributes.getCompound("runes") + .getOrNull() + val runeKind = runeData?.keys?.singleOrNull() + if (runeKind == null) SkyblockId("RUNE") + else SkyblockId("${runeKind.uppercase()}_RUNE;${runeData.getInt(runeKind).getOrNull()}") + } + + "ABICASE" -> { + SkyblockId("ABICASE_${extraAttributes.getString("model").getOrNull()?.uppercase()}") + } + + "ENCHANTED_BOOK" -> { + val enchantmentData = extraAttributes.getCompound("enchantments") + .getOrNull() + val enchantName = enchantmentData?.keys?.singleOrNull() + if (enchantName == null) SkyblockId("ENCHANTED_BOOK") + else SkyblockId("${enchantName.uppercase()};${enchantmentData.getInt(enchantName).getOrNull()}") + } + + "ATTRIBUTE_SHARD" -> { + val attributeData = extraAttributes.getCompound("attributes").getOrNull() + val attributeName = attributeData?.keys?.singleOrNull() + if (attributeName == null) SkyblockId("ATTRIBUTE_SHARD") + else SkyblockId( + "ATTRIBUTE_SHARD_${attributeName.uppercase()};${ + attributeData.getInt(attributeName).getOrNull() + }" + ) + } + + "POTION" -> { + val potionData = extraAttributes.getString("potion").getOrNull() + val potionName = extraAttributes.getString("potion_name").getOrNull() + val potionLevel = extraAttributes.getInt("potion_level").getOrNull() + val potionType = extraAttributes.getString("potion_type").getOrNull() + fun String.potionNormalize() = uppercase().replace(" ", "_") + when { + potionName != null -> SkyblockId("POTION_${potionName.potionNormalize()};$potionLevel") + potionData != null -> SkyblockId("POTION_${potionData.potionNormalize()};$potionLevel") + potionType != null -> SkyblockId("POTION_${potionType.potionNormalize()}") + else -> SkyblockId("WATER_BOTTLE") + } + } + + "PARTY_HAT_SLOTH", "PARTY_HAT_CRAB", "PARTY_HAT_CRAB_ANIMATED" -> { + val partyHatEmoji = extraAttributes.getString("party_hat_emoji").getOrNull() + val partyHatYear = extraAttributes.getInt("party_hat_year").getOrNull() + val partyHatColor = extraAttributes.getString("party_hat_color").getOrNull() + when { + partyHatEmoji != null -> SkyblockId("PARTY_HAT_SLOTH_${partyHatEmoji.uppercase()}") + partyHatYear == 2022 -> SkyblockId("PARTY_HAT_CRAB_${partyHatColor?.uppercase()}_ANIMATED") + else -> SkyblockId("PARTY_HAT_CRAB_${partyHatColor?.uppercase()}") + } + } + + "BALLOON_HAT_2024", "BALLOON_HAT_2025" -> { + val partyHatYear = extraAttributes.getInt("party_hat_year").getOrNull() + val partyHatColor = extraAttributes.getString("party_hat_color").getOrNull() + SkyblockId("BALLOON_HAT_${partyHatYear}_${partyHatColor?.uppercase()}") + } + + else -> { + SkyblockId(id.replace(":", "-")) + } + } + } + diff --git a/src/main/kotlin/util/SortedMapSerializer.kt b/src/main/kotlin/util/SortedMapSerializer.kt new file mode 100644 index 0000000..baa10ad --- /dev/null +++ b/src/main/kotlin/util/SortedMapSerializer.kt @@ -0,0 +1,25 @@ + + +package moe.nea.firmament.util + +import java.util.SortedMap +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +class SortedMapSerializer, V>(val keyDelegate: KSerializer, val valueDelegate: KSerializer) : + KSerializer> { + val mapSerializer = MapSerializer(keyDelegate, valueDelegate) + override val descriptor: SerialDescriptor + get() = mapSerializer.descriptor + + override fun deserialize(decoder: Decoder): SortedMap { + return (mapSerializer.deserialize(decoder).toSortedMap(Comparator.naturalOrder())) + } + + override fun serialize(encoder: Encoder, value: SortedMap) { + mapSerializer.serialize(encoder, value) + } +} diff --git a/src/main/kotlin/util/StringUtil.kt b/src/main/kotlin/util/StringUtil.kt new file mode 100644 index 0000000..50c5367 --- /dev/null +++ b/src/main/kotlin/util/StringUtil.kt @@ -0,0 +1,30 @@ +package moe.nea.firmament.util + +object StringUtil { + fun String.words(): Sequence { + return splitToSequence(" ") // TODO: better boundaries + } + + fun String.camelWords(): Sequence { + return splitToSequence(camelWordStart) + } + + private val camelWordStart = Regex("((?<=[a-z])(?=[A-Z]))| ") + + fun parseIntWithComma(string: String): Int { + return string.replace(",", "").toInt() + } + + fun String.title() = replaceFirstChar { it.titlecase() } + + fun Iterable.unwords() = joinToString(" ") + fun nextLexicographicStringOfSameLength(string: String): String { + val next = StringBuilder(string) + while (next.lastOrNull() == Character.MAX_VALUE) next.setLength(next.length - 1) + if (next.isEmpty()) return "" // There is no upper bound. Fall back to the empty string + val lastIdx = next.indices.last + next[lastIdx] = (next[lastIdx] + 1) + return next.toString() + } + +} diff --git a/src/main/kotlin/util/TemplateUtil.kt b/src/main/kotlin/util/TemplateUtil.kt new file mode 100644 index 0000000..f4ff37c --- /dev/null +++ b/src/main/kotlin/util/TemplateUtil.kt @@ -0,0 +1,86 @@ + + +package moe.nea.firmament.util + +import java.util.* +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.json.Json +import kotlinx.serialization.serializer +import moe.nea.firmament.Firmament + +object TemplateUtil { + + @JvmStatic + fun getTemplatePrefix(data: String): String? { + val decoded = maybeFromBase64Encoded(data) ?: return null + return decoded.replaceAfter("/", "", "").ifBlank { null } + } + + @JvmStatic + fun intoBase64Encoded(raw: String): String { + return Base64.getEncoder().encodeToString(raw.encodeToByteArray()) + } + + private val base64Alphabet = charArrayOf( + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', '=' + ) + + @JvmStatic + fun maybeFromBase64Encoded(raw: String): String? { + val raw = raw.trim() + if (raw.any { it !in base64Alphabet }) { + return null + } + return try { + Base64.getDecoder().decode(raw).decodeToString() + } catch (ex: Exception) { + null + } + } + + + /** + * Returns a base64 encoded string, truncated such that for all `x`, `x.startsWith(prefix)` implies + * `base64Encoded(x).startsWith(getPrefixComparisonSafeBase64Encoding(prefix))` + * (however, the inverse may not always be true). + */ + @JvmStatic + fun getPrefixComparisonSafeBase64Encoding(prefix: String): String { + val rawEncoded = + Base64.getEncoder().encodeToString(prefix.encodeToByteArray()) + .replace("=", "") + return rawEncoded.substring(0, rawEncoded.length - rawEncoded.length % 4) + } + + inline fun encodeTemplate(sharePrefix: String, data: T): String = + encodeTemplate(sharePrefix, data, serializer()) + + fun encodeTemplate(sharePrefix: String, data: T, serializer: SerializationStrategy): String { + require(sharePrefix.endsWith("/")) + return intoBase64Encoded(sharePrefix + Firmament.tightJson.encodeToString(serializer, data)) + } + + inline fun maybeDecodeTemplate(sharePrefix: String, data: String): T? = + maybeDecodeTemplate(sharePrefix, data, serializer()) + + fun maybeDecodeTemplate(sharePrefix: String, data: String, serializer: DeserializationStrategy): T? { + require(sharePrefix.endsWith("/")) + val data = data.trim() + if (!data.startsWith(getPrefixComparisonSafeBase64Encoding(sharePrefix))) + return null + val decoded = maybeFromBase64Encoded(data) ?: return null + if (!decoded.startsWith(sharePrefix)) + return null + return try { + Firmament.json.decodeFromString(serializer, decoded.substring(sharePrefix.length)) + } catch (e: Exception) { + null + } + } + +} diff --git a/src/main/kotlin/util/TestUtil.kt b/src/main/kotlin/util/TestUtil.kt new file mode 100644 index 0000000..da8ba38 --- /dev/null +++ b/src/main/kotlin/util/TestUtil.kt @@ -0,0 +1,10 @@ +package moe.nea.firmament.util + +object TestUtil { + inline fun unlessTesting(block: () -> T): T? = if (isInTest) null else block() + @JvmField + val isInTest = + Thread.currentThread().stackTrace.any { + it.className.startsWith("org.junit.") || it.className.startsWith("io.kotest.") + } +} diff --git a/src/main/kotlin/util/TimeMark.kt b/src/main/kotlin/util/TimeMark.kt new file mode 100644 index 0000000..4a076ac --- /dev/null +++ b/src/main/kotlin/util/TimeMark.kt @@ -0,0 +1,52 @@ +package moe.nea.firmament.util + +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds + +class TimeMark private constructor(private val timeMark: Long) : Comparable { + fun passedTime() = + if (timeMark == 0L) Duration.INFINITE + else (System.currentTimeMillis() - timeMark).milliseconds + + fun passedAt(fakeNow: TimeMark) = + if (timeMark == 0L) Duration.INFINITE + else (fakeNow.timeMark - timeMark).milliseconds + + operator fun minus(other: TimeMark): Duration { + if (other.timeMark == timeMark) + return 0.milliseconds + if (other.timeMark == 0L) + return Duration.INFINITE + if (timeMark == 0L) + return -Duration.INFINITE + return (timeMark - other.timeMark).milliseconds + } + + companion object { + fun now() = TimeMark(System.currentTimeMillis()) + fun farPast() = TimeMark(0L) + fun ago(timeDelta: Duration): TimeMark { + if (timeDelta.isFinite()) { + return TimeMark(System.currentTimeMillis() - timeDelta.inWholeMilliseconds) + } + require(timeDelta.isPositive()) + return farPast() + } + } + + override fun hashCode(): Int { + return timeMark.hashCode() + } + + override fun equals(other: Any?): Boolean { + return other is TimeMark && other.timeMark == timeMark + } + + override fun toString(): String { + return "https://time.is/$timeMark" + } + + override fun compareTo(other: TimeMark): Int { + return this.timeMark.compareTo(other.timeMark) + } +} diff --git a/src/main/kotlin/util/Timer.kt b/src/main/kotlin/util/Timer.kt new file mode 100644 index 0000000..6e9b467 --- /dev/null +++ b/src/main/kotlin/util/Timer.kt @@ -0,0 +1,25 @@ + + +package moe.nea.firmament.util + +import kotlin.time.Duration +import kotlin.time.ExperimentalTime +import kotlin.time.TimeSource + +@OptIn(ExperimentalTime::class) +class Timer { + private var mark: TimeSource.Monotonic.ValueTimeMark? = null + + fun timePassed(): Duration { + return mark?.elapsedNow() ?: Duration.INFINITE + } + + fun markNow() { + mark = TimeSource.Monotonic.markNow() + } + + fun markFarPast() { + mark = null + } + +} diff --git a/src/main/kotlin/util/WarpUtil.kt b/src/main/kotlin/util/WarpUtil.kt new file mode 100644 index 0000000..f733af7 --- /dev/null +++ b/src/main/kotlin/util/WarpUtil.kt @@ -0,0 +1,96 @@ +package moe.nea.firmament.util + +import io.github.moulberry.repo.constants.Islands +import io.github.moulberry.repo.constants.Islands.Warp +import kotlinx.serialization.Serializable +import kotlinx.serialization.serializer +import kotlin.math.sqrt +import kotlin.time.Duration.Companion.seconds +import net.minecraft.text.Text +import net.minecraft.util.math.Position +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.events.ProcessChatEvent +import moe.nea.firmament.repo.RepoManager +import moe.nea.firmament.util.data.ProfileSpecificDataHolder + +object WarpUtil { + val warps: Sequence get() = RepoManager.neuRepo.constants.islands.warps + .asSequence() + .filter { it.warp !in ignoredWarps } + + val ignoredWarps = setOf("carnival", "") + + @Serializable + data class Data( + val excludedWarps: MutableSet = mutableSetOf(), + ) + + object DConfig : ProfileSpecificDataHolder(serializer(), "warp-util", ::Data) + + private var lastAttemptedWarp = "" + private var lastWarpAttempt = TimeMark.farPast() + fun findNearestWarp(island: SkyBlockIsland, pos: Position): Islands.Warp? { + return warps.asSequence().filter { it.mode == island.locrawMode }.minByOrNull { + if (DConfig.data?.excludedWarps?.contains(it.warp) == true) { + return@minByOrNull Double.MAX_VALUE + } else { + return@minByOrNull squaredDist(pos, it) + } + } + } + + private fun squaredDist(pos: Position, warp: Warp): Double { + val dx = pos.x - warp.x + val dy = pos.y - warp.y + val dz = pos.z - warp.z + return dx * dx + dy * dy + dz * dz + } + + fun teleportToNearestWarp(island: SkyBlockIsland, pos: Position) { + val nearestWarp = findNearestWarp(island, pos) + if (nearestWarp == null) { + MC.sendChat(Text.translatable("firmament.warp-util.no-warp-found", island.userFriendlyName)) + return + } + if (island == SBData.skyblockLocation + && sqrt(squaredDist(pos, nearestWarp)) > 1.1 * sqrt(squaredDist((MC.player ?: return).pos, nearestWarp)) + ) { + MC.sendChat(Text.translatable("firmament.warp-util.already-close", nearestWarp.warp)) + return + } + MC.sendChat(Text.translatable("firmament.warp-util.attempting-to-warp", nearestWarp.warp)) + lastWarpAttempt = TimeMark.now() + lastAttemptedWarp = nearestWarp.warp + MC.sendServerCommand("warp ${nearestWarp.warp}") + } + + @Subscribe + fun clearUnlockedWarpsCommand(event: CommandEvent.SubCommand) { + event.subcommand("clearwarps") { + thenExecute { + DConfig.data?.excludedWarps?.clear() + DConfig.markDirty() + source.sendFeedback(Text.translatable("firmament.warp-util.clear-excluded")) + } + } + } + + init { + ProcessChatEvent.subscribe("WarpUtil:processChat") { + if (it.unformattedString == "You haven't unlocked this fast travel destination!" + && lastWarpAttempt.passedTime() < 2.seconds + ) { + DConfig.data?.excludedWarps?.add(lastAttemptedWarp) + DConfig.markDirty() + MC.sendChat(Text.stringifiedTranslatable("firmament.warp-util.mark-excluded", lastAttemptedWarp)) + lastWarpAttempt = TimeMark.farPast() + } + if (it.unformattedString.startsWith("You may now fast travel to")) { + DConfig.data?.excludedWarps?.clear() + DConfig.markDirty() + } + } + } +} diff --git a/src/main/kotlin/util/accessors/GetRectangle.kt b/src/main/kotlin/util/accessors/GetRectangle.kt new file mode 100644 index 0000000..37acfd9 --- /dev/null +++ b/src/main/kotlin/util/accessors/GetRectangle.kt @@ -0,0 +1,17 @@ + + +package moe.nea.firmament.util.accessors + +import me.shedaniel.math.Rectangle +import moe.nea.firmament.mixins.accessor.AccessorHandledScreen +import net.minecraft.client.gui.screen.ingame.HandledScreen + +fun HandledScreen<*>.getRectangle(): Rectangle { + this as AccessorHandledScreen + return Rectangle( + getX_Firmament(), + getY_Firmament(), + getBackgroundWidth_Firmament(), + getBackgroundHeight_Firmament() + ) +} diff --git a/src/main/kotlin/util/accessors/chathud.kt b/src/main/kotlin/util/accessors/chathud.kt new file mode 100644 index 0000000..effac7d --- /dev/null +++ b/src/main/kotlin/util/accessors/chathud.kt @@ -0,0 +1,8 @@ +package moe.nea.firmament.util.accessors + +import net.minecraft.client.gui.hud.ChatHud +import net.minecraft.client.gui.hud.ChatHudLine +import moe.nea.firmament.mixins.accessor.AccessorChatHud + +val ChatHud.messages: MutableList + get() = (this as AccessorChatHud).messages_firmament diff --git a/src/main/kotlin/util/asm/AsmAnnotationUtil.kt b/src/main/kotlin/util/asm/AsmAnnotationUtil.kt new file mode 100644 index 0000000..fb0e92c --- /dev/null +++ b/src/main/kotlin/util/asm/AsmAnnotationUtil.kt @@ -0,0 +1,89 @@ +package moe.nea.firmament.util.asm + +import com.google.common.base.Defaults +import java.lang.reflect.InvocationHandler +import java.lang.reflect.Method +import java.lang.reflect.Proxy +import org.objectweb.asm.Type +import org.objectweb.asm.tree.AnnotationNode + +object AsmAnnotationUtil { + class AnnotationProxy( + val originalType: Class, + val annotationNode: AnnotationNode, + ) : InvocationHandler { + val offsets = annotationNode.values.withIndex() + .chunked(2) + .map { it.first() } + .associate { (idx, value) -> value as String to idx + 1 } + + fun nestArrayType(depth: Int, comp: Class<*>): Class<*> = + if (depth == 0) comp + else java.lang.reflect.Array.newInstance(nestArrayType(depth - 1, comp), 0).javaClass + + fun unmap( + value: Any?, + comp: Class<*>, + depth: Int, + ): Any? { + value ?: return null + if (depth > 0) + return ((value as List) + .map { unmap(it, comp, depth - 1) } as java.util.List) + .toArray(java.lang.reflect.Array.newInstance(nestArrayType(depth - 1, comp), 0) as Array<*>) + if (comp.isEnum) { + comp as Class> + when (value) { + is String -> return java.lang.Enum.valueOf(comp, value) + is List<*> -> return java.lang.Enum.valueOf(comp, value[1] as String) + else -> error("Unknown enum variant $value for $comp") + } + } + when (value) { + is Type -> return Class.forName(value.className) + is AnnotationNode -> return createProxy(comp as Class, value) + is String, is Boolean, is Byte, is Double, is Int, is Float, is Long, is Short, is Char -> return value + } + error("Unknown enum variant $value for $comp") + } + + fun defaultFor(fullType: Class<*>): Any? { + if (fullType.isArray) return java.lang.reflect.Array.newInstance(fullType.componentType, 0) + if (fullType.isPrimitive) { + return Defaults.defaultValue(fullType) + } + if (fullType == String::class.java) + return "" + return null + } + + override fun invoke( + proxy: Any, + method: Method, + args: Array? + ): Any? { + val name = method.name + val ret = method.returnType + val retU = generateSequence(ret) { if (it.isArray) it.componentType else null } + .toList() + val arrayDepth = retU.size - 1 + val componentType = retU.last() + + val off = offsets[name] + if (off == null) { + return defaultFor(ret) + } + return unmap(annotationNode.values[off], componentType, arrayDepth) + } + } + + fun createProxy( + annotationClass: Class, + annotationNode: AnnotationNode + ): T { + require(Type.getType(annotationClass) == Type.getType(annotationNode.desc)) + return Proxy.newProxyInstance(javaClass.classLoader, + arrayOf(annotationClass), + AnnotationProxy(annotationClass, annotationNode)) as T + } +} diff --git a/src/main/kotlin/util/assertions.kt b/src/main/kotlin/util/assertions.kt new file mode 100644 index 0000000..86982be --- /dev/null +++ b/src/main/kotlin/util/assertions.kt @@ -0,0 +1,35 @@ +@file:OptIn(ExperimentalContracts::class) + +package moe.nea.firmament.util + +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * Less aggressive version of `require(obj != null)`, which fails in devenv but continues at runtime. + */ +inline fun assertNotNullOr(obj: T?, message: String? = null, block: () -> T): T { + contract { + callsInPlace(block, InvocationKind.AT_MOST_ONCE) + } + if (message == null) + assert(obj != null) + else + assert(obj != null) { message } + return obj ?: block() +} + + +/** + * Less aggressive version of `require(condition)`, which fails in devenv but continues at runtime. + */ +inline fun assertTrueOr(condition: Boolean, block: () -> Unit) { + contract { + callsInPlace(block, InvocationKind.AT_MOST_ONCE) + } + assert(condition) + if (!condition) block() +} + + diff --git a/src/main/kotlin/util/async/input.kt b/src/main/kotlin/util/async/input.kt new file mode 100644 index 0000000..2c546ba --- /dev/null +++ b/src/main/kotlin/util/async/input.kt @@ -0,0 +1,89 @@ +package moe.nea.firmament.util.async + +import io.github.notenoughupdates.moulconfig.gui.GuiContext +import io.github.notenoughupdates.moulconfig.gui.component.CenterComponent +import io.github.notenoughupdates.moulconfig.gui.component.ColumnComponent +import io.github.notenoughupdates.moulconfig.gui.component.PanelComponent +import io.github.notenoughupdates.moulconfig.gui.component.TextComponent +import io.github.notenoughupdates.moulconfig.gui.component.TextFieldComponent +import io.github.notenoughupdates.moulconfig.observer.GetSetter +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume +import net.minecraft.client.gui.screen.Screen +import moe.nea.firmament.events.HandledScreenKeyPressedEvent +import moe.nea.firmament.gui.FirmButtonComponent +import moe.nea.firmament.keybindings.IKeyBinding +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.MoulConfigUtils +import moe.nea.firmament.util.ScreenUtil + +private object InputHandler { + data class KeyInputContinuation(val keybind: IKeyBinding, val onContinue: () -> Unit) + + private val activeContinuations = mutableListOf() + + fun registerContinuation(keyInputContinuation: KeyInputContinuation): () -> Unit { + synchronized(InputHandler) { + activeContinuations.add(keyInputContinuation) + } + return { + synchronized(this) { + activeContinuations.remove(keyInputContinuation) + } + } + } + + init { + HandledScreenKeyPressedEvent.subscribe("Input:resumeAfterInput") { event -> + synchronized(InputHandler) { + val toRemove = activeContinuations.filter { + event.matches(it.keybind) + } + toRemove.forEach { it.onContinue() } + activeContinuations.removeAll(toRemove) + } + } + } +} + +suspend fun waitForInput(keybind: IKeyBinding): Unit = suspendCancellableCoroutine { cont -> + val unregister = + InputHandler.registerContinuation(InputHandler.KeyInputContinuation(keybind) { cont.resume(Unit) }) + cont.invokeOnCancellation { + unregister() + } +} + + +fun createPromptScreenGuiComponent(suggestion: String, prompt: String, action: Runnable) = (run { + val text = GetSetter.floating(suggestion) + GuiContext( + CenterComponent( + PanelComponent( + ColumnComponent( + TextFieldComponent(text, 120), + FirmButtonComponent(TextComponent(prompt), action = action) + ) + ) + ) + ) to text +}) + +suspend fun waitForTextInput(suggestion: String, prompt: String) = + suspendCancellableCoroutine { cont -> + lateinit var screen: Screen + lateinit var text: GetSetter + val action = { + if (MC.screen === screen) + MC.screen = null + // TODO: should this exit + cont.resume(text.get()) + } + val (gui, text_) = createPromptScreenGuiComponent(suggestion, prompt, action) + text = text_ + screen = MoulConfigUtils.wrapScreen(gui, null, onClose = action) + ScreenUtil.setScreenLater(screen) + cont.invokeOnCancellation { + action() + } + } diff --git a/src/main/kotlin/util/collections/InstanceList.kt b/src/main/kotlin/util/collections/InstanceList.kt new file mode 100644 index 0000000..fd8c786 --- /dev/null +++ b/src/main/kotlin/util/collections/InstanceList.kt @@ -0,0 +1,57 @@ +package moe.nea.firmament.util.collections + +import java.lang.ref.ReferenceQueue +import java.lang.ref.WeakReference + +class InstanceList(val name: String) { + val queue = object : ReferenceQueue() {} + val set = mutableSetOf() + + val size: Int + get() { + clearOldReferences() + return set.size + } + + fun clearOldReferences() { + while (true) { + val reference = queue.poll() ?: break + set.remove(reference) + } + } + + fun getAll(): List { + clearOldReferences() + return set.mapNotNull { it.get() } + } + + fun add(t: T) { + set.add(Ref(t)) + } + + init { + if (init) + allInstances.add(this) + } + + inner class Ref(referent: T) : WeakReference(referent) { + val hashCode = System.identityHashCode(referent) + override fun equals(other: Any?): Boolean { + return other is InstanceList<*>.Ref && hashCode == other.hashCode && get() === other.get() + } + + override fun hashCode(): Int { + return hashCode + } + } + + companion object { + private var init = false + val allInstances = InstanceList>("InstanceLists") + + init { + init = true + allInstances.add(allInstances) + } + } +} diff --git a/src/main/kotlin/util/collections/MutableMapWithMaxSize.kt b/src/main/kotlin/util/collections/MutableMapWithMaxSize.kt new file mode 100644 index 0000000..218bc55 --- /dev/null +++ b/src/main/kotlin/util/collections/MutableMapWithMaxSize.kt @@ -0,0 +1,40 @@ + +package moe.nea.firmament.util.collections + +import moe.nea.firmament.util.IdentityCharacteristics + +fun mutableMapWithMaxSize(maxSize: Int): MutableMap = object : LinkedHashMap() { + override fun removeEldestEntry(eldest: MutableMap.MutableEntry): Boolean { + return size > maxSize + } +} + +fun ((T) -> R).memoizeIdentity(maxCacheSize: Int): (T) -> R { + val memoized = { it: IdentityCharacteristics -> + this(it.value) + }.memoize(maxCacheSize) + return { memoized(IdentityCharacteristics(it)) } +} + +@PublishedApi +internal val SENTINEL_NULL = java.lang.Object() + +/** + * Requires the map to only contain values of type [R] or [SENTINEL_NULL]. This is ensured if the map is only ever + * accessed via this function. + */ +inline fun MutableMap.computeNullableFunction(key: T, crossinline func: () -> R): R { + val value = this.getOrPut(key) { + func() ?: SENTINEL_NULL + } + @Suppress("UNCHECKED_CAST") + return if (value === SENTINEL_NULL) null as R + else value as R +} + +fun ((T) -> R).memoize(maxCacheSize: Int): (T) -> R { + val map = mutableMapWithMaxSize(maxCacheSize) + return { + map.computeNullableFunction(it) { this@memoize(it) } + } +} diff --git a/src/main/kotlin/util/collections/RangeUtil.kt b/src/main/kotlin/util/collections/RangeUtil.kt new file mode 100644 index 0000000..a7029ac --- /dev/null +++ b/src/main/kotlin/util/collections/RangeUtil.kt @@ -0,0 +1,40 @@ +package moe.nea.firmament.util.collections + +import kotlin.math.floor + +val ClosedFloatingPointRange.centre get() = (endInclusive + start) / 2 + +fun ClosedFloatingPointRange.nonNegligibleSubSectionsAlignedWith( + interval: Float +): Iterable { + require(interval.isFinite()) + val range = this + return object : Iterable { + override fun iterator(): Iterator { + return object : FloatIterator() { + var polledValue: Float = range.start + var lastValue: Float = polledValue + + override fun nextFloat(): Float { + if (!hasNext()) throw NoSuchElementException() + lastValue = polledValue + polledValue = Float.NaN + return lastValue + } + + override fun hasNext(): Boolean { + if (!polledValue.isNaN()) { + return true + } + if (lastValue == range.endInclusive) + return false + polledValue = (floor(lastValue / interval) + 1) * interval + if (polledValue > range.endInclusive) { + polledValue = range.endInclusive + } + return true + } + } + } + } +} diff --git a/src/main/kotlin/util/collections/WeakCache.kt b/src/main/kotlin/util/collections/WeakCache.kt new file mode 100644 index 0000000..4a48c63 --- /dev/null +++ b/src/main/kotlin/util/collections/WeakCache.kt @@ -0,0 +1,116 @@ +package moe.nea.firmament.util.collections + +import java.lang.ref.ReferenceQueue +import java.lang.ref.WeakReference +import moe.nea.firmament.features.debug.DebugLogger + +/** + * Cache class that uses [WeakReferences][WeakReference] to only cache values while there is still a life reference to + * the key. Each key can have additional extra data that is used to look up values. That extra data is not required to + * be a life reference. The main Key is compared using strict reference equality. This map is not synchronized. + */ +open class WeakCache(val name: String) { + private val queue = object : ReferenceQueue() {} + private val map = mutableMapOf() + + val size: Int + get() { + clearOldReferences() + return map.size + } + + fun clearOldReferences() { + var successCount = 0 + var totalCount = 0 + while (true) { + val reference = queue.poll() as WeakCache<*, *, *>.Ref? ?: break + totalCount++ + if (reference.shouldBeEvicted() && map.remove(reference) != null) + successCount++ + } + if (totalCount > 0) + logger.log("Cleared $successCount/$totalCount references from queue") + } + + open fun mkRef(key: Key, extraData: ExtraKey): Ref { + return Ref(key, extraData) + } + + fun get(key: Key, extraData: ExtraKey): Value? { + clearOldReferences() + return map[mkRef(key, extraData)] + } + + fun put(key: Key, extraData: ExtraKey, value: Value) { + clearOldReferences() + map[mkRef(key, extraData)] = value + } + + fun getOrPut(key: Key, extraData: ExtraKey, value: (Key, ExtraKey) -> Value): Value { + clearOldReferences() + return map.getOrPut(mkRef(key, extraData)) { value(key, extraData) } + } + + fun clear() { + map.clear() + } + + init { + allInstances.add(this) + } + + companion object { + val allInstances = InstanceList>("WeakCaches") + private val logger = DebugLogger("WeakCache") + fun memoize(name: String, function: (Key) -> Value): + CacheFunction.NoExtraData { + return CacheFunction.NoExtraData(WeakCache(name), function) + } + + fun dontMemoize(name: String, function: (Key, ExtraKey) -> Value) = function + fun memoize(name: String, function: (Key, ExtraKey) -> Value): + CacheFunction.WithExtraData { + return CacheFunction.WithExtraData(WeakCache(name), function) + } + } + + open inner class Ref( + weakInstance: Key, + val extraData: ExtraKey, + ) : WeakReference(weakInstance, queue) { + open fun shouldBeEvicted() = true + val hashCode = System.identityHashCode(weakInstance) * 31 + extraData.hashCode() + override fun equals(other: Any?): Boolean { + if (other !is WeakCache<*, *, *>.Ref) return false + return other.hashCode == this.hashCode + && other.get() === this.get() + && other.extraData == this.extraData + } + + override fun hashCode(): Int { + return hashCode + } + } + + interface CacheFunction { + val cache: WeakCache<*, *, *> + + data class NoExtraData( + override val cache: WeakCache, + val wrapped: (Key) -> Value, + ) : CacheFunction, (Key) -> Value { + override fun invoke(p1: Key): Value { + return cache.getOrPut(p1, Unit, { a, _ -> wrapped(a) }) + } + } + + data class WithExtraData( + override val cache: WeakCache, + val wrapped: (Key, ExtraKey) -> Value, + ) : CacheFunction, (Key, ExtraKey) -> Value { + override fun invoke(p1: Key, p2: ExtraKey): Value { + return cache.getOrPut(p1, p2, wrapped) + } + } + } +} diff --git a/src/main/kotlin/util/collections/listutil.kt b/src/main/kotlin/util/collections/listutil.kt new file mode 100644 index 0000000..7f85a30 --- /dev/null +++ b/src/main/kotlin/util/collections/listutil.kt @@ -0,0 +1,9 @@ + +package moe.nea.firmament.util.collections + +fun List.lastNotNullOfOrNull(func: (T) -> R?): R? { + for (i in indices.reversed()) { + return func(this[i]) ?: continue + } + return null +} diff --git a/src/main/kotlin/util/colorconversion.kt b/src/main/kotlin/util/colorconversion.kt new file mode 100644 index 0000000..d7a5dad --- /dev/null +++ b/src/main/kotlin/util/colorconversion.kt @@ -0,0 +1,13 @@ + + +package moe.nea.firmament.util + +import net.minecraft.text.TextColor +import net.minecraft.util.DyeColor + +fun DyeColor.toShedaniel(): me.shedaniel.math.Color = + me.shedaniel.math.Color.ofOpaque(this.signColor) + +fun DyeColor.toTextColor(): TextColor = + TextColor.fromRgb(this.signColor) + diff --git a/src/main/kotlin/util/compatloader/CompatLoader.kt b/src/main/kotlin/util/compatloader/CompatLoader.kt new file mode 100644 index 0000000..d1073af --- /dev/null +++ b/src/main/kotlin/util/compatloader/CompatLoader.kt @@ -0,0 +1,50 @@ +package moe.nea.firmament.util.compatloader + +import java.util.ServiceLoader +import net.fabricmc.loader.api.FabricLoader +import kotlin.reflect.KClass +import kotlin.streams.asSequence +import moe.nea.firmament.Firmament + +open class CompatLoader(val kClass: Class) { + constructor(kClass: KClass) : this(kClass.java) + + val loader: ServiceLoader = ServiceLoader.load(kClass) + val allValidInstances by lazy { + loader.reload() + loader.stream() + .asSequence() + .filter { provider -> + runCatching { + shouldLoad(provider.type()) + }.getOrElse { + Firmament.logger.error("Could not determine whether to load a ${kClass.name} subclass", it) + false + } + } + .mapNotNull { provider -> + runCatching { + provider.get() + }.getOrElse { + Firmament.logger.error( + "Could not load desired instance ${provider.type().name} for ${kClass.name}", + it) + null + } + } + .toList() + } + val singleInstance by lazy { allValidInstances.singleOrNull() } + + open fun shouldLoad(type: Class): Boolean { + return checkRequiredModsPresent(type) + } + + fun checkRequiredModsPresent(type: Class<*>): Boolean { + val requiredMods = type.getAnnotationsByType(RequireMod::class.java) + return requiredMods.all { FabricLoader.getInstance().isModLoaded(it.modId) } + } + + @Repeatable + annotation class RequireMod(val modId: String) +} diff --git a/src/main/kotlin/util/compatloader/CompatMeta.kt b/src/main/kotlin/util/compatloader/CompatMeta.kt new file mode 100644 index 0000000..cf63645 --- /dev/null +++ b/src/main/kotlin/util/compatloader/CompatMeta.kt @@ -0,0 +1,48 @@ +package moe.nea.firmament.util.compatloader + +import java.util.ServiceLoader +import moe.nea.firmament.events.subscription.SubscriptionList +import moe.nea.firmament.init.AutoDiscoveryPlugin +import moe.nea.firmament.util.ErrorUtil + +/** + * Declares the compat meta interface for the current source set. + * This is used by [CompatLoader], [SubscriptionList], and [AutoDiscoveryPlugin]. Annotate a [ICompatMeta] object with + * this. + */ +annotation class CompatMeta + +interface ICompatMetaGen { + fun owns(className: String): Boolean + val meta: ICompatMeta +} + +interface ICompatMeta { + fun shouldLoad(): Boolean + + companion object { + val allMetas = ServiceLoader + .load(ICompatMetaGen::class.java) + .toList() + + fun shouldLoad(className: String): Boolean { + // TODO: replace this with a more performant package lookup + val meta = if (ErrorUtil.aggressiveErrors) { + val fittingMetas = allMetas.filter { it.owns(className) } + require(fittingMetas.size == 1) { "Orphaned or duplicate owned class $className (${fittingMetas.map { it.meta }}). Consider adding a @CompatMeta object." } + fittingMetas.single() + } else { + allMetas.firstOrNull { it.owns(className) } + } + return meta?.meta?.shouldLoad() ?: true + } + } +} + +object CompatHelper { + fun isOwnedByPackage(className: String, vararg packages: String): Boolean { + // TODO: create package lookup structure once + val packageName = className.substringBeforeLast('.') + return packageName in packages + } +} diff --git a/src/main/kotlin/util/customgui/CoordRememberingSlot.kt b/src/main/kotlin/util/customgui/CoordRememberingSlot.kt new file mode 100644 index 0000000..c61c711 --- /dev/null +++ b/src/main/kotlin/util/customgui/CoordRememberingSlot.kt @@ -0,0 +1,14 @@ + +package moe.nea.firmament.util.customgui + +import net.minecraft.screen.slot.Slot + +interface CoordRememberingSlot { + fun rememberCoords_firmament() + fun restoreCoords_firmament() + fun getOriginalX_firmament(): Int + fun getOriginalY_firmament(): Int +} + +val Slot.originalX get() = (this as CoordRememberingSlot).getOriginalX_firmament() +val Slot.originalY get() = (this as CoordRememberingSlot).getOriginalY_firmament() diff --git a/src/main/kotlin/util/customgui/CustomGui.kt b/src/main/kotlin/util/customgui/CustomGui.kt new file mode 100644 index 0000000..35c60ac --- /dev/null +++ b/src/main/kotlin/util/customgui/CustomGui.kt @@ -0,0 +1,91 @@ +package moe.nea.firmament.util.customgui + +import me.shedaniel.math.Rectangle +import net.minecraft.client.gui.DrawContext +import net.minecraft.screen.slot.Slot +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.HandledScreenPushREIEvent + +abstract class CustomGui { + + abstract fun getBounds(): List + + open fun moveSlot(slot: Slot) { + // TODO: return a Pair maybe? worth an investigation + } + + companion object { + @Subscribe + fun onExclusionZone(event: HandledScreenPushREIEvent) { + val customGui = event.screen.customGui ?: return + event.rectangles.addAll(customGui.getBounds()) + } + } + + open fun render( + drawContext: DrawContext, + delta: Float, + mouseX: Int, + mouseY: Int + ) { + } + + open fun mouseClick(mouseX: Double, mouseY: Double, button: Int): Boolean { + return false + } + + open fun afterSlotRender(context: DrawContext, slot: Slot) {} + open fun beforeSlotRender(context: DrawContext, slot: Slot) {} + open fun mouseScrolled(mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double): Boolean { + return false + } + + open fun isClickOutsideBounds(mouseX: Double, mouseY: Double): Boolean { + return getBounds().none { it.contains(mouseX, mouseY) } + } + + open fun isPointWithinBounds( + x: Int, + y: Int, + width: Int, + height: Int, + pointX: Double, + pointY: Double, + ): Boolean { + return getBounds().any { it.contains(pointX, pointY) } && + Rectangle(x, y, width, height).contains(pointX, pointY) + } + + open fun isPointOverSlot(slot: Slot, xOffset: Int, yOffset: Int, pointX: Double, pointY: Double): Boolean { + return isPointWithinBounds(slot.x + xOffset, slot.y + yOffset, 16, 16, pointX, pointY) + } + + open fun onInit() {} + open fun shouldDrawForeground(): Boolean { + return true + } + + open fun onVoluntaryExit(): Boolean { + return true + } + + open fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { + return false + } + + open fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean { + return false + } + + open fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + return false + } + + open fun charTyped(chr: Char, modifiers: Int): Boolean { + return false + } + + open fun keyReleased(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + return false + } +} diff --git a/src/main/kotlin/util/customgui/HasCustomGui.kt b/src/main/kotlin/util/customgui/HasCustomGui.kt new file mode 100644 index 0000000..edead2e --- /dev/null +++ b/src/main/kotlin/util/customgui/HasCustomGui.kt @@ -0,0 +1,17 @@ + +package moe.nea.firmament.util.customgui + +import net.minecraft.client.gui.screen.ingame.HandledScreen + +@Suppress("FunctionName") +interface HasCustomGui { + fun getCustomGui_Firmament(): CustomGui? + fun setCustomGui_Firmament(gui: CustomGui?) +} + +var > T.customGui: CustomGui? + get() = (this as HasCustomGui).getCustomGui_Firmament() + set(value) { + (this as HasCustomGui).setCustomGui_Firmament(value) + } + diff --git a/src/main/kotlin/util/data/DataHolder.kt b/src/main/kotlin/util/data/DataHolder.kt new file mode 100644 index 0000000..21a6014 --- /dev/null +++ b/src/main/kotlin/util/data/DataHolder.kt @@ -0,0 +1,62 @@ + + +package moe.nea.firmament.util.data + +import java.nio.file.Path +import kotlinx.serialization.KSerializer +import kotlin.io.path.exists +import kotlin.io.path.readText +import kotlin.io.path.writeText +import moe.nea.firmament.Firmament + +abstract class DataHolder( + val serializer: KSerializer, + val name: String, + val default: () -> T +) : IDataHolder { + + + final override var data: T + private set + + init { + data = readValueOrDefault() + IDataHolder.putDataHolder(this::class, this) + } + + private val file: Path get() = Firmament.CONFIG_DIR.resolve("$name.json") + + protected fun readValueOrDefault(): T { + if (file.exists()) + try { + return Firmament.json.decodeFromString( + serializer, + file.readText() + ) + } catch (e: Exception) {/* Expecting IOException and SerializationException, but Kotlin doesn't allow multi catches*/ + IDataHolder.badLoads.add(name) + Firmament.logger.error( + "Exception during loading of config file $name. This will reset this config.", + e + ) + } + return default() + } + + private fun writeValue(t: T) { + file.writeText(Firmament.json.encodeToString(serializer, t)) + } + + override fun save() { + writeValue(data) + } + + override fun load() { + data = readValueOrDefault() + } + + override fun markDirty() { + IDataHolder.markDirty(this::class) + } + +} diff --git a/src/main/kotlin/util/data/IDataHolder.kt b/src/main/kotlin/util/data/IDataHolder.kt new file mode 100644 index 0000000..1e9ba98 --- /dev/null +++ b/src/main/kotlin/util/data/IDataHolder.kt @@ -0,0 +1,71 @@ +package moe.nea.firmament.util.data + +import java.util.concurrent.CopyOnWriteArrayList +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents +import kotlin.reflect.KClass +import net.minecraft.text.Text +import moe.nea.firmament.Firmament +import moe.nea.firmament.events.ScreenChangeEvent +import moe.nea.firmament.util.MC + +interface IDataHolder { + companion object { + internal var badLoads: MutableList = CopyOnWriteArrayList() + private val allConfigs: MutableMap>, IDataHolder<*>> = mutableMapOf() + private val dirty: MutableSet>> = mutableSetOf() + + internal fun , K> putDataHolder(kClass: KClass, inst: IDataHolder) { + allConfigs[kClass] = inst + } + + fun , K> markDirty(kClass: KClass) { + if (kClass !in allConfigs) { + Firmament.logger.error("Tried to markDirty '${kClass.qualifiedName}', which isn't registered as 'IConfigHolder'") + return + } + dirty.add(kClass) + } + + private fun performSaves() { + val toSave = dirty.toList().also { + dirty.clear() + } + for (it in toSave) { + val obj = allConfigs[it] + if (obj == null) { + Firmament.logger.error("Tried to save '${it}', which isn't registered as 'ConfigHolder'") + continue + } + obj.save() + } + } + + private fun warnForResetConfigs() { + if (badLoads.isNotEmpty()) { + MC.sendChat( + Text.literal( + "The following configs have been reset: ${badLoads.joinToString(", ")}. " + + "This can be intentional, but probably isn't." + ) + ) + badLoads.clear() + } + } + + fun registerEvents() { + ScreenChangeEvent.subscribe("IDataHolder:saveOnScreenChange") { event -> + performSaves() + warnForResetConfigs() + } + ClientLifecycleEvents.CLIENT_STOPPING.register(ClientLifecycleEvents.ClientStopping { + performSaves() + }) + } + + } + + val data: T + fun save() + fun markDirty() + fun load() +} diff --git a/src/main/kotlin/util/data/MultiFileDataHolder.kt b/src/main/kotlin/util/data/MultiFileDataHolder.kt new file mode 100644 index 0000000..94c6f05 --- /dev/null +++ b/src/main/kotlin/util/data/MultiFileDataHolder.kt @@ -0,0 +1,63 @@ +package moe.nea.firmament.util.data + +import kotlinx.serialization.KSerializer +import kotlin.io.path.createDirectories +import kotlin.io.path.deleteExisting +import kotlin.io.path.exists +import kotlin.io.path.extension +import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.nameWithoutExtension +import kotlin.io.path.readText +import kotlin.io.path.writeText +import moe.nea.firmament.Firmament + +abstract class MultiFileDataHolder( + val dataSerializer: KSerializer, + val configName: String +) { // TODO: abstract this + ProfileSpecificDataHolder + val configDirectory = Firmament.CONFIG_DIR.resolve(configName) + private var allData = readValues() + protected fun readValues(): MutableMap { + if (!configDirectory.exists()) { + configDirectory.createDirectories() + } + val profileFiles = configDirectory.listDirectoryEntries() + return profileFiles + .filter { it.extension == "json" } + .mapNotNull { + try { + it.nameWithoutExtension to Firmament.json.decodeFromString(dataSerializer, it.readText()) + } catch (e: Exception) { /* Expecting IOException and SerializationException, but Kotlin doesn't allow multi catches*/ + IDataHolder.badLoads.add(configName) + Firmament.logger.error( + "Exception during loading of multi file data holder $it ($configName). This will reset that profiles config.", + e + ) + null + } + }.toMap().toMutableMap() + } + + fun save() { + if (!configDirectory.exists()) { + configDirectory.createDirectories() + } + val c = allData + configDirectory.listDirectoryEntries().forEach { + if (it.nameWithoutExtension !in c.mapKeys { it.toString() }) { + it.deleteExisting() + } + } + c.forEach { (name, value) -> + val f = configDirectory.resolve("$name.json") + f.writeText(Firmament.json.encodeToString(dataSerializer, value)) + } + } + + fun list(): Map = allData + val validPathRegex = "[a-zA-Z0-9_][a-zA-Z0-9\\-_.]*".toPattern() + fun insert(name: String, value: T) { + require(validPathRegex.matcher(name).matches()) { "Not a valid name: $name" } + allData[name] = value + } +} diff --git a/src/main/kotlin/util/data/ProfileSpecificDataHolder.kt b/src/main/kotlin/util/data/ProfileSpecificDataHolder.kt new file mode 100644 index 0000000..1cd4f22 --- /dev/null +++ b/src/main/kotlin/util/data/ProfileSpecificDataHolder.kt @@ -0,0 +1,84 @@ + + +package moe.nea.firmament.util.data + +import java.nio.file.Path +import java.util.UUID +import kotlinx.serialization.KSerializer +import kotlin.io.path.createDirectories +import kotlin.io.path.deleteExisting +import kotlin.io.path.exists +import kotlin.io.path.extension +import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.nameWithoutExtension +import kotlin.io.path.readText +import kotlin.io.path.writeText +import moe.nea.firmament.Firmament +import moe.nea.firmament.util.SBData + +abstract class ProfileSpecificDataHolder( + private val dataSerializer: KSerializer, + val configName: String, + private val configDefault: () -> S +) : IDataHolder { + + var allConfigs: MutableMap + + override val data: S? + get() = SBData.profileId?.let { + allConfigs.computeIfAbsent(it) { configDefault() } + } + + init { + allConfigs = readValues() + IDataHolder.putDataHolder(this::class, this) + } + + private val configDirectory: Path get() = Firmament.CONFIG_DIR.resolve("profiles").resolve(configName) + + private fun readValues(): MutableMap { + if (!configDirectory.exists()) { + configDirectory.createDirectories() + } + val profileFiles = configDirectory.listDirectoryEntries() + return profileFiles + .filter { it.extension == "json" } + .mapNotNull { + try { + UUID.fromString(it.nameWithoutExtension) to Firmament.json.decodeFromString(dataSerializer, it.readText()) + } catch (e: Exception) { /* Expecting IOException and SerializationException, but Kotlin doesn't allow multi catches*/ + IDataHolder.badLoads.add(configName) + Firmament.logger.error( + "Exception during loading of profile specific config file $it ($configName). This will reset that profiles config.", + e + ) + null + } + }.toMap().toMutableMap() + } + + override fun save() { + if (!configDirectory.exists()) { + configDirectory.createDirectories() + } + val c = allConfigs + configDirectory.listDirectoryEntries().forEach { + if (it.nameWithoutExtension !in c.mapKeys { it.toString() }) { + it.deleteExisting() + } + } + c.forEach { (name, value) -> + val f = configDirectory.resolve("$name.json") + f.writeText(Firmament.json.encodeToString(dataSerializer, value)) + } + } + + override fun markDirty() { + IDataHolder.markDirty(this::class) + } + + override fun load() { + allConfigs = readValues() + } + +} diff --git a/src/main/kotlin/util/json/BlockPosSerializer.kt b/src/main/kotlin/util/json/BlockPosSerializer.kt new file mode 100644 index 0000000..144b0a0 --- /dev/null +++ b/src/main/kotlin/util/json/BlockPosSerializer.kt @@ -0,0 +1,25 @@ +package moe.nea.firmament.util.json + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.serializer +import net.minecraft.util.math.BlockPos + +object BlockPosSerializer : KSerializer { + val delegate = serializer>() + + override val descriptor: SerialDescriptor + get() = SerialDescriptor("BlockPos", delegate.descriptor) + + override fun deserialize(decoder: Decoder): BlockPos { + val list = decoder.decodeSerializableValue(delegate) + require(list.size == 3) + return BlockPos(list[0], list[1], list[2]) + } + + override fun serialize(encoder: Encoder, value: BlockPos) { + encoder.encodeSerializableValue(delegate, listOf(value.x, value.y, value.z)) + } +} diff --git a/src/main/kotlin/util/json/DashlessUUIDSerializer.kt b/src/main/kotlin/util/json/DashlessUUIDSerializer.kt new file mode 100644 index 0000000..6bafebe --- /dev/null +++ b/src/main/kotlin/util/json/DashlessUUIDSerializer.kt @@ -0,0 +1,27 @@ + + +package moe.nea.firmament.util.json + +import java.util.UUID +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import moe.nea.firmament.util.parseDashlessUUID +import moe.nea.firmament.util.parsePotentiallyDashlessUUID + +object DashlessUUIDSerializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("DashlessUUIDSerializer", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): UUID { + val str = decoder.decodeString() + return parsePotentiallyDashlessUUID(str) + } + + override fun serialize(encoder: Encoder, value: UUID) { + encoder.encodeString(value.toString().replace("-", "")) + } +} diff --git a/src/main/kotlin/util/json/FirmCodecs.kt b/src/main/kotlin/util/json/FirmCodecs.kt new file mode 100644 index 0000000..c0863bc --- /dev/null +++ b/src/main/kotlin/util/json/FirmCodecs.kt @@ -0,0 +1,20 @@ +package moe.nea.firmament.util.json + +import com.mojang.serialization.Codec +import com.mojang.serialization.DataResult +import com.mojang.serialization.Lifecycle +import com.mojang.util.UndashedUuid +import net.minecraft.util.Uuids + +object FirmCodecs { + @JvmField + val UUID_LENIENT_PREFER_INT_STREAM = Codec.withAlternative(Uuids.INT_STREAM_CODEC, Codec.STRING.comapFlatMap( + { + try { + DataResult.success(UndashedUuid.fromStringLenient(it), Lifecycle.stable()) + } catch (ex: IllegalArgumentException) { + DataResult.error { "Invalid UUID $it: ${ex.message}" } + } + }, + UndashedUuid::toString)) +} diff --git a/src/main/kotlin/util/json/InstantAsLongSerializer.kt b/src/main/kotlin/util/json/InstantAsLongSerializer.kt new file mode 100644 index 0000000..51b5f0a --- /dev/null +++ b/src/main/kotlin/util/json/InstantAsLongSerializer.kt @@ -0,0 +1,22 @@ + + +package moe.nea.firmament.util.json + +import java.time.Instant +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +object InstantAsLongSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("InstantAsLongSerializer", PrimitiveKind.LONG) + override fun deserialize(decoder: Decoder): Instant { + return Instant.ofEpochMilli(decoder.decodeLong()) + } + + override fun serialize(encoder: Encoder, value: Instant) { + encoder.encodeLong(value.toEpochMilli()) + } +} diff --git a/src/main/kotlin/util/json/KJsonOps.kt b/src/main/kotlin/util/json/KJsonOps.kt new file mode 100644 index 0000000..404ea5e --- /dev/null +++ b/src/main/kotlin/util/json/KJsonOps.kt @@ -0,0 +1,131 @@ +package moe.nea.firmament.util.json + +import com.google.gson.internal.LazilyParsedNumber +import com.mojang.datafixers.util.Pair +import com.mojang.serialization.DataResult +import com.mojang.serialization.DynamicOps +import java.util.stream.Stream +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.booleanOrNull +import kotlin.streams.asSequence + +class KJsonOps : DynamicOps { + companion object { + val INSTANCE = KJsonOps() + } + + override fun empty(): JsonElement { + return JsonNull + } + + override fun createNumeric(num: Number): JsonElement { + return JsonPrimitive(num) + } + + override fun createString(str: String): JsonElement { + return JsonPrimitive(str) + } + + override fun remove(input: JsonElement, key: String): JsonElement { + if (input is JsonObject) { + return JsonObject(input.filter { it.key != key }) + } else { + return input + } + } + + override fun createList(stream: Stream): JsonElement { + return JsonArray(stream.toList()) + } + + override fun getStream(input: JsonElement): DataResult> { + if (input is JsonArray) + return DataResult.success(input.stream()) + return DataResult.error { "Not a json array: $input" } + } + + override fun createMap(map: Stream>): JsonElement { + return JsonObject(map.asSequence() + .map { ((it.first as JsonPrimitive).content) to it.second } + .toMap()) + } + + override fun getMapValues(input: JsonElement): DataResult>> { + if (input is JsonObject) { + return DataResult.success(input.entries.stream().map { Pair.of(createString(it.key), it.value) }) + } + return DataResult.error { "Not a JSON object: $input" } + } + + override fun mergeToMap(map: JsonElement, key: JsonElement, value: JsonElement): DataResult { + if (key !is JsonPrimitive || key.isString) { + return DataResult.error { "key is not a string: $key" } + } + val jKey = key.content + val extra = mapOf(jKey to value) + if (map == empty()) { + return DataResult.success(JsonObject(extra)) + } + if (map is JsonObject) { + return DataResult.success(JsonObject(map + extra)) + } + return DataResult.error { "mergeToMap called with not a map: $map" } + } + + override fun mergeToList(list: JsonElement, value: JsonElement): DataResult { + if (list == empty()) + return DataResult.success(JsonArray(listOf(value))) + if (list is JsonArray) { + return DataResult.success(JsonArray(list + value)) + } + return DataResult.error { "mergeToList called with not a list: $list" } + } + + override fun getStringValue(input: JsonElement): DataResult { + if (input is JsonPrimitive && input.isString) { + return DataResult.success(input.content) + } + return DataResult.error { "Not a string: $input" } + } + + override fun getNumberValue(input: JsonElement): DataResult { + if (input is JsonPrimitive && !input.isString && input.booleanOrNull == null) + return DataResult.success(LazilyParsedNumber(input.content)) + return DataResult.error { "not a number: $input" } + } + + override fun createBoolean(value: Boolean): JsonElement { + return JsonPrimitive(value) + } + + override fun getBooleanValue(input: JsonElement): DataResult { + if (input is JsonPrimitive) { + if (input.booleanOrNull != null) + return DataResult.success(input.boolean) + return super.getBooleanValue(input) + } + return DataResult.error { "Not a boolean: $input" } + } + + override fun convertTo(output: DynamicOps, input: JsonElement): U { + if (input is JsonObject) + return output.createMap( + input.entries.stream().map { Pair.of(output.createString(it.key), convertTo(output, it.value)) }) + if (input is JsonArray) + return output.createList(input.stream().map { convertTo(output, it) }) + if (input is JsonNull) + return output.empty() + if (input is JsonPrimitive) { + if (input.isString) + return output.createString(input.content) + if (input.booleanOrNull != null) + return output.createBoolean(input.boolean) + } + error("Unknown json value: $input") + } +} diff --git a/src/main/kotlin/util/json/KJsonUtils.kt b/src/main/kotlin/util/json/KJsonUtils.kt new file mode 100644 index 0000000..b15119b --- /dev/null +++ b/src/main/kotlin/util/json/KJsonUtils.kt @@ -0,0 +1,11 @@ +package moe.nea.firmament.util.json + +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive + +fun List.asJsonArray(): JsonArray { + return JsonArray(this) +} + +fun Iterable.toJsonArray(): JsonArray = map { JsonPrimitive(it) }.asJsonArray() diff --git a/src/main/kotlin/util/json/SingletonSerializableList.kt b/src/main/kotlin/util/json/SingletonSerializableList.kt new file mode 100644 index 0000000..aa543d6 --- /dev/null +++ b/src/main/kotlin/util/json/SingletonSerializableList.kt @@ -0,0 +1,31 @@ + +package moe.nea.firmament.util.json + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonElement + +class SingletonSerializableList(val child: KSerializer) : KSerializer> { + override val descriptor: SerialDescriptor + get() = JsonElement.serializer().descriptor + + override fun deserialize(decoder: Decoder): List { + decoder as JsonDecoder + val list = JsonElement.serializer().deserialize(decoder) + if (list is JsonArray) { + return list.map { + decoder.json.decodeFromJsonElement(child, it) + } + } + return listOf(decoder.json.decodeFromJsonElement(child, list)) + } + + override fun serialize(encoder: Encoder, value: List) { + ListSerializer(child).serialize(encoder, value) + } +} diff --git a/src/main/kotlin/util/math/GChainReconciliation.kt b/src/main/kotlin/util/math/GChainReconciliation.kt new file mode 100644 index 0000000..37998d5 --- /dev/null +++ b/src/main/kotlin/util/math/GChainReconciliation.kt @@ -0,0 +1,102 @@ +package moe.nea.firmament.util.math + +import kotlin.math.min + +/** + * Algorithm for (sort of) cheap reconciliation of two cycles with missing frames. + */ +object GChainReconciliation { + // Step one: Find the most common element and shift the arrays until it is at the start in both (this could be just rotating until minimal levenshtein distance or smth. that would be way better for cycles with duplicates, but i do not want to implement levenshtein as well) + // Step two: Find the first different element. + // Step three: Find the next index of both of the elements. + // Step four: Insert the element that is further away. + + fun Iterable.frequencies(): Map { + val acc = mutableMapOf() + for (t in this) { + acc.compute(t, { _, old -> (old ?: 0) + 1 }) + } + return acc + } + + fun findMostCommonlySharedElement( + leftChain: List, + rightChain: List, + ): T { + val lf = leftChain.frequencies() + val rf = rightChain.frequencies() + val mostCommonlySharedElement = lf.maxByOrNull { min(it.value, rf[it.key] ?: 0) }?.key + if (mostCommonlySharedElement == null || mostCommonlySharedElement !in rf) + error("Could not find a shared element") + return mostCommonlySharedElement + } + + fun List.getMod(index: Int): T { + return this[index.mod(size)] + } + + fun List.rotated(offset: Int): List { + val newList = mutableListOf() + for (index in indices) { + newList.add(getMod(index - offset)) + } + return newList + } + + fun shiftToFront(list: List, element: T): List { + val shiftDistance = list.indexOf(element) + require(shiftDistance >= 0) + return list.rotated(-shiftDistance) + } + + fun List.indexOfOrMaxInt(element: T): Int = indexOf(element).takeUnless { it < 0 } ?: Int.MAX_VALUE + + fun reconcileCycles( + leftChain: List, + rightChain: List, + ): List { + val mostCommonElement = findMostCommonlySharedElement(leftChain, rightChain) + val left = shiftToFront(leftChain, mostCommonElement).toMutableList() + val right = shiftToFront(rightChain, mostCommonElement).toMutableList() + + var index = 0 + while (index < left.size && index < right.size) { + val leftEl = left[index] + val rightEl = right[index] + if (leftEl == rightEl) { + index++ + continue + } + val nextLeftInRight = right.subList(index, right.size) + .indexOfOrMaxInt(leftEl) + + val nextRightInLeft = left.subList(index, left.size) + .indexOfOrMaxInt(rightEl) + if (nextLeftInRight < nextRightInLeft) { + left.add(index, rightEl) + } else if (nextRightInLeft < nextLeftInRight) { + right.add(index, leftEl) + } else { + index++ + } + } + return if (left.size < right.size) right else left + } + + fun isValidCycle(longList: List, cycle: List): Boolean { + for ((i, value) in longList.withIndex()) { + if (cycle.getMod(i) != value) + return false + } + return true + } + + fun List.shortenCycle(): List { + for (i in (1.. register( + id: String, + builderOperator: (ComponentType.Builder) -> Unit + ): ComponentType { + return Registry.register( + Registries.DATA_COMPONENT_TYPE, + Firmament.identifier(id), + ComponentType.builder().also(builderOperator) + .build() + ) + } + + fun errorCodec(message: String): PacketCodec = + object : PacketCodec { + override fun decode(buf: ByteBuf?): T? { + error(message) + } + + override fun encode(buf: ByteBuf?, value: T?) { + error(message) + } + } + + fun > B.neverEncode(message: String = "This element should never be encoded or decoded"): B { + packetCodec(errorCodec(message)) + codec(null) + return this + } + + val IS_BROKEN = register( + "is_broken" + ) { + it.codec(Codec.BOOL.fieldOf("is_broken").codec()) + } + + val CUSTOM_MINING_BLOCK_DATA = register("custom_mining_block") { + it.neverEncode() + } + + +} diff --git a/src/main/kotlin/util/mc/InitLevel.kt b/src/main/kotlin/util/mc/InitLevel.kt new file mode 100644 index 0000000..2c3eedb --- /dev/null +++ b/src/main/kotlin/util/mc/InitLevel.kt @@ -0,0 +1,25 @@ +package moe.nea.firmament.util.mc + +enum class InitLevel { + STARTING, + MC_INIT, + RENDER_INIT, + RENDER, + MAIN_MENU, + ; + + companion object { + var initLevel = InitLevel.STARTING + private set + + @JvmStatic + fun isAtLeast(wantedLevel: InitLevel): Boolean = initLevel >= wantedLevel + + @JvmStatic + fun bump(nextLevel: InitLevel) { + if (nextLevel.ordinal != initLevel.ordinal + 1) + error("Cannot bump initLevel $nextLevel from $initLevel") + initLevel = nextLevel + } + } +} diff --git a/src/main/kotlin/util/mc/IntrospectableItemModelManager.kt b/src/main/kotlin/util/mc/IntrospectableItemModelManager.kt new file mode 100644 index 0000000..e546fd3 --- /dev/null +++ b/src/main/kotlin/util/mc/IntrospectableItemModelManager.kt @@ -0,0 +1,7 @@ +package moe.nea.firmament.util.mc + +import net.minecraft.util.Identifier + +interface IntrospectableItemModelManager { + fun hasModel_firmament(identifier: Identifier): Boolean +} diff --git a/src/main/kotlin/util/mc/InventoryUtil.kt b/src/main/kotlin/util/mc/InventoryUtil.kt new file mode 100644 index 0000000..74f7b9f --- /dev/null +++ b/src/main/kotlin/util/mc/InventoryUtil.kt @@ -0,0 +1,28 @@ +package moe.nea.firmament.util.mc + +import java.util.Spliterator +import java.util.Spliterators +import net.minecraft.inventory.Inventory +import net.minecraft.item.ItemStack + +val Inventory.indices get() = 0 until size() +val Inventory.iterableView + get() = object : Iterable { + override fun spliterator(): Spliterator { + return Spliterators.spliterator(iterator(), size().toLong(), 0) + } + + override fun iterator(): Iterator { + return object : Iterator { + var i = 0 + override fun hasNext(): Boolean { + return i < size() + } + + override fun next(): ItemStack { + if (!hasNext()) throw NoSuchElementException() + return getStack(i++) + } + } + } + } diff --git a/src/main/kotlin/util/mc/ItemUtil.kt b/src/main/kotlin/util/mc/ItemUtil.kt new file mode 100644 index 0000000..13519cf --- /dev/null +++ b/src/main/kotlin/util/mc/ItemUtil.kt @@ -0,0 +1,20 @@ +package moe.nea.firmament.util.mc + +import net.minecraft.item.ItemStack +import net.minecraft.text.Text + +fun ItemStack.appendLore(args: List) { + if (args.isEmpty()) return + modifyLore { + val loreList = loreAccordingToNbt.toMutableList() + for (arg in args) { + loreList.add(arg) + } + loreList + } +} + +fun ItemStack.modifyLore(update: (List) -> List) { + val loreList = loreAccordingToNbt + loreAccordingToNbt = update(loreList) +} diff --git a/src/main/kotlin/util/mc/MCTabListAPI.kt b/src/main/kotlin/util/mc/MCTabListAPI.kt new file mode 100644 index 0000000..66bdd55 --- /dev/null +++ b/src/main/kotlin/util/mc/MCTabListAPI.kt @@ -0,0 +1,96 @@ +package moe.nea.firmament.util.mc + +import com.mojang.serialization.Codec +import com.mojang.serialization.codecs.RecordCodecBuilder +import java.util.Optional +import org.jetbrains.annotations.TestOnly +import net.minecraft.client.gui.hud.PlayerListHud +import net.minecraft.nbt.NbtOps +import net.minecraft.scoreboard.Team +import net.minecraft.text.Text +import net.minecraft.text.TextCodecs +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.commands.thenLiteral +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.events.TickEvent +import moe.nea.firmament.features.debug.DeveloperFeatures +import moe.nea.firmament.features.debug.ExportedTestConstantMeta +import moe.nea.firmament.mixins.accessor.AccessorPlayerListHud +import moe.nea.firmament.util.ClipboardUtils +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.intoOptional +import moe.nea.firmament.util.mc.SNbtFormatter.Companion.toPrettyString + +object MCTabListAPI { + + fun PlayerListHud.cast() = this as AccessorPlayerListHud + + @Subscribe + fun onTick(event: TickEvent) { + _currentTabList = null + } + + @Subscribe + fun devCommand(event: CommandEvent.SubCommand) { + event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) { + thenLiteral("copytablist") { + thenExecute { + currentTabList.body.forEach { + MC.sendChat(Text.literal(TextCodecs.CODEC.encodeStart(NbtOps.INSTANCE, it).orThrow.toString())) + } + var compound = CurrentTabList.CODEC.encodeStart(NbtOps.INSTANCE, currentTabList).orThrow + compound = ExportedTestConstantMeta.SOURCE_CODEC.encode( + ExportedTestConstantMeta.current, + NbtOps.INSTANCE, + compound + ).orThrow + ClipboardUtils.setTextContent( + compound.toPrettyString() + ) + } + } + } + } + + @get:TestOnly + @set:TestOnly + var _currentTabList: CurrentTabList? = null + + val currentTabList get() = _currentTabList ?: getTabListNow().also { _currentTabList = it } + + data class CurrentTabList( + val header: Optional, + val footer: Optional, + val body: List, + ) { + companion object { + val CODEC: Codec = RecordCodecBuilder.create { + it.group( + TextCodecs.CODEC.optionalFieldOf("header").forGetter(CurrentTabList::header), + TextCodecs.CODEC.optionalFieldOf("footer").forGetter(CurrentTabList::footer), + TextCodecs.CODEC.listOf().fieldOf("body").forGetter(CurrentTabList::body), + ).apply(it, ::CurrentTabList) + } + } + } + + private fun getTabListNow(): CurrentTabList { + // This is a precondition for PlayerListHud.collectEntries to be valid + MC.networkHandler ?: return CurrentTabList(Optional.empty(), Optional.empty(), emptyList()) + val hud = MC.inGameHud.playerListHud.cast() + val entries = hud.collectPlayerEntries_firmament() + .map { + it.displayName ?: run { + val team = it.scoreboardTeam + val name = it.profile.name + Team.decorateName(team, Text.literal(name)) + } + } + return CurrentTabList( + header = hud.header_firmament.intoOptional(), + footer = hud.footer_firmament.intoOptional(), + body = entries, + ) + } +} diff --git a/src/main/kotlin/util/mc/NbtItemData.kt b/src/main/kotlin/util/mc/NbtItemData.kt new file mode 100644 index 0000000..0c49862 --- /dev/null +++ b/src/main/kotlin/util/mc/NbtItemData.kt @@ -0,0 +1,22 @@ +package moe.nea.firmament.util.mc + +import net.minecraft.component.DataComponentTypes +import net.minecraft.component.type.LoreComponent +import net.minecraft.item.ItemStack +import net.minecraft.text.Text + +var ItemStack.loreAccordingToNbt: List + get() = get(DataComponentTypes.LORE)?.lines ?: listOf() + set(value) { + set(DataComponentTypes.LORE, LoreComponent(value)) + } + +var ItemStack.displayNameAccordingToNbt: Text + get() = get(DataComponentTypes.CUSTOM_NAME) ?: get(DataComponentTypes.ITEM_NAME) ?: item.name + set(value) { + set(DataComponentTypes.CUSTOM_NAME, value) + } + +fun ItemStack.setCustomName(text: Text) { + set(DataComponentTypes.CUSTOM_NAME, text) +} diff --git a/src/main/kotlin/util/mc/NbtPrism.kt b/src/main/kotlin/util/mc/NbtPrism.kt new file mode 100644 index 0000000..f034210 --- /dev/null +++ b/src/main/kotlin/util/mc/NbtPrism.kt @@ -0,0 +1,91 @@ +package moe.nea.firmament.util.mc + +import com.google.gson.Gson +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonPrimitive +import com.mojang.brigadier.StringReader +import com.mojang.brigadier.arguments.ArgumentType +import com.mojang.brigadier.arguments.StringArgumentType +import com.mojang.brigadier.context.CommandContext +import com.mojang.brigadier.suggestion.Suggestions +import com.mojang.brigadier.suggestion.SuggestionsBuilder +import com.mojang.serialization.JsonOps +import java.util.concurrent.CompletableFuture +import kotlin.collections.indices +import kotlin.collections.map +import kotlin.jvm.optionals.getOrNull +import net.minecraft.nbt.NbtCompound +import net.minecraft.nbt.NbtElement +import net.minecraft.nbt.NbtList +import net.minecraft.nbt.NbtOps +import net.minecraft.nbt.NbtString +import moe.nea.firmament.util.Base64Util + +class NbtPrism(val path: List) { + companion object { + fun fromElement(path: JsonElement): NbtPrism? { + if (path is JsonArray) { + return NbtPrism(path.map { (it as JsonPrimitive).asString }) + } else if (path is JsonPrimitive && path.isString) { + return NbtPrism(path.asString.split(".")) + } + return null + } + } + + object Argument : ArgumentType { + override fun parse(reader: StringReader): NbtPrism? { + return fromElement(JsonPrimitive(StringArgumentType.string().parse(reader))) + } + + override fun getExamples(): Collection? { + return listOf("some.nbt.path", "some.other.*", "some.path.*json.in.a.json.string") + } + } + + override fun toString(): String { + return "Prism($path)" + } + + fun access(root: NbtElement): Collection { + var rootSet = mutableListOf(root) + var switch = mutableListOf() + for (pathSegment in path) { + if (pathSegment == ".") continue + if (pathSegment != "*" && pathSegment.startsWith("*")) { + if (pathSegment == "*json") { + for (element in rootSet) { + val eString = element.asString().getOrNull() ?: continue + val element = Gson().fromJson(eString, JsonElement::class.java) + switch.add(JsonOps.INSTANCE.convertTo(NbtOps.INSTANCE, element)) + } + } else if (pathSegment == "*base64") { + for (element in rootSet) { + val string = element.asString().getOrNull() ?: continue + switch.add(NbtString.of(Base64Util.decodeString(string))) + } + } + } + for (element in rootSet) { + if (element is NbtList) { + if (pathSegment == "*") + switch.addAll(element) + val index = pathSegment.toIntOrNull() ?: continue + if (index !in element.indices) continue + switch.add(element[index]) + } + if (element is NbtCompound) { + if (pathSegment == "*") + element.keys.mapTo(switch) { element.get(it)!! } + switch.add(element.get(pathSegment) ?: continue) + } + } + val temp = switch + switch = rootSet + rootSet = temp + switch.clear() + } + return rootSet + } +} diff --git a/src/main/kotlin/util/mc/NbtUtil.kt b/src/main/kotlin/util/mc/NbtUtil.kt new file mode 100644 index 0000000..2cab1c7 --- /dev/null +++ b/src/main/kotlin/util/mc/NbtUtil.kt @@ -0,0 +1,10 @@ +package moe.nea.firmament.util.mc + +import net.minecraft.nbt.NbtElement +import net.minecraft.nbt.NbtList + +fun Iterable.toNbtList() = NbtList().also { + for (element in this) { + it.add(element) + } +} diff --git a/src/main/kotlin/util/mc/PlayerUtil.kt b/src/main/kotlin/util/mc/PlayerUtil.kt new file mode 100644 index 0000000..53ef1f4 --- /dev/null +++ b/src/main/kotlin/util/mc/PlayerUtil.kt @@ -0,0 +1,7 @@ +package moe.nea.firmament.util.mc + +import net.minecraft.entity.EquipmentSlot +import net.minecraft.entity.player.PlayerEntity + + +val PlayerEntity.mainHandStack get() = this.getEquippedStack(EquipmentSlot.MAINHAND) diff --git a/src/main/kotlin/util/mc/SNbtFormatter.kt b/src/main/kotlin/util/mc/SNbtFormatter.kt new file mode 100644 index 0000000..7617d17 --- /dev/null +++ b/src/main/kotlin/util/mc/SNbtFormatter.kt @@ -0,0 +1,142 @@ +package moe.nea.firmament.util.mc + +import net.minecraft.nbt.AbstractNbtList +import net.minecraft.nbt.NbtByte +import net.minecraft.nbt.NbtByteArray +import net.minecraft.nbt.NbtCompound +import net.minecraft.nbt.NbtDouble +import net.minecraft.nbt.NbtElement +import net.minecraft.nbt.NbtEnd +import net.minecraft.nbt.NbtFloat +import net.minecraft.nbt.NbtInt +import net.minecraft.nbt.NbtIntArray +import net.minecraft.nbt.NbtList +import net.minecraft.nbt.NbtLong +import net.minecraft.nbt.NbtLongArray +import net.minecraft.nbt.NbtShort +import net.minecraft.nbt.NbtString +import net.minecraft.nbt.visitor.NbtElementVisitor + +class SNbtFormatter private constructor() : NbtElementVisitor { + private val result = StringBuilder() + private var indent = 0 + private fun writeIndent() { + result.append("\t".repeat(indent)) + } + + private fun pushIndent() { + indent++ + } + + private fun popIndent() { + indent-- + } + + fun apply(element: NbtElement): StringBuilder { + element.accept(this) + return result + } + + + override fun visitString(element: NbtString) { + result.append(NbtString.escape(element.value)) + } + + override fun visitByte(element: NbtByte) { + result.append(element.numberValue()).append("b") + } + + override fun visitShort(element: NbtShort) { + result.append(element.shortValue()).append("s") + } + + override fun visitInt(element: NbtInt) { + result.append(element.intValue()) + } + + override fun visitLong(element: NbtLong) { + result.append(element.longValue()).append("L") + } + + override fun visitFloat(element: NbtFloat) { + result.append(element.floatValue()).append("f") + } + + override fun visitDouble(element: NbtDouble) { + result.append(element.doubleValue()).append("d") + } + + private fun visitArrayContents(array: AbstractNbtList) { + array.forEachIndexed { index, element -> + writeIndent() + element.accept(this) + if (array.size() != index + 1) { + result.append(",") + } + result.append("\n") + } + } + + private fun writeArray(arrayTypeTag: String, array: AbstractNbtList) { + result.append("[").append(arrayTypeTag).append("\n") + pushIndent() + visitArrayContents(array) + popIndent() + writeIndent() + result.append("]") + + } + + override fun visitByteArray(element: NbtByteArray) { + writeArray("B;", element) + } + + override fun visitIntArray(element: NbtIntArray) { + writeArray("I;", element) + } + + override fun visitLongArray(element: NbtLongArray) { + writeArray("L;", element) + } + + override fun visitList(element: NbtList) { + writeArray("", element) + } + + override fun visitCompound(compound: NbtCompound) { + result.append("{\n") + pushIndent() + val keys = compound.keys.sorted() + keys.forEachIndexed { index, key -> + writeIndent() + val element = compound[key] ?: error("Key '$key' found but not present in compound: $compound") + val escapedName = escapeName(key) + result.append(escapedName).append(": ") + element.accept(this) + if (keys.size != index + 1) { + result.append(",") + } + result.append("\n") + } + popIndent() + writeIndent() + result.append("}") + } + + override fun visitEnd(element: NbtEnd) { + result.append("END") + } + + companion object { + fun prettify(nbt: NbtElement): String { + return SNbtFormatter().apply(nbt).toString() + } + + fun NbtElement.toPrettyString() = prettify(this) + + fun escapeName(key: String): String = + if (key.matches(SIMPLE_NAME)) key else NbtString.escape(key) + + val SIMPLE_NAME = "[A-Za-z0-9._+-]+".toRegex() + } +} diff --git a/src/main/kotlin/util/mc/ScreenUtil.kt b/src/main/kotlin/util/mc/ScreenUtil.kt new file mode 100644 index 0000000..36feb6b --- /dev/null +++ b/src/main/kotlin/util/mc/ScreenUtil.kt @@ -0,0 +1,26 @@ +package moe.nea.firmament.util.mc + +import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.gui.screen.ingame.HandledScreen +import net.minecraft.entity.player.PlayerInventory +import net.minecraft.screen.slot.Slot + +object ScreenUtil { + private var lastScreen: Screen? = null + private var slotsByIndex: Map = mapOf() + + data class SlotIndex(val index: Int, val isPlayerInventory: Boolean) + + fun Screen.getSlotsByIndex(): Map { + if (this !is HandledScreen<*>) return mapOf() + if (lastScreen === this) return slotsByIndex + lastScreen = this + slotsByIndex = this.screenHandler.slots.associate { + SlotIndex(it.index, it.inventory is PlayerInventory) to it + } + return slotsByIndex + } + + fun Screen.getSlotByIndex( index: Int, isPlayerInventory: Boolean): Slot? = + getSlotsByIndex()[SlotIndex(index, isPlayerInventory)] +} diff --git a/src/main/kotlin/util/mc/SkullItemData.kt b/src/main/kotlin/util/mc/SkullItemData.kt new file mode 100644 index 0000000..3a4c508 --- /dev/null +++ b/src/main/kotlin/util/mc/SkullItemData.kt @@ -0,0 +1,86 @@ +@file:UseSerializers(DashlessUUIDSerializer::class, InstantAsLongSerializer::class) + +package moe.nea.firmament.util.mc + +import com.mojang.authlib.GameProfile +import com.mojang.authlib.minecraft.MinecraftProfileTexture +import com.mojang.authlib.properties.Property +import java.time.Instant +import java.util.UUID +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers +import net.minecraft.component.DataComponentTypes +import net.minecraft.component.type.ProfileComponent +import net.minecraft.item.ItemStack +import net.minecraft.item.Items +import moe.nea.firmament.Firmament +import moe.nea.firmament.util.Base64Util.padToValidBase64 +import moe.nea.firmament.util.assertTrueOr +import moe.nea.firmament.util.json.DashlessUUIDSerializer +import moe.nea.firmament.util.json.InstantAsLongSerializer + +@Serializable +data class MinecraftProfileTextureKt( + val url: String, + val metadata: Map = mapOf(), +) + +@Serializable +data class MinecraftTexturesPayloadKt( + val textures: Map = mapOf(), + val profileId: UUID? = null, + val profileName: String? = null, + val isPublic: Boolean = true, + val timestamp: Instant = Instant.now(), +) + +fun GameProfile.setTextures(textures: MinecraftTexturesPayloadKt) { + val json = Firmament.json.encodeToString(textures) + val encoded = java.util.Base64.getEncoder().encodeToString(json.encodeToByteArray()) + properties.put(propertyTextures, Property(propertyTextures, encoded)) +} + +private val propertyTextures = "textures" + +fun ItemStack.setEncodedSkullOwner(uuid: UUID, encodedData: String) { + assert(this.item == Items.PLAYER_HEAD) + val gameProfile = GameProfile(uuid, "LameGuy123") + gameProfile.properties.put(propertyTextures, Property(propertyTextures, encodedData.padToValidBase64())) + this.set(DataComponentTypes.PROFILE, ProfileComponent(gameProfile)) +} + +val arbitraryUUID = UUID.fromString("d3cb85e2-3075-48a1-b213-a9bfb62360c1") +fun createSkullItem(uuid: UUID, url: String) = ItemStack(Items.PLAYER_HEAD) + .also { it.setSkullOwner(uuid, url) } + +fun ItemStack.setSkullOwner(uuid: UUID, url: String) { + assert(this.item == Items.PLAYER_HEAD) + val gameProfile = GameProfile(uuid, "nea89") + gameProfile.setTextures( + MinecraftTexturesPayloadKt( + textures = mapOf(MinecraftProfileTexture.Type.SKIN to MinecraftProfileTextureKt(url)), + profileId = uuid, + profileName = "nea89", + ) + ) + this.set(DataComponentTypes.PROFILE, ProfileComponent(gameProfile)) +} + + +fun decodeProfileTextureProperty(property: Property): MinecraftTexturesPayloadKt? { + assertTrueOr(property.name == propertyTextures) { return null } + return try { + var encodedF: String = property.value + while (encodedF.length % 4 != 0 && encodedF.last() == '=') { + encodedF = encodedF.substring(0, encodedF.length - 1) + } + val json = java.util.Base64.getDecoder().decode(encodedF).decodeToString() + Firmament.json.decodeFromString(json) + } catch (e: Exception) { + // Malformed profile data + if (Firmament.DEBUG) + e.printStackTrace() + null + } +} + diff --git a/src/main/kotlin/util/mc/SlotUtils.kt b/src/main/kotlin/util/mc/SlotUtils.kt new file mode 100644 index 0000000..9eb4918 --- /dev/null +++ b/src/main/kotlin/util/mc/SlotUtils.kt @@ -0,0 +1,47 @@ +package moe.nea.firmament.util.mc + +import org.lwjgl.glfw.GLFW +import net.minecraft.screen.ScreenHandler +import net.minecraft.screen.slot.Slot +import net.minecraft.screen.slot.SlotActionType +import moe.nea.firmament.util.MC + +object SlotUtils { + fun Slot.clickMiddleMouseButton(handler: ScreenHandler) { + MC.interactionManager?.clickSlot( + handler.syncId, + this.id, + GLFW.GLFW_MOUSE_BUTTON_MIDDLE, + SlotActionType.CLONE, + MC.player + ) + } + + fun Slot.swapWithHotBar(handler: ScreenHandler, hotbarIndex: Int) { + MC.interactionManager?.clickSlot( + handler.syncId, this.id, + hotbarIndex, SlotActionType.SWAP, + MC.player + ) + } + + fun Slot.clickRightMouseButton(handler: ScreenHandler) { + MC.interactionManager?.clickSlot( + handler.syncId, + this.id, + GLFW.GLFW_MOUSE_BUTTON_RIGHT, + SlotActionType.PICKUP, + MC.player + ) + } + + fun Slot.clickLeftMouseButton(handler: ScreenHandler) { + MC.interactionManager?.clickSlot( + handler.syncId, + this.id, + GLFW.GLFW_MOUSE_BUTTON_LEFT, + SlotActionType.PICKUP, + MC.player + ) + } +} diff --git a/src/main/kotlin/util/mc/TolerantRegistriesOps.kt b/src/main/kotlin/util/mc/TolerantRegistriesOps.kt new file mode 100644 index 0000000..ce596a0 --- /dev/null +++ b/src/main/kotlin/util/mc/TolerantRegistriesOps.kt @@ -0,0 +1,29 @@ +package moe.nea.firmament.util.mc + +import com.mojang.serialization.DynamicOps +import java.util.Optional +import net.minecraft.registry.Registry +import net.minecraft.registry.RegistryKey +import net.minecraft.registry.RegistryOps +import net.minecraft.registry.RegistryWrapper +import net.minecraft.registry.entry.RegistryEntryOwner + +class TolerantRegistriesOps( + delegate: DynamicOps, + registryInfoGetter: RegistryInfoGetter +) : RegistryOps(delegate, registryInfoGetter) { + constructor(delegate: DynamicOps, registry: RegistryWrapper.WrapperLookup) : + this(delegate, CachedRegistryInfoGetter(registry)) + + class TolerantOwner : RegistryEntryOwner { + override fun ownerEquals(other: RegistryEntryOwner?): Boolean { + return true + } + } + + override fun getOwner(registryRef: RegistryKey>?): Optional> { + return super.getOwner(registryRef).map { + TolerantOwner() + } + } +} diff --git a/src/main/kotlin/util/mc/asFakeServer.kt b/src/main/kotlin/util/mc/asFakeServer.kt new file mode 100644 index 0000000..d3811bd --- /dev/null +++ b/src/main/kotlin/util/mc/asFakeServer.kt @@ -0,0 +1,37 @@ +package moe.nea.firmament.util.mc + +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource +import net.minecraft.server.command.CommandOutput +import net.minecraft.server.command.ServerCommandSource +import net.minecraft.text.Text + +fun FabricClientCommandSource.asFakeServer(): ServerCommandSource { + val source = this + return ServerCommandSource( + object : CommandOutput { + override fun sendMessage(message: Text?) { + source.player.sendMessage(message, false) + } + + override fun shouldReceiveFeedback(): Boolean { + return true + } + + override fun shouldTrackOutput(): Boolean { + return true + } + + override fun shouldBroadcastConsoleToOps(): Boolean { + return true + } + }, + source.position, + source.rotation, + null, + 0, + "FakeServerCommandSource", + Text.literal("FakeServerCommandSource"), + null, + source.player + ) +} diff --git a/src/main/kotlin/util/propertyutil.kt b/src/main/kotlin/util/propertyutil.kt new file mode 100644 index 0000000..795a0d2 --- /dev/null +++ b/src/main/kotlin/util/propertyutil.kt @@ -0,0 +1,9 @@ + + +package moe.nea.firmament.util + +import kotlin.properties.ReadOnlyProperty + +fun ReadOnlyProperty.map(mapper: (V) -> M): ReadOnlyProperty { + return ReadOnlyProperty { thisRef, property -> mapper(this@map.getValue(thisRef, property)) } +} diff --git a/src/main/kotlin/util/regex.kt b/src/main/kotlin/util/regex.kt new file mode 100644 index 0000000..be6bcfb --- /dev/null +++ b/src/main/kotlin/util/regex.kt @@ -0,0 +1,73 @@ +@file:OptIn(ExperimentalTypeInference::class, ExperimentalContracts::class) + +package moe.nea.firmament.util + +import java.util.regex.Matcher +import java.util.regex.Pattern +import org.intellij.lang.annotations.Language +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract +import kotlin.experimental.ExperimentalTypeInference +import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds + +inline fun String.ifMatches(regex: Regex, block: (MatchResult) -> T): T? = + regex.matchEntire(this)?.let(block) + +inline fun Pattern.useMatch(string: String?, block: Matcher.() -> T): T? { + contract { + callsInPlace(block, InvocationKind.AT_MOST_ONCE) + } + return string + ?.let(this::matcher) + ?.takeIf(Matcher::matches) + ?.let(block) +} + +fun String.ifDropLast(suffix: String, block: (String) -> T): T? { + if (endsWith(suffix)) { + return block(dropLast(suffix.length)) + } + return null +} + +@Language("RegExp") +val TIME_PATTERN = "[0-9]+[ms]" + +@Language("RegExp") +val SHORT_NUMBER_FORMAT = "[0-9]+(?:,[0-9]+)*(?:\\.[0-9]+)?[kKmMbB]?" + + +val siScalars = mapOf( + 'k' to 1_000.0, + 'K' to 1_000.0, + 'm' to 1_000_000.0, + 'M' to 1_000_000.0, + 'b' to 1_000_000_000.0, + 'B' to 1_000_000_000.0, +) + +fun parseTimePattern(text: String): Duration { + val length = text.dropLast(1).toInt() + return when (text.last()) { + 'm' -> length.minutes + 's' -> length.seconds + else -> error("Invalid pattern for time $text") + } +} + +fun parseShortNumber(string: String): Double { + if (string.startsWith("-")) return -parseShortNumber(string.substring(1)) + if (string.startsWith("+")) return parseShortNumber(string.substring(1)) + var k = string.replace(",", "") + val scalar = k.last() + var scalarMultiplier = siScalars[scalar] + if (scalarMultiplier == null) { + scalarMultiplier = 1.0 + } else { + k = k.dropLast(1) + } + return k.toDouble() * scalarMultiplier +} diff --git a/src/main/kotlin/util/render/CustomRenderLayers.kt b/src/main/kotlin/util/render/CustomRenderLayers.kt new file mode 100644 index 0000000..2da1de7 --- /dev/null +++ b/src/main/kotlin/util/render/CustomRenderLayers.kt @@ -0,0 +1,106 @@ +package util.render + +import com.mojang.blaze3d.pipeline.BlendFunction +import com.mojang.blaze3d.pipeline.RenderPipeline +import com.mojang.blaze3d.platform.DepthTestFunction +import com.mojang.blaze3d.vertex.VertexFormat.DrawMode +import java.util.function.Function +import net.minecraft.client.gl.RenderPipelines +import net.minecraft.client.gl.UniformType +import net.minecraft.client.render.RenderLayer +import net.minecraft.client.render.RenderPhase +import net.minecraft.client.render.VertexFormats +import net.minecraft.util.Identifier +import net.minecraft.util.TriState +import net.minecraft.util.Util +import moe.nea.firmament.Firmament + +object CustomRenderPipelines { + val GUI_TEXTURED_NO_DEPTH_TRIS = + RenderPipeline.builder(RenderPipelines.POSITION_TEX_COLOR_SNIPPET) + .withVertexFormat(VertexFormats.POSITION_TEXTURE_COLOR, DrawMode.TRIANGLES) + .withLocation(Firmament.identifier("gui_textured_overlay_tris")) + .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) + .withCull(false) + .withDepthWrite(false) + .build() + val OMNIPRESENT_LINES = RenderPipeline + .builder(RenderPipelines.RENDERTYPE_LINES_SNIPPET) + .withLocation(Firmament.identifier("lines")) + .withDepthWrite(false) + .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) + .build() + val COLORED_OMNIPRESENT_QUADS = + RenderPipeline.builder(RenderPipelines.MATRICES_COLOR_SNIPPET)// TODO: split this up to support better transparent ordering. + .withLocation(Firmament.identifier("colored_omnipresent_quads")) + .withVertexShader("core/position_color") + .withFragmentShader("core/position_color") + .withVertexFormat(VertexFormats.POSITION_COLOR, DrawMode.QUADS) + .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST) + .withCull(false) + .withDepthWrite(false) + .withBlend(BlendFunction.TRANSLUCENT) + .build() + + val CIRCLE_FILTER_TRANSLUCENT_GUI_TRIS = + RenderPipeline.builder(RenderPipelines.POSITION_TEX_COLOR_SNIPPET) + .withVertexFormat(VertexFormats.POSITION_TEXTURE_COLOR, DrawMode.TRIANGLES) + .withLocation(Firmament.identifier("gui_textured_overlay_tris_circle")) + .withUniform("InnerCutoutRadius", UniformType.FLOAT) + .withFragmentShader(Firmament.identifier("circle_discard_color")) + .withBlend(BlendFunction.TRANSLUCENT) + .build() + val PARALLAX_CAPE_SHADER = + RenderPipeline.builder(RenderPipelines.ENTITY_SNIPPET) + .withLocation(Firmament.identifier("parallax_cape")) + .withFragmentShader(Firmament.identifier("cape/parallax")) + .withSampler("Sampler0") + .withSampler("Sampler1") + .withSampler("Sampler3") + .withUniform("Animation", UniformType.FLOAT) + .build() +} + +object CustomRenderLayers { + inline fun memoizeTextured(crossinline func: (Identifier) -> RenderLayer) = memoize(func) + inline fun memoize(crossinline func: (T) -> R): Function { + return Util.memoize { it: T -> func(it) } + } + + val GUI_TEXTURED_NO_DEPTH_TRIS = memoizeTextured { texture -> + RenderLayer.of( + "firmament_gui_textured_overlay_tris", + RenderLayer.DEFAULT_BUFFER_SIZE, + CustomRenderPipelines.GUI_TEXTURED_NO_DEPTH_TRIS, + RenderLayer.MultiPhaseParameters.builder().texture( + RenderPhase.Texture(texture, TriState.DEFAULT, false) + ) + .build(false) + ) + } + val LINES = RenderLayer.of( + "firmament_lines", + RenderLayer.DEFAULT_BUFFER_SIZE, + CustomRenderPipelines.OMNIPRESENT_LINES, + RenderLayer.MultiPhaseParameters.builder() // TODO: accept linewidth here + .build(false) + ) + val COLORED_QUADS = RenderLayer.of( + "firmament_quads", + RenderLayer.DEFAULT_BUFFER_SIZE, + false, true, + CustomRenderPipelines.COLORED_OMNIPRESENT_QUADS, + RenderLayer.MultiPhaseParameters.builder() + .lightmap(RenderPhase.DISABLE_LIGHTMAP) + .build(false) + ) + + val TRANSLUCENT_CIRCLE_GUI = + RenderLayer.of( + "firmament_circle_gui", + RenderLayer.DEFAULT_BUFFER_SIZE, + CustomRenderPipelines.CIRCLE_FILTER_TRANSLUCENT_GUI_TRIS, + RenderLayer.MultiPhaseParameters.builder() + .build(false) + ) +} diff --git a/src/main/kotlin/util/render/DrawContextExt.kt b/src/main/kotlin/util/render/DrawContextExt.kt new file mode 100644 index 0000000..a833c86 --- /dev/null +++ b/src/main/kotlin/util/render/DrawContextExt.kt @@ -0,0 +1,68 @@ +package moe.nea.firmament.util.render + +import com.mojang.blaze3d.systems.RenderSystem +import me.shedaniel.math.Color +import org.joml.Matrix4f +import util.render.CustomRenderLayers +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.render.RenderLayer +import net.minecraft.util.Identifier +import moe.nea.firmament.util.MC + +fun DrawContext.isUntranslatedGuiDrawContext(): Boolean { + return (matrices.peek().positionMatrix.properties() and Matrix4f.PROPERTY_IDENTITY.toInt()) != 0 +} + +@Deprecated("Use the other drawGuiTexture") +fun DrawContext.drawGuiTexture( + x: Int, y: Int, z: Int, width: Int, height: Int, sprite: Identifier +) = this.drawGuiTexture(RenderLayer::getGuiTextured, sprite, x, y, width, height) + +fun DrawContext.drawGuiTexture( + sprite: Identifier, + x: Int, y: Int, width: Int, height: Int +) = this.drawGuiTexture(RenderLayer::getGuiTextured, sprite, x, y, width, height) + +fun DrawContext.drawTexture( + sprite: Identifier, + x: Int, + y: Int, + u: Float, + v: Float, + width: Int, + height: Int, + textureWidth: Int, + textureHeight: Int +) { + this.drawTexture(RenderLayer::getGuiTextured, + sprite, + x, + y, + u, + v, + width, + height, + width, + height, + textureWidth, + textureHeight) +} + +fun DrawContext.drawLine(fromX: Int, fromY: Int, toX: Int, toY: Int, color: Color) { + // TODO: push scissors + // TODO: use matrix translations and a different render layer + if (toY < fromY) { + drawLine(toX, toY, fromX, fromY, color) + return + } + RenderSystem.lineWidth(MC.window.scaleFactor.toFloat()) + draw { vertexConsumers -> + val buf = vertexConsumers.getBuffer(CustomRenderLayers.LINES) + val matrix = this.matrices.peek() + buf.vertex(matrix, fromX.toFloat(), fromY.toFloat(), 0F).color(color.color) + .normal(toX - fromX.toFloat(), toY - fromY.toFloat(), 0F) + buf.vertex(matrix, toX.toFloat(), toY.toFloat(), 0F).color(color.color) + .normal(toX - fromX.toFloat(), toY - fromY.toFloat(), 0F) + } +} + diff --git a/src/main/kotlin/util/render/FacingThePlayerContext.kt b/src/main/kotlin/util/render/FacingThePlayerContext.kt new file mode 100644 index 0000000..670beb6 --- /dev/null +++ b/src/main/kotlin/util/render/FacingThePlayerContext.kt @@ -0,0 +1,92 @@ + +package moe.nea.firmament.util.render + +import io.github.notenoughupdates.moulconfig.platform.next +import org.joml.Matrix4f +import net.minecraft.client.font.TextRenderer +import net.minecraft.client.render.LightmapTextureManager +import net.minecraft.client.render.RenderLayer +import net.minecraft.client.render.VertexConsumer +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import net.minecraft.util.math.BlockPos +import moe.nea.firmament.util.FirmFormatters +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.assertTrueOr + +@RenderContextDSL +class FacingThePlayerContext(val worldContext: RenderInWorldContext) { + val matrixStack by worldContext::matrixStack + fun waypoint(position: BlockPos, label: Text) { + text( + label, + Text.literal("§e${FirmFormatters.formatDistance(MC.player?.pos?.distanceTo(position.toCenterPos()) ?: 42069.0)}") + ) + } + + fun text( + vararg texts: Text, + verticalAlign: RenderInWorldContext.VerticalAlign = RenderInWorldContext.VerticalAlign.CENTER, + background: Int = 0x70808080, + ) { + assertTrueOr(texts.isNotEmpty()) { return@text } + for ((index, text) in texts.withIndex()) { + worldContext.matrixStack.push() + val width = MC.font.getWidth(text) + worldContext.matrixStack.translate(-width / 2F, verticalAlign.align(index, texts.size), 0F) + val vertexConsumer: VertexConsumer = + worldContext.vertexConsumers.getBuffer(RenderLayer.getTextBackgroundSeeThrough()) + val matrix4f = worldContext.matrixStack.peek().positionMatrix + vertexConsumer.vertex(matrix4f, -1.0f, -1.0f, 0.0f).color(background) + .light(LightmapTextureManager.MAX_BLOCK_LIGHT_COORDINATE).next() + vertexConsumer.vertex(matrix4f, -1.0f, MC.font.fontHeight.toFloat(), 0.0f).color(background) + .light(LightmapTextureManager.MAX_BLOCK_LIGHT_COORDINATE).next() + vertexConsumer.vertex(matrix4f, width.toFloat(), MC.font.fontHeight.toFloat(), 0.0f) + .color(background) + .light(LightmapTextureManager.MAX_BLOCK_LIGHT_COORDINATE).next() + vertexConsumer.vertex(matrix4f, width.toFloat(), -1.0f, 0.0f).color(background) + .light(LightmapTextureManager.MAX_BLOCK_LIGHT_COORDINATE).next() + worldContext.matrixStack.translate(0F, 0F, 0.01F) + + MC.font.draw( + text, + 0F, + 0F, + -1, + false, + worldContext.matrixStack.peek().positionMatrix, + worldContext.vertexConsumers, + TextRenderer.TextLayerType.SEE_THROUGH, + 0, + LightmapTextureManager.MAX_LIGHT_COORDINATE + ) + worldContext.matrixStack.pop() + } + } + + + fun texture( + texture: Identifier, width: Int, height: Int, + u1: Float, v1: Float, + u2: Float, v2: Float, + ) { + val buf = worldContext.vertexConsumers.getBuffer(RenderLayer.getGuiTexturedOverlay(texture)) + val hw = width / 2F + val hh = height / 2F + val matrix4f: Matrix4f = worldContext.matrixStack.peek().positionMatrix + buf.vertex(matrix4f, -hw, -hh, 0F) + .color(-1) + .texture(u1, v1).next() + buf.vertex(matrix4f, -hw, +hh, 0F) + .color(-1) + .texture(u1, v2).next() + buf.vertex(matrix4f, +hw, +hh, 0F) + .color(-1) + .texture(u2, v2).next() + buf.vertex(matrix4f, +hw, -hh, 0F) + .color(-1) + .texture(u2, v1).next() + worldContext.vertexConsumers.draw() + } + +} diff --git a/src/main/kotlin/util/render/FirmamentShaders.kt b/src/main/kotlin/util/render/FirmamentShaders.kt new file mode 100644 index 0000000..cc6cd49 --- /dev/null +++ b/src/main/kotlin/util/render/FirmamentShaders.kt @@ -0,0 +1,20 @@ +package moe.nea.firmament.util.render + +import com.mojang.blaze3d.vertex.VertexFormat +import net.minecraft.client.gl.CompiledShader +import net.minecraft.client.gl.Defines +import net.minecraft.client.gl.ShaderProgram +import net.minecraft.client.render.RenderPhase +import net.minecraft.client.render.VertexFormats +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.DebugInstantiateEvent +import moe.nea.firmament.util.MC + +object FirmamentShaders { + + @Subscribe + fun debugLoad(event: DebugInstantiateEvent) { + // TODO: do i still need to work with shaders like this? + } +} diff --git a/src/main/kotlin/util/render/LerpUtils.kt b/src/main/kotlin/util/render/LerpUtils.kt new file mode 100644 index 0000000..e7f226c --- /dev/null +++ b/src/main/kotlin/util/render/LerpUtils.kt @@ -0,0 +1,40 @@ +package moe.nea.firmament.util.render + +import me.shedaniel.math.Color +import kotlin.math.absoluteValue + +val π = Math.PI +val τ = Math.PI * 2 +fun lerpAngle(a: Float, b: Float, progress: Float): Float { + // TODO: there is at least 10 mods to many in here lol + if (((b - a).absoluteValue - π).absoluteValue < 0.0001) { + return lerp(a, b, progress) + } + val shortestAngle = ((((b.mod(τ) - a.mod(τ)).mod(τ)) + τ + π).mod(τ)) - π + return ((a + (shortestAngle) * progress).mod(τ)).toFloat() +} + +fun wrapAngle(angle: Float): Float = (angle.mod(τ) + τ).mod(τ).toFloat() +fun wrapAngle(angle: Double): Double = (angle.mod(τ) + τ).mod(τ) + +fun lerp(a: Float, b: Float, progress: Float): Float { + return a + (b - a) * progress +} + +fun lerp(a: Int, b: Int, progress: Float): Int { + return (a + (b - a) * progress).toInt() +} + +fun ilerp(a: Float, b: Float, value: Float): Float { + return (value - a) / (b - a) +} + +fun lerp(a: Color, b: Color, progress: Float): Color { + return Color.ofRGBA( + lerp(a.red, b.red, progress), + lerp(a.green, b.green, progress), + lerp(a.blue, b.blue, progress), + lerp(a.alpha, b.alpha, progress), + ) +} + diff --git a/src/main/kotlin/util/render/RenderCircleProgress.kt b/src/main/kotlin/util/render/RenderCircleProgress.kt new file mode 100644 index 0000000..81dde6f --- /dev/null +++ b/src/main/kotlin/util/render/RenderCircleProgress.kt @@ -0,0 +1,101 @@ +package moe.nea.firmament.util.render + +import com.mojang.blaze3d.systems.RenderSystem +import com.mojang.blaze3d.vertex.VertexFormat +import io.github.notenoughupdates.moulconfig.platform.next +import java.util.OptionalInt +import org.joml.Matrix4f +import util.render.CustomRenderLayers +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.render.BufferBuilder +import net.minecraft.client.render.RenderLayer +import net.minecraft.client.util.BufferAllocator +import net.minecraft.util.Identifier +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.collections.nonNegligibleSubSectionsAlignedWith +import moe.nea.firmament.util.math.Projections + +object RenderCircleProgress { + + fun renderCircularSlice( + drawContext: DrawContext, + layer: RenderLayer, + u1: Float, + u2: Float, + v1: Float, + v2: Float, + angleRadians: ClosedFloatingPointRange, + color: Int = -1, + innerCutoutRadius: Float = 0F + ) { + drawContext.draw() + val sections = angleRadians.nonNegligibleSubSectionsAlignedWith((τ / 8f).toFloat()) + .zipWithNext().toList() + BufferAllocator(layer.vertexFormat.vertexSize * sections.size * 3).use { allocator -> + + val bufferBuilder = BufferBuilder(allocator, VertexFormat.DrawMode.TRIANGLES, layer.vertexFormat) + val matrix: Matrix4f = drawContext.matrices.peek().positionMatrix + + for ((sectionStart, sectionEnd) in sections) { + val firstPoint = Projections.Two.projectAngleOntoUnitBox(sectionStart.toDouble()) + val secondPoint = Projections.Two.projectAngleOntoUnitBox(sectionEnd.toDouble()) + fun ilerp(f: Float): Float = + ilerp(-1f, 1f, f) + + bufferBuilder + .vertex(matrix, secondPoint.x, secondPoint.y, 0F) + .texture(lerp(u1, u2, ilerp(secondPoint.x)), lerp(v1, v2, ilerp(secondPoint.y))) + .color(color) + .next() + bufferBuilder + .vertex(matrix, firstPoint.x, firstPoint.y, 0F) + .texture(lerp(u1, u2, ilerp(firstPoint.x)), lerp(v1, v2, ilerp(firstPoint.y))) + .color(color) + .next() + bufferBuilder + .vertex(matrix, 0F, 0F, 0F) + .texture(lerp(u1, u2, ilerp(0F)), lerp(v1, v2, ilerp(0F))) + .color(color) + .next() + } + + bufferBuilder.end().use { buffer -> + // TODO: write a better utility to pass uniforms :sob: ill even take a mixin at this point + if (innerCutoutRadius <= 0) { + layer.draw(buffer) + return + } + val vertexBuffer = layer.vertexFormat.uploadImmediateVertexBuffer(buffer.buffer) + val indexBufferConstructor = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.TRIANGLES) + val indexBuffer = indexBufferConstructor.getIndexBuffer(buffer.drawParameters.indexCount) + RenderSystem.getDevice().createCommandEncoder().createRenderPass( + MC.instance.framebuffer.colorAttachment, + OptionalInt.empty(), + ).use { renderPass -> + renderPass.setPipeline(layer.pipeline) + renderPass.setUniform("InnerCutoutRadius", innerCutoutRadius) + renderPass.setIndexBuffer(indexBuffer, indexBufferConstructor.indexType) + renderPass.setVertexBuffer(0, vertexBuffer) + renderPass.drawIndexed(0, buffer.drawParameters.indexCount) + } + } + } + } + + fun renderCircle( + drawContext: DrawContext, + texture: Identifier, + progress: Float, + u1: Float, + u2: Float, + v1: Float, + v2: Float, + ) { + renderCircularSlice( + drawContext, + CustomRenderLayers.GUI_TEXTURED_NO_DEPTH_TRIS.apply(texture), + u1, u2, v1, v2, + (-τ / 4).toFloat()..(progress * τ - τ / 4).toFloat() + ) + } +} diff --git a/src/main/kotlin/util/render/RenderContextDSL.kt b/src/main/kotlin/util/render/RenderContextDSL.kt new file mode 100644 index 0000000..9bb4431 --- /dev/null +++ b/src/main/kotlin/util/render/RenderContextDSL.kt @@ -0,0 +1,6 @@ + +package moe.nea.firmament.util.render + +@DslMarker +annotation class RenderContextDSL { +} diff --git a/src/main/kotlin/util/render/RenderInWorldContext.kt b/src/main/kotlin/util/render/RenderInWorldContext.kt new file mode 100644 index 0000000..c30ee19 --- /dev/null +++ b/src/main/kotlin/util/render/RenderInWorldContext.kt @@ -0,0 +1,316 @@ +package moe.nea.firmament.util.render + +import com.mojang.blaze3d.systems.RenderSystem +import io.github.notenoughupdates.moulconfig.platform.next +import java.lang.Math.pow +import org.joml.Matrix4f +import org.joml.Vector3f +import util.render.CustomRenderLayers +import net.minecraft.client.render.Camera +import net.minecraft.client.render.RenderLayer +import net.minecraft.client.render.RenderTickCounter +import net.minecraft.client.render.VertexConsumer +import net.minecraft.client.render.VertexConsumerProvider +import net.minecraft.client.texture.Sprite +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Vec3d +import moe.nea.firmament.events.WorldRenderLastEvent +import moe.nea.firmament.util.FirmFormatters +import moe.nea.firmament.util.MC + +@RenderContextDSL +class RenderInWorldContext private constructor( + val matrixStack: MatrixStack, + private val camera: Camera, + private val tickCounter: RenderTickCounter, + val vertexConsumers: VertexConsumerProvider.Immediate, +) { + + + @Deprecated("stateful color management is no longer a thing") + fun color(color: me.shedaniel.math.Color) { + color(color.red / 255F, color.green / 255f, color.blue / 255f, color.alpha / 255f) + } + + @Deprecated("stateful color management is no longer a thing") + fun color(red: Float, green: Float, blue: Float, alpha: Float) { + RenderSystem.setShaderColor(red, green, blue, alpha) + } + + fun block(blockPos: BlockPos, color: Int) { + matrixStack.push() + matrixStack.translate(blockPos.x.toFloat(), blockPos.y.toFloat(), blockPos.z.toFloat()) + buildCube(matrixStack.peek().positionMatrix, vertexConsumers.getBuffer(CustomRenderLayers.COLORED_QUADS), color) + matrixStack.pop() + } + + fun sharedVoxelSurface(blocks: Set, color: Int) { + val m = BlockPos.Mutable() + val l = vertexConsumers.getBuffer(CustomRenderLayers.COLORED_QUADS) + blocks.forEach { + matrixStack.push() + matrixStack.translate(it.x.toFloat(), it.y.toFloat(), it.z.toFloat()) + val p = matrixStack.peek().positionMatrix + m.set(it) + if (m.setX(it.x + 1) !in blocks) { + buildFaceXP(p, l, color) + } + if (m.setX(it.x - 1) !in blocks) { + buildFaceXN(p, l, color) + } + m.set(it) + if (m.setY(it.y + 1) !in blocks) { + buildFaceYP(p, l, color) + } + if (m.setY(it.y - 1) !in blocks) { + buildFaceYN(p, l, color) + } + m.set(it) + if (m.setZ(it.z + 1) !in blocks) { + buildFaceZP(p, l, color) + } + if (m.setZ(it.z - 1) !in blocks) { + buildFaceZN(p, l, color) + } + matrixStack.pop() + } + } + + enum class VerticalAlign { + TOP, BOTTOM, CENTER; + + fun align(index: Int, count: Int): Float { + return when (this) { + CENTER -> (index - count / 2F) * (1 + MC.font.fontHeight.toFloat()) + BOTTOM -> (index - count) * (1 + MC.font.fontHeight.toFloat()) + TOP -> (index) * (1 + MC.font.fontHeight.toFloat()) + } + } + } + + fun waypoint(position: BlockPos, vararg label: Text) { + text( + position.toCenterPos(), + *label, + Text.literal("§e${FirmFormatters.formatDistance(MC.player?.pos?.distanceTo(position.toCenterPos()) ?: 42069.0)}"), + background = 0xAA202020.toInt() + ) + } + + fun withFacingThePlayer(position: Vec3d, block: FacingThePlayerContext.() -> Unit) { + matrixStack.push() + matrixStack.translate(position.x, position.y, position.z) + val actualCameraDistance = position.distanceTo(camera.pos) + val distanceToMoveTowardsCamera = if (actualCameraDistance < 10) 0.0 else -(actualCameraDistance - 10.0) + val vec = position.subtract(camera.pos).multiply(distanceToMoveTowardsCamera / actualCameraDistance) + matrixStack.translate(vec.x, vec.y, vec.z) + matrixStack.multiply(camera.rotation) + matrixStack.scale(0.025F, -0.025F, 1F) + + FacingThePlayerContext(this).run(block) + + matrixStack.pop() + vertexConsumers.drawCurrentLayer() + } + + fun sprite(position: Vec3d, sprite: Sprite, width: Int, height: Int) { + texture( + position, sprite.atlasId, width, height, sprite.minU, sprite.minV, sprite.maxU, sprite.maxV + ) + } + + fun texture( + position: Vec3d, texture: Identifier, width: Int, height: Int, + u1: Float, v1: Float, + u2: Float, v2: Float, + ) { + withFacingThePlayer(position) { + texture(texture, width, height, u1, v1, u2, v2) + } + } + + fun text( + position: Vec3d, + vararg texts: Text, + verticalAlign: VerticalAlign = VerticalAlign.CENTER, + background: Int = 0x70808080 + ) { + withFacingThePlayer(position) { + text(*texts, verticalAlign = verticalAlign, background = background) + } + } + + fun tinyBlock(vec3d: Vec3d, size: Float, color: Int) { + matrixStack.push() + matrixStack.translate(vec3d.x, vec3d.y, vec3d.z) + matrixStack.scale(size, size, size) + matrixStack.translate(-.5, -.5, -.5) + buildCube(matrixStack.peek().positionMatrix, vertexConsumers.getBuffer(CustomRenderLayers.COLORED_QUADS), color) + matrixStack.pop() + vertexConsumers.draw() + } + + fun wireframeCube(blockPos: BlockPos, lineWidth: Float = 10F) { + val buf = vertexConsumers.getBuffer(RenderLayer.LINES) + matrixStack.push() + // TODO: this does not render through blocks (or water layers) anymore + RenderSystem.lineWidth(lineWidth / pow(camera.pos.squaredDistanceTo(blockPos.toCenterPos()), 0.25).toFloat()) + matrixStack.translate(blockPos.x.toFloat(), blockPos.y.toFloat(), blockPos.z.toFloat()) + buildWireFrameCube(matrixStack.peek(), buf) + matrixStack.pop() + vertexConsumers.draw() + } + + fun line(vararg points: Vec3d, lineWidth: Float = 10F) { + line(points.toList(), lineWidth) + } + + fun tracer(toWhere: Vec3d, lineWidth: Float = 3f) { + val cameraForward = Vector3f(0f, 0f, -1f).rotate(camera.rotation) + line(camera.pos.add(Vec3d(cameraForward)), toWhere, lineWidth = lineWidth) + } + + fun line(points: List, lineWidth: Float = 10F) { + RenderSystem.lineWidth(lineWidth) + val buffer = vertexConsumers.getBuffer(CustomRenderLayers.LINES) + + val matrix = matrixStack.peek() + var lastNormal: Vector3f? = null + points.zipWithNext().forEach { (a, b) -> + val normal = Vector3f(b.x.toFloat(), b.y.toFloat(), b.z.toFloat()) + .sub(a.x.toFloat(), a.y.toFloat(), a.z.toFloat()) + .normalize() + val lastNormal0 = lastNormal ?: normal + lastNormal = normal + buffer.vertex(matrix.positionMatrix, a.x.toFloat(), a.y.toFloat(), a.z.toFloat()) + .color(-1) + .normal(matrix, lastNormal0.x, lastNormal0.y, lastNormal0.z) + .next() + buffer.vertex(matrix.positionMatrix, b.x.toFloat(), b.y.toFloat(), b.z.toFloat()) + .color(-1) + .normal(matrix, normal.x, normal.y, normal.z) + .next() + } + + } + // TODO: put the favourite icons in front of items again + + companion object { + private fun doLine( + matrix: MatrixStack.Entry, + buf: VertexConsumer, + i: Float, + j: Float, + k: Float, + x: Float, + y: Float, + z: Float + ) { + val normal = Vector3f(x, y, z) + .sub(i, j, k) + .normalize() + buf.vertex(matrix.positionMatrix, i, j, k) + .normal(matrix, normal.x, normal.y, normal.z) + .color(-1) + .next() + buf.vertex(matrix.positionMatrix, x, y, z) + .normal(matrix, normal.x, normal.y, normal.z) + .color(-1) + .next() + } + + + private fun buildWireFrameCube(matrix: MatrixStack.Entry, buf: VertexConsumer) { + for (i in 0..1) { + for (j in 0..1) { + val i = i.toFloat() + val j = j.toFloat() + doLine(matrix, buf, 0F, i, j, 1F, i, j) + doLine(matrix, buf, i, 0F, j, i, 1F, j) + doLine(matrix, buf, i, j, 0F, i, j, 1F) + } + } + } + + private fun buildFaceZP(matrix: Matrix4f, buf: VertexConsumer, rgba: Int) { + buf.vertex(matrix, 0F, 0F, 1F).color(rgba) + buf.vertex(matrix, 0F, 1F, 1F).color(rgba) + buf.vertex(matrix, 1F, 1F, 1F).color(rgba) + buf.vertex(matrix, 1F, 0F, 1F).color(rgba) + } + + private fun buildFaceZN(matrix: Matrix4f, buf: VertexConsumer, rgba: Int) { + buf.vertex(matrix, 0F, 0F, 0F).color(rgba) + buf.vertex(matrix, 1F, 0F, 0F).color(rgba) + buf.vertex(matrix, 1F, 1F, 0F).color(rgba) + buf.vertex(matrix, 0F, 1F, 0F).color(rgba) + } + + private fun buildFaceXP(matrix: Matrix4f, buf: VertexConsumer, rgba: Int) { + buf.vertex(matrix, 1F, 0F, 0F).color(rgba) + buf.vertex(matrix, 1F, 1F, 0F).color(rgba) + buf.vertex(matrix, 1F, 1F, 1F).color(rgba) + buf.vertex(matrix, 1F, 0F, 1F).color(rgba) + } + + private fun buildFaceXN(matrix: Matrix4f, buf: VertexConsumer, rgba: Int) { + buf.vertex(matrix, 0F, 0F, 0F).color(rgba) + buf.vertex(matrix, 0F, 0F, 1F).color(rgba) + buf.vertex(matrix, 0F, 1F, 1F).color(rgba) + buf.vertex(matrix, 0F, 1F, 0F).color(rgba) + } + + private fun buildFaceYN(matrix: Matrix4f, buf: VertexConsumer, rgba: Int) { + buf.vertex(matrix, 0F, 0F, 0F).color(rgba) + buf.vertex(matrix, 0F, 0F, 1F).color(rgba) + buf.vertex(matrix, 1F, 0F, 1F).color(rgba) + buf.vertex(matrix, 1F, 0F, 0F).color(rgba) + } + + private fun buildFaceYP(matrix: Matrix4f, buf: VertexConsumer, rgba: Int) { + buf.vertex(matrix, 0F, 1F, 0F).color(rgba) + buf.vertex(matrix, 1F, 1F, 0F).color(rgba) + buf.vertex(matrix, 1F, 1F, 1F).color(rgba) + buf.vertex(matrix, 0F, 1F, 1F).color(rgba) + } + + private fun buildCube(matrix4f: Matrix4f, buf: VertexConsumer, rgba: Int) { + buildFaceXP(matrix4f, buf, rgba) + buildFaceXN(matrix4f, buf, rgba) + buildFaceYP(matrix4f, buf, rgba) + buildFaceYN(matrix4f, buf, rgba) + buildFaceZP(matrix4f, buf, rgba) + buildFaceZN(matrix4f, buf, rgba) + } + + fun renderInWorld(event: WorldRenderLastEvent, block: RenderInWorldContext. () -> Unit) { + // TODO: there should be *no more global state*. the only thing we should be doing is render layers. that includes settings like culling, blending, shader color, and depth testing + // For now i will let these functions remain, but this needs to go before i do a full (non-beta) release +// RenderSystem.disableDepthTest() +// RenderSystem.enableBlend() +// RenderSystem.defaultBlendFunc() +// RenderSystem.disableCull() + + event.matrices.push() + event.matrices.translate(-event.camera.pos.x, -event.camera.pos.y, -event.camera.pos.z) + + val ctx = RenderInWorldContext( + event.matrices, + event.camera, + event.tickCounter, + event.vertexConsumers + ) + + block(ctx) + + event.matrices.pop() + event.vertexConsumers.draw() + RenderSystem.setShaderColor(1F, 1F, 1F, 1F) + } + } +} + + diff --git a/src/main/kotlin/util/render/TintedOverlayTexture.kt b/src/main/kotlin/util/render/TintedOverlayTexture.kt new file mode 100644 index 0000000..0677846 --- /dev/null +++ b/src/main/kotlin/util/render/TintedOverlayTexture.kt @@ -0,0 +1,35 @@ +package moe.nea.firmament.util.render + +import me.shedaniel.math.Color +import net.minecraft.client.render.OverlayTexture +import net.minecraft.util.math.ColorHelper +import moe.nea.firmament.util.ErrorUtil + +class TintedOverlayTexture : OverlayTexture() { + companion object { + val size = 16 + } + + private var lastColor: Color? = null + fun setColor(color: Color): TintedOverlayTexture { + val image = ErrorUtil.notNullOr(texture.image, "Disposed TintedOverlayTexture written to") { return this } + if (color == lastColor) return this + lastColor = color + + for (i in 0.., + val cooldown: Duration?, + ) + + @JvmInline + value class AbilityActivation( + val label: String + ) { + companion object { + val RIGHT_CLICK = AbilityActivation("RIGHT CLICK") + val SNEAK_RIGHT_CLICK = AbilityActivation("SNEAK RIGHT CLICK") + val SNEAK = AbilityActivation("SNEAK") + val EMPTY = AbilityActivation("") + fun of(text: String?): AbilityActivation { + val trimmed = text?.trim() + if (trimmed.isNullOrBlank()) + return EMPTY + return AbilityActivation(trimmed) + } + } + } + + private val abilityNameRegex = "Ability: (?.*?) *".toPattern() + private fun findAbility(iterator: ListIterator): ItemAbility? { + if (!iterator.hasNext()) { + return null + } + val line = iterator.next() + // The actual information about abilities is stored in the siblings + if (line.directLiteralStringContent != "") return null + var powerScroll: Boolean = false // This should instead determine the power scroll based on text colour + var abilityName: String? = null + var activation: String? = null + var hasProcessedActivation = false + for (sibling in line.siblings) { + val directContent = sibling.directLiteralStringContent ?: continue + if (directContent == "⦾ ") { + powerScroll = true + continue + } + if (!hasProcessedActivation && abilityName != null) { + hasProcessedActivation = true + activation = directContent + continue + } + abilityNameRegex.useMatch(directContent) { + abilityName = group("name") + continue + } + if (abilityName != null) { + ErrorUtil.softError("Found abilityName $abilityName without finding but encountered unprocessable element in: $line") + } + return null + } + if (abilityName == null) return null + val descriptionLines = mutableListOf() + var manaCost: Int? = null + var cooldown: Duration? = null + while (iterator.hasNext()) { + val descriptionLine = iterator.next() + if (descriptionLine.unformattedString == "") break + var nextIsManaCost = false + var isSpecialLine = false + var nextIsDuration = false + for (sibling in descriptionLine.siblings) { + val directContent = sibling.directLiteralStringContent ?: continue + if ("Mana Cost: " == directContent) { // TODO: 'Soulflow Cost: ' support (or maybe a generic 'XXX Cost: ') + nextIsManaCost = true + isSpecialLine = true + continue + } + if ("Cooldown: " == directContent) { + nextIsDuration = true + isSpecialLine = true + continue + } + if (nextIsDuration) { + nextIsDuration = false + cooldown = parseTimePattern(directContent) + continue + } + if (nextIsManaCost) { + nextIsManaCost = false + manaCost = parseShortNumber(directContent).toInt() + continue + } + if (isSpecialLine) { + ErrorUtil.softError("Unknown special line segment: '$sibling' in '$descriptionLine'") + } + } + if (!isSpecialLine) { + descriptionLines.add(descriptionLine) + } + } + return ItemAbility( + abilityName, + powerScroll, + AbilityActivation.of(activation), + manaCost, + descriptionLines, + cooldown + ) + } + + fun getAbilities(lore: List): List { + val iterator = lore.listIterator() + val abilities = mutableListOf() + while (iterator.hasNext()) { + findAbility(iterator)?.let(abilities::add) + } + + return abilities + } + + // TODO: memoize + fun getAbilities(itemStack: ItemStack): List { + return getAbilities(itemStack.loreAccordingToNbt) + } + +} diff --git a/src/main/kotlin/util/skyblock/DungeonUtil.kt b/src/main/kotlin/util/skyblock/DungeonUtil.kt new file mode 100644 index 0000000..488b158 --- /dev/null +++ b/src/main/kotlin/util/skyblock/DungeonUtil.kt @@ -0,0 +1,33 @@ +package moe.nea.firmament.util.skyblock + +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.ScoreboardUtil +import moe.nea.firmament.util.SkyBlockIsland +import moe.nea.firmament.util.TIME_PATTERN + +object DungeonUtil { + val isInDungeonIsland get() = SBData.skyblockLocation == SkyBlockIsland.DUNGEON + private val timeElapsedRegex = "Time Elapsed: $TIME_PATTERN".toRegex() + val isInActiveDungeon get() = isInDungeonIsland && ScoreboardUtil.simplifiedScoreboardLines.any { it.matches( + timeElapsedRegex) } + +/*Title: + +§f§lSKYBLOCK§B§L CO-OP + +' Late Spring 7th' +' §75:20am' +' §7⏣ §cThe Catacombs §7(M3)' +' §7♲ §7Ironman' +' ' +'Keys: §c■ §c✗ §8■ §a1x' +'Time Elapsed: §a46s' +'Cleared: §660% §8(105)' +' ' +'§e[B] §b151_Dragon §e2,062§c❤' +'§e[A] §6Lennart0312 §a17,165§c' +'§e[T] §b187i §a14,581§c❤' +'§e[H] §bFlameeke §a8,998§c❤' +' ' +'§ewww.hypixel.net'*/ +} diff --git a/src/main/kotlin/util/skyblock/ItemType.kt b/src/main/kotlin/util/skyblock/ItemType.kt new file mode 100644 index 0000000..7a776b5 --- /dev/null +++ b/src/main/kotlin/util/skyblock/ItemType.kt @@ -0,0 +1,75 @@ +package moe.nea.firmament.util.skyblock + +import net.minecraft.item.ItemStack +import moe.nea.firmament.util.directLiteralStringContent +import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.petData + + +@JvmInline +value class ItemType private constructor(val name: String) { + companion object { + fun ofName(name: String): ItemType { + return ItemType(name) + } + + private val obfuscatedRegex = "§[kK].*?(§[0-9a-fA-FrR]|$)".toRegex() + fun fromEscapeCodeLore(lore: String): ItemType? { + return lore.replace(obfuscatedRegex, "").trim().substringAfter(" ", "") + .takeIf { it.isNotEmpty() } + ?.let(::ofName) + } + + fun fromItemStack(itemStack: ItemStack): ItemType? { + if (itemStack.petData != null) + return PET + val typeText = + itemStack.loreAccordingToNbt.lastOrNull() + ?.siblings?.find { + !it.style.isObfuscated && !it.directLiteralStringContent.isNullOrBlank() + }?.directLiteralStringContent + if (typeText != null) { + val type = typeText.substringAfter(' ', missingDelimiterValue = "").trim() + if (type.isEmpty()) return null + return ofName(type) + } + return itemStack.loreAccordingToNbt.lastOrNull()?.directLiteralStringContent?.let(::fromEscapeCodeLore) + } + + // TODO: some of those are not actual in game item types, but rather ones included in the repository to splat to multiple in game types. codify those somehow + + val SWORD = ofName("SWORD") + val DRILL = ofName("DRILL") + val PICKAXE = ofName("PICKAXE") + val GAUNTLET = ofName("GAUNTLET") + val LONGSWORD = ofName("LONG SWORD") + val EQUIPMENT = ofName("EQUIPMENT") + val FISHING_WEAPON = ofName("FISHING WEAPON") + val CLOAK = ofName("CLOAK") + val BELT = ofName("BELT") + val NECKLACE = ofName("NECKLACE") + val BRACELET = ofName("BRACELET") + val GLOVES = ofName("GLOVES") + val ROD = ofName("ROD") + val FISHING_ROD = ofName("FISHING ROD") + val VACUUM = ofName("VACUUM") + val CHESTPLATE = ofName("CHESTPLATE") + val LEGGINGS = ofName("LEGGINGS") + val HELMET = ofName("HELMET") + val BOOTS = ofName("BOOTS") + val NIL = ofName("__NIL") + + /** + * This one is not really official (it never shows up in game). + */ + val PET = ofName("PET") + } + + val dungeonVariant get() = ofName("DUNGEON $name") + + val isDungeon get() = name.startsWith("DUNGEON ") + + override fun toString(): String { + return name + } +} diff --git a/src/main/kotlin/util/skyblock/Rarity.kt b/src/main/kotlin/util/skyblock/Rarity.kt new file mode 100644 index 0000000..2507256 --- /dev/null +++ b/src/main/kotlin/util/skyblock/Rarity.kt @@ -0,0 +1,99 @@ +package moe.nea.firmament.util.skyblock + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import net.minecraft.item.ItemStack +import net.minecraft.text.Style +import net.minecraft.text.Text +import net.minecraft.util.Formatting +import moe.nea.firmament.util.StringUtil.words +import moe.nea.firmament.util.collections.lastNotNullOfOrNull +import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.petData +import moe.nea.firmament.util.unformattedString + +typealias RepoRarity = io.github.moulberry.repo.data.Rarity + +@Serializable(with = Rarity.Serializer::class) +enum class Rarity(vararg altNames: String) { + COMMON, + UNCOMMON, + RARE, + EPIC, + LEGENDARY("LEGENJERRY"), + MYTHIC, + DIVINE, + SUPREME, + SPECIAL, + VERY_SPECIAL, + ULTIMATE, + UNKNOWN + ; + + object Serializer : KSerializer { + override val descriptor: SerialDescriptor + get() = PrimitiveSerialDescriptor(Rarity::class.java.name, PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): Rarity { + return valueOf(decoder.decodeString().replace(" ", "_")) + } + + override fun serialize(encoder: Encoder, value: Rarity) { + encoder.encodeString(value.name) + } + } + + val names = setOf(name) + altNames + val text: Text get() = Text.literal(name).setStyle(Style.EMPTY.withColor(colourMap[this])) + val neuRepoRarity: RepoRarity? = RepoRarity.entries.find { it.name == name } + + companion object { + // TODO: inline those formattings as fields + val colourMap = mapOf( + Rarity.COMMON to Formatting.WHITE, + Rarity.UNCOMMON to Formatting.GREEN, + Rarity.RARE to Formatting.BLUE, + Rarity.EPIC to Formatting.DARK_PURPLE, + Rarity.LEGENDARY to Formatting.GOLD, + Rarity.MYTHIC to Formatting.LIGHT_PURPLE, + Rarity.DIVINE to Formatting.AQUA, + Rarity.SPECIAL to Formatting.RED, + Rarity.VERY_SPECIAL to Formatting.RED, + Rarity.SUPREME to Formatting.DARK_RED, + Rarity.ULTIMATE to Formatting.DARK_RED, + ) + val byName = entries.flatMap { en -> en.names.map { it to en } }.toMap() + val fromNeuRepo = entries.associateBy { it.neuRepoRarity } + + fun fromNeuRepo(repo: RepoRarity): Rarity? { + return fromNeuRepo[repo] + } + + fun fromString(name: String): Rarity? { + return byName[name] + } + + fun fromTier(tier: Int): Rarity? { + return entries.getOrNull(tier) + } + + fun fromItem(itemStack: ItemStack): Rarity? { + return fromLore(itemStack.loreAccordingToNbt) ?: fromPetItem(itemStack) + } + + fun fromPetItem(itemStack: ItemStack): Rarity? = + itemStack.petData?.tier?.let(::fromNeuRepo) + + fun fromLore(lore: List): Rarity? = + lore.lastNotNullOfOrNull { + it.unformattedString.words() + .firstNotNullOfOrNull(::fromString) + } + + } +} diff --git a/src/main/kotlin/util/skyblock/SBItemUtil.kt b/src/main/kotlin/util/skyblock/SBItemUtil.kt new file mode 100644 index 0000000..3901b60 --- /dev/null +++ b/src/main/kotlin/util/skyblock/SBItemUtil.kt @@ -0,0 +1,21 @@ +package moe.nea.firmament.util.skyblock + +import net.minecraft.item.ItemStack +import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.unformattedString + +object SBItemUtil { + fun ItemStack.getSearchName(): String { + val name = this.name.unformattedString + if (name.contains("Enchanted Book")) { + val enchant = loreAccordingToNbt.firstOrNull()?.unformattedString + if (enchant != null) return enchant + } + if (name.startsWith("[Lvl")) { + val closing = name.indexOf(']') + if (closing > 0) + return name.substring(closing) + } + return name + } +} diff --git a/src/main/kotlin/util/skyblock/SackUtil.kt b/src/main/kotlin/util/skyblock/SackUtil.kt new file mode 100644 index 0000000..c46542e --- /dev/null +++ b/src/main/kotlin/util/skyblock/SackUtil.kt @@ -0,0 +1,115 @@ +package moe.nea.firmament.util.skyblock + +import kotlinx.serialization.Serializable +import kotlinx.serialization.serializer +import net.minecraft.client.gui.screen.ingame.GenericContainerScreen +import net.minecraft.text.HoverEvent +import net.minecraft.text.Text +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.ChestInventoryUpdateEvent +import moe.nea.firmament.events.ProcessChatEvent +import moe.nea.firmament.repo.ItemNameLookup +import moe.nea.firmament.util.SHORT_NUMBER_FORMAT +import moe.nea.firmament.util.SkyblockId +import moe.nea.firmament.util.data.ProfileSpecificDataHolder +import moe.nea.firmament.util.mc.displayNameAccordingToNbt +import moe.nea.firmament.util.mc.iterableView +import moe.nea.firmament.util.mc.loreAccordingToNbt +import moe.nea.firmament.util.parseShortNumber +import moe.nea.firmament.util.skyBlockId +import moe.nea.firmament.util.unformattedString +import moe.nea.firmament.util.useMatch + +object SackUtil { + @Serializable + data class SackContents( + // TODO: store the certainty of knowledge for each item. + val contents: MutableMap = mutableMapOf(), +// val sackTypes: + ) + + object Store : ProfileSpecificDataHolder(serializer(), "Sacks", ::SackContents) + + val items get() = Store.data?.contents ?: mutableMapOf() + val storedRegex = "^Stored: (?$SHORT_NUMBER_FORMAT)/(?$SHORT_NUMBER_FORMAT)$".toPattern() + + @Subscribe + fun storeDataFromInventory(event: ChestInventoryUpdateEvent) { + val screen = event.inventory as? GenericContainerScreen ?: return + if (!screen.title.unformattedString.endsWith(" Sack")) return + val inv = screen.screenHandler?.inventory ?: return + if (inv.size() < 18) return + val backSlot = inv.getStack(inv.size() - 5) + if (backSlot.displayNameAccordingToNbt.unformattedString != "Go Back") return + if (backSlot.loreAccordingToNbt.map { it.unformattedString } != listOf("To Sack of Sacks")) return + for (itemStack in inv.iterableView) { + // TODO: handle runes and gemstones + val stored = itemStack.loreAccordingToNbt.firstNotNullOfOrNull { + storedRegex.useMatch(it.unformattedString) { + val stored = parseShortNumber(group("stored")).toLong() + val max = parseShortNumber(group("max")).toLong() + stored + } + } ?: continue + val itemId = itemStack.skyBlockId ?: continue + items[itemId] = stored + } + Store.markDirty() + } + + @Subscribe + fun updateFromChat(event: ProcessChatEvent) { + if (!event.unformattedString.startsWith("[Sacks]")) return + getUpdatesFromMessage(event.text) + } + + fun getUpdatesFromMessage(text: Text): List { + val update = ChatUpdate() + text.siblings.forEach(update::updateFromHoverText) + return update.updates + } + + data class SackUpdate( + val itemId: SkyblockId?, + val itemName: String, + val changeAmount: Long, + ) + + private class ChatUpdate { + val updates = mutableListOf() + var foundAdded = false + var foundRemoved = false + + fun updateFromCleanText(cleanedText: String) { + cleanedText.split("\n").forEach { line -> + changePattern.useMatch(line) { + val amount = parseShortNumber(group("amount")).toLong() + val itemName = group("itemName") + val itemId = ItemNameLookup.guessItemByName(itemName, false) + updates.add(SackUpdate(itemId, itemName, amount)) + } + } + } + + fun updateFromHoverText(text: Text) { + text.siblings.forEach(::updateFromHoverText) + val hoverText = (text.style.hoverEvent as? HoverEvent.ShowText)?.value ?: return + val cleanedText = hoverText.unformattedString + if (cleanedText.startsWith("Added items:\n")) { + if (!foundAdded) { + updateFromCleanText(cleanedText) + foundAdded = true + } + } + if (cleanedText.startsWith("Removed items:\n")) { + if (!foundRemoved) { + updateFromCleanText(cleanedText) + foundRemoved = true + } + } + } + + } + + val changePattern = " (?[+\\-]$SHORT_NUMBER_FORMAT) (?[^(]+) \\(.*\\)".toPattern() +} diff --git a/src/main/kotlin/util/skyblock/SkyBlockItems.kt b/src/main/kotlin/util/skyblock/SkyBlockItems.kt new file mode 100644 index 0000000..32c4aab --- /dev/null +++ b/src/main/kotlin/util/skyblock/SkyBlockItems.kt @@ -0,0 +1,24 @@ +package moe.nea.firmament.util.skyblock + +import moe.nea.firmament.util.SkyblockId + +object SkyBlockItems { + val COINS = SkyblockId("SKYBLOCK_COIN") + val ROTTEN_FLESH = SkyblockId("ROTTEN_FLESH") + val ENCHANTED_DIAMOND = SkyblockId("ENCHANTED_DIAMOND") + val DIAMOND = SkyblockId("DIAMOND") + val ANCESTRAL_SPADE = SkyblockId("ANCESTRAL_SPADE") + val REFORGE_ANVIL = SkyblockId("REFORGE_ANVIL") + val SLICE_OF_BLUEBERRY_CAKE = SkyblockId("SLICE_OF_BLUEBERRY_CAKE") + val SLICE_OF_CHEESECAKE = SkyblockId("SLICE_OF_CHEESECAKE") + val SLICE_OF_GREEN_VELVET_CAKE = SkyblockId("SLICE_OF_GREEN_VELVET_CAKE") + val SLICE_OF_RED_VELVET_CAKE = SkyblockId("SLICE_OF_RED_VELVET_CAKE") + val SLICE_OF_STRAWBERRY_SHORTCAKE = SkyblockId("SLICE_OF_STRAWBERRY_SHORTCAKE") + val ASPECT_OF_THE_VOID = SkyblockId("ASPECT_OF_THE_VOID") + val ASPECT_OF_THE_END = SkyblockId("ASPECT_OF_THE_END") + val BONE_BOOMERANG = SkyblockId("BONE_BOOMERANG") + val STARRED_BONE_BOOMERANG = SkyblockId("STARRED_BONE_BOOMERANG") + val TRIBAL_SPEAR = SkyblockId("TRIBAL_SPEAR") + val BLOCK_ZAPPER = SkyblockId("BLOCK_ZAPPER") + val HUNTING_TOOLKIT = SkyblockId("HUNTING_TOOLKIT") +} diff --git a/src/main/kotlin/util/skyblock/TabListAPI.kt b/src/main/kotlin/util/skyblock/TabListAPI.kt new file mode 100644 index 0000000..6b937da --- /dev/null +++ b/src/main/kotlin/util/skyblock/TabListAPI.kt @@ -0,0 +1,41 @@ +package moe.nea.firmament.util.skyblock + +import org.intellij.lang.annotations.Language +import net.minecraft.text.Text +import moe.nea.firmament.util.StringUtil.title +import moe.nea.firmament.util.StringUtil.unwords +import moe.nea.firmament.util.mc.MCTabListAPI +import moe.nea.firmament.util.unformattedString + +object TabListAPI { + + fun getWidgetLines(widgetName: WidgetName, includeTitle: Boolean = false, from: MCTabListAPI.CurrentTabList = MCTabListAPI.currentTabList): List { + return from.body + .dropWhile { !widgetName.matchesTitle(it) } + .takeWhile { it.string.isNotBlank() && !it.string.startsWith(" ") } + .let { if (includeTitle) it else it.drop(1) } + } + + enum class WidgetName(regex: Regex?) { + COMMISSIONS, + SKILLS("Skills:( .*)?"), + PROFILE("Profile: (.*)"), + COLLECTION, + ESSENCE, + PET + ; + + fun matchesTitle(it: Text): Boolean { + return regex.matches(it.unformattedString) + } + + constructor() : this(null) + constructor(@Language("RegExp") regex: String) : this(Regex(regex)) + + val label = + name.split("_").map { it.lowercase().title() }.unwords() + val regex = regex ?: Regex.fromLiteral("$label:") + + } + +} diff --git a/src/main/kotlin/util/textutil.kt b/src/main/kotlin/util/textutil.kt new file mode 100644 index 0000000..cfda2e9 --- /dev/null +++ b/src/main/kotlin/util/textutil.kt @@ -0,0 +1,210 @@ +package moe.nea.firmament.util + +import java.util.Optional +import net.minecraft.text.ClickEvent +import net.minecraft.text.HoverEvent +import net.minecraft.text.MutableText +import net.minecraft.text.OrderedText +import net.minecraft.text.PlainTextContent +import net.minecraft.text.StringVisitable +import net.minecraft.text.Style +import net.minecraft.text.Text +import net.minecraft.text.TextColor +import net.minecraft.text.TranslatableTextContent +import net.minecraft.util.Formatting + + +val formattingChars = "kmolnrKMOLNR".toSet() +fun CharSequence.removeColorCodes(keepNonColorCodes: Boolean = false): String { + var nextParagraph = indexOf('§') + if (nextParagraph < 0) return this.toString() + val stringBuffer = StringBuilder(this.length) + var readIndex = 0 + while (nextParagraph >= 0) { + stringBuffer.append(this, readIndex, nextParagraph) + if (keepNonColorCodes && nextParagraph + 1 < length && this[nextParagraph + 1] in formattingChars) { + readIndex = nextParagraph + nextParagraph = indexOf('§', startIndex = readIndex + 1) + } else { + readIndex = nextParagraph + 2 + nextParagraph = indexOf('§', startIndex = readIndex) + } + if (readIndex > this.length) + readIndex = this.length + } + stringBuffer.append(this, readIndex, this.length) + return stringBuffer.toString() +} + +fun OrderedText.reconstitute(): MutableText { + val base = Text.literal("") + base.setStyle(Style.EMPTY.withItalic(false)) + var lastColorCode = Style.EMPTY + val text = StringBuilder() + this.accept { index, style, codePoint -> + if (style != lastColorCode) { + if (text.isNotEmpty()) + base.append(Text.literal(text.toString()).setStyle(lastColorCode)) + lastColorCode = style + text.clear() + } + text.append(codePoint.toChar()) + true + } + if (text.isNotEmpty()) + base.append(Text.literal(text.toString()).setStyle(lastColorCode)) + return base + +} + +fun StringVisitable.reconstitute(): MutableText { + val base = Text.literal("") + base.setStyle(Style.EMPTY.withItalic(false)) + var lastColorCode = Style.EMPTY + val text = StringBuilder() + this.visit({ style, string -> + if (style != lastColorCode) { + if (text.isNotEmpty()) + base.append(Text.literal(text.toString()).setStyle(lastColorCode)) + lastColorCode = style + text.clear() + } + text.append(string) + Optional.empty() + }, Style.EMPTY) + if (text.isNotEmpty()) + base.append(Text.literal(text.toString()).setStyle(lastColorCode)) + return base + +} + +val Text.unformattedString: String + get() = string.removeColorCodes() // TODO: maybe shortcircuit this with .visit + +val Text.directLiteralStringContent: String? get() = (this.content as? PlainTextContent)?.string() + +fun Text.getLegacyFormatString(trimmed: Boolean = false): String = + run { + var lastCode = "§r" + val sb = StringBuilder() + fun appendCode(code: String) { + if (code != lastCode || !trimmed) { + sb.append(code) + lastCode = code + } + } + for (component in iterator()) { + if (component.directLiteralStringContent.isNullOrEmpty() && component.siblings.isEmpty()) { + continue + } + appendCode(component.style.let { style -> + var color = style.color?.toChatFormatting()?.toString() ?: "§r" + if (style.isBold) + color += LegacyFormattingCode.BOLD.formattingCode + if (style.isItalic) + color += LegacyFormattingCode.ITALIC.formattingCode + if (style.isUnderlined) + color += LegacyFormattingCode.UNDERLINE.formattingCode + if (style.isObfuscated) + color += LegacyFormattingCode.OBFUSCATED.formattingCode + if (style.isStrikethrough) + color += LegacyFormattingCode.STRIKETHROUGH.formattingCode + color + }) + sb.append(component.directLiteralStringContent) + if (!trimmed) + appendCode("§r") + } + sb.toString() + }.also { + var it = it + if (trimmed) { + it = it.removeSuffix("§r") + if (it.length == 2 && it.startsWith("§")) + it = "" + } + it + } + +private val textColorLUT = Formatting.entries + .mapNotNull { formatting -> formatting.colorValue?.let { it to formatting } } + .toMap() + +fun TextColor.toChatFormatting(): Formatting? { + return textColorLUT[this.rgb] +} + +fun Text.iterator(): Sequence { + return sequenceOf(this) + siblings.asSequence() + .flatMap { it.iterator() } // TODO: in theory we want to properly inherit styles here +} + +fun Text.allSiblings(): List = listOf(this) + siblings.flatMap { it.allSiblings() } + +fun MutableText.withColor(formatting: Formatting): MutableText = this.styled { + it.withColor(formatting) + .withItalic(false) + .withBold(false) +} + +fun MutableText.blue() = withColor(Formatting.BLUE) +fun MutableText.aqua() = withColor(Formatting.AQUA) +fun MutableText.lime() = withColor(Formatting.GREEN) +fun MutableText.darkGreen() = withColor(Formatting.DARK_GREEN) +fun MutableText.purple() = withColor(Formatting.DARK_PURPLE) +fun MutableText.pink() = withColor(Formatting.LIGHT_PURPLE) +fun MutableText.yellow() = withColor(Formatting.YELLOW) +fun MutableText.gold() = withColor(Formatting.GOLD) +fun MutableText.grey() = withColor(Formatting.GRAY) +fun MutableText.darkGrey() = withColor(Formatting.DARK_GRAY) +fun MutableText.red() = withColor(Formatting.RED) +fun MutableText.white() = withColor(Formatting.WHITE) +fun MutableText.bold(): MutableText = styled { it.withBold(true) } +fun MutableText.hover(text: Text): MutableText = styled { it.withHoverEvent(HoverEvent.ShowText(text)) } + + +fun MutableText.clickCommand(command: String): MutableText { + require(command.startsWith("/")) + return this.styled { + it.withClickEvent(ClickEvent.RunCommand(command)) + } +} + +fun MutableText.prepend(text: Text): MutableText { + siblings.addFirst(text) + return this +} + +fun Text.transformEachRecursively(function: (Text) -> Text): Text { + val c = this.content + if (c is TranslatableTextContent) { + return Text.translatableWithFallback(c.key, c.fallback, *c.args.map { + (if (it is Text) it else Text.literal(it.toString())).transformEachRecursively(function) + }.toTypedArray()).also { new -> + new.style = this.style + new.siblings.clear() + this.siblings.forEach { child -> + new.siblings.add(child.transformEachRecursively(function)) + } + } + } + return function(this.copy().also { it.siblings.clear() }).also { tt -> + this.siblings.forEach { + tt.siblings.add(it.transformEachRecursively(function)) + } + } +} + +fun tr(key: String, default: String): MutableText = error("Compiler plugin did not run.") +fun trResolved(key: String, vararg args: Any): MutableText = Text.stringifiedTranslatable(key, *args) +fun titleCase(str: String): String { + return str + .lowercase() + .replace("_", " ") + .split(" ") + .joinToString(" ") { word -> + word.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } + } +} + + diff --git a/src/main/kotlin/util/uuid.kt b/src/main/kotlin/util/uuid.kt new file mode 100644 index 0000000..14aa83d --- /dev/null +++ b/src/main/kotlin/util/uuid.kt @@ -0,0 +1,16 @@ +package moe.nea.firmament.util + +import java.math.BigInteger +import java.util.UUID + +fun parsePotentiallyDashlessUUID(unknownFormattedUUID: String): UUID { + if ("-" in unknownFormattedUUID) + return UUID.fromString(unknownFormattedUUID) + return parseDashlessUUID(unknownFormattedUUID) +} + +fun parseDashlessUUID(dashlessUuid: String): UUID { + val most = BigInteger(dashlessUuid.substring(0, 16), 16) + val least = BigInteger(dashlessUuid.substring(16, 32), 16) + return UUID(most.toLong(), least.toLong()) +} diff --git a/src/main/resources/assets/firmament/filters/screen/always.json b/src/main/resources/assets/firmament/filters/screen/always.json new file mode 100644 index 0000000..6c21cc9 --- /dev/null +++ b/src/main/resources/assets/firmament/filters/screen/always.json @@ -0,0 +1,5 @@ +{ + "title": { + "regex": ".*" + } +} diff --git a/src/main/resources/assets/firmament/filters/screen/always.json.license b/src/main/resources/assets/firmament/filters/screen/always.json.license new file mode 100644 index 0000000..5f0659f --- /dev/null +++ b/src/main/resources/assets/firmament/filters/screen/always.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 Linnea Gräf + +SPDX-License-Identifier: CC0-1.0 diff --git a/src/main/resources/assets/firmament/gui/anniversary_pig.xml b/src/main/resources/assets/firmament/gui/anniversary_pig.xml new file mode 100644 index 0000000..7cefac3 --- /dev/null +++ b/src/main/resources/assets/firmament/gui/anniversary_pig.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/assets/firmament/gui/button_editor_fragment.xml b/src/main/resources/assets/firmament/gui/button_editor_fragment.xml new file mode 100644 index 0000000..6444236 --- /dev/null +++ b/src/main/resources/assets/firmament/gui/button_editor_fragment.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/assets/firmament/gui/carnival/minesweeper_tutorial.xml b/src/main/resources/assets/firmament/gui/carnival/minesweeper_tutorial.xml new file mode 100644 index 0000000..b03cb4a --- /dev/null +++ b/src/main/resources/assets/firmament/gui/carnival/minesweeper_tutorial.xml @@ -0,0 +1,49 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+
+
+
+
+
+
diff --git a/src/main/resources/assets/firmament/gui/config/category.xml b/src/main/resources/assets/firmament/gui/config/category.xml new file mode 100644 index 0000000..e34b018 --- /dev/null +++ b/src/main/resources/assets/firmament/gui/config/category.xml @@ -0,0 +1,42 @@ + + +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ + + +
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/assets/firmament/gui/config/macros/combos.xml b/src/main/resources/assets/firmament/gui/config/macros/combos.xml new file mode 100644 index 0000000..5141125 --- /dev/null +++ b/src/main/resources/assets/firmament/gui/config/macros/combos.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/assets/firmament/gui/config/macros/editor_combo.xml b/src/main/resources/assets/firmament/gui/config/macros/editor_combo.xml new file mode 100644 index 0000000..50a1d99 --- /dev/null +++ b/src/main/resources/assets/firmament/gui/config/macros/editor_combo.xml @@ -0,0 +1,42 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/src/main/resources/assets/firmament/gui/config/macros/editor_wheel.xml b/src/main/resources/assets/firmament/gui/config/macros/editor_wheel.xml new file mode 100644 index 0000000..e4dc2b4 --- /dev/null +++ b/src/main/resources/assets/firmament/gui/config/macros/editor_wheel.xml @@ -0,0 +1,43 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/src/main/resources/assets/firmament/gui/config/macros/index.xml b/src/main/resources/assets/firmament/gui/config/macros/index.xml new file mode 100644 index 0000000..f6a1545 --- /dev/null +++ b/src/main/resources/assets/firmament/gui/config/macros/index.xml @@ -0,0 +1,27 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/src/main/resources/assets/firmament/gui/config/macros/wheel.xml b/src/main/resources/assets/firmament/gui/config/macros/wheel.xml new file mode 100644 index 0000000..19922fe --- /dev/null +++ b/src/main/resources/assets/firmament/gui/config/macros/wheel.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/assets/firmament/gui/config/main.xml b/src/main/resources/assets/firmament/gui/config/main.xml new file mode 100644 index 0000000..8953ada --- /dev/null +++ b/src/main/resources/assets/firmament/gui/config/main.xml @@ -0,0 +1,29 @@ + + +
+ + + + + + + +
+ +
+ +
+ + + +
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/assets/firmament/gui/license_viewer/index.xml b/src/main/resources/assets/firmament/gui/license_viewer/index.xml new file mode 100644 index 0000000..c23153d --- /dev/null +++ b/src/main/resources/assets/firmament/gui/license_viewer/index.xml @@ -0,0 +1,65 @@ + + +
+ + +
+ + + +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+ + + + +
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/assets/firmament/gui/mining_block_info/index.xml b/src/main/resources/assets/firmament/gui/mining_block_info/index.xml new file mode 100644 index 0000000..6404995 --- /dev/null +++ b/src/main/resources/assets/firmament/gui/mining_block_info/index.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + +
+ +
+ +
+ + + + + + + + +
+
+
+
+
+
+
+
diff --git a/src/main/resources/assets/firmament/gui/npc_waypoints.xml b/src/main/resources/assets/firmament/gui/npc_waypoints.xml new file mode 100644 index 0000000..25027e3 --- /dev/null +++ b/src/main/resources/assets/firmament/gui/npc_waypoints.xml @@ -0,0 +1,36 @@ + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + +
+
+
+
diff --git a/src/main/resources/assets/firmament/gui/pristine_profit.xml b/src/main/resources/assets/firmament/gui/pristine_profit.xml new file mode 100644 index 0000000..8787b8f --- /dev/null +++ b/src/main/resources/assets/firmament/gui/pristine_profit.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/main/resources/assets/firmament/gui/repo_reload.xml b/src/main/resources/assets/firmament/gui/repo_reload.xml new file mode 100644 index 0000000..9397bf1 --- /dev/null +++ b/src/main/resources/assets/firmament/gui/repo_reload.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/main/resources/assets/firmament/logo.png b/src/main/resources/assets/firmament/logo.png new file mode 100644 index 0000000..e3f063a Binary files /dev/null and b/src/main/resources/assets/firmament/logo.png differ diff --git a/src/main/resources/assets/firmament/shaders/cape/parallax.fsh b/src/main/resources/assets/firmament/shaders/cape/parallax.fsh new file mode 100644 index 0000000..bc9a440 --- /dev/null +++ b/src/main/resources/assets/firmament/shaders/cape/parallax.fsh @@ -0,0 +1,53 @@ +#version 150 + +#moj_import +#define M_PI 3.1415926535897932384626433832795 +#define M_TAU (2.0 * M_PI) +uniform sampler2D Sampler0; +uniform sampler2D Sampler1; +uniform sampler2D Sampler3; + +uniform vec4 ColorModulator; +uniform float FogStart; +uniform float FogEnd; +uniform vec4 FogColor; +uniform float Animation; + +in float vertexDistance; +in vec4 vertexColor; +in vec4 lightMapColor; +in vec4 overlayColor; +in vec2 texCoord0; + +out vec4 fragColor; + +float highlightDistance(vec2 coord, vec2 direction, float time) { + vec2 dir = normalize(direction); + float projection = dot(coord, dir); + float animationTime = sin(projection + time * 13 * M_TAU); + if (animationTime < 0.997) { + return 0.0; + } + return animationTime; +} + +void main() { + vec4 color = texture(Sampler0, texCoord0); + if (color.g > 0.99) { + // TODO: maybe this speed in each direction should be a uniform + color = texture(Sampler1, texCoord0 + Animation * vec2(3.0, -2.0)); + } + + vec4 highlightColor = texture(Sampler3, texCoord0); + if (highlightColor.a > 0.5) { + color = highlightColor; + float animationHighlight = highlightDistance(texCoord0, vec2(-12.0, 2.0), Animation); + color.rgb += (animationHighlight); + } + #ifdef ALPHA_CUTOUT + if (color.a < ALPHA_CUTOUT) { + discard; + } + #endif + fragColor = linear_fog(color, vertexDistance, FogStart, FogEnd, FogColor); +} diff --git a/src/main/resources/assets/firmament/shaders/circle_discard_color.fsh b/src/main/resources/assets/firmament/shaders/circle_discard_color.fsh new file mode 100644 index 0000000..ae46059 --- /dev/null +++ b/src/main/resources/assets/firmament/shaders/circle_discard_color.fsh @@ -0,0 +1,22 @@ +#version 150 + +in vec4 vertexColor; +in vec2 texCoord0; + +uniform vec4 ColorModulator; +uniform float InnerCutoutRadius; + +out vec4 fragColor; + +void main() { + vec4 color = vertexColor; + if (color.a == 0.0) { + discard; + } + float d = length(texCoord0 - vec2(0.5)); + if (d > 0.5 || d < InnerCutoutRadius) + { + discard; + } + fragColor = color * ColorModulator; +} diff --git a/src/main/resources/assets/firmament/shaders/core/rendertype_lines.fsh b/src/main/resources/assets/firmament/shaders/core/rendertype_lines.fsh new file mode 100644 index 0000000..057f31f --- /dev/null +++ b/src/main/resources/assets/firmament/shaders/core/rendertype_lines.fsh @@ -0,0 +1,18 @@ +#version 150 + +#moj_import + +uniform vec4 ColorModulator; +uniform float FogStart; +uniform float FogEnd; +uniform vec4 FogColor; + +in float vertexDistance; +in vec4 vertexColor; + +out vec4 fragColor; + +void main() { + vec4 color = vertexColor * ColorModulator; + fragColor = linear_fog(color, vertexDistance, FogStart, FogEnd, FogColor); +} diff --git a/src/main/resources/assets/firmament/shaders/core/rendertype_lines.json b/src/main/resources/assets/firmament/shaders/core/rendertype_lines.json new file mode 100644 index 0000000..e4537ca --- /dev/null +++ b/src/main/resources/assets/firmament/shaders/core/rendertype_lines.json @@ -0,0 +1,17 @@ +{ + "vertex": "firmament:core/rendertype_lines", + "fragment": "firmament:core/rendertype_lines", + "samplers": [ + ], + "uniforms": [ + { "name": "ModelViewMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] }, + { "name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] }, + { "name": "ColorModulator", "type": "float", "count": 4, "values": [ 1.0, 1.0, 1.0, 1.0 ] }, + { "name": "LineWidth", "type": "float", "count": 1, "values": [ 1.0 ] }, + { "name": "ScreenSize", "type": "float", "count": 2, "values": [ 1.0, 1.0 ] }, + { "name": "FogStart", "type": "float", "count": 1, "values": [ 0.0 ] }, + { "name": "FogEnd", "type": "float", "count": 1, "values": [ 1.0 ] }, + { "name": "FogColor", "type": "float", "count": 4, "values": [ 0.0, 0.0, 0.0, 0.0 ] }, + { "name": "FogShape", "type": "int", "count": 1, "values": [ 0 ] } + ] +} diff --git a/src/main/resources/assets/firmament/shaders/core/rendertype_lines.vsh b/src/main/resources/assets/firmament/shaders/core/rendertype_lines.vsh new file mode 100644 index 0000000..b2d0f99 --- /dev/null +++ b/src/main/resources/assets/firmament/shaders/core/rendertype_lines.vsh @@ -0,0 +1,62 @@ +#version 150 + +#moj_import + +in vec3 Position; +in vec4 Color; +in vec3 Normal; + +uniform mat4 ModelViewMat; +uniform mat4 ProjMat; +uniform float LineWidth; +uniform vec2 ScreenSize; +uniform int FogShape; + +out float vertexDistance; +out vec4 vertexColor; + +const float VIEW_SHRINK = 1.0 - (1.0 / 256.0); +const mat4 VIEW_SCALE = mat4( + VIEW_SHRINK, 0.0, 0.0, 0.0, + 0.0, VIEW_SHRINK, 0.0, 0.0, + 0.0, 0.0, VIEW_SHRINK, 0.0, + 0.0, 0.0, 0.0, 1.0 +); + +void main() { + vec4 linePosStart = ProjMat * VIEW_SCALE * ModelViewMat * vec4(Position, 1.0); + vec4 linePosEnd = ProjMat * VIEW_SCALE * ModelViewMat * vec4(Position + Normal, 1.0); + + vec3 ndc1 = linePosStart.xyz / linePosStart.w; + vec3 ndc2 = linePosEnd.xyz / linePosEnd.w; + + bool linePosStartBehind = ndc1.z <= -1; + bool linePosEndBehind = ndc2.z <= -1; + + if ((linePosStartBehind && linePosEndBehind)) { + gl_Position = vec4(-2.0, -2.0, -2.0, 1.0); + return; // I don't care for these people + } + if ((linePosStartBehind || linePosEndBehind) && false) { + ndc1.z = 0.0; + ndc2.z = 0.0; + linePosStart.w = 1.0; + // TODO: use mx + b to find move the two coordinates around to extend lines + } + + vec2 lineScreenDirection = normalize((ndc2.xy - ndc1.xy) * ScreenSize); + vec2 lineOffset = vec2(-lineScreenDirection.y, lineScreenDirection.x) * LineWidth / ScreenSize; + + if (lineOffset.x < 0.0) { + lineOffset *= -1.0; + } + + if (gl_VertexID % 2 == 0) { + gl_Position = vec4((ndc1 + vec3(lineOffset, 0.0)) * linePosStart.w, linePosStart.w); + } else { + gl_Position = vec4((ndc1 - vec3(lineOffset, 0.0)) * linePosStart.w, linePosStart.w); + } + + vertexDistance = fog_distance(Position, FogShape); + vertexColor = Color; +} diff --git a/src/main/resources/assets/firmament/textures/cape/REUSE.toml b/src/main/resources/assets/firmament/textures/cape/REUSE.toml new file mode 100644 index 0000000..ba721f7 --- /dev/null +++ b/src/main/resources/assets/firmament/textures/cape/REUSE.toml @@ -0,0 +1,19 @@ +#SPDX-FileCopyrightText: 2025 Linnea Gräf +# +#SPDX-License-Identifier: CC0-1.0 +version = 1 + +[[annotations]] +path = ["firmament_star.png", "parallax_background.png", "parallax_template.png"] +SPDX-License-Identifier = "CC-BY-4.0" +SPDX-FileCopyrightText = ["ic22487", "Linnea Gräf"] + +[[annotations]] +path = ["firm_static.png"] +SPDX-License-Identifier = "CC-BY-4.0" +SPDX-FileCopyrightText = ["ic22487", "kathund"] + +[[annotations]] +path = ["fsr_static.png"] +SPDX-License-Identifier = "CC-BY-4.0" +SPDX-FileCopyrightText = ["Tendan"] diff --git a/src/main/resources/assets/firmament/textures/cape/firm_static.png b/src/main/resources/assets/firmament/textures/cape/firm_static.png new file mode 100644 index 0000000..b01511c Binary files /dev/null and b/src/main/resources/assets/firmament/textures/cape/firm_static.png differ diff --git a/src/main/resources/assets/firmament/textures/cape/firmament_star.png b/src/main/resources/assets/firmament/textures/cape/firmament_star.png new file mode 100644 index 0000000..520d309 Binary files /dev/null and b/src/main/resources/assets/firmament/textures/cape/firmament_star.png differ diff --git a/src/main/resources/assets/firmament/textures/cape/fsr_static.png b/src/main/resources/assets/firmament/textures/cape/fsr_static.png new file mode 100644 index 0000000..de9cf35 Binary files /dev/null and b/src/main/resources/assets/firmament/textures/cape/fsr_static.png differ diff --git a/src/main/resources/assets/firmament/textures/cape/parallax_background.png b/src/main/resources/assets/firmament/textures/cape/parallax_background.png new file mode 100644 index 0000000..05ef0fa Binary files /dev/null and b/src/main/resources/assets/firmament/textures/cape/parallax_background.png differ diff --git a/src/main/resources/assets/firmament/textures/cape/parallax_template.png b/src/main/resources/assets/firmament/textures/cape/parallax_template.png new file mode 100644 index 0000000..7084c12 Binary files /dev/null and b/src/main/resources/assets/firmament/textures/cape/parallax_template.png differ diff --git a/src/main/resources/assets/firmament/textures/gui/bar.png b/src/main/resources/assets/firmament/textures/gui/bar.png new file mode 100644 index 0000000..97a3ccc Binary files /dev/null and b/src/main/resources/assets/firmament/textures/gui/bar.png differ diff --git a/src/main/resources/assets/firmament/textures/gui/circle.png b/src/main/resources/assets/firmament/textures/gui/circle.png new file mode 100644 index 0000000..ffd3fab Binary files /dev/null and b/src/main/resources/assets/firmament/textures/gui/circle.png differ diff --git a/src/main/resources/assets/firmament/textures/gui/circle.png.license b/src/main/resources/assets/firmament/textures/gui/circle.png.license new file mode 100644 index 0000000..d3069b8 --- /dev/null +++ b/src/main/resources/assets/firmament/textures/gui/circle.png.license @@ -0,0 +1,4 @@ +SPDX-FileCopyrightText: 2024 june_hibiscus + +# This line is needed because of https://github.com/fsfe/reuse-tool/issues/1057 +SPDX-License-Identifier: CC-BY-4.0 diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/completed_commission_background.png b/src/main/resources/assets/firmament/textures/gui/sprites/completed_commission_background.png new file mode 100644 index 0000000..a19f227 Binary files /dev/null and b/src/main/resources/assets/firmament/textures/gui/sprites/completed_commission_background.png differ diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/generic_vanilla_widget.png b/src/main/resources/assets/firmament/textures/gui/sprites/generic_vanilla_widget.png new file mode 100644 index 0000000..83fae16 Binary files /dev/null and b/src/main/resources/assets/firmament/textures/gui/sprites/generic_vanilla_widget.png differ diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/generic_vanilla_widget.png.mcmeta b/src/main/resources/assets/firmament/textures/gui/sprites/generic_vanilla_widget.png.mcmeta new file mode 100644 index 0000000..9d84425 --- /dev/null +++ b/src/main/resources/assets/firmament/textures/gui/sprites/generic_vanilla_widget.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 24, + "height": 41, + "border": 5 + } + } +} diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/hotm_perk_preset.png b/src/main/resources/assets/firmament/textures/gui/sprites/hotm_perk_preset.png new file mode 100644 index 0000000..a19f227 Binary files /dev/null and b/src/main/resources/assets/firmament/textures/gui/sprites/hotm_perk_preset.png differ diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/inventory_button_background.png b/src/main/resources/assets/firmament/textures/gui/sprites/inventory_button_background.png new file mode 100644 index 0000000..3e1e769 Binary files /dev/null and b/src/main/resources/assets/firmament/textures/gui/sprites/inventory_button_background.png differ diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/item_rarity_background.png b/src/main/resources/assets/firmament/textures/gui/sprites/item_rarity_background.png new file mode 100644 index 0000000..c7192c3 Binary files /dev/null and b/src/main/resources/assets/firmament/textures/gui/sprites/item_rarity_background.png differ diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/selected_pet_background.png b/src/main/resources/assets/firmament/textures/gui/sprites/selected_pet_background.png new file mode 100644 index 0000000..a19f227 Binary files /dev/null and b/src/main/resources/assets/firmament/textures/gui/sprites/selected_pet_background.png differ diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/slot_locked.png b/src/main/resources/assets/firmament/textures/gui/sprites/slot_locked.png new file mode 100644 index 0000000..612d2e3 Binary files /dev/null and b/src/main/resources/assets/firmament/textures/gui/sprites/slot_locked.png differ diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.png b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.png new file mode 100644 index 0000000..8dccb7f Binary files /dev/null and b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.png differ diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png new file mode 100644 index 0000000..10a3d83 Binary files /dev/null and b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png differ diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta new file mode 100644 index 0000000..94b9a1d --- /dev/null +++ b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 17, + "height": 18, + "border": 2 + } + } +} diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_knob.png b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_knob.png new file mode 100644 index 0000000..8ced28a Binary files /dev/null and b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_knob.png differ diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png new file mode 100644 index 0000000..c897840 Binary files /dev/null and b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png differ diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png.mcmeta b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png.mcmeta new file mode 100644 index 0000000..5964a6f --- /dev/null +++ b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 91, + "height": 184, + "border": 7 + } + } +} diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png new file mode 100644 index 0000000..5ffc990 Binary files /dev/null and b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png differ diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta new file mode 100644 index 0000000..cd2857e --- /dev/null +++ b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta @@ -0,0 +1,9 @@ +{ + "gui": { + "scaling": { + "type": "tile", + "width": 162, + "height": 18 + } + } +} diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png new file mode 100644 index 0000000..8362bb6 Binary files /dev/null and b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png differ diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta new file mode 100644 index 0000000..a29299d --- /dev/null +++ b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 176, + "height": 222, + "border": 10 + } + } +} diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/uuid_locked.png b/src/main/resources/assets/firmament/textures/gui/sprites/uuid_locked.png new file mode 100644 index 0000000..9e66cb5 Binary files /dev/null and b/src/main/resources/assets/firmament/textures/gui/sprites/uuid_locked.png differ diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/widget/button_active.png b/src/main/resources/assets/firmament/textures/gui/sprites/widget/button_active.png new file mode 100644 index 0000000..36144a6 Binary files /dev/null and b/src/main/resources/assets/firmament/textures/gui/sprites/widget/button_active.png differ diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/widget/checkbox_checked.png b/src/main/resources/assets/firmament/textures/gui/sprites/widget/checkbox_checked.png new file mode 100644 index 0000000..1b87c55 Binary files /dev/null and b/src/main/resources/assets/firmament/textures/gui/sprites/widget/checkbox_checked.png differ diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/widget/checkbox_unchecked.png b/src/main/resources/assets/firmament/textures/gui/sprites/widget/checkbox_unchecked.png new file mode 100644 index 0000000..dcd9aa4 Binary files /dev/null and b/src/main/resources/assets/firmament/textures/gui/sprites/widget/checkbox_unchecked.png differ diff --git a/src/main/resources/assets/firmament/textures/gui/waypoint_selected.png b/src/main/resources/assets/firmament/textures/gui/waypoint_selected.png new file mode 100644 index 0000000..80108ad Binary files /dev/null and b/src/main/resources/assets/firmament/textures/gui/waypoint_selected.png differ diff --git a/src/main/resources/assets/firmament/textures/gui/waypoint_unselected.png b/src/main/resources/assets/firmament/textures/gui/waypoint_unselected.png new file mode 100644 index 0000000..3c47e75 Binary files /dev/null and b/src/main/resources/assets/firmament/textures/gui/waypoint_unselected.png differ diff --git a/src/main/resources/assets/firmament/textures/socials/discord.png b/src/main/resources/assets/firmament/textures/socials/discord.png new file mode 100644 index 0000000..5a1b8f3 Binary files /dev/null and b/src/main/resources/assets/firmament/textures/socials/discord.png differ diff --git a/src/main/resources/assets/firmament/textures/socials/git.png b/src/main/resources/assets/firmament/textures/socials/git.png new file mode 100644 index 0000000..d7ab359 Binary files /dev/null and b/src/main/resources/assets/firmament/textures/socials/git.png differ diff --git a/src/main/resources/assets/firmament/textures/socials/modrinth.png b/src/main/resources/assets/firmament/textures/socials/modrinth.png new file mode 100644 index 0000000..bfc9403 Binary files /dev/null and b/src/main/resources/assets/firmament/textures/socials/modrinth.png differ diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..115778f --- /dev/null +++ b/src/main/resources/fabric.mod.json @@ -0,0 +1,75 @@ +{ + "schemaVersion": 1, + "id": "firmament", + "version": "${version}", + "name": "Firmament", + "description": "Firmament powered by NEU", + "authors": [ + { + "name": "Linnea Gräf", + "contact": { + "email": "nea@nea.moe" + } + } + ], + "contact": { + "discord": "https://discord.gg/64pFP94AWA", + "sources": "https://github.com/nea89o/Firmament", + "modrinth": "https://modrinth.com/mod/firmament" + }, + "license": "GPL 3.0 or Later", + "accessWidener": "firmament.accesswidener", + "icon": "assets/firmament/logo.png", + "environment": "client", + "entrypoints": { + "main": [ + "moe.nea.firmament.Firmament::onInitialize" + ], + "mm_shedaniel:early_risers": [ + "moe.nea.firmament.init.EarlyRiser" + ], + "client": [ + "moe.nea.firmament.Firmament::onClientInitialize" + ], + "rei_client": [ + "moe.nea.firmament.compat.rei.FirmamentReiPlugin" + ], + "rei_common": [ + "moe.nea.firmament.compat.rei.FirmamentReiCommonPlugin" + ], + "modmenu": [ + "moe.nea.firmament.compat.modmenu.FirmamentModMenuPlugin" + ], + "jade": [ + "moe.nea.firmament.compat.jade.FirmamentJadePlugin" + ], + "jarvis": [ + "moe.nea.firmament.jarvis.JarvisIntegration" + ] + }, + "mixins": [ + "firmament.mixins.json" + ], + "depends": { + "fabric-api": ">=${fabric_api_version}", + "fabric-language-kotlin": ">=${fabric_kotlin_version}", + "minecraft": ">=${minecraft_version}" + }, + "custom": { + "configured": { + "providers": [ + "moe.nea.firmament.compat.configured.ConfiguredCompat" + ] + }, + "modmenu": { + "links": { + "modmenu.discord": "https://discord.gg/64pFP94AWA" + } + }, + "mc-publish": { + "dependencies": [ + "roughlyenoughitems(recommended){modrinth:rei}" + ] + } + } +} diff --git a/src/main/resources/fabric.mod.json.license b/src/main/resources/fabric.mod.json.license new file mode 100644 index 0000000..c01d463 --- /dev/null +++ b/src/main/resources/fabric.mod.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2023 Linnea Gräf + +SPDX-License-Identifier: CC0-1.0 diff --git a/src/main/resources/firmament.accesswidener b/src/main/resources/firmament.accesswidener new file mode 100644 index 0000000..0b7b830 --- /dev/null +++ b/src/main/resources/firmament.accesswidener @@ -0,0 +1,36 @@ +accessWidener v2 named +accessible class net/minecraft/client/render/RenderLayer$MultiPhase +accessible class net/minecraft/client/render/RenderLayer$MultiPhaseParameters +accessible class net/minecraft/client/font/TextRenderer$Drawer + +accessible field net/minecraft/client/gui/hud/InGameHud SCOREBOARD_ENTRY_COMPARATOR Ljava/util/Comparator; + +accessible field net/minecraft/client/network/ClientPlayNetworkHandler combinedDynamicRegistries Lnet/minecraft/registry/DynamicRegistryManager$Immutable; +accessible method net/minecraft/registry/RegistryOps (Lcom/mojang/serialization/DynamicOps;Lnet/minecraft/registry/RegistryOps$RegistryInfoGetter;)V +accessible class net/minecraft/registry/RegistryOps$CachedRegistryInfoGetter +accessible class net/minecraft/client/render/model/ModelBaker$BakerImpl +accessible method net/minecraft/client/render/model/ModelBaker$BakerImpl (Lnet/minecraft/client/render/model/ModelBaker;Lnet/minecraft/client/render/model/ErrorCollectingSpriteGetter;)V + +accessible field net/minecraft/entity/mob/CreeperEntity CHARGED Lnet/minecraft/entity/data/TrackedData; +accessible method net/minecraft/entity/decoration/ArmorStandEntity setSmall (Z)V +accessible method net/minecraft/resource/NamespaceResourceManager loadMetadata (Lnet/minecraft/resource/InputSupplier;)Lnet/minecraft/resource/metadata/ResourceMetadata; +accessible method net/minecraft/client/gui/DrawContext drawTexturedQuad (Ljava/util/function/Function;Lnet/minecraft/util/Identifier;IIIIFFFFI)V + +accessible class net/minecraft/client/render/model/BlockStatesLoader$LoadedBlockStateDefinition +accessible field net/minecraft/client/render/model/BlockStatesLoader FINDER Lnet/minecraft/resource/ResourceFinder; +accessible method net/minecraft/client/render/model/BlockStatesLoader$LoadedBlockStateDefinition (Ljava/lang/String;Lnet/minecraft/client/render/model/json/BlockModelDefinition;)V +accessible method net/minecraft/client/render/model/BlockStatesLoader combine (Lnet/minecraft/util/Identifier;Lnet/minecraft/state/StateManager;Ljava/util/List;)Lnet/minecraft/client/render/model/BlockStatesLoader$LoadedModels; + +mutable field net/minecraft/screen/slot/Slot x I +mutable field net/minecraft/screen/slot/Slot y I + +accessible field net/minecraft/entity/player/PlayerEntity PLAYER_MODEL_PARTS Lnet/minecraft/entity/data/TrackedData; +accessible field net/minecraft/client/render/WorldRenderer chunks Lnet/minecraft/client/render/BuiltChunkStorage; +accessible field net/minecraft/client/render/OverlayTexture texture Lnet/minecraft/client/texture/NativeImageBackedTexture; + +accessible method net/minecraft/client/render/RenderPhase$Texture getId ()Ljava/util/Optional; +accessible field net/minecraft/client/render/RenderLayer$MultiPhase phases Lnet/minecraft/client/render/RenderLayer$MultiPhaseParameters; +accessible field net/minecraft/client/render/RenderLayer$MultiPhaseParameters texture Lnet/minecraft/client/render/RenderPhase$TextureBase; +accessible field net/minecraft/client/network/ClientPlayerInteractionManager currentBreakingPos Lnet/minecraft/util/math/BlockPos; + +mutable field net/minecraft/client/render/entity/state/LivingEntityRenderState headItemRenderState Lnet/minecraft/client/render/item/ItemRenderState; diff --git a/src/main/resources/firmament.accesswidener.license b/src/main/resources/firmament.accesswidener.license new file mode 100644 index 0000000..c01d463 --- /dev/null +++ b/src/main/resources/firmament.accesswidener.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2023 Linnea Gräf + +SPDX-License-Identifier: CC0-1.0 diff --git a/src/main/resources/firmament.mixins.json b/src/main/resources/firmament.mixins.json new file mode 100644 index 0000000..d78d124 --- /dev/null +++ b/src/main/resources/firmament.mixins.json @@ -0,0 +1,10 @@ +{ + "required": true, + "plugin": "moe.nea.firmament.init.MixinPlugin", + "package": "moe.nea.firmament.mixins", + "compatibilityLevel": "JAVA_21", + "refmap": "Firmament-refmap.json", + "injectors": { + "defaultRequire": 1 + } +} diff --git a/src/main/resources/firmament.mixins.json.license b/src/main/resources/firmament.mixins.json.license new file mode 100644 index 0000000..5f0659f --- /dev/null +++ b/src/main/resources/firmament.mixins.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 Linnea Gräf + +SPDX-License-Identifier: CC0-1.0 diff --git a/src/main/resources/hotswap-agent.properties b/src/main/resources/hotswap-agent.properties new file mode 100644 index 0000000..36b065f --- /dev/null +++ b/src/main/resources/hotswap-agent.properties @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: 2023 Linnea Grf +# +# SPDX-License-Identifier: CC0-1.0 + +disabledPlugins=Log4j2,Proxy +LOGGER=info diff --git a/src/main/resources/legacy_data/effects.json b/src/main/resources/legacy_data/effects.json new file mode 100644 index 0000000..0b885b5 --- /dev/null +++ b/src/main/resources/legacy_data/effects.json @@ -0,0 +1,140 @@ +[ + { + "id": 1, + "name": "Speed", + "displayName": "Speed", + "type": "good" + }, + { + "id": 2, + "name": "Slowness", + "displayName": "Slowness", + "type": "bad" + }, + { + "id": 3, + "name": "Haste", + "displayName": "Haste", + "type": "good" + }, + { + "id": 4, + "name": "MiningFatigue", + "displayName": "Mining Fatigue", + "type": "bad" + }, + { + "id": 5, + "name": "Strength", + "displayName": "Strength", + "type": "good" + }, + { + "id": 6, + "name": "InstantHealth", + "displayName": "Instant Health", + "type": "good" + }, + { + "id": 7, + "name": "InstantDamage", + "displayName": "Instant Damage", + "type": "bad" + }, + { + "id": 8, + "name": "JumpBoost", + "displayName": "Jump Boost", + "type": "good" + }, + { + "id": 9, + "name": "Nausea", + "displayName": "Nausea", + "type": "bad" + }, + { + "id": 10, + "name": "Regeneration", + "displayName": "Regeneration", + "type": "good" + }, + { + "id": 11, + "name": "Resistance", + "displayName": "Resistance", + "type": "good" + }, + { + "id": 12, + "name": "FireResistance", + "displayName": "Fire Resistance", + "type": "good" + }, + { + "id": 13, + "name": "WaterBreathing", + "displayName": "Water Breathing", + "type": "good" + }, + { + "id": 14, + "name": "Invisibility", + "displayName": "Invisibility", + "type": "good" + }, + { + "id": 15, + "name": "Blindness", + "displayName": "Blindness", + "type": "bad" + }, + { + "id": 16, + "name": "NightVision", + "displayName": "Night Vision", + "type": "good" + }, + { + "id": 17, + "name": "Hunger", + "displayName": "Hunger", + "type": "bad" + }, + { + "id": 18, + "name": "Weakness", + "displayName": "Weakness", + "type": "bad" + }, + { + "id": 19, + "name": "Poison", + "displayName": "Poison", + "type": "bad" + }, + { + "id": 20, + "name": "Wither", + "displayName": "Wither", + "type": "bad" + }, + { + "id": 21, + "name": "HealthBoost", + "displayName": "Health Boost", + "type": "good" + }, + { + "id": 22, + "name": "Absorption", + "displayName": "Absorption", + "type": "good" + }, + { + "id": 23, + "name": "Saturation", + "displayName": "Saturation", + "type": "good" + } +] diff --git a/src/main/resources/legacy_data/enchantments.json b/src/main/resources/legacy_data/enchantments.json new file mode 100644 index 0000000..8eeaa6e --- /dev/null +++ b/src/main/resources/legacy_data/enchantments.json @@ -0,0 +1,560 @@ +[ + { + "id": 0, + "name": "protection", + "displayName": "Protection", + "maxLevel": 4, + "minCost": { + "a": 11, + "b": -10 + }, + "maxCost": { + "a": 11, + "b": 1 + }, + "exclude": [ + "blast_protection", + "fire_protection", + "projectile_protection" + ], + "category": "armor", + "weight": 10, + "treasureOnly": false, + "curse": false, + "tradeable": true, + "discoverable": true + }, + { + "id": 1, + "name": "fire_protection", + "displayName": "Fire Protection", + "maxLevel": 4, + "minCost": { + "a": 8, + "b": 2 + }, + "maxCost": { + "a": 8, + "b": 10 + }, + "exclude": [ + "blast_protection", + "protection", + "projectile_protection" + ], + "category": "armor", + "weight": 5, + "treasureOnly": false, + "curse": false, + "tradeable": true, + "discoverable": true + }, + { + "id": 2, + "name": "feather_falling", + "displayName": "Feather Falling", + "maxLevel": 4, + "minCost": { + "a": 6, + "b": -1 + }, + "maxCost": { + "a": 6, + "b": 5 + }, + "exclude": [], + "category": "armor_feet", + "weight": 5, + "treasureOnly": false, + "curse": false, + "tradeable": true, + "discoverable": true + }, + { + "id": 3, + "name": "blast_protection", + "displayName": "Blast Protection", + "maxLevel": 4, + "minCost": { + "a": 8, + "b": -3 + }, + "maxCost": { + "a": 8, + "b": 5 + }, + "exclude": [ + "fire_protection", + "protection", + "projectile_protection" + ], + "category": "armor", + "weight": 2, + "treasureOnly": false, + "curse": false, + "tradeable": true, + "discoverable": true + }, + { + "id": 4, + "name": "projectile_protection", + "displayName": "Projectile Protection", + "maxLevel": 4, + "minCost": { + "a": 6, + "b": -3 + }, + "maxCost": { + "a": 6, + "b": 3 + }, + "exclude": [ + "protection", + "blast_protection", + "fire_protection" + ], + "category": "armor", + "weight": 5, + "treasureOnly": false, + "curse": false, + "tradeable": true, + "discoverable": true + }, + { + "id": 5, + "name": "respiration", + "displayName": "Respiration", + "maxLevel": 3, + "minCost": { + "a": 10, + "b": 0 + }, + "maxCost": { + "a": 10, + "b": 30 + }, + "exclude": [], + "category": "armor_head", + "weight": 2, + "treasureOnly": false, + "curse": false, + "tradeable": true, + "discoverable": true + }, + { + "id": 6, + "name": "aqua_affinity", + "displayName": "Aqua Affinity", + "maxLevel": 1, + "minCost": { + "a": 0, + "b": 1 + }, + "maxCost": { + "a": 0, + "b": 41 + }, + "exclude": [], + "category": "armor_head", + "weight": 2, + "treasureOnly": false, + "curse": false, + "tradeable": true, + "discoverable": true + }, + { + "id": 7, + "name": "thorns", + "displayName": "Thorns", + "maxLevel": 3, + "minCost": { + "a": 20, + "b": -10 + }, + "maxCost": { + "a": 10, + "b": 51 + }, + "exclude": [], + "category": "armor_chest", + "weight": 1, + "treasureOnly": false, + "curse": false, + "tradeable": true, + "discoverable": true + }, + { + "id": 8, + "name": "depth_strider", + "displayName": "Depth Strider", + "maxLevel": 3, + "minCost": { + "a": 10, + "b": 0 + }, + "maxCost": { + "a": 10, + "b": 15 + }, + "exclude": [ + "frost_walker" + ], + "category": "armor_feet", + "weight": 2, + "treasureOnly": false, + "curse": false, + "tradeable": true, + "discoverable": true + }, + { + "id": 16, + "name": "sharpness", + "displayName": "Sharpness", + "maxLevel": 5, + "minCost": { + "a": 11, + "b": -10 + }, + "maxCost": { + "a": 11, + "b": 10 + }, + "exclude": [ + "smite", + "bane_of_arthropods" + ], + "category": "weapon", + "weight": 10, + "treasureOnly": false, + "curse": false, + "tradeable": true, + "discoverable": true + }, + { + "id": 17, + "name": "smite", + "displayName": "Smite", + "maxLevel": 5, + "minCost": { + "a": 8, + "b": -3 + }, + "maxCost": { + "a": 8, + "b": 17 + }, + "exclude": [ + "sharpness", + "bane_of_arthropods" + ], + "category": "weapon", + "weight": 5, + "treasureOnly": false, + "curse": false, + "tradeable": true, + "discoverable": true + }, + { + "id": 18, + "name": "bane_of_arthropods", + "displayName": "Bane of Arthropods", + "maxLevel": 5, + "minCost": { + "a": 8, + "b": -3 + }, + "maxCost": { + "a": 8, + "b": 17 + }, + "exclude": [ + "smite", + "sharpness" + ], + "category": "weapon", + "weight": 5, + "treasureOnly": false, + "curse": false, + "tradeable": true, + "discoverable": true + }, + { + "id": 19, + "name": "knockback", + "displayName": "Knockback", + "maxLevel": 2, + "minCost": { + "a": 20, + "b": -15 + }, + "maxCost": { + "a": 10, + "b": 51 + }, + "exclude": [], + "category": "weapon", + "weight": 5, + "treasureOnly": false, + "curse": false, + "tradeable": true, + "discoverable": true + }, + { + "id": 20, + "name": "fire_aspect", + "displayName": "Fire Aspect", + "maxLevel": 2, + "minCost": { + "a": 20, + "b": -10 + }, + "maxCost": { + "a": 10, + "b": 51 + }, + "exclude": [], + "category": "weapon", + "weight": 2, + "treasureOnly": false, + "curse": false, + "tradeable": true, + "discoverable": true + }, + { + "id": 21, + "name": "looting", + "displayName": "Looting", + "maxLevel": 3, + "minCost": { + "a": 9, + "b": 6 + }, + "maxCost": { + "a": 10, + "b": 51 + }, + "exclude": [], + "category": "weapon", + "weight": 2, + "treasureOnly": false, + "curse": false, + "tradeable": true, + "discoverable": true + }, + { + "id": 32, + "name": "efficiency", + "displayName": "Efficiency", + "maxLevel": 5, + "minCost": { + "a": 10, + "b": -9 + }, + "maxCost": { + "a": 10, + "b": 51 + }, + "exclude": [], + "category": "digger", + "weight": 10, + "treasureOnly": false, + "curse": false, + "tradeable": true, + "discoverable": true + }, + { + "id": 33, + "name": "silk_touch", + "displayName": "Silk Touch", + "maxLevel": 1, + "minCost": { + "a": 0, + "b": 15 + }, + "maxCost": { + "a": 10, + "b": 51 + }, + "exclude": [ + "fortune" + ], + "category": "digger", + "weight": 1, + "treasureOnly": false, + "curse": false, + "tradeable": true, + "discoverable": true + }, + { + "id": 34, + "name": "unbreaking", + "displayName": "Unbreaking", + "maxLevel": 3, + "minCost": { + "a": 8, + "b": -3 + }, + "maxCost": { + "a": 10, + "b": 51 + }, + "exclude": [], + "category": "breakable", + "weight": 5, + "treasureOnly": false, + "curse": false, + "tradeable": true, + "discoverable": true + }, + { + "id": 35, + "name": "fortune", + "displayName": "Fortune", + "maxLevel": 3, + "minCost": { + "a": 9, + "b": 6 + }, + "maxCost": { + "a": 10, + "b": 51 + }, + "exclude": [ + "silk_touch" + ], + "category": "digger", + "weight": 2, + "treasureOnly": false, + "curse": false, + "tradeable": true, + "discoverable": true + }, + { + "id": 48, + "name": "power", + "displayName": "Power", + "maxLevel": 5, + "minCost": { + "a": 10, + "b": -9 + }, + "maxCost": { + "a": 10, + "b": 6 + }, + "exclude": [], + "category": "bow", + "weight": 10, + "treasureOnly": false, + "curse": false, + "tradeable": true, + "discoverable": true + }, + { + "id": 49, + "name": "punch", + "displayName": "Punch", + "maxLevel": 2, + "minCost": { + "a": 20, + "b": -8 + }, + "maxCost": { + "a": 20, + "b": 17 + }, + "exclude": [], + "category": "bow", + "weight": 2, + "treasureOnly": false, + "curse": false, + "tradeable": true, + "discoverable": true + }, + { + "id": 50, + "name": "flame", + "displayName": "Flame", + "maxLevel": 1, + "minCost": { + "a": 0, + "b": 20 + }, + "maxCost": { + "a": 0, + "b": 50 + }, + "exclude": [], + "category": "bow", + "weight": 2, + "treasureOnly": false, + "curse": false, + "tradeable": true, + "discoverable": true + }, + { + "id": 51, + "name": "infinity", + "displayName": "Infinity", + "maxLevel": 1, + "minCost": { + "a": 0, + "b": 20 + }, + "maxCost": { + "a": 0, + "b": 50 + }, + "exclude": [ + "mending" + ], + "category": "bow", + "weight": 1, + "treasureOnly": false, + "curse": false, + "tradeable": true, + "discoverable": true + }, + { + "id": 61, + "name": "luck_of_the_sea", + "displayName": "Luck of the Sea", + "maxLevel": 3, + "minCost": { + "a": 9, + "b": 6 + }, + "maxCost": { + "a": 10, + "b": 51 + }, + "exclude": [], + "category": "fishing_rod", + "weight": 2, + "treasureOnly": false, + "curse": false, + "tradeable": true, + "discoverable": true + }, + { + "id": 62, + "name": "lure", + "displayName": "Lure", + "maxLevel": 3, + "minCost": { + "a": 9, + "b": 6 + }, + "maxCost": { + "a": 10, + "b": 51 + }, + "exclude": [], + "category": "fishing_rod", + "weight": 2, + "treasureOnly": false, + "curse": false, + "tradeable": true, + "discoverable": true + } +] diff --git a/src/main/resources/legacy_data/items.json b/src/main/resources/legacy_data/items.json new file mode 100644 index 0000000..a32702c --- /dev/null +++ b/src/main/resources/legacy_data/items.json @@ -0,0 +1,3733 @@ +[ + { + "id": 1, + "displayName": "Stone", + "name": "stone", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "Stone" + }, + { + "metadata": 1, + "displayName": "Granite" + }, + { + "metadata": 2, + "displayName": "Polished Granite" + }, + { + "metadata": 3, + "displayName": "Diorite" + }, + { + "metadata": 4, + "displayName": "Polished Diorite" + }, + { + "metadata": 5, + "displayName": "Andesite" + }, + { + "metadata": 6, + "displayName": "Polished Andesite" + } + ] + }, + { + "id": 2, + "displayName": "Grass Block", + "name": "grass", + "stackSize": 64 + }, + { + "id": 3, + "displayName": "Dirt", + "name": "dirt", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "Dirt" + }, + { + "metadata": 1, + "displayName": "Coarse Dirt" + }, + { + "metadata": 2, + "displayName": "Podzol" + } + ] + }, + { + "id": 4, + "displayName": "Cobblestone", + "name": "cobblestone", + "stackSize": 64 + }, + { + "id": 5, + "displayName": "Wooden Planks", + "name": "planks", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "Oak Wood Planks" + }, + { + "metadata": 1, + "displayName": "Spruce Wood Planks" + }, + { + "metadata": 2, + "displayName": "Birch Wood Planks" + }, + { + "metadata": 3, + "displayName": "Jungle Wood Planks" + }, + { + "metadata": 4, + "displayName": "Acacia Wood Planks" + }, + { + "metadata": 5, + "displayName": "Dark Oak Wood Planks" + } + ] + }, + { + "id": 6, + "displayName": "Sapling", + "name": "sapling", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "Oak Sapling" + }, + { + "metadata": 1, + "displayName": "Spruce Sapling" + }, + { + "metadata": 2, + "displayName": "Birch Sapling" + }, + { + "metadata": 3, + "displayName": "Jungle Sapling" + }, + { + "metadata": 4, + "displayName": "Acacia Sapling" + }, + { + "metadata": 5, + "displayName": "Dark Oak Sapling" + } + ] + }, + { + "id": 7, + "displayName": "Bedrock", + "name": "bedrock", + "stackSize": 64 + }, + { + "id": 12, + "displayName": "Sand", + "name": "sand", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "Sand" + }, + { + "metadata": 1, + "displayName": "Red Sand" + } + ] + }, + { + "id": 13, + "displayName": "Gravel", + "name": "gravel", + "stackSize": 64 + }, + { + "id": 14, + "displayName": "Gold Ore", + "name": "gold_ore", + "stackSize": 64 + }, + { + "id": 15, + "displayName": "Iron Ore", + "name": "iron_ore", + "stackSize": 64 + }, + { + "id": 16, + "displayName": "Coal Ore", + "name": "coal_ore", + "stackSize": 64 + }, + { + "id": 17, + "displayName": "Wood", + "name": "log", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "Oak Wood" + }, + { + "metadata": 1, + "displayName": "Spruce Wood" + }, + { + "metadata": 2, + "displayName": "Birch Wood" + }, + { + "metadata": 3, + "displayName": "Jungle Wood" + }, + { + "metadata": 4, + "displayName": "Acacia Wood" + }, + { + "metadata": 5, + "displayName": "Dark Oak Wood" + } + ] + }, + { + "id": 18, + "displayName": "Leaves", + "name": "leaves", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "Oak Leaves" + }, + { + "metadata": 1, + "displayName": "Spruce Leaves" + }, + { + "metadata": 2, + "displayName": "Birch Leaves" + }, + { + "metadata": 3, + "displayName": "Jungle Leaves" + } + ] + }, + { + "id": 19, + "displayName": "Sponge", + "name": "sponge", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "Sponge" + }, + { + "metadata": 1, + "displayName": "Wet Sponge" + } + ] + }, + { + "id": 20, + "displayName": "Glass", + "name": "glass", + "stackSize": 64 + }, + { + "id": 21, + "displayName": "Lapis Lazuli Ore", + "name": "lapis_ore", + "stackSize": 64 + }, + { + "id": 22, + "displayName": "Lapis Lazuli Block", + "name": "lapis_block", + "stackSize": 64 + }, + { + "id": 23, + "displayName": "Dispenser", + "name": "dispenser", + "stackSize": 64 + }, + { + "id": 24, + "displayName": "Sandstone", + "name": "sandstone", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "Sandstone" + }, + { + "metadata": 1, + "displayName": "Chiseled Sandstone" + }, + { + "metadata": 2, + "displayName": "Smooth Sandstone" + } + ] + }, + { + "id": 25, + "displayName": "Note Block", + "name": "noteblock", + "stackSize": 64 + }, + { + "id": 27, + "displayName": "Powered Rail", + "name": "golden_rail", + "stackSize": 64 + }, + { + "id": 28, + "displayName": "Detector Rail", + "name": "detector_rail", + "stackSize": 64 + }, + { + "id": 29, + "displayName": "Sticky Piston", + "name": "sticky_piston", + "stackSize": 64 + }, + { + "id": 30, + "displayName": "Cobweb", + "name": "web", + "stackSize": 64 + }, + { + "id": 31, + "displayName": "Grass", + "name": "tallgrass", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "Shrub" + }, + { + "metadata": 1, + "displayName": "Tall Grass" + }, + { + "metadata": 2, + "displayName": "Fern" + } + ] + }, + { + "id": 32, + "displayName": "Dead Bush", + "name": "deadbush", + "stackSize": 64 + }, + { + "id": 33, + "displayName": "Piston", + "name": "piston", + "stackSize": 64 + }, + { + "id": 35, + "displayName": "Wool", + "name": "wool", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "White Wool" + }, + { + "metadata": 1, + "displayName": "Orange Wool" + }, + { + "metadata": 2, + "displayName": "Magenta Wool" + }, + { + "metadata": 3, + "displayName": "Light blue Wool" + }, + { + "metadata": 4, + "displayName": "Yellow Wool" + }, + { + "metadata": 5, + "displayName": "Lime Wool" + }, + { + "metadata": 6, + "displayName": "Pink Wool" + }, + { + "metadata": 7, + "displayName": "Gray Wool" + }, + { + "metadata": 8, + "displayName": "Light gray Wool" + }, + { + "metadata": 9, + "displayName": "Cyan Wool" + }, + { + "metadata": 10, + "displayName": "Purple Wool" + }, + { + "metadata": 11, + "displayName": "Blue Wool" + }, + { + "metadata": 12, + "displayName": "Brown Wool" + }, + { + "metadata": 13, + "displayName": "Green Wool" + }, + { + "metadata": 14, + "displayName": "Red Wool" + }, + { + "metadata": 15, + "displayName": "Black Wool" + } + ] + }, + { + "id": 37, + "displayName": "Dandelion", + "name": "yellow_flower", + "stackSize": 64 + }, + { + "id": 38, + "displayName": "Poppy", + "name": "red_flower", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "Poppy" + }, + { + "metadata": 1, + "displayName": "Blue Orchid" + }, + { + "metadata": 2, + "displayName": "Allium" + }, + { + "metadata": 3, + "displayName": "Azure Bluet" + }, + { + "metadata": 4, + "displayName": "Red Tulip" + }, + { + "metadata": 5, + "displayName": "Orange Tulip" + }, + { + "metadata": 6, + "displayName": "White Tulip" + }, + { + "metadata": 7, + "displayName": "Pink Tulip" + }, + { + "metadata": 8, + "displayName": "Oxeye Daisy" + } + ] + }, + { + "id": 39, + "displayName": "Brown Mushroom", + "name": "brown_mushroom", + "stackSize": 64 + }, + { + "id": 40, + "displayName": "Red Mushroom", + "name": "red_mushroom", + "stackSize": 64 + }, + { + "id": 41, + "displayName": "Block of Gold", + "name": "gold_block", + "stackSize": 64 + }, + { + "id": 42, + "displayName": "Block of Iron", + "name": "iron_block", + "stackSize": 64 + }, + { + "id": 44, + "displayName": "Stone Slab", + "name": "stone_slab", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "Stone Slab" + }, + { + "metadata": 1, + "displayName": "Sandstone Slab" + }, + { + "metadata": 2, + "displayName": "Wooden Slab" + }, + { + "metadata": 3, + "displayName": "Cobblestone Slab" + }, + { + "metadata": 4, + "displayName": "Bricks Slab" + }, + { + "metadata": 5, + "displayName": "Stone Bricks Slab" + }, + { + "metadata": 6, + "displayName": "Nether Brick Slab" + }, + { + "metadata": 7, + "displayName": "Quartz Slab" + } + ] + }, + { + "id": 45, + "displayName": "Brick", + "name": "brick_block", + "stackSize": 64 + }, + { + "id": 46, + "displayName": "TNT", + "name": "tnt", + "stackSize": 64 + }, + { + "id": 47, + "displayName": "Bookshelf", + "name": "bookshelf", + "stackSize": 64 + }, + { + "id": 48, + "displayName": "Moss Stone", + "name": "mossy_cobblestone", + "stackSize": 64 + }, + { + "id": 49, + "displayName": "Obsidian", + "name": "obsidian", + "stackSize": 64 + }, + { + "id": 50, + "displayName": "Torch", + "name": "torch", + "stackSize": 64 + }, + { + "id": 52, + "displayName": "Monster Spawner", + "name": "mob_spawner", + "stackSize": 64 + }, + { + "id": 53, + "displayName": "Oak Wood Stairs", + "name": "oak_stairs", + "stackSize": 64 + }, + { + "id": 54, + "displayName": "Chest", + "name": "chest", + "stackSize": 64 + }, + { + "id": 56, + "displayName": "Diamond Ore", + "name": "diamond_ore", + "stackSize": 64 + }, + { + "id": 57, + "displayName": "Block of Diamond", + "name": "diamond_block", + "stackSize": 64 + }, + { + "id": 58, + "displayName": "Crafting Table", + "name": "crafting_table", + "stackSize": 64 + }, + { + "id": 60, + "displayName": "Farmland", + "name": "farmland", + "stackSize": 64 + }, + { + "id": 61, + "displayName": "Furnace", + "name": "furnace", + "stackSize": 64 + }, + { + "id": 65, + "displayName": "Ladder", + "name": "ladder", + "stackSize": 64 + }, + { + "id": 66, + "displayName": "Rail", + "name": "rail", + "stackSize": 64 + }, + { + "id": 67, + "displayName": "Cobblestone Stairs", + "name": "stone_stairs", + "stackSize": 64 + }, + { + "id": 69, + "displayName": "Lever", + "name": "lever", + "stackSize": 64 + }, + { + "id": 70, + "displayName": "Stone Pressure Plate", + "name": "stone_pressure_plate", + "stackSize": 64 + }, + { + "id": 72, + "displayName": "Wooden Pressure Plate", + "name": "wooden_pressure_plate", + "stackSize": 64 + }, + { + "id": 73, + "displayName": "Redstone Ore", + "name": "redstone_ore", + "stackSize": 64 + }, + { + "id": 76, + "displayName": "Redstone Torch", + "name": "redstone_torch", + "stackSize": 64 + }, + { + "id": 77, + "displayName": "Stone Button", + "name": "stone_button", + "stackSize": 64 + }, + { + "id": 78, + "displayName": "Snow", + "name": "snow_layer", + "stackSize": 64 + }, + { + "id": 79, + "displayName": "Ice", + "name": "ice", + "stackSize": 64 + }, + { + "id": 80, + "displayName": "Snow", + "name": "snow", + "stackSize": 64 + }, + { + "id": 81, + "displayName": "Cactus", + "name": "cactus", + "stackSize": 64 + }, + { + "id": 82, + "displayName": "Clay", + "name": "clay", + "stackSize": 64 + }, + { + "id": 84, + "displayName": "Jukebox", + "name": "jukebox", + "stackSize": 64 + }, + { + "id": 85, + "displayName": "Oak Fence", + "name": "fence", + "stackSize": 64 + }, + { + "id": 86, + "displayName": "Pumpkin", + "name": "pumpkin", + "stackSize": 64 + }, + { + "id": 87, + "displayName": "Netherrack", + "name": "netherrack", + "stackSize": 64 + }, + { + "id": 88, + "displayName": "Soul Sand", + "name": "soul_sand", + "stackSize": 64 + }, + { + "id": 89, + "displayName": "Glowstone", + "name": "glowstone", + "stackSize": 64 + }, + { + "id": 91, + "displayName": "Jack o'Lantern", + "name": "lit_pumpkin", + "stackSize": 64 + }, + { + "id": 95, + "displayName": "Stained Glass", + "name": "stained_glass", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "White Stained Glass" + }, + { + "metadata": 1, + "displayName": "Orange Stained Glass" + }, + { + "metadata": 2, + "displayName": "Magenta Stained Glass" + }, + { + "metadata": 3, + "displayName": "Light Blue Stained Glass" + }, + { + "metadata": 4, + "displayName": "Yellow Stained Glass" + }, + { + "metadata": 5, + "displayName": "Lime Stained Glass" + }, + { + "metadata": 6, + "displayName": "Pink Stained Glass" + }, + { + "metadata": 7, + "displayName": "Gray Stained Glass" + }, + { + "metadata": 8, + "displayName": "Light Gray Stained Glass" + }, + { + "metadata": 9, + "displayName": "Cyan Stained Glass" + }, + { + "metadata": 10, + "displayName": "Purple Stained Glass" + }, + { + "metadata": 11, + "displayName": "Blue Stained Glass" + }, + { + "metadata": 12, + "displayName": "Brown Stained Glass" + }, + { + "metadata": 13, + "displayName": "Green Stained Glass" + }, + { + "metadata": 14, + "displayName": "Red Stained Glass" + }, + { + "metadata": 15, + "displayName": "Black Stained Glass" + } + ] + }, + { + "id": 96, + "displayName": "Wooden Trapdoor", + "name": "trapdoor", + "stackSize": 64 + }, + { + "id": 97, + "displayName": "Monster Egg", + "name": "monster_egg", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "Stone Monster Egg" + }, + { + "metadata": 1, + "displayName": "Cobblestone Monster Egg" + }, + { + "metadata": 2, + "displayName": "Stone Brick Monster Egg" + }, + { + "metadata": 3, + "displayName": "Mossy Stone Brick Monster Egg" + }, + { + "metadata": 4, + "displayName": "Cracked Stone Brick Monster Egg" + }, + { + "metadata": 5, + "displayName": "Chiseled Stone Brick Monster Egg" + } + ] + }, + { + "id": 98, + "displayName": "Stone Bricks", + "name": "stonebrick", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "Stone Bricks" + }, + { + "metadata": 1, + "displayName": "Mossy Stone Bricks" + }, + { + "metadata": 2, + "displayName": "Cracked Stone Bricks" + }, + { + "metadata": 3, + "displayName": "Chiseled Stone Bricks" + } + ] + }, + { + "id": 99, + "displayName": "Brown Mushroom Block", + "name": "brown_mushroom_block", + "stackSize": 64 + }, + { + "id": 100, + "displayName": "Red Mushroom Block", + "name": "red_mushroom_block", + "stackSize": 64 + }, + { + "id": 101, + "displayName": "Iron Bars", + "name": "iron_bars", + "stackSize": 64 + }, + { + "id": 102, + "displayName": "Glass Pane", + "name": "glass_pane", + "stackSize": 64 + }, + { + "id": 103, + "displayName": "Melon", + "name": "melon_block", + "stackSize": 64 + }, + { + "id": 106, + "displayName": "Vines", + "name": "vine", + "stackSize": 64 + }, + { + "id": 107, + "displayName": "Oak Fence Gate", + "name": "fence_gate", + "stackSize": 64 + }, + { + "id": 108, + "displayName": "Brick Stairs", + "name": "brick_stairs", + "stackSize": 64 + }, + { + "id": 109, + "displayName": "Stone Brick Stairs", + "name": "stone_brick_stairs", + "stackSize": 64 + }, + { + "id": 110, + "displayName": "Mycelium", + "name": "mycelium", + "stackSize": 64 + }, + { + "id": 111, + "displayName": "Lily Pad", + "name": "waterlily", + "stackSize": 64 + }, + { + "id": 112, + "displayName": "Nether Brick", + "name": "nether_brick", + "stackSize": 64 + }, + { + "id": 113, + "displayName": "Nether Brick Fence", + "name": "nether_brick_fence", + "stackSize": 64 + }, + { + "id": 114, + "displayName": "Nether Brick Stairs", + "name": "nether_brick_stairs", + "stackSize": 64 + }, + { + "id": 116, + "displayName": "Enchantment Table", + "name": "enchanting_table", + "stackSize": 64 + }, + { + "id": 120, + "displayName": "End Portal Frame", + "name": "end_portal_frame", + "stackSize": 64 + }, + { + "id": 121, + "displayName": "End Stone", + "name": "end_stone", + "stackSize": 64 + }, + { + "id": 122, + "displayName": "Dragon Egg", + "name": "dragon_egg", + "stackSize": 64 + }, + { + "id": 123, + "displayName": "Redstone Lamp", + "name": "redstone_lamp", + "stackSize": 64 + }, + { + "id": 126, + "displayName": "Wood Slab", + "name": "wooden_slab", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "Oak Wood Slab" + }, + { + "metadata": 1, + "displayName": "Spruce Wood Slab" + }, + { + "metadata": 2, + "displayName": "Birch Wood Slab" + }, + { + "metadata": 3, + "displayName": "Jungle Wood Slab" + }, + { + "metadata": 4, + "displayName": "Acacia Wood Slab" + }, + { + "metadata": 5, + "displayName": "Dark Oak Wood Slab" + } + ] + }, + { + "id": 128, + "displayName": "Sandstone Stairs", + "name": "sandstone_stairs", + "stackSize": 64 + }, + { + "id": 129, + "displayName": "Emerald Ore", + "name": "emerald_ore", + "stackSize": 64 + }, + { + "id": 130, + "displayName": "Ender Chest", + "name": "ender_chest", + "stackSize": 64 + }, + { + "id": 131, + "displayName": "Tripwire Hook", + "name": "tripwire_hook", + "stackSize": 64 + }, + { + "id": 133, + "displayName": "Block of Emerald", + "name": "emerald_block", + "stackSize": 64 + }, + { + "id": 134, + "displayName": "Spruce Wood Stairs", + "name": "spruce_stairs", + "stackSize": 64 + }, + { + "id": 135, + "displayName": "Birch Wood Stairs", + "name": "birch_stairs", + "stackSize": 64 + }, + { + "id": 136, + "displayName": "Jungle Wood Stairs", + "name": "jungle_stairs", + "stackSize": 64 + }, + { + "id": 137, + "displayName": "Command Block", + "name": "command_block", + "stackSize": 64 + }, + { + "id": 138, + "displayName": "Beacon", + "name": "beacon", + "stackSize": 64 + }, + { + "id": 139, + "displayName": "Cobblestone Wall", + "name": "cobblestone_wall", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "Cobblestone Wall" + }, + { + "metadata": 1, + "displayName": "Mossy Cobblestone Wall" + } + ] + }, + { + "id": 143, + "displayName": "Wooden Button", + "name": "wooden_button", + "stackSize": 64 + }, + { + "id": 145, + "displayName": "Anvil", + "name": "anvil", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "Anvil" + }, + { + "metadata": 1, + "displayName": "Slightly Damaged Anvil" + }, + { + "metadata": 2, + "displayName": "Very Damaged Anvil" + } + ] + }, + { + "id": 146, + "displayName": "Trapped Chest", + "name": "trapped_chest", + "stackSize": 64 + }, + { + "id": 147, + "displayName": "Weighted Pressure Plate (Light)", + "name": "light_weighted_pressure_plate", + "stackSize": 64 + }, + { + "id": 148, + "displayName": "Weighted Pressure Plate (Heavy)", + "name": "heavy_weighted_pressure_plate", + "stackSize": 64 + }, + { + "id": 151, + "displayName": "Daylight Detector", + "name": "daylight_detector", + "stackSize": 64 + }, + { + "id": 152, + "displayName": "Block of Redstone", + "name": "redstone_block", + "stackSize": 64 + }, + { + "id": 153, + "displayName": "Nether Quartz", + "name": "quartz_ore", + "stackSize": 64 + }, + { + "id": 154, + "displayName": "Hopper", + "name": "hopper", + "stackSize": 64 + }, + { + "id": 155, + "displayName": "Block of Quartz", + "name": "quartz_block", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "Block of Quartz" + }, + { + "metadata": 1, + "displayName": "Chiseled Quartz Block" + }, + { + "metadata": 2, + "displayName": "Pillar Quartz Block" + } + ] + }, + { + "id": 156, + "displayName": "Quartz Stairs", + "name": "quartz_stairs", + "stackSize": 64 + }, + { + "id": 157, + "displayName": "Activator Rail", + "name": "activator_rail", + "stackSize": 64 + }, + { + "id": 158, + "displayName": "Dropper", + "name": "dropper", + "stackSize": 64 + }, + { + "id": 159, + "displayName": "Stained Clay", + "name": "stained_hardened_clay", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "White Stained Clay" + }, + { + "metadata": 1, + "displayName": "Orange Stained Clay" + }, + { + "metadata": 2, + "displayName": "Magenta Stained Clay" + }, + { + "metadata": 3, + "displayName": "Light Blue Stained Clay" + }, + { + "metadata": 4, + "displayName": "Yellow Stained Clay" + }, + { + "metadata": 5, + "displayName": "Lime Stained Clay" + }, + { + "metadata": 6, + "displayName": "Pink Stained Clay" + }, + { + "metadata": 7, + "displayName": "Gray Stained Clay" + }, + { + "metadata": 8, + "displayName": "Light Gray Stained Clay" + }, + { + "metadata": 9, + "displayName": "Cyan Stained Clay" + }, + { + "metadata": 10, + "displayName": "Purple Stained Clay" + }, + { + "metadata": 11, + "displayName": "Blue Stained Clay" + }, + { + "metadata": 12, + "displayName": "Brown Stained Clay" + }, + { + "metadata": 13, + "displayName": "Green Stained Clay" + }, + { + "metadata": 14, + "displayName": "Red Stained Clay" + }, + { + "metadata": 15, + "displayName": "Black Stained Clay" + } + ] + }, + { + "id": 160, + "displayName": "Stained Glass Pane", + "name": "stained_glass_pane", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "White Stained Glass Pane" + }, + { + "metadata": 1, + "displayName": "Orange Stained Glass Pane" + }, + { + "metadata": 2, + "displayName": "Magenta Stained Glass Pane" + }, + { + "metadata": 3, + "displayName": "Light Blue Stained Glass Pane" + }, + { + "metadata": 4, + "displayName": "Yellow Stained Glass Pane" + }, + { + "metadata": 5, + "displayName": "Lime Stained Glass Pane" + }, + { + "metadata": 6, + "displayName": "Pink Stained Glass Pane" + }, + { + "metadata": 7, + "displayName": "Gray Stained Glass Pane" + }, + { + "metadata": 8, + "displayName": "Light Gray Stained Glass Pane" + }, + { + "metadata": 9, + "displayName": "Cyan Stained Glass Pane" + }, + { + "metadata": 10, + "displayName": "Purple Stained Glass Pane" + }, + { + "metadata": 11, + "displayName": "Blue Stained Glass Pane" + }, + { + "metadata": 12, + "displayName": "Brown Stained Glass Pane" + }, + { + "metadata": 13, + "displayName": "Green Stained Glass Pane" + }, + { + "metadata": 14, + "displayName": "Red Stained Glass Pane" + }, + { + "metadata": 15, + "displayName": "Black Stained Glass Pane" + } + ] + }, + { + "id": 161, + "displayName": "Leaves", + "name": "leaves2", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "Acacia Leaves" + }, + { + "metadata": 1, + "displayName": "Dark Oak Leaves" + } + ] + }, + { + "id": 162, + "displayName": "Wood", + "name": "log2", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "Acacia Wood" + }, + { + "metadata": 1, + "displayName": "Dark Oak Wood" + } + ] + }, + { + "id": 163, + "displayName": "Acacia Wood Stairs", + "name": "acacia_stairs", + "stackSize": 64 + }, + { + "id": 164, + "displayName": "Dark Oak Wood Stairs", + "name": "dark_oak_stairs", + "stackSize": 64 + }, + { + "id": 165, + "displayName": "Slime Block", + "name": "slime", + "stackSize": 64 + }, + { + "id": 166, + "displayName": "Barrier", + "name": "barrier", + "stackSize": 64 + }, + { + "id": 167, + "displayName": "Iron Trapdoor", + "name": "iron_trapdoor", + "stackSize": 64 + }, + { + "id": 168, + "displayName": "Prismarine", + "name": "prismarine", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "Prismarine" + }, + { + "metadata": 1, + "displayName": "Prismarine Bricks" + }, + { + "metadata": 2, + "displayName": "Dark Prismarine" + } + ] + }, + { + "id": 169, + "displayName": "Sea Lantern", + "name": "sea_lantern", + "stackSize": 64 + }, + { + "id": 170, + "displayName": "Hay Bale", + "name": "hay_block", + "stackSize": 64 + }, + { + "id": 171, + "displayName": "Carpet", + "name": "carpet", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "White Carpet" + }, + { + "metadata": 1, + "displayName": "Orange Carpet" + }, + { + "metadata": 2, + "displayName": "Magenta Carpet" + }, + { + "metadata": 3, + "displayName": "Light Blue Carpet" + }, + { + "metadata": 4, + "displayName": "Yellow Carpet" + }, + { + "metadata": 5, + "displayName": "Lime Carpet" + }, + { + "metadata": 6, + "displayName": "Pink Carpet" + }, + { + "metadata": 7, + "displayName": "Gray Carpet" + }, + { + "metadata": 8, + "displayName": "Light Gray Carpet" + }, + { + "metadata": 9, + "displayName": "Cyan Carpet" + }, + { + "metadata": 10, + "displayName": "Purple Carpet" + }, + { + "metadata": 11, + "displayName": "Blue Carpet" + }, + { + "metadata": 12, + "displayName": "Brown Carpet" + }, + { + "metadata": 13, + "displayName": "Green Carpet" + }, + { + "metadata": 14, + "displayName": "Red Carpet" + }, + { + "metadata": 15, + "displayName": "Black Carpet" + } + ] + }, + { + "id": 172, + "displayName": "Hardened Clay", + "name": "hardened_clay", + "stackSize": 64 + }, + { + "id": 173, + "displayName": "Block of Coal", + "name": "coal_block", + "stackSize": 64 + }, + { + "id": 174, + "displayName": "Packed Ice", + "name": "packed_ice", + "stackSize": 64 + }, + { + "id": 175, + "displayName": "Large Flowers", + "name": "double_plant", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "Sunflower" + }, + { + "metadata": 1, + "displayName": "Lilac" + }, + { + "metadata": 2, + "displayName": "Double Tallgrass" + }, + { + "metadata": 3, + "displayName": "Large Fern" + }, + { + "metadata": 4, + "displayName": "Rose Bush" + }, + { + "metadata": 5, + "displayName": "Peony" + } + ] + }, + { + "id": 179, + "displayName": "Red Sandstone", + "name": "red_sandstone", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "Red Sandstone" + }, + { + "metadata": 1, + "displayName": "Chiseled Red Sandstone" + }, + { + "metadata": 2, + "displayName": "Smooth Red Sandstone" + } + ] + }, + { + "id": 180, + "displayName": "Red Sandstone Stairs", + "name": "red_sandstone_stairs", + "stackSize": 64 + }, + { + "id": 182, + "displayName": "Red Sandstone Slab", + "name": "stone_slab2", + "stackSize": 64 + }, + { + "id": 183, + "displayName": "Spruce Fence Gate", + "name": "spruce_fence_gate", + "stackSize": 64 + }, + { + "id": 184, + "displayName": "Birch Fence Gate", + "name": "birch_fence_gate", + "stackSize": 64 + }, + { + "id": 185, + "displayName": "Jungle Fence Gate", + "name": "jungle_fence_gate", + "stackSize": 64 + }, + { + "id": 186, + "displayName": "Dark Oak Fence Gate", + "name": "dark_oak_fence_gate", + "stackSize": 64 + }, + { + "id": 187, + "displayName": "Acacia Fence Gate", + "name": "acacia_fence_gate", + "stackSize": 64 + }, + { + "id": 188, + "displayName": "Spruce Fence", + "name": "spruce_fence", + "stackSize": 64 + }, + { + "id": 189, + "displayName": "Birch Fence", + "name": "birch_fence", + "stackSize": 64 + }, + { + "id": 190, + "displayName": "Jungle Fence", + "name": "jungle_fence", + "stackSize": 64 + }, + { + "id": 191, + "displayName": "Dark Oak Fence", + "name": "dark_oak_fence", + "stackSize": 64 + }, + { + "id": 192, + "displayName": "Acacia Fence", + "name": "acacia_fence", + "stackSize": 64 + }, + { + "id": 256, + "displayName": "Iron Shovel", + "name": "iron_shovel", + "stackSize": 1, + "maxDurability": 250, + "enchantCategories": [ + "digger", + "breakable", + "vanishable" + ], + "repairWith": [ + "iron_ingot" + ] + }, + { + "id": 257, + "displayName": "Iron Pickaxe", + "name": "iron_pickaxe", + "stackSize": 1, + "maxDurability": 250, + "enchantCategories": [ + "digger", + "breakable", + "vanishable" + ], + "repairWith": [ + "iron_ingot" + ] + }, + { + "id": 258, + "displayName": "Iron Axe", + "name": "iron_axe", + "stackSize": 1, + "maxDurability": 250, + "enchantCategories": [ + "digger", + "breakable", + "vanishable" + ], + "repairWith": [ + "iron_ingot" + ] + }, + { + "id": 259, + "displayName": "Flint and Steel", + "name": "flint_and_steel", + "stackSize": 1, + "maxDurability": 64, + "enchantCategories": [ + "breakable", + "vanishable" + ] + }, + { + "id": 260, + "displayName": "Apple", + "name": "apple", + "stackSize": 64 + }, + { + "id": 261, + "displayName": "Bow", + "name": "bow", + "stackSize": 1, + "maxDurability": 384, + "enchantCategories": [ + "breakable", + "bow", + "vanishable" + ] + }, + { + "id": 262, + "displayName": "Arrow", + "name": "arrow", + "stackSize": 64 + }, + { + "id": 263, + "displayName": "Coal", + "name": "coal", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "Coal" + }, + { + "metadata": 1, + "displayName": "Charcoal" + } + ] + }, + { + "id": 264, + "displayName": "Diamond", + "name": "diamond", + "stackSize": 64 + }, + { + "id": 265, + "displayName": "Iron Ingot", + "name": "iron_ingot", + "stackSize": 64 + }, + { + "id": 266, + "displayName": "Gold Ingot", + "name": "gold_ingot", + "stackSize": 64 + }, + { + "id": 267, + "displayName": "Iron Sword", + "name": "iron_sword", + "stackSize": 1, + "maxDurability": 250, + "enchantCategories": [ + "weapon", + "breakable", + "vanishable" + ], + "repairWith": [ + "iron_ingot" + ] + }, + { + "id": 268, + "displayName": "Wooden Sword", + "name": "wooden_sword", + "stackSize": 1, + "maxDurability": 59, + "enchantCategories": [ + "weapon", + "breakable", + "vanishable" + ], + "repairWith": [ + "oak_planks", + "spruce_planks", + "birch_planks", + "jungle_planks", + "acacia_planks", + "dark_oak_planks", + "crimson_planks", + "warped_planks" + ] + }, + { + "id": 269, + "displayName": "Wooden Shovel", + "name": "wooden_shovel", + "stackSize": 1, + "maxDurability": 59, + "enchantCategories": [ + "digger", + "breakable", + "vanishable" + ], + "repairWith": [ + "oak_planks", + "spruce_planks", + "birch_planks", + "jungle_planks", + "acacia_planks", + "dark_oak_planks", + "crimson_planks", + "warped_planks" + ] + }, + { + "id": 270, + "displayName": "Wooden Pickaxe", + "name": "wooden_pickaxe", + "stackSize": 1, + "maxDurability": 59, + "enchantCategories": [ + "digger", + "breakable", + "vanishable" + ], + "repairWith": [ + "oak_planks", + "spruce_planks", + "birch_planks", + "jungle_planks", + "acacia_planks", + "dark_oak_planks", + "crimson_planks", + "warped_planks" + ] + }, + { + "id": 271, + "displayName": "Wooden Axe", + "name": "wooden_axe", + "stackSize": 1, + "maxDurability": 59, + "enchantCategories": [ + "digger", + "breakable", + "vanishable" + ], + "repairWith": [ + "oak_planks", + "spruce_planks", + "birch_planks", + "jungle_planks", + "acacia_planks", + "dark_oak_planks", + "crimson_planks", + "warped_planks" + ] + }, + { + "id": 272, + "displayName": "Stone Sword", + "name": "stone_sword", + "stackSize": 1, + "maxDurability": 131, + "enchantCategories": [ + "weapon", + "breakable", + "vanishable" + ], + "repairWith": [ + "cobblestone", + "blackstone" + ] + }, + { + "id": 273, + "displayName": "Stone Shovel", + "name": "stone_shovel", + "stackSize": 1, + "maxDurability": 131, + "enchantCategories": [ + "digger", + "breakable", + "vanishable" + ], + "repairWith": [ + "cobblestone", + "blackstone" + ] + }, + { + "id": 274, + "displayName": "Stone Pickaxe", + "name": "stone_pickaxe", + "stackSize": 1, + "maxDurability": 131, + "enchantCategories": [ + "digger", + "breakable", + "vanishable" + ], + "repairWith": [ + "cobblestone", + "blackstone" + ] + }, + { + "id": 275, + "displayName": "Stone Axe", + "name": "stone_axe", + "stackSize": 1, + "maxDurability": 131, + "enchantCategories": [ + "digger", + "breakable", + "vanishable" + ], + "repairWith": [ + "cobblestone", + "blackstone" + ] + }, + { + "id": 276, + "displayName": "Diamond Sword", + "name": "diamond_sword", + "stackSize": 1, + "maxDurability": 1561, + "enchantCategories": [ + "weapon", + "breakable", + "vanishable" + ], + "repairWith": [ + "diamond" + ] + }, + { + "id": 277, + "displayName": "Diamond Shovel", + "name": "diamond_shovel", + "stackSize": 1, + "maxDurability": 1561, + "enchantCategories": [ + "digger", + "breakable", + "vanishable" + ], + "repairWith": [ + "diamond" + ] + }, + { + "id": 278, + "displayName": "Diamond Pickaxe", + "name": "diamond_pickaxe", + "stackSize": 1, + "maxDurability": 1561, + "enchantCategories": [ + "digger", + "breakable", + "vanishable" + ], + "repairWith": [ + "diamond" + ] + }, + { + "id": 279, + "displayName": "Diamond Axe", + "name": "diamond_axe", + "stackSize": 1, + "maxDurability": 1561, + "enchantCategories": [ + "digger", + "breakable", + "vanishable" + ], + "repairWith": [ + "diamond" + ] + }, + { + "id": 280, + "displayName": "Stick", + "name": "stick", + "stackSize": 64 + }, + { + "id": 281, + "displayName": "Bowl", + "name": "bowl", + "stackSize": 64 + }, + { + "id": 282, + "displayName": "Mushroom Stew", + "name": "mushroom_stew", + "stackSize": 1 + }, + { + "id": 283, + "displayName": "Golden Sword", + "name": "golden_sword", + "stackSize": 1, + "maxDurability": 32, + "enchantCategories": [ + "weapon", + "breakable", + "vanishable" + ], + "repairWith": [ + "gold_ingot" + ] + }, + { + "id": 284, + "displayName": "Golden Shovel", + "name": "golden_shovel", + "stackSize": 1, + "maxDurability": 32, + "enchantCategories": [ + "digger", + "breakable", + "vanishable" + ], + "repairWith": [ + "gold_ingot" + ] + }, + { + "id": 285, + "displayName": "Golden Pickaxe", + "name": "golden_pickaxe", + "stackSize": 1, + "maxDurability": 32, + "enchantCategories": [ + "digger", + "breakable", + "vanishable" + ], + "repairWith": [ + "gold_ingot" + ] + }, + { + "id": 286, + "displayName": "Golden Axe", + "name": "golden_axe", + "stackSize": 1, + "maxDurability": 32, + "enchantCategories": [ + "digger", + "breakable", + "vanishable" + ], + "repairWith": [ + "gold_ingot" + ] + }, + { + "id": 287, + "displayName": "String", + "name": "string", + "stackSize": 64 + }, + { + "id": 288, + "displayName": "Feather", + "name": "feather", + "stackSize": 64 + }, + { + "id": 289, + "displayName": "Gunpowder", + "name": "gunpowder", + "stackSize": 64 + }, + { + "id": 290, + "displayName": "Wooden Hoe", + "name": "wooden_hoe", + "stackSize": 1, + "maxDurability": 59, + "enchantCategories": [ + "digger", + "breakable", + "vanishable" + ], + "repairWith": [ + "oak_planks", + "spruce_planks", + "birch_planks", + "jungle_planks", + "acacia_planks", + "dark_oak_planks", + "crimson_planks", + "warped_planks" + ] + }, + { + "id": 291, + "displayName": "Stone Hoe", + "name": "stone_hoe", + "stackSize": 1, + "maxDurability": 131, + "enchantCategories": [ + "digger", + "breakable", + "vanishable" + ], + "repairWith": [ + "cobblestone", + "blackstone" + ] + }, + { + "id": 292, + "displayName": "Iron Hoe", + "name": "iron_hoe", + "stackSize": 1, + "maxDurability": 250, + "enchantCategories": [ + "digger", + "breakable", + "vanishable" + ], + "repairWith": [ + "iron_ingot" + ] + }, + { + "id": 293, + "displayName": "Diamond Hoe", + "name": "diamond_hoe", + "stackSize": 1, + "maxDurability": 1561, + "enchantCategories": [ + "digger", + "breakable", + "vanishable" + ], + "repairWith": [ + "diamond" + ] + }, + { + "id": 294, + "displayName": "Golden Hoe", + "name": "golden_hoe", + "stackSize": 1, + "maxDurability": 32, + "enchantCategories": [ + "digger", + "breakable", + "vanishable" + ], + "repairWith": [ + "gold_ingot" + ] + }, + { + "id": 295, + "displayName": "Seeds", + "name": "wheat_seeds", + "stackSize": 64 + }, + { + "id": 296, + "displayName": "Wheat", + "name": "wheat", + "stackSize": 64 + }, + { + "id": 297, + "displayName": "Bread", + "name": "bread", + "stackSize": 64 + }, + { + "id": 298, + "displayName": "Leather Cap", + "name": "leather_helmet", + "stackSize": 1, + "maxDurability": 55, + "enchantCategories": [ + "armor", + "armor_head", + "breakable", + "wearable", + "vanishable" + ], + "repairWith": [ + "leather" + ] + }, + { + "id": 299, + "displayName": "Leather Tunic", + "name": "leather_chestplate", + "stackSize": 1, + "maxDurability": 80, + "enchantCategories": [ + "armor", + "armor_chest", + "breakable", + "wearable", + "vanishable" + ], + "repairWith": [ + "leather" + ] + }, + { + "id": 300, + "displayName": "Leather Pants", + "name": "leather_leggings", + "stackSize": 1, + "maxDurability": 75, + "enchantCategories": [ + "armor", + "breakable", + "wearable", + "vanishable" + ], + "repairWith": [ + "leather" + ] + }, + { + "id": 301, + "displayName": "Leather Boots", + "name": "leather_boots", + "stackSize": 1, + "maxDurability": 65, + "enchantCategories": [ + "armor", + "armor_feet", + "breakable", + "wearable", + "vanishable" + ], + "repairWith": [ + "leather" + ] + }, + { + "id": 302, + "displayName": "Chain Helmet", + "name": "chainmail_helmet", + "stackSize": 1, + "maxDurability": 165, + "enchantCategories": [ + "armor", + "armor_head", + "breakable", + "wearable", + "vanishable" + ], + "repairWith": [ + "iron_ingot" + ] + }, + { + "id": 303, + "displayName": "Chain Chestplate", + "name": "chainmail_chestplate", + "stackSize": 1, + "maxDurability": 240, + "enchantCategories": [ + "armor", + "armor_chest", + "breakable", + "wearable", + "vanishable" + ], + "repairWith": [ + "iron_ingot" + ] + }, + { + "id": 304, + "displayName": "Chain Leggings", + "name": "chainmail_leggings", + "stackSize": 1, + "maxDurability": 225, + "enchantCategories": [ + "armor", + "breakable", + "wearable", + "vanishable" + ], + "repairWith": [ + "iron_ingot" + ] + }, + { + "id": 305, + "displayName": "Chain Boots", + "name": "chainmail_boots", + "stackSize": 1, + "maxDurability": 195, + "enchantCategories": [ + "armor", + "armor_feet", + "breakable", + "wearable", + "vanishable" + ], + "repairWith": [ + "iron_ingot" + ] + }, + { + "id": 306, + "displayName": "Iron Helmet", + "name": "iron_helmet", + "stackSize": 1, + "maxDurability": 165, + "enchantCategories": [ + "armor", + "armor_head", + "breakable", + "wearable", + "vanishable" + ], + "repairWith": [ + "iron_ingot" + ] + }, + { + "id": 307, + "displayName": "Iron Chestplate", + "name": "iron_chestplate", + "stackSize": 1, + "maxDurability": 240, + "enchantCategories": [ + "armor", + "armor_chest", + "breakable", + "wearable", + "vanishable" + ], + "repairWith": [ + "iron_ingot" + ] + }, + { + "id": 308, + "displayName": "Iron Leggings", + "name": "iron_leggings", + "stackSize": 1, + "maxDurability": 225, + "enchantCategories": [ + "armor", + "breakable", + "wearable", + "vanishable" + ], + "repairWith": [ + "iron_ingot" + ] + }, + { + "id": 309, + "displayName": "Iron Boots", + "name": "iron_boots", + "stackSize": 1, + "maxDurability": 195, + "enchantCategories": [ + "armor", + "armor_feet", + "breakable", + "wearable", + "vanishable" + ], + "repairWith": [ + "iron_ingot" + ] + }, + { + "id": 310, + "displayName": "Diamond Helmet", + "name": "diamond_helmet", + "stackSize": 1, + "maxDurability": 363, + "enchantCategories": [ + "armor", + "armor_head", + "breakable", + "wearable", + "vanishable" + ], + "repairWith": [ + "diamond" + ] + }, + { + "id": 311, + "displayName": "Diamond Chestplate", + "name": "diamond_chestplate", + "stackSize": 1, + "maxDurability": 528, + "enchantCategories": [ + "armor", + "armor_chest", + "breakable", + "wearable", + "vanishable" + ], + "repairWith": [ + "diamond" + ] + }, + { + "id": 312, + "displayName": "Diamond Leggings", + "name": "diamond_leggings", + "stackSize": 1, + "maxDurability": 495, + "enchantCategories": [ + "armor", + "breakable", + "wearable", + "vanishable" + ], + "repairWith": [ + "diamond" + ] + }, + { + "id": 313, + "displayName": "Diamond Boots", + "name": "diamond_boots", + "stackSize": 1, + "maxDurability": 429, + "enchantCategories": [ + "armor", + "armor_feet", + "breakable", + "wearable", + "vanishable" + ], + "repairWith": [ + "diamond" + ] + }, + { + "id": 314, + "displayName": "Golden Helmet", + "name": "golden_helmet", + "stackSize": 1, + "maxDurability": 77, + "enchantCategories": [ + "armor", + "armor_head", + "breakable", + "wearable", + "vanishable" + ], + "repairWith": [ + "gold_ingot" + ] + }, + { + "id": 315, + "displayName": "Golden Chestplate", + "name": "golden_chestplate", + "stackSize": 1, + "maxDurability": 112, + "enchantCategories": [ + "armor", + "armor_chest", + "breakable", + "wearable", + "vanishable" + ], + "repairWith": [ + "gold_ingot" + ] + }, + { + "id": 316, + "displayName": "Golden Leggings", + "name": "golden_leggings", + "stackSize": 1, + "maxDurability": 105, + "enchantCategories": [ + "armor", + "breakable", + "wearable", + "vanishable" + ], + "repairWith": [ + "gold_ingot" + ] + }, + { + "id": 317, + "displayName": "Golden Boots", + "name": "golden_boots", + "stackSize": 1, + "maxDurability": 91, + "enchantCategories": [ + "armor", + "armor_feet", + "breakable", + "wearable", + "vanishable" + ], + "repairWith": [ + "gold_ingot" + ] + }, + { + "id": 318, + "displayName": "Flint", + "name": "flint", + "stackSize": 64 + }, + { + "id": 319, + "displayName": "Raw Porkchop", + "name": "porkchop", + "stackSize": 64 + }, + { + "id": 320, + "displayName": "Cooked Porkchop", + "name": "cooked_porkchop", + "stackSize": 64 + }, + { + "id": 321, + "displayName": "Painting", + "name": "painting", + "stackSize": 64 + }, + { + "id": 322, + "displayName": "Golden Apple", + "name": "golden_apple", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "Golden Apple" + }, + { + "metadata": 1, + "displayName": "Enchanted Golden Apple" + } + ] + }, + { + "id": 323, + "displayName": "Sign", + "name": "sign", + "stackSize": 16 + }, + { + "id": 324, + "displayName": "Oak Door", + "name": "wooden_door", + "stackSize": 64 + }, + { + "id": 325, + "displayName": "Bucket", + "name": "bucket", + "stackSize": 16 + }, + { + "id": 326, + "displayName": "Water Bucket", + "name": "water_bucket", + "stackSize": 1 + }, + { + "id": 327, + "displayName": "Lava Bucket", + "name": "lava_bucket", + "stackSize": 1 + }, + { + "id": 328, + "displayName": "Minecart", + "name": "minecart", + "stackSize": 1 + }, + { + "id": 329, + "displayName": "Saddle", + "name": "saddle", + "stackSize": 1 + }, + { + "id": 330, + "displayName": "Iron Door", + "name": "iron_door", + "stackSize": 64 + }, + { + "id": 331, + "displayName": "Redstone", + "name": "redstone", + "stackSize": 64 + }, + { + "id": 332, + "displayName": "Snowball", + "name": "snowball", + "stackSize": 16 + }, + { + "id": 333, + "displayName": "Boat", + "name": "boat", + "stackSize": 1 + }, + { + "id": 334, + "displayName": "Leather", + "name": "leather", + "stackSize": 64 + }, + { + "id": 335, + "displayName": "Milk", + "name": "milk_bucket", + "stackSize": 1 + }, + { + "id": 336, + "displayName": "Brick", + "name": "brick", + "stackSize": 64 + }, + { + "id": 337, + "displayName": "Clay", + "name": "clay_ball", + "stackSize": 64 + }, + { + "id": 338, + "displayName": "Sugar Canes", + "name": "reeds", + "stackSize": 64 + }, + { + "id": 339, + "displayName": "Paper", + "name": "paper", + "stackSize": 64 + }, + { + "id": 340, + "displayName": "Book", + "name": "book", + "stackSize": 64 + }, + { + "id": 341, + "displayName": "Slimeball", + "name": "slime_ball", + "stackSize": 64 + }, + { + "id": 342, + "displayName": "Minecart with Chest", + "name": "chest_minecart", + "stackSize": 1 + }, + { + "id": 343, + "displayName": "Minecart with Furnace", + "name": "furnace_minecart", + "stackSize": 1 + }, + { + "id": 344, + "displayName": "Egg", + "name": "egg", + "stackSize": 16 + }, + { + "id": 345, + "displayName": "Compass", + "name": "compass", + "stackSize": 64 + }, + { + "id": 346, + "displayName": "Fishing Rod", + "name": "fishing_rod", + "stackSize": 1, + "maxDurability": 64, + "enchantCategories": [ + "breakable", + "fishing_rod", + "vanishable" + ] + }, + { + "id": 347, + "displayName": "Clock", + "name": "clock", + "stackSize": 64 + }, + { + "id": 348, + "displayName": "Glowstone Dust", + "name": "glowstone_dust", + "stackSize": 64 + }, + { + "id": 349, + "displayName": "Fish", + "name": "fish", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "Raw Fish" + }, + { + "metadata": 1, + "displayName": "Raw Salmon" + }, + { + "metadata": 2, + "displayName": "Clownfish" + }, + { + "metadata": 3, + "displayName": "Pufferfish" + } + ] + }, + { + "id": 350, + "displayName": "Cooked Fish", + "name": "cooked_fish", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "Cooked Fish" + }, + { + "metadata": 1, + "displayName": "Cooked Salmon" + } + ] + }, + { + "id": 351, + "displayName": "Dye", + "name": "dye", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "Ink Sac" + }, + { + "metadata": 1, + "displayName": "Rose Red" + }, + { + "metadata": 2, + "displayName": "Cactus Green" + }, + { + "metadata": 3, + "displayName": "Cocoa Beans" + }, + { + "metadata": 4, + "displayName": "Lapis Lazuli" + }, + { + "metadata": 5, + "displayName": "Purple Dye" + }, + { + "metadata": 6, + "displayName": "Cyan Dye" + }, + { + "metadata": 7, + "displayName": "Light Gray Dye" + }, + { + "metadata": 8, + "displayName": "Gray Dye" + }, + { + "metadata": 9, + "displayName": "Pink Dye" + }, + { + "metadata": 10, + "displayName": "Lime Dye" + }, + { + "metadata": 11, + "displayName": "Dandelion Yellow" + }, + { + "metadata": 12, + "displayName": "Light Blue Dye" + }, + { + "metadata": 13, + "displayName": "Magenta Dye" + }, + { + "metadata": 14, + "displayName": "Orange Dye" + }, + { + "metadata": 15, + "displayName": "Bone Meal" + } + ] + }, + { + "id": 352, + "displayName": "Bone", + "name": "bone", + "stackSize": 64 + }, + { + "id": 353, + "displayName": "Sugar", + "name": "sugar", + "stackSize": 64 + }, + { + "id": 354, + "displayName": "Cake", + "name": "cake", + "stackSize": 1 + }, + { + "id": 355, + "displayName": "Bed", + "name": "bed", + "stackSize": 1 + }, + { + "id": 356, + "displayName": "Redstone Repeater", + "name": "repeater", + "stackSize": 64 + }, + { + "id": 357, + "displayName": "Cookie", + "name": "cookie", + "stackSize": 64 + }, + { + "id": 358, + "displayName": "Map", + "name": "filled_map", + "stackSize": 64 + }, + { + "id": 359, + "displayName": "Shears", + "name": "shears", + "stackSize": 1, + "maxDurability": 238, + "enchantCategories": [ + "breakable", + "vanishable" + ] + }, + { + "id": 360, + "displayName": "Melon", + "name": "melon", + "stackSize": 64 + }, + { + "id": 361, + "displayName": "Pumpkin Seeds", + "name": "pumpkin_seeds", + "stackSize": 64 + }, + { + "id": 362, + "displayName": "Melon Seeds", + "name": "melon_seeds", + "stackSize": 64 + }, + { + "id": 363, + "displayName": "Raw Beef", + "name": "beef", + "stackSize": 64 + }, + { + "id": 364, + "displayName": "Steak", + "name": "cooked_beef", + "stackSize": 64 + }, + { + "id": 365, + "displayName": "Raw Chicken", + "name": "chicken", + "stackSize": 64 + }, + { + "id": 366, + "displayName": "Cooked Chicken", + "name": "cooked_chicken", + "stackSize": 64 + }, + { + "id": 367, + "displayName": "Rotten Flesh", + "name": "rotten_flesh", + "stackSize": 64 + }, + { + "id": 368, + "displayName": "Ender Pearl", + "name": "ender_pearl", + "stackSize": 16 + }, + { + "id": 369, + "displayName": "Blaze Rod", + "name": "blaze_rod", + "stackSize": 64 + }, + { + "id": 370, + "displayName": "Ghast Tear", + "name": "ghast_tear", + "stackSize": 64 + }, + { + "id": 371, + "displayName": "Gold Nugget", + "name": "gold_nugget", + "stackSize": 64 + }, + { + "id": 372, + "displayName": "Nether Wart", + "name": "nether_wart", + "stackSize": 64 + }, + { + "id": 373, + "displayName": "Potion", + "name": "potion", + "stackSize": 1 + }, + { + "id": 374, + "displayName": "Glass Bottle", + "name": "glass_bottle", + "stackSize": 64 + }, + { + "id": 375, + "displayName": "Spider Eye", + "name": "spider_eye", + "stackSize": 64 + }, + { + "id": 376, + "displayName": "Fermented Spider Eye", + "name": "fermented_spider_eye", + "stackSize": 64 + }, + { + "id": 377, + "displayName": "Blaze Powder", + "name": "blaze_powder", + "stackSize": 64 + }, + { + "id": 378, + "displayName": "Magma Cream", + "name": "magma_cream", + "stackSize": 64 + }, + { + "id": 379, + "displayName": "Brewing Stand", + "name": "brewing_stand", + "stackSize": 64 + }, + { + "id": 380, + "displayName": "Cauldron", + "name": "cauldron", + "stackSize": 64 + }, + { + "id": 381, + "displayName": "Eye of Ender", + "name": "ender_eye", + "stackSize": 64 + }, + { + "id": 382, + "displayName": "Glistering Melon", + "name": "speckled_melon", + "stackSize": 64 + }, + { + "id": 383, + "displayName": "Spawn Egg", + "name": "spawn_egg", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "Spawn" + }, + { + "metadata": 1, + "displayName": "Spawn Dropped item" + }, + { + "metadata": 7, + "displayName": "Spawn Thrown egg" + }, + { + "metadata": 8, + "displayName": "Spawn Lead knot" + }, + { + "metadata": 10, + "displayName": "Spawn Shot arrow" + }, + { + "metadata": 11, + "displayName": "Spawn Thrown snowball" + }, + { + "metadata": 12, + "displayName": "Spawn Ghast fireball" + }, + { + "metadata": 13, + "displayName": "Spawn Blaze fireball" + }, + { + "metadata": 14, + "displayName": "Spawn Thrown Ender Pearl" + }, + { + "metadata": 15, + "displayName": "Spawn Thrown Eye of Ender" + }, + { + "metadata": 16, + "displayName": "Spawn Thrown splash potion" + }, + { + "metadata": 17, + "displayName": "Spawn Thrown Bottle o' Enchanting" + }, + { + "metadata": 18, + "displayName": "Spawn Item Frame" + }, + { + "metadata": 19, + "displayName": "Spawn Wither Skull" + }, + { + "metadata": 20, + "displayName": "Spawn Primed TNT" + }, + { + "metadata": 21, + "displayName": "Spawn Falling block" + }, + { + "metadata": 21, + "displayName": "Spawn Falling block" + }, + { + "metadata": 22, + "displayName": "Spawn Firework Rocket" + }, + { + "metadata": 30, + "displayName": "Spawn Armor Stand" + }, + { + "metadata": 41, + "displayName": "Spawn Boat" + }, + { + "metadata": 42, + "displayName": "Spawn Minecart" + }, + { + "metadata": 42, + "displayName": "Spawn Minecart" + }, + { + "metadata": 42, + "displayName": "Spawn Minecart" + }, + { + "metadata": 48, + "displayName": "Spawn Mob" + }, + { + "metadata": 49, + "displayName": "Spawn Monster" + }, + { + "metadata": 50, + "displayName": "Spawn Creeper" + }, + { + "metadata": 51, + "displayName": "Spawn Skeleton" + }, + { + "metadata": 52, + "displayName": "Spawn Spider" + }, + { + "metadata": 53, + "displayName": "Spawn Giant" + }, + { + "metadata": 54, + "displayName": "Spawn Zombie" + }, + { + "metadata": 55, + "displayName": "Spawn Slime" + }, + { + "metadata": 56, + "displayName": "Spawn Ghast" + }, + { + "metadata": 57, + "displayName": "Spawn Zombie Pigman" + }, + { + "metadata": 58, + "displayName": "Spawn Enderman" + }, + { + "metadata": 59, + "displayName": "Spawn Cave Spider" + }, + { + "metadata": 60, + "displayName": "Spawn Silverfish" + }, + { + "metadata": 61, + "displayName": "Spawn Blaze" + }, + { + "metadata": 62, + "displayName": "Spawn Magma Cube" + }, + { + "metadata": 63, + "displayName": "Spawn Ender Dragon" + }, + { + "metadata": 64, + "displayName": "Spawn Wither" + }, + { + "metadata": 65, + "displayName": "Spawn Bat" + }, + { + "metadata": 66, + "displayName": "Spawn Witch" + }, + { + "metadata": 67, + "displayName": "Spawn Endermite" + }, + { + "metadata": 68, + "displayName": "Spawn Guardian" + }, + { + "metadata": 90, + "displayName": "Spawn Pig" + }, + { + "metadata": 91, + "displayName": "Spawn Sheep" + }, + { + "metadata": 92, + "displayName": "Spawn Cow" + }, + { + "metadata": 93, + "displayName": "Spawn Chicken" + }, + { + "metadata": 94, + "displayName": "Spawn Squid" + }, + { + "metadata": 95, + "displayName": "Spawn Wolf" + }, + { + "metadata": 96, + "displayName": "Spawn Mooshroom" + }, + { + "metadata": 97, + "displayName": "Spawn Snow Golem" + }, + { + "metadata": 98, + "displayName": "Spawn Ocelot" + }, + { + "metadata": 99, + "displayName": "Spawn Iron Golem" + }, + { + "metadata": 100, + "displayName": "Spawn Horse" + }, + { + "metadata": 101, + "displayName": "Spawn Rabbit" + }, + { + "metadata": 120, + "displayName": "Spawn Villager" + }, + { + "metadata": 200, + "displayName": "Spawn Ender Crystal" + } + ] + }, + { + "id": 384, + "displayName": "Bottle o' Enchanting", + "name": "experience_bottle", + "stackSize": 64 + }, + { + "id": 385, + "displayName": "Fire Charge", + "name": "fire_charge", + "stackSize": 64 + }, + { + "id": 386, + "displayName": "Book and Quill", + "name": "writable_book", + "stackSize": 1 + }, + { + "id": 387, + "displayName": "Written Book", + "name": "written_book", + "stackSize": 16 + }, + { + "id": 388, + "displayName": "Emerald", + "name": "emerald", + "stackSize": 64 + }, + { + "id": 389, + "displayName": "Item Frame", + "name": "item_frame", + "stackSize": 64 + }, + { + "id": 390, + "displayName": "Flower Pot", + "name": "flower_pot", + "stackSize": 64 + }, + { + "id": 391, + "displayName": "Carrot", + "name": "carrot", + "stackSize": 64 + }, + { + "id": 392, + "displayName": "Potato", + "name": "potato", + "stackSize": 64 + }, + { + "id": 393, + "displayName": "Baked Potato", + "name": "baked_potato", + "stackSize": 64 + }, + { + "id": 394, + "displayName": "Poisonous Potato", + "name": "poisonous_potato", + "stackSize": 64 + }, + { + "id": 395, + "displayName": "Empty Map", + "name": "map", + "stackSize": 64 + }, + { + "id": 396, + "displayName": "Golden Carrot", + "name": "golden_carrot", + "stackSize": 64 + }, + { + "id": 397, + "displayName": "Skull", + "name": "skull", + "stackSize": 64, + "variations": [ + { + "metadata": 0, + "displayName": "Skeleton Skull" + }, + { + "metadata": 1, + "displayName": "Wither Skeleton Skull" + }, + { + "metadata": 2, + "displayName": "Zombie Head" + }, + { + "metadata": 3, + "displayName": "Head" + }, + { + "metadata": 4, + "displayName": "Creeper Head" + } + ] + }, + { + "id": 398, + "displayName": "Carrot on a Stick", + "name": "carrot_on_a_stick", + "stackSize": 1, + "maxDurability": 25, + "enchantCategories": [ + "breakable", + "vanishable" + ] + }, + { + "id": 399, + "displayName": "Nether Star", + "name": "nether_star", + "stackSize": 64 + }, + { + "id": 400, + "displayName": "Pumpkin Pie", + "name": "pumpkin_pie", + "stackSize": 64 + }, + { + "id": 401, + "displayName": "Firework Rocket", + "name": "fireworks", + "stackSize": 64 + }, + { + "id": 402, + "displayName": "Firework Star", + "name": "firework_charge", + "stackSize": 64 + }, + { + "id": 403, + "displayName": "Enchanted Book", + "name": "enchanted_book", + "stackSize": 1 + }, + { + "id": 404, + "displayName": "Redstone Comparator", + "name": "comparator", + "stackSize": 64 + }, + { + "id": 405, + "displayName": "Nether Brick", + "name": "netherbrick", + "stackSize": 64 + }, + { + "id": 406, + "displayName": "Nether Quartz", + "name": "quartz", + "stackSize": 64 + }, + { + "id": 407, + "displayName": "Minecart with TNT", + "name": "tnt_minecart", + "stackSize": 1 + }, + { + "id": 408, + "displayName": "Minecart with Hopper", + "name": "hopper_minecart", + "stackSize": 1 + }, + { + "id": 409, + "displayName": "Prismarine Shard", + "name": "prismarine_shard", + "stackSize": 64 + }, + { + "id": 410, + "displayName": "Prismarine Crystals", + "name": "prismarine_crystals", + "stackSize": 64 + }, + { + "id": 411, + "displayName": "Raw Rabbit", + "name": "rabbit", + "stackSize": 64 + }, + { + "id": 412, + "displayName": "Cooked Rabbit", + "name": "cooked_rabbit", + "stackSize": 64 + }, + { + "id": 413, + "displayName": "Rabbit Stew", + "name": "rabbit_stew", + "stackSize": 1 + }, + { + "id": 414, + "displayName": "Rabbit's Foot", + "name": "rabbit_foot", + "stackSize": 64 + }, + { + "id": 415, + "displayName": "Rabbit Hide", + "name": "rabbit_hide", + "stackSize": 64 + }, + { + "id": 416, + "displayName": "Armor Stand", + "name": "armor_stand", + "stackSize": 16 + }, + { + "id": 417, + "displayName": "Iron Horse Armor", + "name": "iron_horse_armor", + "stackSize": 1 + }, + { + "id": 418, + "displayName": "Gold Horse Armor", + "name": "golden_horse_armor", + "stackSize": 1 + }, + { + "id": 419, + "displayName": "Diamond Horse Armor", + "name": "diamond_horse_armor", + "stackSize": 1 + }, + { + "id": 420, + "displayName": "Lead", + "name": "lead", + "stackSize": 64 + }, + { + "id": 421, + "displayName": "Name Tag", + "name": "name_tag", + "stackSize": 64 + }, + { + "id": 422, + "displayName": "Minecart with Command Block", + "name": "command_block_minecart", + "stackSize": 1 + }, + { + "id": 423, + "displayName": "Raw Mutton", + "name": "mutton", + "stackSize": 64 + }, + { + "id": 424, + "displayName": "Cooked Mutton", + "name": "cooked_mutton", + "stackSize": 64 + }, + { + "id": 425, + "displayName": "Banner", + "name": "banner", + "stackSize": 16, + "variations": [ + { + "metadata": 0, + "displayName": "Black Banner" + }, + { + "metadata": 1, + "displayName": "Red Banner" + }, + { + "metadata": 2, + "displayName": "Green Banner" + }, + { + "metadata": 3, + "displayName": "Brown Banner" + }, + { + "metadata": 4, + "displayName": "Blue Banner" + }, + { + "metadata": 5, + "displayName": "Purple Banner" + }, + { + "metadata": 6, + "displayName": "Cyan Banner" + }, + { + "metadata": 7, + "displayName": "Light Gray Banner" + }, + { + "metadata": 8, + "displayName": "Gray Banner" + }, + { + "metadata": 9, + "displayName": "Pink Banner" + }, + { + "metadata": 10, + "displayName": "Lime Banner" + }, + { + "metadata": 11, + "displayName": "Yellow Banner" + }, + { + "metadata": 12, + "displayName": "Light Blue Banner" + }, + { + "metadata": 13, + "displayName": "Magenta Banner" + }, + { + "metadata": 14, + "displayName": "Orange Banner" + }, + { + "metadata": 15, + "displayName": "White Banner" + } + ] + }, + { + "id": 427, + "displayName": "Spruce Door", + "name": "spruce_door", + "stackSize": 64 + }, + { + "id": 428, + "displayName": "Birch Door", + "name": "birch_door", + "stackSize": 64 + }, + { + "id": 429, + "displayName": "Jungle Door", + "name": "jungle_door", + "stackSize": 64 + }, + { + "id": 430, + "displayName": "Acacia Door", + "name": "acacia_door", + "stackSize": 64 + }, + { + "id": 431, + "displayName": "Dark Oak Door", + "name": "dark_oak_door", + "stackSize": 64 + }, + { + "id": 2256, + "displayName": "13 Disc", + "name": "record_13", + "stackSize": 1 + }, + { + "id": 2257, + "displayName": "Cat Disc", + "name": "record_cat", + "stackSize": 1 + }, + { + "id": 2258, + "displayName": "Blocks Disc", + "name": "record_blocks", + "stackSize": 1 + }, + { + "id": 2259, + "displayName": "Chirp Disc", + "name": "record_chirp", + "stackSize": 1 + }, + { + "id": 2260, + "displayName": "Far Disc", + "name": "record_far", + "stackSize": 1 + }, + { + "id": 2261, + "displayName": "Mall Disc", + "name": "record_mall", + "stackSize": 1 + }, + { + "id": 2262, + "displayName": "Mellohi Disc", + "name": "record_mellohi", + "stackSize": 1 + }, + { + "id": 2263, + "displayName": "Stal Disc", + "name": "record_stal", + "stackSize": 1 + }, + { + "id": 2264, + "displayName": "Strad Disc", + "name": "record_strad", + "stackSize": 1 + }, + { + "id": 2265, + "displayName": "Ward Disc", + "name": "record_ward", + "stackSize": 1 + }, + { + "id": 2266, + "displayName": "11 Disc", + "name": "record_11", + "stackSize": 1 + }, + { + "id": 2267, + "displayName": "Wait Disc", + "name": "record_wait", + "stackSize": 1 + } +] \ No newline at end of file diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/inventory_button_background.png b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/inventory_button_background.png new file mode 100644 index 0000000..46c86f4 Binary files /dev/null and b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/inventory_button_background.png differ diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.png b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.png new file mode 100644 index 0000000..1831ef3 Binary files /dev/null and b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.png differ diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png new file mode 100644 index 0000000..5b774b2 Binary files /dev/null and b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png differ diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta new file mode 100644 index 0000000..94b9a1d --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 17, + "height": 18, + "border": 2 + } + } +} diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png new file mode 100644 index 0000000..10d41dd Binary files /dev/null and b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png differ diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png.mcmeta b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png.mcmeta new file mode 100644 index 0000000..5964a6f --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 91, + "height": 184, + "border": 7 + } + } +} diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png new file mode 100644 index 0000000..61e9ee5 Binary files /dev/null and b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png differ diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta new file mode 100644 index 0000000..cd2857e --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta @@ -0,0 +1,9 @@ +{ + "gui": { + "scaling": { + "type": "tile", + "width": 162, + "height": 18 + } + } +} diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png new file mode 100644 index 0000000..653a99e Binary files /dev/null and b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png differ diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta new file mode 100644 index 0000000..a29299d --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 176, + "height": 222, + "border": 10 + } + } +} diff --git a/src/main/resources/resourcepacks/transparent_overlay/pack.mcmeta b/src/main/resources/resourcepacks/transparent_overlay/pack.mcmeta new file mode 100644 index 0000000..035feaa --- /dev/null +++ b/src/main/resources/resourcepacks/transparent_overlay/pack.mcmeta @@ -0,0 +1,10 @@ +{ + "pack": { + "pack_format": 15, + "supported_formats": { + "min_inclusive": 15, + "max_inclusive": 2147483647 + }, + "description": "Adds a more transparent overlay for Firmament" + } +} diff --git a/src/test/kotlin/MixinTest.kt b/src/test/kotlin/MixinTest.kt new file mode 100644 index 0000000..55aa7c2 --- /dev/null +++ b/src/test/kotlin/MixinTest.kt @@ -0,0 +1,34 @@ +package moe.nea.firmament.test + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.spongepowered.asm.mixin.MixinEnvironment +import org.spongepowered.asm.mixin.transformer.IMixinTransformer +import moe.nea.firmament.init.MixinPlugin + +class MixinTest { + @Test + fun mixinAudit() { + FirmTestBootstrap.bootstrapMinecraft() + MixinEnvironment.getCurrentEnvironment().audit() + val mp = MixinPlugin.instances.single() + Assertions.assertEquals( + mp.expectedFullPathMixins, + mp.appliedFullPathMixins, + ) + Assertions.assertNotEquals( + 0, + mp.mixins.size + ) + + } + + @Test + fun hasInstalledMixinTransformer() { + Assertions.assertInstanceOf( + IMixinTransformer::class.java, + MixinEnvironment.getCurrentEnvironment().activeTransformer + ) + } +} + diff --git a/src/test/kotlin/features/macros/KeyComboTrieCreation.kt b/src/test/kotlin/features/macros/KeyComboTrieCreation.kt new file mode 100644 index 0000000..f0e7a1b --- /dev/null +++ b/src/test/kotlin/features/macros/KeyComboTrieCreation.kt @@ -0,0 +1,103 @@ +package moe.nea.firmament.test.features.macros + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import net.minecraft.client.util.InputUtil +import moe.nea.firmament.features.macros.Branch +import moe.nea.firmament.features.macros.ComboKeyAction +import moe.nea.firmament.features.macros.CommandAction +import moe.nea.firmament.features.macros.KeyComboTrie +import moe.nea.firmament.features.macros.Leaf +import moe.nea.firmament.keybindings.SavedKeyBinding + +class KeyComboTrieCreation { + val basicAction = CommandAction("ac Hello") + val aPress = SavedKeyBinding(InputUtil.GLFW_KEY_A) + val bPress = SavedKeyBinding(InputUtil.GLFW_KEY_B) + val cPress = SavedKeyBinding(InputUtil.GLFW_KEY_C) + + @Test + fun testValidShortTrie() { + val actions = listOf( + ComboKeyAction(basicAction, listOf(aPress)), + ComboKeyAction(basicAction, listOf(bPress)), + ComboKeyAction(basicAction, listOf(cPress)), + ) + Assertions.assertEquals( + Branch( + mapOf( + aPress to Leaf(basicAction), + bPress to Leaf(basicAction), + cPress to Leaf(basicAction), + ), + ), KeyComboTrie.fromComboList(actions) + ) + } + + @Test + fun testOverlappingLeafs() { + Assertions.assertThrows(IllegalStateException::class.java) { + KeyComboTrie.fromComboList( + listOf( + ComboKeyAction(basicAction, listOf(aPress, aPress)), + ComboKeyAction(basicAction, listOf(aPress, aPress)), + ) + ) + } + Assertions.assertThrows(IllegalStateException::class.java) { + KeyComboTrie.fromComboList( + listOf( + ComboKeyAction(basicAction, listOf(aPress)), + ComboKeyAction(basicAction, listOf(aPress)), + ) + ) + } + } + + @Test + fun testBranchOverlappingLeaf() { + Assertions.assertThrows(IllegalStateException::class.java) { + KeyComboTrie.fromComboList( + listOf( + ComboKeyAction(basicAction, listOf(aPress)), + ComboKeyAction(basicAction, listOf(aPress, aPress)), + ) + ) + } + } + @Test + fun testLeafOverlappingBranch() { + Assertions.assertThrows(IllegalStateException::class.java) { + KeyComboTrie.fromComboList( + listOf( + ComboKeyAction(basicAction, listOf(aPress, aPress)), + ComboKeyAction(basicAction, listOf(aPress)), + ) + ) + } + } + + + @Test + fun testValidNestedTrie() { + val actions = listOf( + ComboKeyAction(basicAction, listOf(aPress, aPress)), + ComboKeyAction(basicAction, listOf(aPress, bPress)), + ComboKeyAction(basicAction, listOf(cPress)), + ) + Assertions.assertEquals( + Branch( + mapOf( + aPress to Branch( + mapOf( + aPress to Leaf(basicAction), + bPress to Leaf(basicAction), + ) + ), + cPress to Leaf(basicAction), + ), + ), KeyComboTrie.fromComboList(actions) + ) + } + +} diff --git a/src/test/kotlin/root.kt b/src/test/kotlin/root.kt new file mode 100644 index 0000000..000ddda --- /dev/null +++ b/src/test/kotlin/root.kt @@ -0,0 +1,30 @@ +package moe.nea.firmament.test + +import net.minecraft.Bootstrap +import net.minecraft.SharedConstants +import moe.nea.firmament.util.TimeMark + +object FirmTestBootstrap { + val loadStart = TimeMark.now() + + init { + println("Bootstrap started at $loadStart") + } + + init { + SharedConstants.createGameVersion() + Bootstrap.initialize() + } + + val loadEnd = TimeMark.now() + + val loadDuration = loadStart.passedAt(loadEnd) + + init { + println("Bootstrap completed at $loadEnd after $loadDuration") + } + + @JvmStatic + fun bootstrapMinecraft() { + } +} diff --git a/src/test/kotlin/testutil/AutoBootstrapExtension.kt b/src/test/kotlin/testutil/AutoBootstrapExtension.kt new file mode 100644 index 0000000..6f225a0 --- /dev/null +++ b/src/test/kotlin/testutil/AutoBootstrapExtension.kt @@ -0,0 +1,14 @@ +package moe.nea.firmament.test.testutil + +import com.google.auto.service.AutoService +import org.junit.jupiter.api.extension.BeforeAllCallback +import org.junit.jupiter.api.extension.Extension +import org.junit.jupiter.api.extension.ExtensionContext +import moe.nea.firmament.test.FirmTestBootstrap + +@AutoService(Extension::class) +class AutoBootstrapExtension : Extension, BeforeAllCallback { + override fun beforeAll(p0: ExtensionContext) { + FirmTestBootstrap.bootstrapMinecraft() + } +} diff --git a/src/test/kotlin/testutil/ItemResources.kt b/src/test/kotlin/testutil/ItemResources.kt new file mode 100644 index 0000000..e996fc2 --- /dev/null +++ b/src/test/kotlin/testutil/ItemResources.kt @@ -0,0 +1,94 @@ +package moe.nea.firmament.test.testutil + +import com.mojang.datafixers.DSL +import com.mojang.serialization.Dynamic +import com.mojang.serialization.JsonOps +import net.minecraft.SharedConstants +import net.minecraft.datafixer.Schemas +import net.minecraft.datafixer.TypeReferences +import net.minecraft.item.ItemStack +import net.minecraft.nbt.NbtCompound +import net.minecraft.nbt.NbtElement +import net.minecraft.nbt.NbtOps +import net.minecraft.nbt.NbtString +import net.minecraft.nbt.StringNbtReader +import net.minecraft.registry.RegistryOps +import net.minecraft.text.Text +import net.minecraft.text.TextCodecs +import moe.nea.firmament.features.debug.ExportedTestConstantMeta +import moe.nea.firmament.test.FirmTestBootstrap +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.mc.MCTabListAPI + +object ItemResources { + init { + FirmTestBootstrap.bootstrapMinecraft() + } + + fun loadString(path: String): String { + require(!path.startsWith("/")) + return ItemResources::class.java.classLoader + .getResourceAsStream(path)!! + .readAllBytes().decodeToString() + } + + fun loadSNbt(path: String): NbtCompound { + return StringNbtReader.readCompound(loadString(path)) + } + + fun getNbtOps(): RegistryOps = MC.currentOrDefaultRegistries.getOps(NbtOps.INSTANCE) + + fun tryMigrateNbt( + nbtCompound: NbtCompound, + typ: DSL.TypeReference?, + ): NbtElement { + val source = nbtCompound.get("source", ExportedTestConstantMeta.CODEC) + nbtCompound.remove("source") + if (source.isPresent) { + val wrappedNbtSource = if (typ == TypeReferences.TEXT_COMPONENT && source.get().dataVersion < 4325) { + // Per 1.21.5 text components are wrapped in a string, which firmament unwrapped in the snbt files + NbtString.of( + NbtOps.INSTANCE.convertTo(JsonOps.INSTANCE, nbtCompound) + .toString() + ) + } else { + nbtCompound + } + if (typ != null) { + return Schemas.getFixer() + .update( + typ, + Dynamic(NbtOps.INSTANCE, wrappedNbtSource), + source.get().dataVersion, + SharedConstants.getGameVersion().saveVersion.id + ).value + } else { + wrappedNbtSource + } + } + return nbtCompound + } + + fun loadTablist(name: String): MCTabListAPI.CurrentTabList { + return MCTabListAPI.CurrentTabList.CODEC.parse( + getNbtOps(), + tryMigrateNbt(loadSNbt("testdata/tablist/$name.snbt"), null), + ).getOrThrow { IllegalStateException("Could not load tablist '$name': $it") } + } + + fun loadText(name: String): Text { + return TextCodecs.CODEC.parse( + getNbtOps(), + tryMigrateNbt(loadSNbt("testdata/chat/$name.snbt"), TypeReferences.TEXT_COMPONENT) + ).getOrThrow { IllegalStateException("Could not load test chat '$name': $it") } + } + + fun loadItem(name: String): ItemStack { + try { + val itemNbt = loadSNbt("testdata/items/$name.snbt") + return ItemStack.CODEC.parse(getNbtOps(), tryMigrateNbt(itemNbt, TypeReferences.ITEM_STACK)).orThrow + } catch (ex: Exception) { + throw RuntimeException("Could not load item resource '$name'", ex) + } + } +} diff --git a/src/test/kotlin/util/ColorCodeTest.kt b/src/test/kotlin/util/ColorCodeTest.kt new file mode 100644 index 0000000..7c581c5 --- /dev/null +++ b/src/test/kotlin/util/ColorCodeTest.kt @@ -0,0 +1,57 @@ +package moe.nea.firmament.test.util + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import moe.nea.firmament.util.removeColorCodes + + +class ColorCodeTest { + @Test + fun testWhatever() { + Assertions.assertEquals("", "".removeColorCodes()) + Assertions.assertEquals("", "§".removeColorCodes()) + Assertions.assertEquals("", "§a".removeColorCodes()) + Assertions.assertEquals("ab", "a§ab".removeColorCodes()) + Assertions.assertEquals("ab", "a§ab§§".removeColorCodes()) + Assertions.assertEquals("abc", "a§ab§§c".removeColorCodes()) + Assertions.assertEquals("bc", "§ab§§c".removeColorCodes()) + Assertions.assertEquals("b§lc", "§ab§l§§c".removeColorCodes(true)) + Assertions.assertEquals("b§lc§l", "§ab§l§§c§l".removeColorCodes(true)) + Assertions.assertEquals("§lb§lc", "§l§ab§l§§c".removeColorCodes(true)) + } + + @Test + fun testEdging() { + Assertions.assertEquals("", "§".removeColorCodes()) + Assertions.assertEquals("a", "a§".removeColorCodes()) + Assertions.assertEquals("b", "§ab§".removeColorCodes()) + } + + @Test + fun `testDouble§`() { + Assertions.assertEquals("1", "§§1".removeColorCodes()) + } + + @Test + fun testKeepNonColor() { + Assertions.assertEquals("§k§l§m§n§o§r", "§k§l§m§f§n§o§r".removeColorCodes(true)) + } + + @Test + fun testPlainString() { + Assertions.assertEquals("bcdefgp", "bcdefgp".removeColorCodes()) + Assertions.assertEquals("", "".removeColorCodes()) + } + + @Test + fun testSomeNormalTestCases() { + Assertions.assertEquals( + "You are not currently in a party.", + "§r§cYou are not currently in a party.§r".removeColorCodes() + ) + Assertions.assertEquals( + "Ancient Necron's Chestplate ✪✪✪✪", + "§dAncient Necron's Chestplate §6✪§6✪§6✪§6✪".removeColorCodes() + ) + } +} diff --git a/src/test/kotlin/util/TextUtilText.kt b/src/test/kotlin/util/TextUtilText.kt new file mode 100644 index 0000000..94ab222 --- /dev/null +++ b/src/test/kotlin/util/TextUtilText.kt @@ -0,0 +1,18 @@ +package moe.nea.firmament.test.util + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import moe.nea.firmament.test.testutil.ItemResources +import moe.nea.firmament.util.getLegacyFormatString + +class TextUtilText { + @Test + fun testThing() { + // TODO: add more tests that are directly validated with 1.8.9 code + val text = ItemResources.loadText("all-chat") + Assertions.assertEquals( + "§r§r§8[§r§9302§r§8] §r§6♫ §r§b[MVP§r§d+§r§b] lrg89§r§f: test§r", + text.getLegacyFormatString() + ) + } +} diff --git a/src/test/kotlin/util/math/GChainReconciliationTest.kt b/src/test/kotlin/util/math/GChainReconciliationTest.kt new file mode 100644 index 0000000..380ea5c --- /dev/null +++ b/src/test/kotlin/util/math/GChainReconciliationTest.kt @@ -0,0 +1,75 @@ +package moe.nea.firmament.test.util.math + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import moe.nea.firmament.util.math.GChainReconciliation +import moe.nea.firmament.util.math.GChainReconciliation.rotated + +class GChainReconciliationTest { + + fun assertEqualCycles( + expected: List, + actual: List + ) { + for (offset in expected.indices) { + val rotated = expected.rotated(offset) + val matchesAtRotation = run { + for ((i, v) in actual.withIndex()) { + if (rotated[i % rotated.size] != v) + return@run false + } + true + } + if (matchesAtRotation) + return + } + assertEquals(expected, actual, "Expected arrays to be cycle equivalent") + } + + @Test + fun testUnfixableCycleNotBeingModified() { + assertEquals( + listOf(1, 2, 3, 4, 6, 1, 2, 3, 4, 6), + GChainReconciliation.reconcileCycles( + listOf(1, 2, 3, 4, 6, 1, 2, 3, 4, 6), + listOf(2, 3, 4, 5, 1, 2, 3, 4, 5, 1) + ) + ) + } + + @Test + fun testMultipleIndependentHoles() { + assertEqualCycles( + listOf(1, 2, 3, 4, 5, 6), + GChainReconciliation.reconcileCycles( + listOf(1, 3, 4, 5, 6, 1, 3, 4, 5, 6), + listOf(2, 3, 4, 5, 1, 2, 3, 4, 5, 1) + ) + ) + + } + + @Test + fun testBigHole() { + assertEqualCycles( + listOf(1, 2, 3, 4, 5, 6), + GChainReconciliation.reconcileCycles( + listOf(1, 4, 5, 6, 1, 4, 5, 6), + listOf(2, 3, 4, 5, 1, 2, 3, 4, 5, 1) + ) + ) + + } + + @Test + fun testOneMissingBeingDetected() { + assertEqualCycles( + listOf(1, 2, 3, 4, 5, 6), + GChainReconciliation.reconcileCycles( + listOf(1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6), + listOf(2, 3, 4, 5, 1, 2, 3, 4, 5, 1) + ) + ) + } +} diff --git a/src/test/kotlin/util/math/ProjectionsBoxTest.kt b/src/test/kotlin/util/math/ProjectionsBoxTest.kt new file mode 100644 index 0000000..04720a3 --- /dev/null +++ b/src/test/kotlin/util/math/ProjectionsBoxTest.kt @@ -0,0 +1,28 @@ +package moe.nea.firmament.test.util.math + +import java.util.stream.Stream +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.TestFactory +import kotlin.streams.asStream +import net.minecraft.util.math.Vec2f +import moe.nea.firmament.util.math.Projections + +class ProjectionsBoxTest { + val Double.degrees get() = Math.toRadians(this) + + @TestFactory + fun testProjections(): Stream { + return sequenceOf( + 0.0.degrees to Vec2f(1F, 0F), + 63.4349.degrees to Vec2f(0.5F, 1F), + ).map { (angle, expected) -> + DynamicTest.dynamicTest("ProjectionsBoxTest::projectAngleOntoUnitBox(${angle})") { + val actual = Projections.Two.projectAngleOntoUnitBox(angle) + fun msg() = "Expected (${expected.x}, ${expected.y}) got (${actual.x}, ${actual.y})" + Assertions.assertEquals(expected.x, actual.x, 0.0001F, ::msg) + Assertions.assertEquals(expected.y, actual.y, 0.0001F, ::msg) + } + }.asStream() + } +} diff --git a/src/test/kotlin/util/skyblock/AbilityUtilsTest.kt b/src/test/kotlin/util/skyblock/AbilityUtilsTest.kt new file mode 100644 index 0000000..9d25aad --- /dev/null +++ b/src/test/kotlin/util/skyblock/AbilityUtilsTest.kt @@ -0,0 +1,87 @@ +package moe.nea.firmament.test.util.skyblock + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds +import net.minecraft.text.Text +import moe.nea.firmament.test.testutil.ItemResources +import moe.nea.firmament.util.skyblock.AbilityUtils +import moe.nea.firmament.util.unformattedString + +class AbilityUtilsTest { + + fun List.stripDescriptions() = map { + it.copy(descriptionLines = it.descriptionLines.map { Text.literal(it.unformattedString) }) + } + + @Test + fun testUnpoweredDrill() { + Assertions.assertEquals( + listOf( + AbilityUtils.ItemAbility( + "Pickobulus", + false, + AbilityUtils.AbilityActivation.RIGHT_CLICK, + null, + listOf( + "Throw your pickaxe to create an", + "explosion mining all ores in a 3 block", + "radius." + ).map(Text::literal), + 48.seconds + ) + ), + AbilityUtils.getAbilities(ItemResources.loadItem("titanium-drill")).stripDescriptions() + ) + } + + @Test + fun testPoweredPickaxe() { + Assertions.assertEquals( + listOf( + AbilityUtils.ItemAbility( + "Mining Speed Boost", + true, + AbilityUtils.AbilityActivation.RIGHT_CLICK, + null, + listOf( + "Grants +200% ⸕ Mining Speed for", + "10s." + ).map(Text::literal), + 2.minutes + ) + ), + AbilityUtils.getAbilities(ItemResources.loadItem("diamond-pickaxe")).stripDescriptions() + ) + } + + @Test + fun testAOTV() { + Assertions.assertEquals( + listOf( + AbilityUtils.ItemAbility( + "Instant Transmission", true, AbilityUtils.AbilityActivation.RIGHT_CLICK, 23, + listOf( + "Teleport 12 blocks ahead of you and", + "gain +50 ✦ Speed for 3 seconds." + ).map(Text::literal), + null + ), + AbilityUtils.ItemAbility( + "Ether Transmission", + false, + AbilityUtils.AbilityActivation.SNEAK_RIGHT_CLICK, + 90, + listOf( + "Teleport to your targeted block up", + "to 61 blocks away.", + "Soulflow Cost: 1" + ).map(Text::literal), + null + ) + ), + AbilityUtils.getAbilities(ItemResources.loadItem("aspect-of-the-void")).stripDescriptions() + ) + } +} diff --git a/src/test/kotlin/util/skyblock/ItemTypeTest.kt b/src/test/kotlin/util/skyblock/ItemTypeTest.kt new file mode 100644 index 0000000..c0ef2a3 --- /dev/null +++ b/src/test/kotlin/util/skyblock/ItemTypeTest.kt @@ -0,0 +1,28 @@ +package moe.nea.firmament.test.util.skyblock + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.TestFactory +import moe.nea.firmament.test.testutil.ItemResources +import moe.nea.firmament.util.skyblock.ItemType + +class ItemTypeTest { + @TestFactory + fun fromItemstack() = + listOf( + "pets/lion-item" to ItemType.PET, + "pets/rabbit-selected" to ItemType.PET, + "pets/mithril-golem-not-selected" to ItemType.PET, + "aspect-of-the-void" to ItemType.SWORD, + "titanium-drill" to ItemType.DRILL, + "diamond-pickaxe" to ItemType.PICKAXE, + "gemstone-gauntlet" to ItemType.GAUNTLET, + ).map { (name, typ) -> + DynamicTest.dynamicTest("return $typ for $name") { + Assertions.assertEquals( + typ, + ItemType.fromItemStack(ItemResources.loadItem(name)) + ) + } + } +} diff --git a/src/test/kotlin/util/skyblock/SackUtilTest.kt b/src/test/kotlin/util/skyblock/SackUtilTest.kt new file mode 100644 index 0000000..e0e3e63 --- /dev/null +++ b/src/test/kotlin/util/skyblock/SackUtilTest.kt @@ -0,0 +1,29 @@ +package moe.nea.firmament.test.util.skyblock + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import moe.nea.firmament.test.testutil.ItemResources +import moe.nea.firmament.util.skyblock.SackUtil +import moe.nea.firmament.util.skyblock.SkyBlockItems + +class SackUtilTest { + @Test + fun testOneRottenFlesh() { + Assertions.assertEquals( + listOf( + SackUtil.SackUpdate(SkyBlockItems.ROTTEN_FLESH, "Rotten Flesh", 1) + ), + SackUtil.getUpdatesFromMessage(ItemResources.loadText("sacks/gain-rotten-flesh")) + ) + } + + @Test + fun testAFewRegularItems() { + Assertions.assertEquals( + listOf( + SackUtil.SackUpdate(SkyBlockItems.ROTTEN_FLESH, "Rotten Flesh", 1) + ), + SackUtil.getUpdatesFromMessage(ItemResources.loadText("sacks/gain-and-lose-regular")) + ) + } +} diff --git a/src/test/kotlin/util/skyblock/TabListAPITest.kt b/src/test/kotlin/util/skyblock/TabListAPITest.kt new file mode 100644 index 0000000..26eafe0 --- /dev/null +++ b/src/test/kotlin/util/skyblock/TabListAPITest.kt @@ -0,0 +1,48 @@ +package moe.nea.firmament.test.util.skyblock + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import moe.nea.firmament.test.testutil.ItemResources +import moe.nea.firmament.util.skyblock.TabListAPI + +class TabListAPITest { + val tablist = ItemResources.loadTablist("dungeon_hub") + + @Test + fun checkWithTitle() { + Assertions.assertEquals( + listOf( + "Profile: Strawberry", + " SB Level: [210] 26/100 XP", + " Bank: 1.4B", + " Interest: 12 Hours (689.1k)", + ), + TabListAPI.getWidgetLines(TabListAPI.WidgetName.PROFILE, includeTitle = true, from = tablist).map { it.string }) + } + + @Test + fun checkEndOfColumn() { + Assertions.assertEquals( + listOf( + " Bonzo IV: 110/150", + " Scarf II: 25/50", + " The Professor IV: 141/150", + " Thorn I: 29/50", + " Livid II: 91/100", + " Sadan V: 388/500", + " Necron VI: 531/750", + ), + TabListAPI.getWidgetLines(TabListAPI.WidgetName.COLLECTION, from = tablist).map { it.string } + ) + } + + @Test + fun checkWithoutTitle() { + Assertions.assertEquals( + listOf( + " Undead: 1,907", + " Wither: 318", + ), + TabListAPI.getWidgetLines(TabListAPI.WidgetName.ESSENCE, from = tablist).map { it.string }) + } +} diff --git a/src/test/kotlin/util/skyblock/TimestampTest.kt b/src/test/kotlin/util/skyblock/TimestampTest.kt new file mode 100644 index 0000000..b960cb9 --- /dev/null +++ b/src/test/kotlin/util/skyblock/TimestampTest.kt @@ -0,0 +1,28 @@ +package moe.nea.firmament.test.util.skyblock + +import java.time.Instant +import java.time.ZonedDateTime +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import moe.nea.firmament.test.testutil.ItemResources +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.timestamp + +class TimestampTest { + + @Test + fun testLongTimestamp() { + Assertions.assertEquals( + Instant.ofEpochSecond(1658091600), + ItemResources.loadItem("hyperion").timestamp + ) + } + + @Test + fun testStringTimestamp() { + Assertions.assertEquals( + ZonedDateTime.of(2021, 10, 11, 15, 39, 0, 0, SBData.hypixelTimeZone).toInstant(), + ItemResources.loadItem("backpack-in-menu").timestamp + ) + } +} diff --git a/src/test/resources/testdata/chat/all-chat.snbt b/src/test/resources/testdata/chat/all-chat.snbt new file mode 100644 index 0000000..386194b --- /dev/null +++ b/src/test/resources/testdata/chat/all-chat.snbt @@ -0,0 +1,165 @@ +{ + source: { + dataVersion: 4189, + }, + extra: [ + { + bold: 0b, + color: "#555555", + hoverEvent: { + action: "show_text", + contents: { + clickEvent: { + action: "run_command", + value: "command" + }, + strikethrough: 0b, + text: "§b[MVP§d+§b] lrg89§f + +§7SkyBlock Level: §8[§9302§8] +§7Skill Average: §648.1 + +§7Emblem: §6♫ +§8Harp Master + +§7§8Unlocked for All Harp Songs Perfected. + +§7Perfect Completions: §b37" + } + }, + italic: 0b, + obfuscated: 0b, + strikethrough: 0b, + text: "[", + underlined: 0b + }, + { + bold: 0b, + color: "#5555FF", + hoverEvent: { + action: "show_text", + contents: { + clickEvent: { + action: "run_command", + value: "command" + }, + strikethrough: 0b, + text: "§b[MVP§d+§b] lrg89§f + +§7SkyBlock Level: §8[§9302§8] +§7Skill Average: §648.1 + +§7Emblem: §6♫ +§8Harp Master + +§7§8Unlocked for All Harp Songs Perfected. + +§7Perfect Completions: §b37" + } + }, + italic: 0b, + obfuscated: 0b, + strikethrough: 0b, + text: "302", + underlined: 0b + }, + { + bold: 0b, + color: "#555555", + hoverEvent: { + action: "show_text", + contents: { + clickEvent: { + action: "run_command", + value: "command" + }, + strikethrough: 0b, + text: "§b[MVP§d+§b] lrg89§f + +§7SkyBlock Level: §8[§9302§8] +§7Skill Average: §648.1 + +§7Emblem: §6♫ +§8Harp Master + +§7§8Unlocked for All Harp Songs Perfected. + +§7Perfect Completions: §b37" + } + }, + italic: 0b, + obfuscated: 0b, + strikethrough: 0b, + text: "] ", + underlined: 0b + }, + { + bold: 0b, + color: "#FFAA00", + hoverEvent: { + action: "show_text", + contents: { + clickEvent: { + action: "run_command", + value: "command" + }, + strikethrough: 0b, + text: "§b[MVP§d+§b] lrg89§f + +§7SkyBlock Level: §8[§9302§8] +§7Skill Average: §648.1 + +§7Emblem: §6♫ +§8Harp Master + +§7§8Unlocked for All Harp Songs Perfected. + +§7Perfect Completions: §b37" + } + }, + italic: 0b, + obfuscated: 0b, + strikethrough: 0b, + text: "♫ ", + underlined: 0b + }, + { + bold: 0b, + color: "#55FFFF", + italic: 0b, + obfuscated: 0b, + strikethrough: 0b, + text: "[MVP", + underlined: 0b + }, + { + bold: 0b, + color: "#FF55FF", + italic: 0b, + obfuscated: 0b, + strikethrough: 0b, + text: "+", + underlined: 0b + }, + { + bold: 0b, + color: "#55FFFF", + italic: 0b, + obfuscated: 0b, + strikethrough: 0b, + text: "] lrg89", + underlined: 0b + }, + { + bold: 0b, + color: "#FFFFFF", + italic: 0b, + obfuscated: 0b, + strikethrough: 0b, + text: ": test", + underlined: 0b + } + ], + strikethrough: 0b, + text: "" +} diff --git a/src/test/resources/testdata/chat/sacks/gain-and-lose-regular.snbt b/src/test/resources/testdata/chat/sacks/gain-and-lose-regular.snbt new file mode 100644 index 0000000..d7b8b90 --- /dev/null +++ b/src/test/resources/testdata/chat/sacks/gain-and-lose-regular.snbt @@ -0,0 +1,104 @@ +{ + source: { + dataVersion: 4189, + }, + color: "#FFAA00", + extra: [ + { + color: "#55FF55", + hoverEvent: { + action: "show_text", + contents: { + color: "#55FF55", + extra: [ + { + color: "#55FF55", + strikethrough: 0b, + text: " +1 " + }, + { + color: "#FFFF55", + strikethrough: 0b, + text: "Rotten Flesh" + }, + { + color: "#555555", + strikethrough: 0b, + text: " (Combat Sack)" + }, + { + strikethrough: 0b, + text: " + +" + }, + { + color: "#555555", + strikethrough: 0b, + text: "This message can be disabled in the settings." + } + ], + strikethrough: 0b, + text: "Added items: +" + } + }, + strikethrough: 0b, + text: "+1" + }, + { + color: "#FFFF55", + hoverEvent: { + action: "show_text", + contents: { + color: "#55FF55", + extra: [ + { + color: "#55FF55", + strikethrough: 0b, + text: " +1 " + }, + { + color: "#FFFF55", + strikethrough: 0b, + text: "Rotten Flesh" + }, + { + color: "#555555", + strikethrough: 0b, + text: " (Combat Sack)" + }, + { + strikethrough: 0b, + text: " + +" + }, + { + color: "#555555", + strikethrough: 0b, + text: "This message can be disabled in the settings." + } + ], + strikethrough: 0b, + text: "Added items: +" + } + }, + strikethrough: 0b, + text: " item" + }, + { + color: "#FFFF55", + strikethrough: 0b, + text: "." + }, + { + color: "#555555", + strikethrough: 0b, + text: " (Last 5s.)" + } + ], + strikethrough: 0b, + text: "[Sacks] " +} diff --git a/src/test/resources/testdata/chat/sacks/gain-rotten-flesh.snbt b/src/test/resources/testdata/chat/sacks/gain-rotten-flesh.snbt new file mode 100644 index 0000000..d7b8b90 --- /dev/null +++ b/src/test/resources/testdata/chat/sacks/gain-rotten-flesh.snbt @@ -0,0 +1,104 @@ +{ + source: { + dataVersion: 4189, + }, + color: "#FFAA00", + extra: [ + { + color: "#55FF55", + hoverEvent: { + action: "show_text", + contents: { + color: "#55FF55", + extra: [ + { + color: "#55FF55", + strikethrough: 0b, + text: " +1 " + }, + { + color: "#FFFF55", + strikethrough: 0b, + text: "Rotten Flesh" + }, + { + color: "#555555", + strikethrough: 0b, + text: " (Combat Sack)" + }, + { + strikethrough: 0b, + text: " + +" + }, + { + color: "#555555", + strikethrough: 0b, + text: "This message can be disabled in the settings." + } + ], + strikethrough: 0b, + text: "Added items: +" + } + }, + strikethrough: 0b, + text: "+1" + }, + { + color: "#FFFF55", + hoverEvent: { + action: "show_text", + contents: { + color: "#55FF55", + extra: [ + { + color: "#55FF55", + strikethrough: 0b, + text: " +1 " + }, + { + color: "#FFFF55", + strikethrough: 0b, + text: "Rotten Flesh" + }, + { + color: "#555555", + strikethrough: 0b, + text: " (Combat Sack)" + }, + { + strikethrough: 0b, + text: " + +" + }, + { + color: "#555555", + strikethrough: 0b, + text: "This message can be disabled in the settings." + } + ], + strikethrough: 0b, + text: "Added items: +" + } + }, + strikethrough: 0b, + text: " item" + }, + { + color: "#FFFF55", + strikethrough: 0b, + text: "." + }, + { + color: "#555555", + strikethrough: 0b, + text: " (Last 5s.)" + } + ], + strikethrough: 0b, + text: "[Sacks] " +} diff --git a/src/test/resources/testdata/items/aspect-of-the-void.snbt b/src/test/resources/testdata/items/aspect-of-the-void.snbt new file mode 100644 index 0000000..9ffd385 --- /dev/null +++ b/src/test/resources/testdata/items/aspect-of-the-void.snbt @@ -0,0 +1,62 @@ +{ + source: { + dataVersion: 4189, + }, + components: { + "minecraft:attribute_modifiers": { + modifiers: [ + ], + show_in_tooltip: 0b + }, + "minecraft:custom_data": { + donated_museum: 1b, + enchantments: { + ultimate_wise: 5 + }, + ethermerge: 1b, + gems: { + }, + id: "ASPECT_OF_THE_VOID", + modifier: "heroic", + originTag: "ASPECT_OF_THE_VOID", + power_ability_scroll: "SAPPHIRE_POWER_SCROLL", + timestamp: 1641640380000L, + tuned_transmission: 4, + uuid: "b0572534-eb14-46cd-90c6-0df878fd56a2" + }, + "minecraft:custom_name": '{"extra":[{"color":"dark_purple","text":"Heroic Aspect of the Void"}],"italic":false,"text":""}', + "minecraft:enchantment_glint_override": 1b, + "minecraft:hide_additional_tooltip": { + }, + "minecraft:lore": [ + '{"extra":[{"color":"gray","text":"Damage: "},{"color":"red","text":"+120"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Strength: "},{"color":"red","text":"+132 "},{"color":"blue","text":"(+32)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Bonus Attack Speed: "},{"color":"red","text":"+3% "},{"color":"blue","text":"(+3%)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Intelligence: "},{"color":"green","text":"+80 "},{"color":"blue","text":"(+80)"}],"italic":false,"text":""}', + '{"extra":[" ",{"color":"dark_gray","text":"["},{"color":"gray","text":"✎"},{"color":"dark_gray","text":"]"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":""},{"bold":true,"color":"light_purple","text":"Ultimate Wise V"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Reduces the ability mana cost of this"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"item by "},{"color":"green","text":"50%"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"aqua","text":"⦾ "},{"color":"gold","text":"Ability: Instant Transmission "},{"bold":true,"color":"yellow","text":"RIGHT CLICK"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Teleport "},{"color":"green","text":"12 blocks"},{"color":"gray","text":" ahead of you and"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"gain "},{"color":"green","text":"+50 "},{"color":"white","text":"✦ Speed"},{"color":"gray","text":" for "},{"color":"green","text":"3 seconds"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"extra":[{"color":"dark_gray","text":"Mana Cost: "},{"color":"dark_aqua","text":"23"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"Ability: Ether Transmission "},{"bold":true,"color":"yellow","text":"SNEAK RIGHT CLICK"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Teleport to your targeted block up"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"to "},{"color":"green","text":"61 blocks "},{"color":"gray","text":"away."}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"dark_gray","text":"Soulflow Cost: "},{"color":"dark_aqua","text":"1"}],"italic":false,"text":""}', + '{"extra":[{"color":"dark_gray","text":"Mana Cost: "},{"color":"dark_aqua","text":"90"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"dark_gray","text":"* "},{"color":"dark_gray","text":"Co-op Soulbound "},{"bold":true,"color":"dark_gray","text":"*"}],"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"dark_purple","text":"EPIC SWORD"}],"italic":false,"text":""}' + ], + "minecraft:unbreakable": { + show_in_tooltip: 0b + } + }, + count: 1, + id: "minecraft:diamond_shovel" +} diff --git a/src/test/resources/testdata/items/backpack-in-menu.snbt b/src/test/resources/testdata/items/backpack-in-menu.snbt new file mode 100644 index 0000000..2f22768 --- /dev/null +++ b/src/test/resources/testdata/items/backpack-in-menu.snbt @@ -0,0 +1,122 @@ +{ + components: { + "minecraft:custom_data": { + backpack_color: "BROWN", + originTag: "CRAFTING_GRID_COLLECT", + timestamp: "10/11/21 3:39 PM", + uuid: "3d7c83e8-c619-4603-8cfb-c95ceed90864" + }, + "minecraft:custom_name": { + extra: [ + { + color: "gold", + text: "Backpack Slot 3" + } + ], + italic: 0b, + text: "" + }, + "minecraft:lore": [ + { + extra: [ + { + color: "gold", + text: "Jumbo Backpack" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + { + color: "gray", + text: "" + }, + { + color: "gray", + text: "This backpack has " + }, + { + color: "green", + text: "45" + }, + { + color: "gray", + text: " slots." + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + " " + ], + italic: 0b, + text: "" + }, + { + extra: [ + { + color: "gray", + text: "" + }, + { + color: "yellow", + text: "Left-click to open!" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + { + color: "gray", + text: "" + }, + { + color: "yellow", + text: "Right-click to remove!" + } + ], + italic: 0b, + text: "" + } + ], + "minecraft:profile": { + id: [I; + 1252359403, + 1319582828, + -1927151386, + 833492163 + ], + properties: [ + { + name: "textures", + signature: "U/49v6SXIw8bAmqM6T7t1BIR736N3Adpx7MlWncnT8zcFEm97zwRx9/tyaUy/XxBHaPGSL6BbgW2TdBtfb9gf0emCAZyWmnzSTtqDGiWpxnQM8v3+gHS8zD7Xrho0a/hU33xTbQ2knj2iRz8C+FReoJFxCjS++aXq6IqliIb3GhqB5b1egaiG2Q3t+yerl2Xue4nhdYM3wtGsYApC/ClR3TEuBcJv1WUVZM8rEoU29pbVnyMCKineG6mIN7W86SmzcT2SF+zMVyD0/mI7R2hRT2lbXnkMpM6FFscdnlvzjjPB9brtAWY7JGJ63b9C+khnvZUlhlQ/3E/08dFnON31VeabJXOmfrbfAgsF0Hgfs7Io+HzoXSXr/FCxNCCFMWlSwORmG2WCT4VRFzG2SThatPVPGJkuR/tLLOLzXo4RKOMzY5EIwa2XSxRUI4+5z2SZY11ofGic3bZD3wvICs2EZ54Pi508ZOda0qI9w5Q/TazC+jX/I5Nq2TLqLj+uU/+UX8eKXvHdk8QpBynyv9SyHo21jVXpiUgL1AsdzBp9cTZHNJuYtBxgDogr3SyAKPmw3BOzVeUi6qW8k4lgtefLKYteVSh52PjFgvQZUR1GNmFaJ+hlgKz8yONp+wXhw3nyL4dMOd2Z/dVVSywBp0tyHuN5l3PfaInK4s8qSydaW0=", + value: "ewogICJ0aW1lc3RhbXAiIDogMTcxOTUzODgxNTgyNCwKICAicHJvZmlsZUlkIiA6ICJkOWYxNTlhYWYxZjY0NGZlOTEwOTg0NzI2ZDBjMWJjMCIsCiAgInByb2ZpbGVOYW1lIiA6ICJtYW5vbmFtaXNzaW9uRyIsCiAgInNpZ25hdHVyZVJlcXVpcmVkIiA6IHRydWUsCiAgInRleHR1cmVzIiA6IHsKICAgICJTS0lOIiA6IHsKICAgICAgInVybCIgOiAiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS81YWQwYjQwNTIxMjYyYjdhM2Y5OWU2M2JkZGQ0YTNlNTQxOTY1Njc3ZTE0MTRlYWZhMTQyZThiYmE5ZGZlNDgxIiwKICAgICAgIm1ldGFkYXRhIiA6IHsKICAgICAgICAibW9kZWwiIDogInNsaW0iCiAgICAgIH0KICAgIH0KICB9Cn0=" + } + ] + }, + "minecraft:tooltip_display": { + hidden_components: [ + "minecraft:jukebox_playable", + "minecraft:painting/variant", + "minecraft:map_id", + "minecraft:fireworks", + "minecraft:attribute_modifiers", + "minecraft:unbreakable", + "minecraft:written_book_content", + "minecraft:banner_patterns", + "minecraft:trim", + "minecraft:potion_contents", + "minecraft:block_entity_data", + "minecraft:dyed_color" + ] + } + }, + count: 3, + id: "minecraft:player_head" +} diff --git a/src/test/resources/testdata/items/books/feather_falling.snbt b/src/test/resources/testdata/items/books/feather_falling.snbt new file mode 100644 index 0000000..4a0b7c6 --- /dev/null +++ b/src/test/resources/testdata/items/books/feather_falling.snbt @@ -0,0 +1,39 @@ +{ + source: { + dataVersion: 4189, + }, + components: { + "minecraft:attribute_modifiers": { + modifiers: [ + ], + show_in_tooltip: 0b + }, + "minecraft:custom_data": { + enchantments: { + feather_falling: 6 + }, + id: "ENCHANTED_BOOK", + timestamp: 1737123521091L, + uuid: "b8128489-9ed0-4a1a-94c0-d3279ffe45ac" + }, + "minecraft:custom_name": '{"extra":[{"color":"blue","text":"Enchanted Book"}],"italic":false,"text":""}', + "minecraft:hide_additional_tooltip": { + }, + "minecraft:lore": [ + '{"extra":[{"color":"blue","text":"Feather Falling VI"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Increases how high you can fall"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"before taking fall damage by "},{"color":"green","text":"6"},{"color":"gray","text":" and"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"reduces fall damage by "},{"color":"green","text":"30%"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Applicable on: "},{"color":"blue","text":"Boots"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Apply Cost: "},{"color":"dark_aqua","text":"60 Exp Levels"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Use this on an item in an Anvil to"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"apply it!"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"blue","text":"RARE"}],"italic":false,"text":""}' + ] + }, + count: 1, + id: "minecraft:enchanted_book" +} diff --git a/src/test/resources/testdata/items/diamond-pickaxe.snbt b/src/test/resources/testdata/items/diamond-pickaxe.snbt new file mode 100644 index 0000000..aa5e590 --- /dev/null +++ b/src/test/resources/testdata/items/diamond-pickaxe.snbt @@ -0,0 +1,51 @@ +{ + source: { + dataVersion: 4189, + }, + components: { + "minecraft:attribute_modifiers": { + modifiers: [ + ], + show_in_tooltip: 0b + }, + "minecraft:custom_data": { + enchantments: { + efficiency: 10 + }, + id: "DIAMOND_PICKAXE", + power_ability_scroll: "SAPPHIRE_POWER_SCROLL", + timestamp: 1659795180000L, + uuid: "d213f48e-d927-4748-a58c-eb80735025b7" + }, + "minecraft:custom_name": '{"extra":[{"color":"green","text":"Diamond Pickaxe"}],"italic":false,"text":""}', + "minecraft:enchantments": { + levels: { + } + }, + "minecraft:hide_additional_tooltip": { + }, + "minecraft:lore": [ + '{"extra":[{"color":"dark_gray","text":"Breaking Power 4"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Damage: "},{"color":"red","text":"+30"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Mining Speed: "},{"color":"green","text":"+220"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Efficiency X"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Increases how quickly your tool"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"breaks blocks."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"aqua","text":"⦾ "},{"color":"gold","text":"Ability: Mining Speed Boost "},{"bold":true,"color":"yellow","text":"RIGHT CLICK"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Grants "},{"color":"gold","text":"+200% "},{"color":"gold","text":"⸕ Mining Speed "},{"color":"gray","text":"for"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"green","text":"10s"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"extra":[{"color":"dark_gray","text":"Cooldown: "},{"color":"green","text":"120s"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"dark_gray","text":"This item can be reforged!"}],"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"green","text":"UNCOMMON PICKAXE"}],"italic":false,"text":""}' + ], + "minecraft:unbreakable": { + show_in_tooltip: 0b + } + }, + count: 1, + id: "minecraft:diamond_pickaxe" +} diff --git a/src/test/resources/testdata/items/gemstone-gauntlet.snbt b/src/test/resources/testdata/items/gemstone-gauntlet.snbt new file mode 100644 index 0000000..92bb806 --- /dev/null +++ b/src/test/resources/testdata/items/gemstone-gauntlet.snbt @@ -0,0 +1,106 @@ +{ + source: { + dataVersion: 4189, + }, + components: { + "minecraft:attribute_modifiers": { + modifiers: [ + ], + show_in_tooltip: 0b + }, + "minecraft:custom_data": { + compact_blocks: 287507, + donated_museum: 1b, + enchantments: { + compact: 8, + critical: 5, + efficiency: 5, + experience: 3, + first_strike: 4, + fortune: 4, + giant_killer: 5, + pristine: 3, + scavenger: 3, + sharpness: 5, + telekinesis: 1 + }, + gems: { + AMBER_0: "FINE", + AMETHYST_0: "FINE", + JADE_0: "FINE", + SAPPHIRE_0: "FINE", + TOPAZ_0: "FINE" + }, + id: "GEMSTONE_GAUNTLET", + modifier: "auspicious", + originTag: "QUICK_CRAFTING", + timestamp: 1642718160000L, + uuid: "af56dd7b-c4b1-4e26-8d09-1854680a93c3" + }, + "minecraft:custom_name": '{"extra":[{"color":"gold","text":"Auspicious Gemstone Gauntlet"}],"italic":false,"text":""}', + "minecraft:enchantments": { + levels: { + "minecraft:efficiency": 5 + } + }, + "minecraft:hide_additional_tooltip": { + }, + "minecraft:lore": [ + '{"extra":[{"color":"dark_gray","text":"Breaking Power 8"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Damage: "},{"color":"red","text":"+300"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Strength: "},{"color":"red","text":"+50"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Crit Damage: "},{"color":"red","text":"+50%"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Defense: "},{"color":"green","text":"+10 "},{"color":"light_purple","text":"(+10)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Intelligence: "},{"color":"green","text":"+11 "},{"color":"light_purple","text":"(+11)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Mining Speed: "},{"color":"green","text":"+886 "},{"color":"blue","text":"(+50) "},{"color":"light_purple","text":"(+36)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Pristine: "},{"color":"green","text":"+2.7 "},{"color":"light_purple","text":"(+1.2)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Mining Fortune: "},{"color":"green","text":"+81 "},{"color":"blue","text":"(+16) "},{"color":"light_purple","text":"(+20)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Mining Wisdom: "},{"color":"green","text":"+8"}],"italic":false,"text":""}', + '{"extra":[" ",{"color":"blue","text":"["},{"color":"green","text":"☘"},{"color":"blue","text":"] "},{"color":"blue","text":"["},{"color":"gold","text":"⸕"},{"color":"blue","text":"] "},{"color":"blue","text":"["},{"color":"yellow","text":"✧"},{"color":"blue","text":"] "},{"color":"blue","text":"["},{"color":"aqua","text":"✎"},{"color":"blue","text":"] "},{"color":"blue","text":"["},{"color":"dark_purple","text":"❈"},{"color":"blue","text":"]"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Compact VIII"},{"color":"blue","text":", "},{"color":"blue","text":"Critical V"},{"color":"blue","text":", "},{"color":"blue","text":"Efficiency V"}],"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Experience III"},{"color":"blue","text":", "},{"color":"blue","text":"First Strike IV"},{"color":"blue","text":", "},{"color":"blue","text":"Fortune IV"}],"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Giant Killer V"},{"color":"blue","text":", "},{"color":"blue","text":"Prismatic III"},{"color":"blue","text":", "},{"color":"blue","text":"Scavenger III"}],"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Sharpness V"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gold","text":"Ability: Reduced To Atoms"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Mobs killed on "},{"color":"aqua","text":"Mining Islands "},{"color":"gray","text":"drop the same"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"light_purple","text":"Gemstones "},{"color":"gray","text":"as your filled Gemstone Slots."}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"dark_gray","text":"(2s cooldown)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gold","text":"Ability: Kinetic"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Killing mobs on "},{"color":"aqua","text":"Mining Islands "},{"color":"gray","text":"reduces your"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gold","text":"Forge Timers "},{"color":"gray","text":"by "},{"color":"green","text":"0s"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"dark_gray","text":"(+0.5s per Perfect Gemstone)"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"Ability: Mining Speed Boost "},{"bold":true,"color":"yellow","text":"RIGHT CLICK"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Grants "},{"color":"gold","text":"+200% "},{"color":"gold","text":"⸕ Mining Speed "},{"color":"gray","text":"for"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"green","text":"10s"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"extra":[{"color":"dark_gray","text":"Cooldown: "},{"color":"green","text":"120s"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Auspicious Bonus"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Grants "},{"color":"gold","text":"+0.8% "},{"color":"gold","text":"☘ Mining Fortune"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"dark_gray","text":"* "},{"color":"dark_gray","text":"Co-op Soulbound "},{"bold":true,"color":"dark_gray","text":"*"}],"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"gold","text":"LEGENDARY GAUNTLET"}],"italic":false,"text":""}' + ], + "minecraft:profile": { + id: [I; + -861744046, + -959235637, + -1231724855, + 724395817 + ], + properties: [ + { + name: "textures", + signature: "", + value: "ewogICJ0aW1lc3RhbXAiIDogMTYxODUyMTY2MzY1NCwKICAicHJvZmlsZUlkIiA6ICIxZDUyMzNkMzg4NjI0YmFmYjAwZTMxNTBhN2FhM2E4OSIsCiAgInByb2ZpbGVOYW1lIiA6ICIwMDAwMDAwMDAwMDAwMDBKIiwKICAic2lnbmF0dXJlUmVxdWlyZWQiIDogdHJ1ZSwKICAidGV4dHVyZXMiIDogewogICAgIlNLSU4iIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzdiZjAxYzE5OGY2ZTE2OTY1ZTIzMDIzNWNkMjJhNWE5ZjRhNDBlNDA5NDEyMzQ0Nzg5NDhmZjlhNTZlNTE3NzUiLAogICAgICAibWV0YWRhdGEiIDogewogICAgICAgICJtb2RlbCIgOiAic2xpbSIKICAgICAgfQogICAgfQogIH0KfQ==" + } + ] + } + }, + count: 1, + id: "minecraft:player_head" +} diff --git a/src/test/resources/testdata/items/hyperion.snbt b/src/test/resources/testdata/items/hyperion.snbt new file mode 100644 index 0000000..f0025b9 --- /dev/null +++ b/src/test/resources/testdata/items/hyperion.snbt @@ -0,0 +1,99 @@ +{ + source: { + dataVersion: 4189, + }, + components: { + "minecraft:attribute_modifiers": { + modifiers: [ + ], + show_in_tooltip: 0b + }, + "minecraft:custom_data": { + ability_scroll: [ + "IMPLOSION_SCROLL", + "WITHER_SHIELD_SCROLL", + "SHADOW_WARP_SCROLL" + ], + art_of_war_count: 1, + champion_combat_xp: 1.3556020889209766E7d, + donated_museum: 1b, + enchantments: { + champion: 10, + cleave: 5, + critical: 6, + cubism: 5, + ender_slayer: 6, + execute: 5, + experience: 3, + fire_aspect: 2, + first_strike: 4, + giant_killer: 6, + impaling: 3, + lethality: 5, + looting: 4, + luck: 6, + scavenger: 4, + smite: 7, + syphon: 4, + thunderlord: 6, + ultimate_wise: 5, + vampirism: 5, + venomous: 5 + }, + hot_potato_count: 15, + id: "HYPERION", + modifier: "heroic", + rarity_upgrades: 1, + stats_book: 65934, + timestamp: 1658091600000L, + upgrade_level: 5, + uuid: "a45337aa-9eaa-4e6f-aa27-26a42f8eca95" + }, + "minecraft:custom_name": '{"extra":[{"color":"light_purple","text":"Heroic Hyperion "},{"color":"gold","text":"✪✪✪✪✪"}],"italic":false,"text":""}', + "minecraft:enchantment_glint_override": 1b, + "minecraft:hide_additional_tooltip": { + }, + "minecraft:lore": [ + '{"extra":[{"color":"gray","text":"Gear Score: "},{"color":"light_purple","text":"1145 "},{"color":"dark_gray","text":"(4271)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Damage: "},{"color":"red","text":"+355 "},{"color":"yellow","text":"(+30) "},{"color":"dark_gray","text":"(+1,490.37)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Strength: "},{"color":"red","text":"+250 "},{"color":"yellow","text":"(+30) "},{"color":"gold","text":"[+5] "},{"color":"blue","text":"(+50) "},{"color":"dark_gray","text":"(+1,064.55)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Crit Damage: "},{"color":"red","text":"+70% "},{"color":"dark_gray","text":"(+317.1%)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Bonus Attack Speed: "},{"color":"red","text":"+7% "},{"color":"blue","text":"(+7%) "},{"color":"dark_gray","text":"(+10.5%)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Intelligence: "},{"color":"green","text":"+588 "},{"color":"blue","text":"(+125) "},{"color":"dark_gray","text":"(+2,505.09)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Ferocity: "},{"color":"green","text":"+33 "},{"color":"dark_gray","text":"(+45)"}],"italic":false,"text":""}', + '{"extra":[" ",{"color":"dark_gray","text":"["},{"color":"dark_gray","text":"✎"},{"color":"dark_gray","text":"] "},{"color":"dark_gray","text":"["},{"color":"dark_gray","text":"⚔"},{"color":"dark_gray","text":"]"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"light_purple","text":""},{"bold":true,"color":"light_purple","text":"Ultimate Wise V"},{"color":"blue","text":", "},{"color":"blue","text":"Champion X"},{"color":"blue","text":", "},{"color":"blue","text":"Cleave V"}],"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Critical VI"},{"color":"blue","text":", "},{"color":"blue","text":"Cubism V"},{"color":"blue","text":", "},{"color":"blue","text":"Ender Slayer VI"}],"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Execute V"},{"color":"blue","text":", "},{"color":"blue","text":"Experience III"},{"color":"blue","text":", "},{"color":"blue","text":"Fire Aspect II"}],"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"First Strike IV"},{"color":"blue","text":", "},{"color":"blue","text":"Giant Killer VI"},{"color":"blue","text":", "},{"color":"blue","text":"Impaling III"}],"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Lethality V"},{"color":"blue","text":", "},{"color":"blue","text":"Looting IV"},{"color":"blue","text":", "},{"color":"blue","text":"Luck VI"}],"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Scavenger IV"},{"color":"blue","text":", "},{"color":"blue","text":"Smite VII"},{"color":"blue","text":", "},{"color":"blue","text":"Syphon IV"}],"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Thunderlord VI"},{"color":"blue","text":", "},{"color":"blue","text":"Vampirism V"},{"color":"blue","text":", "},{"color":"blue","text":"Venomous V"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Deals "},{"color":"red","text":"+50% "},{"color":"gray","text":"damage to Withers."}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Grants "},{"color":"red","text":"+1 "},{"color":"red","text":"❁ Damage "},{"color":"gray","text":"and "},{"color":"green","text":"+2 "},{"color":"aqua","text":"✎"}],"italic":false,"text":""}', + '{"extra":[{"color":"aqua","text":"Intelligence "},{"color":"gray","text":"per "},{"color":"red","text":"Catacombs "},{"color":"gray","text":"level."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"green","text":"Scroll Abilities:"}],"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"Ability: Wither Impact "},{"bold":true,"color":"yellow","text":"RIGHT CLICK"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Teleport "},{"color":"green","text":"10 blocks"},{"color":"gray","text":" ahead of you."}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Then implode dealing "},{"color":"red","text":"21,658 "},{"color":"gray","text":"damage"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"to nearby enemies. Also applies the"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"wither shield scroll ability reducing"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"damage taken and granting an"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"absorption shield for "},{"color":"yellow","text":"5 "},{"color":"gray","text":"seconds."}],"italic":false,"text":""}', + '{"extra":[{"color":"dark_gray","text":"Mana Cost: "},{"color":"dark_aqua","text":"150"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"white","text":"Kills: "},{"color":"gold","text":"65,934"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"dark_gray","text":"* "},{"color":"dark_gray","text":"Co-op Soulbound "},{"bold":true,"color":"dark_gray","text":"*"}],"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"light_purple","obfuscated":true,"text":"a"},"",{"bold":false,"extra":[" "],"italic":false,"obfuscated":false,"strikethrough":false,"text":"","underlined":false},{"bold":true,"color":"light_purple","text":"MYTHIC DUNGEON SWORD "},{"bold":true,"color":"light_purple","obfuscated":true,"text":"a"}],"italic":false,"text":""}' + ], + "minecraft:unbreakable": { + show_in_tooltip: 0b + } + }, + count: 1, + id: "minecraft:iron_sword" +} diff --git a/src/test/resources/testdata/items/implosion-belt.snbt b/src/test/resources/testdata/items/implosion-belt.snbt new file mode 100644 index 0000000..875047d --- /dev/null +++ b/src/test/resources/testdata/items/implosion-belt.snbt @@ -0,0 +1,108 @@ +{ + source: { + dataVersion: 4189, + }, + components: { + "minecraft:attribute_modifiers": { + modifiers: [ + ], + show_in_tooltip: 0b + }, + "minecraft:custom_data": { + attributes: { + dominance: 1, + experience: 1 + }, + id: "IMPLOSION_BELT", + timestamp: "12/5/22 5:17 PM", + uuid: "5c04f47e-7c6c-4ced-96b1-b8f83187b0a5" + }, + "minecraft:custom_name": '{"extra":[{"color":"dark_purple","text":"Implosion Belt"}],"italic":false,"text":""}', + "minecraft:hide_additional_tooltip": { + }, + "minecraft:lore": [ + '{"extra":[{"color":"gray","text":"Defense: "},{"color":"green","text":"+70"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"red","text":"Dominance I ✖"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Gain "},{"color":"red","text":"+1.5% "},{"color":"gray","text":"damage when at full health."}],"italic":false,"text":""}', + '{"extra":[{"color":"aqua","text":"Experience I"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Gain "},{"color":"green","text":"+10% "},{"color":"gray","text":"more experience orbs"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"from killing mobs."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"Ability: Consolidated "}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Increases all explosion damage dealt by "},{"color":"green","text":"25%"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"dark_gray","text":"This item can be reforged!"}],"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"dark_purple","text":"EPIC BELT"}],"italic":false,"text":""}' + ], + "minecraft:profile": { + id: [I; + -896440193, + -59755884, + -1280665573, + -1297214643 + ], + properties: [ + { + name: "textures", + signature: "", + value: "ewogICJ0aW1lc3RhbXAiIDogMTY0MzYwMjI5OTA2MSwKICAicHJvZmlsZUlkIiA6ICI0ZTMwZjUwZTdiYWU0M2YzYWZkMmE3NDUyY2ViZTI5YyIsCiAgInByb2ZpbGVOYW1lIiA6ICJfdG9tYXRvel8iLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjFkMmIwMzZkZDY2NGJiOTBjOWQ0NDNjMTk5OGZiNTI2Mzk4YWI0ZGRkZWI3OWI4NDAxYjE2YjlhNGQxMGJhMyIsCiAgICAgICJtZXRhZGF0YSIgOiB7CiAgICAgICAgIm1vZGVsIiA6ICJzbGltIgogICAgICB9CiAgICB9CiAgfQp9" + } + ] + } + }, + count: 1, + id: "minecraft:player_head" +}{ + components: { + "minecraft:attribute_modifiers": { + modifiers: [ + ], + show_in_tooltip: 0b + }, + "minecraft:custom_data": { + attributes: { + dominance: 1, + experience: 1 + }, + id: "IMPLOSION_BELT", + timestamp: "12/5/22 5:17 PM", + uuid: "5c04f47e-7c6c-4ced-96b1-b8f83187b0a5" + }, + "minecraft:custom_name": '{"extra":[{"color":"dark_purple","text":"Implosion Belt"}],"italic":false,"text":""}', + "minecraft:hide_additional_tooltip": { + }, + "minecraft:lore": [ + '{"extra":[{"color":"gray","text":"Defense: "},{"color":"green","text":"+70"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"red","text":"Dominance I ✖"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Gain "},{"color":"red","text":"+1.5% "},{"color":"gray","text":"damage when at full health."}],"italic":false,"text":""}', + '{"extra":[{"color":"aqua","text":"Experience I"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Gain "},{"color":"green","text":"+10% "},{"color":"gray","text":"more experience orbs"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"from killing mobs."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"Ability: Consolidated "}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Increases all explosion damage dealt by "},{"color":"green","text":"25%"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"dark_gray","text":"This item can be reforged!"}],"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"dark_purple","text":"EPIC BELT"}],"italic":false,"text":""}' + ], + "minecraft:profile": { + id: [I; + -896440193, + -59755884, + -1280665573, + -1297214643 + ], + properties: [ + { + name: "textures", + signature: "", + value: "ewogICJ0aW1lc3RhbXAiIDogMTY0MzYwMjI5OTA2MSwKICAicHJvZmlsZUlkIiA6ICI0ZTMwZjUwZTdiYWU0M2YzYWZkMmE3NDUyY2ViZTI5YyIsCiAgInByb2ZpbGVOYW1lIiA6ICJfdG9tYXRvel8iLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjFkMmIwMzZkZDY2NGJiOTBjOWQ0NDNjMTk5OGZiNTI2Mzk4YWI0ZGRkZWI3OWI4NDAxYjE2YjlhNGQxMGJhMyIsCiAgICAgICJtZXRhZGF0YSIgOiB7CiAgICAgICAgIm1vZGVsIiA6ICJzbGltIgogICAgICB9CiAgICB9CiAgfQp9" + } + ] + } + }, + count: 1, + id: "minecraft:player_head" + } diff --git a/src/test/resources/testdata/items/necron-boots.snbt b/src/test/resources/testdata/items/necron-boots.snbt new file mode 100644 index 0000000..fd740ce --- /dev/null +++ b/src/test/resources/testdata/items/necron-boots.snbt @@ -0,0 +1,71 @@ +{ + source: { + dataVersion: 4189, + }, + components: { + "minecraft:attribute_modifiers": { + modifiers: [ + ], + show_in_tooltip: 0b + }, + "minecraft:custom_data": { + enchantments: { + depth_strider: 3, + feather_falling: 10, + growth: 5, + protection: 5 + }, + id: "POWER_WITHER_BOOTS", + modifier: "ancient", + rarity_upgrades: 1, + timestamp: 1704550620000L, + upgrade_level: 5, + uuid: "8b6c7485-cb59-44d3-ac8f-9e52a611cc64" + }, + "minecraft:custom_name": '{"extra":[{"color":"light_purple","text":"Ancient Necron\'s Boots "},{"color":"gold","text":"✪✪✪✪✪"}],"italic":false,"text":""}', + "minecraft:dyed_color": { + rgb: 15167036, + show_in_tooltip: 0b + }, + "minecraft:enchantments": { + levels: { + "minecraft:depth_strider": 3 + } + }, + "minecraft:hide_additional_tooltip": { + }, + "minecraft:lore": [ + '{"extra":[{"color":"gray","text":"Gear Score: "},{"color":"light_purple","text":"713 "},{"color":"dark_gray","text":"(2753)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Strength: "},{"color":"red","text":"+79 "},{"color":"blue","text":"(+35) "},{"color":"dark_gray","text":"(+333.75)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Crit Chance: "},{"color":"red","text":"+15% "},{"color":"blue","text":"(+15%) "},{"color":"dark_gray","text":"(+23.1%)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Crit Damage: "},{"color":"red","text":"+71% "},{"color":"blue","text":"(+38%) "},{"color":"dark_gray","text":"(+302.6%)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Health: "},{"color":"green","text":"+241.5 "},{"color":"blue","text":"(+7) "},{"color":"dark_gray","text":"(+1,010.15)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Defense: "},{"color":"green","text":"+120.5 "},{"color":"blue","text":"(+7) "},{"color":"dark_gray","text":"(+498.4)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Intelligence: "},{"color":"green","text":"+36 "},{"color":"blue","text":"(+25) "},{"color":"dark_gray","text":"(+155.75)"}],"italic":false,"text":""}', + '{"extra":[" ",{"color":"dark_gray","text":"["},{"color":"dark_gray","text":"❁"},{"color":"dark_gray","text":"] "},{"color":"dark_gray","text":"["},{"color":"dark_gray","text":"⚔"},{"color":"dark_gray","text":"]"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Depth Strider III"},{"color":"blue","text":", "},{"color":"blue","text":"Feather Falling X"},{"color":"blue","text":", "},{"color":"blue","text":"Growth V"}],"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Protection V"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Reduces the damage you take from"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"withers by "},{"color":"red","text":"10%"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"Full Set Bonus: Witherborn "},{"color":"gray","text":"(3/4)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Spawns a wither minion every "},{"color":"yellow","text":"30"}],"italic":false,"text":""}', + '{"extra":[{"color":"yellow","text":""},{"color":"gray","text":"seconds up to a maximum "},{"color":"green","text":"1 "},{"color":"gray","text":"wither."}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Your withers will travel to and"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"explode on nearby enemies."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Ancient Bonus"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Grants "},{"color":"green","text":"+1 "},{"color":"blue","text":"☠ Crit Damage "},{"color":"gray","text":"per"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"red","text":"Catacombs "},{"color":"gray","text":"level."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"light_purple","obfuscated":true,"text":"a"},"",{"bold":false,"extra":[" "],"italic":false,"obfuscated":false,"strikethrough":false,"text":"","underlined":false},{"bold":true,"color":"light_purple","text":"MYTHIC DUNGEON BOOTS "},{"bold":true,"color":"light_purple","obfuscated":true,"text":"a"}],"italic":false,"text":""}' + ], + "minecraft:unbreakable": { + show_in_tooltip: 0b + } + }, + count: 1, + id: "minecraft:leather_boots" +} diff --git a/src/test/resources/testdata/items/pets/lion-item.snbt b/src/test/resources/testdata/items/pets/lion-item.snbt new file mode 100644 index 0000000..c364032 --- /dev/null +++ b/src/test/resources/testdata/items/pets/lion-item.snbt @@ -0,0 +1,65 @@ +{ + source: { + dataVersion: 4189, + }, + components: { + "minecraft:attribute_modifiers": { + modifiers: [ + ], + show_in_tooltip: 0b + }, + "minecraft:custom_data": { + id: "PET", + petInfo: '{"type":"LION","active":false,"exp":0.0,"tier":"LEGENDARY","hideInfo":false,"candyUsed":0,"uuid":"c7f57149-458e-4fde-a9bc-fcc14932310a","uniqueId":"d668f085-26a6-48fe-b75b-c7b8227b5ac8","hideRightClick":false,"noMove":false}', + timestamp: 1732719293542L, + uuid: "c7f57149-458e-4fde-a9bc-fcc14932310a" + }, + "minecraft:custom_name": '{"extra":[{"color":"gray","text":"[Lvl 1] "},{"color":"gold","text":"Lion"}],"italic":false,"text":""}', + "minecraft:hide_additional_tooltip": { + }, + "minecraft:lore": [ + '{"extra":[{"color":"dark_gray","text":"Foraging Pet"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Speed: "},{"color":"green","text":"+0.25"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Strength: "},{"color":"red","text":"+0.5"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Ferocity: "},{"color":"green","text":"+0.05"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"Primal Force"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Adds "},{"color":"red","text":"+0.2 "},{"color":"red","text":"❁ Damage "},{"color":"gray","text":"and "},{"color":"red","text":"+0.2 "},{"color":"red","text":"❁"}],"italic":false,"text":""}', + '{"extra":[{"color":"red","text":"Strength "},{"color":"gray","text":"to your weapons."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"First Pounce"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"First Strike"},{"color":"gray","text":", Triple-Strike"},{"color":"gray","text":", and"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"bold":true,"color":"light_purple","text":"Combo "},{"color":"gray","text":"are "},{"color":"green","text":"1% "},{"color":"gray","text":"more effective."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"King of the Jungle"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Deal "},{"color":"red","text":"+1.5% "},{"color":"red","text":"❁ Damage "},{"color":"gray","text":"against mobs"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"that have attacked you."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Progress to Level 2: "},{"color":"yellow","text":"0%"}],"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"white","strikethrough":true,"text":" "},"",{"bold":false,"extra":[" "],"italic":false,"obfuscated":false,"strikethrough":false,"text":"","underlined":false},{"color":"yellow","text":"0"},{"color":"gold","text":"/"},{"color":"yellow","text":"660"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"yellow","text":"Right-click to add this pet to your"}],"italic":false,"text":""}', + '{"extra":[{"color":"yellow","text":"pet menu!"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"gold","text":"LEGENDARY"}],"italic":false,"text":""}' + ], + "minecraft:profile": { + id: [I; + 100496274, + -783272221, + -1215808471, + -1437268588 + ], + properties: [ + { + name: "textures", + signature: "", + value: "eyJ0aW1lc3RhbXAiOjE1MDMzMTY0Njg1ODAsInByb2ZpbGVJZCI6ImUzYjQ0NWM4NDdmNTQ4ZmI4YzhmYTNmMWY3ZWZiYThlIiwicHJvZmlsZU5hbWUiOiJNaW5pRGlnZ2VyVGVzdCIsInNpZ25hdHVyZVJlcXVpcmVkIjp0cnVlLCJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMzhmZjQ3M2JkNTJiNGRiMmMwNmYxYWM4N2ZlMTM2N2JjZTc1NzRmYWMzMzBmZmFjNzk1NjIyOWY4MmVmYmExIn19fQ==" + } + ] + } + }, + count: 1, + id: "minecraft:player_head" +} diff --git a/src/test/resources/testdata/items/pets/mithril-golem-not-selected.snbt b/src/test/resources/testdata/items/pets/mithril-golem-not-selected.snbt new file mode 100644 index 0000000..79f32c9 --- /dev/null +++ b/src/test/resources/testdata/items/pets/mithril-golem-not-selected.snbt @@ -0,0 +1,55 @@ +{ + source: { + dataVersion: 4189, + }, + components: { + "minecraft:custom_data": { + id: "PET", + petInfo: '{"type":"MITHRIL_GOLEM","active":false,"exp":9232689.767527012,"tier":"LEGENDARY","hideInfo":false,"candyUsed":0,"uuid":"2ebbdaff-c9b6-4fe1-8b87-44905eb5867d","uniqueId":"26be73f1-2955-4003-9e6a-72848cea8e08","hideRightClick":false,"noMove":false}', + uuid: "2ebbdaff-c9b6-4fe1-8b87-44905eb5867d" + }, + "minecraft:custom_name": '{"extra":[{"color":"gray","text":"[Lvl 86] "},{"color":"gold","text":"Mithril Golem"}],"italic":false,"text":""}', + "minecraft:lore": [ + '{"extra":[{"color":"dark_gray","text":"Mining Pet"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"True Defense: "},{"color":"green","text":"+43"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Mining Fortune: "},{"color":"green","text":"+21.5"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"Mithril Affinity"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Grants "},{"color":"gold","text":"+172⸕ Mining Speed "},{"color":"gray","text":"when"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"mining "},{"color":"dark_green","text":"Mithril"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"Subterranean Battler"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Grants "},{"color":"green","text":"+17.2% "},{"color":"gray","text":"of most combat stats"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"while on "},{"color":"aqua","text":"Mining Islands"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"The Smell Of Powder"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Grants "},{"color":"dark_green","text":"+17.2% ᠅ Mithril Powder "},{"color":"gray","text":"from"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"all sources."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Progress to Level 87: "},{"color":"yellow","text":"89%"}],"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"dark_green","strikethrough":true,"text":" "},{"bold":true,"color":"white","strikethrough":true,"text":" "},"",{"bold":false,"extra":[" "],"italic":false,"obfuscated":false,"strikethrough":false,"text":"","underlined":false},{"color":"yellow","text":"593,259.8"},{"color":"gold","text":"/"},{"color":"yellow","text":"666.7k"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"yellow","text":"Left-click to summon!"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"yellow","text":"Shift Left-click to toggle as favorite!"}],"italic":false,"text":""}', + '{"extra":[{"color":"yellow","text":"Right-click to convert to an item!"}],"italic":false,"text":""}' + ], + "minecraft:profile": { + id: [I; + 972784821, + 1926443553, + -1284265423, + 1586851459 + ], + properties: [ + { + name: "textures", + signature: "", + value: "ewogICJ0aW1lc3RhbXAiIDogMTYxMDY0OTEwODI4NCwKICAicHJvZmlsZUlkIiA6ICI2ZmU4OTUxZDVhY2M0NDc3OWI2ZmYxMmU3YzFlOTQ2MyIsCiAgInByb2ZpbGVOYW1lIiA6ICJlcGhlbXJhIiwKICAic2lnbmF0dXJlUmVxdWlyZWQiIDogdHJ1ZSwKICAidGV4dHVyZXMiIDogewogICAgIlNLSU4iIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlL2MxYjJkZmU4ZWQ1ZGZmYzViMTY4N2JjMWMyNDljMzlkZTJkOGE2YzNkOTAzMDVjOTVmNmQxYTFhMzMwYTBiMSIKICAgIH0KICB9Cn0=" + } + ] + } + }, + count: 1, + id: "minecraft:player_head" +} diff --git a/src/test/resources/testdata/items/pets/rabbit-selected.snbt b/src/test/resources/testdata/items/pets/rabbit-selected.snbt new file mode 100644 index 0000000..d4c7235 --- /dev/null +++ b/src/test/resources/testdata/items/pets/rabbit-selected.snbt @@ -0,0 +1,63 @@ +{ + source: { + dataVersion: 4189, + }, + components: { + "minecraft:custom_data": { + id: "PET", + petInfo: '{"type":"RABBIT","active":true,"exp":3.429132435816353E7,"tier":"MYTHIC","hideInfo":false,"heldItem":"YELLOW_BANDANA","candyUsed":0,"uuid":"30a05aae-2ccd-41c0-a686-5bce0df15e2d","uniqueId":"71a8949b-7444-4ead-9464-999fe549e703","hideRightClick":false,"noMove":false}', + uuid: "30a05aae-2ccd-41c0-a686-5bce0df15e2d" + }, + "minecraft:custom_name": '{"extra":[{"color":"gray","text":"[Lvl 100] "},{"color":"light_purple","text":"Rabbit"}],"italic":false,"text":""}', + "minecraft:lore": [ + '{"extra":[{"color":"dark_gray","text":"Farming Pet"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Health: "},{"color":"green","text":"+100"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Speed: "},{"color":"green","text":"+20"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Farming Fortune: "},{"color":"green","text":"+30"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"Happy Feet"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Jump potions also give "},{"color":"green","text":"+50 "},{"color":"gray","text":"speed"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"Farming Wisdom Boost"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":""},{"color":"gray","text":"Grants "},{"color":"dark_aqua","text":"+30☯ Farming Wisdom"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"Efficient Farming"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Farming minions work "},{"color":"green","text":"30% "},{"color":"gray","text":"faster"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"while on your island"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"Chocolate Injections"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Increases "},{"color":"gold","text":"Chocolate Factory"}],"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":""},{"color":"gray","text":"production by "},{"color":"green","text":"+0.05x"},{"color":"gray","text":". Duplicate"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"green","text":"Chocolate Rabbits "},{"color":"gray","text":"that you find"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"grant "},{"color":"gold","text":"+33% Chocolate"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"Held Item: "},{"color":"blue","text":"Yellow Bandana"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Grants "},{"color":"gold","text":"+30☘ Farming Fortune"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"aqua","text":"MAX LEVEL"}],"italic":false,"text":""}', + '{"extra":[{"color":"dark_gray","text":"▸ 34,291,324 XP"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"red","text":"Click to despawn!"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"yellow","text":"Shift Left-click to toggle as favorite!"}],"italic":false,"text":""}', + '{"extra":[{"color":"yellow","text":"Right-click to convert to an item!"}],"italic":false,"text":""}' + ], + "minecraft:profile": { + id: [I; + -363097213, + -1822870885, + -1670199288, + -1615169037 + ], + properties: [ + { + name: "textures", + signature: "", + value: "ewogICJ0aW1lc3RhbXAiIDogMTYwNDU4NjI3ODg1NywKICAicHJvZmlsZUlkIiA6ICI0ZTMwZjUwZTdiYWU0M2YzYWZkMmE3NDUyY2ViZTI5YyIsCiAgInByb2ZpbGVOYW1lIiA6ICJfdG9tYXRvel8iLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNjM0Mzg1NTVlODk5YmQ5YTA1MWE5NWRiZWE0OWViMmVjZmE1MmE2OWRiYmE4OTk4ZjM2NzM4MTllMjc3ZmRmNSIKICAgIH0KICB9Cn0=" + } + ] + } + }, + count: 1, + id: "minecraft:player_head" +} diff --git a/src/test/resources/testdata/items/rune-in-sack.snbt b/src/test/resources/testdata/items/rune-in-sack.snbt new file mode 100644 index 0000000..4624c0f --- /dev/null +++ b/src/test/resources/testdata/items/rune-in-sack.snbt @@ -0,0 +1,34 @@ +{ + source: { + dataVersion: 4189, + }, + components: { + "minecraft:custom_data": { + }, + "minecraft:custom_name": '{"extra":[{"color":"dark_blue","text":"◆ "},{"bold":true,"color":"dark_blue","text":"Tidal Rune"}],"italic":false,"text":""}', + "minecraft:lore": [ + '{"extra":[{"color":"dark_gray","text":"Rune Sack"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"yellow","text":"I"},{"color":"gray","text":": "},{"color":"yellow","text":"1"},{"color":"gray","text":"/64"}],"italic":false,"text":""}', + '{"extra":[{"color":"yellow","text":"II"},{"color":"gray","text":": "},{"color":"dark_gray","text":"0"},{"color":"gray","text":"/64"}],"italic":false,"text":""}', + '{"extra":[{"color":"yellow","text":"III"},{"color":"gray","text":": "},{"color":"dark_gray","text":"0"},{"color":"gray","text":"/64"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Tier: "},{"color":"gold","text":"Legendary"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"yellow","text":"Click to pickup!"}],"italic":false,"text":""}' + ], + "minecraft:profile": { + id: "30ea28d50755386a90924ae91af1b7e5", + name: "30ea28d50755386a", + properties: [ + { + name: "textures", + signature: "", + value: "ewogICJ0aW1lc3RhbXAiIDogMTcxOTUwMzQ3NzI1MSwKICAicHJvZmlsZUlkIiA6ICIxOWY1YzkwMWEzMjQ0YzVmYTM4NThjZGVhNDk5ZWMwYSIsCiAgInByb2ZpbGVOYW1lIiA6ICJzb2RpdW16aXAiLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNjk2NzEwNDdjNmFkZThhOWM0ZDZhNTgxYmMyNmQyODRhNTRhZTMyZTg1YzM0Y2U2OWQ4MWY5Mjc5OWJmM2ZiYiIsCiAgICAgICJtZXRhZGF0YSIgOiB7CiAgICAgICAgIm1vZGVsIiA6ICJzbGltIgogICAgICB9CiAgICB9CiAgfQp9" + } + ] + } + }, + count: 1, + id: "minecraft:player_head" +} diff --git a/src/test/resources/testdata/items/titanium-drill.snbt b/src/test/resources/testdata/items/titanium-drill.snbt new file mode 100644 index 0000000..e49c6b0 --- /dev/null +++ b/src/test/resources/testdata/items/titanium-drill.snbt @@ -0,0 +1,100 @@ +{ + source: { + dataVersion: 4189, + }, + components: { + "minecraft:attribute_modifiers": { + modifiers: [ + ], + show_in_tooltip: 0b + }, + "minecraft:custom_data": { + compact_blocks: 1023815, + donated_museum: 1b, + drill_fuel: 16621, + drill_part_fuel_tank: "titanium_fuel_tank", + drill_part_upgrade_module: "goblin_omelette_blue_cheese", + enchantments: { + compact: 10, + efficiency: 5, + experience: 3, + fortune: 3, + paleontologist: 2, + pristine: 5 + }, + gems: { + AMBER_0: { + quality: "PERFECT", + uuid: "d28be6ae-75eb-49e4-90d8-31759db18d79" + }, + JADE_0: { + quality: "PERFECT", + uuid: "657fea0b-88e2-483d-9d2c-0b821797a55a" + }, + MINING_0: { + quality: "PERFECT", + uuid: "257bdcd2-585b-48b9-9517-a2e841dc0574" + }, + MINING_0_gem: "TOPAZ", + unlocked_slots: [ + "JADE_0", + "MINING_0" + ] + }, + id: "TITANIUM_DRILL_4", + modifier: "auspicious", + rarity_upgrades: 1, + timestamp: 1700577120000L, + uuid: "367b85ab-5bb4-43b6-a055-084cbaaafc1c" + }, + "minecraft:custom_name": '{"extra":[{"color":"light_purple","text":"Auspicious Titanium Drill DR-X655"}],"italic":false,"text":""}', + "minecraft:enchantment_glint_override": 1b, + "minecraft:hide_additional_tooltip": { + }, + "minecraft:lore": [ + '{"extra":[{"color":"dark_gray","text":"Breaking Power 9"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Damage: "},{"color":"red","text":"+75"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Mining Speed: "},{"color":"green","text":"+1,885 "},{"color":"blue","text":"(+75) "},{"color":"light_purple","text":"(+100)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Pristine: "},{"color":"green","text":"+4.5 "},{"color":"light_purple","text":"(+2)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Mining Fortune: "},{"color":"green","text":"+220 "},{"color":"blue","text":"(+20) "},{"color":"light_purple","text":"(+50)"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Mining Wisdom: "},{"color":"green","text":"+10"}],"italic":false,"text":""}', + '{"extra":[" ",{"color":"gold","text":"["},{"color":"gold","text":"⸕"},{"color":"gold","text":"] "},{"color":"gold","text":"["},{"color":"green","text":"☘"},{"color":"gold","text":"] "},{"color":"gold","text":"["},{"color":"yellow","text":"✦"},{"color":"gold","text":"]"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Compact X"}],"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Efficiency V"}],"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Experience III"}],"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Fortune III"}],"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Paleontologist II"}],"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Prismatic V"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"green","text":"Titanium-Infused Fuel Tank."}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":""},{"color":"dark_green","text":"25,000 Max Fuel Capacity."}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":""},{"color":"green","text":"-4% Pickaxe Ability Cooldown."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Drill Engine: "},{"color":"red","text":"Not Installed"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Increases "},{"color":"gold","text":"⸕ Mining Speed "},{"color":"gray","text":"with part"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"installed."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"green","text":"Blue Cheese Goblin Omelette Part."}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Adds "},{"color":"green","text":"+1 Level "},{"color":"gray","text":"to all of your unlocked "},{"color":"dark_purple","text":"Heart of"}],"italic":false,"text":""}', + '{"extra":[{"color":"dark_purple","text":"the Mountain "},{"color":"gray","text":"perks."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Fuel: "},{"color":"dark_green","text":"16,621"},{"color":"dark_gray","text":"/25k"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"gold","text":"Ability: Pickobulus "},{"bold":true,"color":"yellow","text":"RIGHT CLICK"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Throw your pickaxe to create an"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"explosion mining all ores in a "},{"color":"green","text":"3 "},{"color":"gray","text":"block"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"radius."}],"italic":false,"text":""}', + '{"extra":[{"color":"dark_gray","text":"Cooldown: "},{"color":"green","text":"48s"}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"color":"blue","text":"Auspicious Bonus"}],"italic":false,"text":""}', + '{"extra":[{"color":"gray","text":"Grants "},{"color":"gold","text":"+0.9% "},{"color":"gold","text":"☘ Mining Fortune"},{"color":"gray","text":"."}],"italic":false,"text":""}', + '{"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"dark_gray","text":"* "},{"color":"dark_gray","text":"Co-op Soulbound "},{"bold":true,"color":"dark_gray","text":"*"}],"italic":false,"text":""}', + '{"extra":[{"bold":true,"color":"light_purple","obfuscated":true,"text":"a"},"",{"bold":false,"extra":[" "],"italic":false,"obfuscated":false,"strikethrough":false,"text":"","underlined":false},{"bold":true,"color":"light_purple","text":"MYTHIC DRILL "},{"bold":true,"color":"light_purple","obfuscated":true,"text":"a"}],"italic":false,"text":""}' + ] + }, + count: 1, + id: "minecraft:prismarine_shard" +} diff --git a/src/test/resources/testdata/tablist/dungeon_hub.snbt b/src/test/resources/testdata/tablist/dungeon_hub.snbt new file mode 100644 index 0000000..fed57ad --- /dev/null +++ b/src/test/resources/testdata/tablist/dungeon_hub.snbt @@ -0,0 +1,1170 @@ +{ + body: [ + { + extra: [ + " ", + { + bold: 1b, + color: "green", + text: "Players " + }, + { + color: "white", + text: "(15)" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + { + color: "dark_gray", + text: "[" + }, + { + color: "aqua", + text: "210" + }, + { + color: "dark_gray", + text: "] " + }, + { + color: "aqua", + text: "lrg89" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + { + color: "dark_gray", + text: "[" + }, + { + color: "light_purple", + text: "322" + }, + { + color: "dark_gray", + text: "] " + }, + { + color: "aqua", + text: "Basilickk" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + { + color: "dark_gray", + text: "[" + }, + { + color: "light_purple", + text: "330" + }, + { + color: "dark_gray", + text: "] " + }, + { + color: "aqua", + text: "Schauli23 " + }, + { + color: "gray", + text: "Σ" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + { + color: "dark_gray", + text: "[" + }, + { + color: "dark_green", + text: "187" + }, + { + color: "dark_gray", + text: "] " + }, + { + color: "aqua", + text: "bombardiro13" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + { + color: "dark_gray", + text: "[" + }, + { + color: "yellow", + text: "119" + }, + { + color: "dark_gray", + text: "] " + }, + { + color: "aqua", + text: "Horuu" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + { + color: "dark_gray", + text: "[" + }, + { + color: "dark_green", + text: "188" + }, + { + color: "dark_gray", + text: "] " + }, + { + color: "green", + text: "Kirito_Hacker " + }, + { + bold: 1b, + color: "gray", + text: "ꕁ" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + { + color: "dark_gray", + text: "[" + }, + { + color: "blue", + text: "281" + }, + { + color: "dark_gray", + text: "] " + }, + { + color: "green", + text: "LasseFTW1N " + }, + { + bold: 1b, + color: "dark_purple", + text: "࿇" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + { + color: "dark_gray", + text: "[" + }, + { + color: "dark_aqua", + text: "274" + }, + { + color: "dark_gray", + text: "] " + }, + { + color: "green", + text: "VN_Tuan " + }, + { + bold: 1b, + color: "aqua", + text: "ᛝ" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + { + color: "dark_gray", + text: "[" + }, + { + color: "aqua", + text: "205" + }, + { + color: "dark_gray", + text: "] " + }, + { + color: "green", + text: "buttonpurse_1212" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + { + color: "dark_gray", + text: "[" + }, + { + color: "dark_green", + text: "193" + }, + { + color: "dark_gray", + text: "] " + }, + { + color: "green", + text: "Moly____ " + }, + { + bold: 1b, + color: "gray", + text: "⚛" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + { + color: "dark_gray", + text: "[" + }, + { + color: "dark_green", + text: "187" + }, + { + color: "dark_gray", + text: "] " + }, + { + color: "green", + text: "BehavingTurtle4" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + { + color: "dark_gray", + text: "[" + }, + { + color: "dark_green", + text: "169" + }, + { + color: "dark_gray", + text: "] " + }, + { + color: "green", + text: "Kalmaria " + }, + { + color: "gold", + text: "ௐ" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + { + color: "dark_gray", + text: "[" + }, + { + color: "yellow", + text: "84" + }, + { + color: "dark_gray", + text: "] " + }, + { + color: "green", + text: "Cxter" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + { + color: "dark_gray", + text: "[" + }, + { + color: "white", + text: "48" + }, + { + color: "dark_gray", + text: "] " + }, + { + color: "gray", + text: "FredyFazballs" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + { + color: "dark_gray", + text: "[" + }, + { + color: "gray", + text: "21" + }, + { + color: "dark_gray", + text: "] " + }, + { + color: "gray", + text: "Finn1446" + } + ], + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + extra: [ + " ", + { + bold: 1b, + color: "green", + text: "Players " + }, + { + color: "white", + text: "(15)" + } + ], + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + extra: [ + " ", + { + bold: 1b, + color: "dark_aqua", + text: "Info" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + { + bold: 1b, + color: "aqua", + text: "Area: " + }, + { + color: "gray", + text: "Dungeon Hub" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + " Server: ", + { + color: "dark_gray", + text: "mini90J" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + " Gems: ", + { + color: "green", + text: "65" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + " Fairy Souls: ", + { + color: "light_purple", + text: "7" + }, + { + color: "dark_purple", + text: "/" + }, + { + color: "light_purple", + text: "7" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + " Unclaimed chests: ", + { + color: "gold", + text: "0" + } + ], + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + extra: [ + { + bold: 1b, + text: "" + }, + { + bold: 1b, + color: "yellow", + text: "Profile: " + }, + { + color: "green", + text: "Strawberry" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + " SB Level", + { + color: "white", + text: ": " + }, + { + color: "dark_gray", + text: "[" + }, + { + color: "aqua", + text: "210" + }, + { + color: "dark_gray", + text: "] " + }, + { + color: "aqua", + text: "26" + }, + { + color: "dark_aqua", + text: "/" + }, + { + color: "aqua", + text: "100 XP" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + " Bank: ", + { + color: "gold", + text: "1.4B" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + " Interest: ", + { + color: "yellow", + text: "12 Hours" + }, + { + color: "gold", + text: " (689.1k)" + } + ], + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + extra: [ + { + bold: 1b, + color: "yellow", + text: "Collection:" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + " Bonzo IV: ", + { + color: "yellow", + text: "110" + }, + { + color: "gold", + text: "/" + }, + { + color: "yellow", + text: "150" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + " Scarf II: ", + { + color: "yellow", + text: "25" + }, + { + color: "gold", + text: "/" + }, + { + color: "yellow", + text: "50" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + " The Professor IV: ", + { + color: "yellow", + text: "141" + }, + { + color: "gold", + text: "/" + }, + { + color: "yellow", + text: "150" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + " Thorn I: ", + { + color: "yellow", + text: "29" + }, + { + color: "gold", + text: "/" + }, + { + color: "yellow", + text: "50" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + " Livid II: ", + { + color: "yellow", + text: "91" + }, + { + color: "gold", + text: "/" + }, + { + color: "yellow", + text: "100" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + " Sadan V: ", + { + color: "yellow", + text: "388" + }, + { + color: "gold", + text: "/" + }, + { + color: "yellow", + text: "500" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + " Necron VI: ", + { + color: "yellow", + text: "531" + }, + { + color: "gold", + text: "/" + }, + { + color: "yellow", + text: "750" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + " ", + { + bold: 1b, + color: "dark_aqua", + text: "Info" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + { + bold: 1b, + color: "gold", + text: "Dungeons:" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + " ", + { + color: "white", + text: "Catacombs 39: " + }, + { + color: "green", + text: "15%" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + " ", + { + color: "green", + text: "Mage 36: " + }, + { + color: "green", + text: "12.9%" + } + ], + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + extra: [ + { + bold: 1b, + color: "light_purple", + text: "RNG Meter" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + " ", + { + color: "green", + text: "Catacombs Floor I" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + " ", + { + color: "gray", + text: "None" + } + ], + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + extra: [ + { + bold: 1b, + color: "aqua", + text: "Essence:" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + " Undead: ", + { + color: "light_purple", + text: "1,907" + } + ], + italic: 0b, + text: "" + }, + { + extra: [ + " Wither: ", + { + color: "light_purple", + text: "318" + } + ], + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + extra: [ + { + bold: 1b, + color: "aqua", + text: "Party: " + }, + { + color: "gray", + text: "No party" + } + ], + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + }, + { + italic: 0b, + text: "" + } + ], + footer: { + extra: [ + "\n", + { + extra: [ + { + bold: 1b, + color: "green", + text: "Active Effects" + } + ], + italic: 0b, + text: "" + }, + "\n", + { + extra: [ + { + color: "gray", + text: "" + }, + { + color: "gray", + text: "You have " + }, + { + color: "yellow", + text: "2 " + }, + { + color: "gray", + text: 'active effects. Use "' + }, + { + color: "gold", + text: "/effects" + }, + { + color: "gray", + text: '" to see them!' + } + ], + italic: 0b, + text: "" + }, + "\n", + { + extra: [ + { + color: "yellow", + text: "Haste II" + }, + "", + { + bold: 0b, + italic: 0b, + obfuscated: 0b, + strikethrough: 0b, + text: "", + underlined: 0b + } + ], + italic: 0b, + text: "" + }, + "\n", + { + extra: [ + "", + { + bold: 0b, + extra: [ + "§s" + ], + italic: 0b, + obfuscated: 0b, + strikethrough: 0b, + text: "", + underlined: 0b + } + ], + italic: 0b, + text: "" + }, + "\n", + { + extra: [ + { + bold: 1b, + color: "light_purple", + text: "Cookie Buff" + } + ], + italic: 0b, + text: "" + }, + "\n", + { + extra: [ + { + color: "gray", + text: "" + }, + { + color: "gray", + text: "Not active! Obtain booster cookies from the community" + } + ], + italic: 0b, + text: "" + }, + "\n", + { + extra: [ + { + color: "gray", + text: "shop in the hub." + } + ], + italic: 0b, + text: "" + }, + "\n", + { + extra: [ + "", + { + bold: 0b, + extra: [ + "§s" + ], + italic: 0b, + obfuscated: 0b, + strikethrough: 0b, + text: "", + underlined: 0b + } + ], + italic: 0b, + text: "" + }, + "\n", + { + extra: [ + { + color: "green", + extra: [ + { + bold: 1b, + color: "red", + text: "STORE.HYPIXEL.NET" + } + ], + text: "Ranks, Boosters & MORE! " + } + ], + italic: 0b, + text: "" + } + ], + italic: 0b, + text: "" + }, + header: { + extra: [ + { + color: "aqua", + extra: [ + { + bold: 1b, + color: "yellow", + text: "MC.HYPIXEL.NET" + } + ], + text: "You are playing on " + }, + "\n", + { + extra: [ + "", + { + bold: 0b, + extra: [ + "§s" + ], + italic: 0b, + obfuscated: 0b, + strikethrough: 0b, + text: "", + underlined: 0b + } + ], + italic: 0b, + text: "" + } + ], + italic: 0b, + text: "" + }, + source: { + dataVersion: 4325, + modVersion: "Firmament 3.1.0-dev+mc1.21.5+g2de6cfb" + } +} diff --git a/src/texturePacks/README.md b/src/texturePacks/README.md new file mode 100644 index 0000000..8932817 --- /dev/null +++ b/src/texturePacks/README.md @@ -0,0 +1,13 @@ + + +# Technical Notes for the texture pack implementation + +Relevant classes: + +`ItemModelManager` can be used to select an `ItemModel`. This is done from the `ITEM_MODEL` component which is defaulted by the `Item` class. + +The list of available `ItemModel`s (as in `Identifier` -> `ItemModel` maps) is loaded by `BakedModelManager`. To this end, item models in particular are loaded from `ItemAssetsLoader#load`. Those `ItemAssets` are found in `assets//items/` directly (not in the model folder) and can be used to select other models, similar to how predicates used to work diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/Compat.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/Compat.kt new file mode 100644 index 0000000..d95712b --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/Compat.kt @@ -0,0 +1,11 @@ +package moe.nea.firmament.features.texturepack + +import moe.nea.firmament.util.compatloader.CompatMeta +import moe.nea.firmament.util.compatloader.ICompatMeta + +@CompatMeta +object Compat : ICompatMeta { + override fun shouldLoad(): Boolean { + return true + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomBlockTextures.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomBlockTextures.kt new file mode 100644 index 0000000..2d7a978 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomBlockTextures.kt @@ -0,0 +1,453 @@ +@file:UseSerializers(BlockPosSerializer::class, IdentifierSerializer::class) + +package moe.nea.firmament.features.texturepack + +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.mojang.serialization.JsonOps +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Executor +import java.util.function.Function +import net.fabricmc.loader.api.FabricLoader +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlinx.serialization.UseSerializers +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.serializer +import kotlin.jvm.optionals.getOrNull +import net.minecraft.block.Block +import net.minecraft.block.BlockState +import net.minecraft.block.Blocks +import net.minecraft.client.render.model.Baker +import net.minecraft.client.render.model.BlockStateModel +import net.minecraft.client.render.model.BlockStatesLoader +import net.minecraft.client.render.model.ReferencedModelsCollector +import net.minecraft.client.render.model.SimpleBlockStateModel +import net.minecraft.client.render.model.json.BlockModelDefinition +import net.minecraft.client.render.model.json.ModelVariant +import net.minecraft.registry.Registries +import net.minecraft.registry.RegistryKey +import net.minecraft.registry.RegistryKeys +import net.minecraft.resource.Resource +import net.minecraft.resource.ResourceManager +import net.minecraft.resource.SinglePreparationResourceReloader +import net.minecraft.state.StateManager +import net.minecraft.util.Identifier +import net.minecraft.util.math.BlockPos +import net.minecraft.util.profiler.Profiler +import net.minecraft.util.thread.AsyncHelper +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.EarlyResourceReloadEvent +import moe.nea.firmament.events.FinalizeResourceManagerEvent +import moe.nea.firmament.events.SkyblockServerUpdateEvent +import moe.nea.firmament.features.texturepack.CustomBlockTextures.createBakedModels +import moe.nea.firmament.features.texturepack.CustomGlobalTextures.logger +import moe.nea.firmament.util.ErrorUtil +import moe.nea.firmament.util.IdentifierSerializer +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.SkyBlockIsland +import moe.nea.firmament.util.json.BlockPosSerializer +import moe.nea.firmament.util.json.SingletonSerializableList + + +object CustomBlockTextures { + @Serializable + data class CustomBlockOverride( + val modes: @Serializable(SingletonSerializableList::class) List, + val area: List? = null, + val replacements: Map, + ) + + @Serializable(with = Replacement.Serializer::class) + data class Replacement( + val block: Identifier, + val sound: Identifier?, + ) { + fun replace(block: BlockState): BlockStateModel? { + blockStateMap?.let { return it[block] } + return blockModel + } + + @Transient + lateinit var overridingBlock: Block + + @Transient + val blockModelIdentifier get() = block.withPrefixedPath("block/") + + /** + * Guaranteed to be set after [BakedReplacements.modelBakingFuture] is complete, if [unbakedBlockStateMap] is set. + */ + @Transient + var blockStateMap: Map? = null + + @Transient + var unbakedBlockStateMap: Map? = null + + /** + * Guaranteed to be set after [BakedReplacements.modelBakingFuture] is complete. Prefer [blockStateMap] if present. + */ + @Transient + lateinit var blockModel: BlockStateModel + + @OptIn(ExperimentalSerializationApi::class) + @kotlinx.serialization.Serializer(Replacement::class) + object DefaultSerializer : KSerializer + + object Serializer : KSerializer { + val delegate = serializer() + override val descriptor: SerialDescriptor + get() = delegate.descriptor + + override fun deserialize(decoder: Decoder): Replacement { + val jsonElement = decoder.decodeSerializableValue(delegate) + if (jsonElement is JsonPrimitive) { + require(jsonElement.isString) + return Replacement(Identifier.tryParse(jsonElement.content)!!, null) + } + return (decoder as JsonDecoder).json.decodeFromJsonElement(DefaultSerializer, jsonElement) + } + + override fun serialize(encoder: Encoder, value: Replacement) { + encoder.encodeSerializableValue(DefaultSerializer, value) + } + } + } + + @Serializable + data class Area( + val min: BlockPos, + val max: BlockPos, + ) { + @Transient + val realMin = BlockPos( + minOf(min.x, max.x), + minOf(min.y, max.y), + minOf(min.z, max.z), + ) + + @Transient + val realMax = BlockPos( + maxOf(min.x, max.x), + maxOf(min.y, max.y), + maxOf(min.z, max.z), + ) + + fun roughJoin(other: Area): Area { + return Area( + BlockPos( + minOf(realMin.x, other.realMin.x), + minOf(realMin.y, other.realMin.y), + minOf(realMin.z, other.realMin.z), + ), + BlockPos( + maxOf(realMax.x, other.realMax.x), + maxOf(realMax.y, other.realMax.y), + maxOf(realMax.z, other.realMax.z), + ) + ) + } + + fun contains(blockPos: BlockPos): Boolean { + return (blockPos.x in realMin.x..realMax.x) && + (blockPos.y in realMin.y..realMax.y) && + (blockPos.z in realMin.z..realMax.z) + } + } + + data class LocationReplacements( + val lookup: Map> + ) { + init { + lookup.forEach { (block, replacements) -> + for (replacement in replacements) { + replacement.replacement.overridingBlock = block + } + } + } + } + + data class BlockReplacement( + val checks: List?, + val replacement: Replacement, + ) { + val roughCheck by lazy(LazyThreadSafetyMode.NONE) { + if (checks == null || checks.size < 3) return@lazy null + checks.reduce { acc, next -> acc.roughJoin(next) } + } + } + + data class BakedReplacements(val data: Map) { + /** + * Fulfilled by [createBakedModels] which is called during model baking. Once completed, all [Replacement.blockModel] will be set. + */ + val modelBakingFuture = CompletableFuture() + + /** + * @returns a list of all [Replacement]s. + */ + fun collectAllReplacements(): Sequence { + return data.values.asSequence() + .flatMap { it.lookup.values } + .flatten() + .map { it.replacement } + } + } + + var allLocationReplacements: BakedReplacements = BakedReplacements(mapOf()) + var currentIslandReplacements: LocationReplacements? = null + + fun refreshReplacements() { + val location = SBData.skyblockLocation + val replacements = + if (CustomSkyBlockTextures.TConfig.enableBlockOverrides) location?.let(allLocationReplacements.data::get) + else null + val lastReplacements = currentIslandReplacements + currentIslandReplacements = replacements + if (lastReplacements != replacements) { + MC.nextTick { + MC.worldRenderer.chunks?.chunks?.forEach { + // false schedules rebuilds outside a 27 block radius to happen async + it.scheduleRebuild(false) + } + sodiumReloadTask?.run() + } + } + } + + private val sodiumReloadTask = runCatching { + val r = Class.forName("moe.nea.firmament.compat.sodium.SodiumChunkReloader") + .getConstructor() + .newInstance() as Runnable + r.run() + r + }.getOrElse { + if (FabricLoader.getInstance().isModLoaded("sodium")) + logger.error("Could not create sodium chunk reloader") + null + } + + + fun matchesPosition(replacement: BlockReplacement, blockPos: BlockPos?): Boolean { + if (blockPos == null) return true + val rc = replacement.roughCheck + if (rc != null && !rc.contains(blockPos)) return false + val areas = replacement.checks + if (areas != null && !areas.any { it.contains(blockPos) }) return false + return true + } + + @JvmStatic + fun getReplacementModel(block: BlockState, blockPos: BlockPos?): BlockStateModel? { + return getReplacement(block, blockPos)?.replace(block) + } + + @JvmStatic + fun getReplacement(block: BlockState, blockPos: BlockPos?): Replacement? { + if (isInFallback() && blockPos == null) { + return null + } + val replacements = currentIslandReplacements?.lookup?.get(block.block) ?: return null + for (replacement in replacements) { + if (replacement.checks == null || matchesPosition(replacement, blockPos)) + return replacement.replacement + } + return null + } + + + @Subscribe + fun onLocation(event: SkyblockServerUpdateEvent) { + refreshReplacements() + } + + @Volatile + @get:JvmStatic + var preparationFuture: CompletableFuture = CompletableFuture.completedFuture( + BakedReplacements( + mapOf() + ) + ) + + val insideFallbackCall = ThreadLocal.withInitial { 0 } + + @JvmStatic + fun enterFallbackCall() { + insideFallbackCall.set(insideFallbackCall.get() + 1) + } + + fun isInFallback() = insideFallbackCall.get() > 0 + + @JvmStatic + fun exitFallbackCall() { + insideFallbackCall.set(insideFallbackCall.get() - 1) + } + + @Subscribe + fun onEarlyReload(event: EarlyResourceReloadEvent) { + preparationFuture = CompletableFuture + .supplyAsync( + { prepare(event.resourceManager) }, event.preparationExecutor + ) + } + + private fun prepare(manager: ResourceManager): BakedReplacements { + val resources = manager.findResources("overrides/blocks") { + it.namespace == "firmskyblock" && it.path.endsWith(".json") + } + val map = mutableMapOf>>() + for ((file, resource) in resources) { + val json = + Firmament.tryDecodeJsonFromStream(resource.inputStream) + .getOrElse { ex -> + logger.error("Failed to load block texture override at $file", ex) + continue + } + for (mode in json.modes) { + val island = SkyBlockIsland.forMode(mode) + val islandMpa = map.getOrPut(island, ::mutableMapOf) + for ((blockId, replacement) in json.replacements) { + val block = MC.defaultRegistries.getOrThrow(RegistryKeys.BLOCK) + .getOptional(RegistryKey.of(RegistryKeys.BLOCK, blockId)) + .getOrNull() + if (block == null) { + logger.error("Failed to load block texture override at ${file}: unknown block '$blockId'") + continue + } + val replacements = islandMpa.getOrPut(block.value(), ::mutableListOf) + replacements.add(BlockReplacement(json.area, replacement)) + } + } + } + + return BakedReplacements(map.mapValues { LocationReplacements(it.value) }) + } + + @Subscribe + fun onStart(event: FinalizeResourceManagerEvent) { + event.resourceManager.registerReloader(object : + SinglePreparationResourceReloader() { + override fun prepare(manager: ResourceManager, profiler: Profiler): BakedReplacements { + return preparationFuture.join().also { + it.modelBakingFuture.join() + } + } + + override fun apply(prepared: BakedReplacements, manager: ResourceManager, profiler: Profiler?) { + allLocationReplacements = prepared + refreshReplacements() + } + }) + } + + fun simpleBlockModel(blockId: Identifier): SimpleBlockStateModel.Unbaked { + // TODO: does this need to be shared between resolving and baking? I think not, but it would probably be wise to do so in the future. + return SimpleBlockStateModel.Unbaked( + ModelVariant(blockId) + ) + } + + /** + * Used by [moe.nea.firmament.init.SectionBuilderRiser] + */ + + @JvmStatic + fun patchIndigo(original: BlockStateModel, pos: BlockPos?, state: BlockState): BlockStateModel { + return getReplacementModel(state, pos) ?: original + } + + @JvmStatic + fun collectExtraModels(modelsCollector: ReferencedModelsCollector) { + preparationFuture.join().collectAllReplacements() + .forEach { + modelsCollector.resolve(simpleBlockModel(it.blockModelIdentifier)) + it.unbakedBlockStateMap?.values?.forEach { + modelsCollector.resolve(it) + } + } + } + + @JvmStatic + fun createBakedModels(baker: Baker, executor: Executor): CompletableFuture { + return preparationFuture.thenComposeAsync(Function { replacements -> + val allBlockStates = CompletableFuture.allOf( + *replacements.collectAllReplacements().filter { it.unbakedBlockStateMap != null }.map { + CompletableFuture.supplyAsync({ + it.blockStateMap = it.unbakedBlockStateMap + ?.map { + it.key to it.value.bake(it.key, baker) + } + ?.toMap() + }, executor) + }.toList().toTypedArray() + ) + val byModel = replacements.collectAllReplacements().groupBy { it.blockModelIdentifier } + val modelBakingTask = AsyncHelper.mapValues(byModel, { blockId, replacements -> + val unbakedModel = SimpleBlockStateModel.Unbaked( + ModelVariant(blockId) + ) + val baked = unbakedModel.bake(baker) + replacements.forEach { + it.blockModel = baked + } + }, executor) + modelBakingTask.thenComposeAsync { + allBlockStates + }.thenAcceptAsync { + replacements.modelBakingFuture.complete(Unit) + } + }, executor) + } + + @JvmStatic + fun collectExtraBlockStateMaps( + extra: BakedReplacements, + original: Map>, + stateManagers: Function?> + ) { + extra.collectAllReplacements().forEach { + val blockId = Registries.BLOCK.getKey(it.overridingBlock).getOrNull()?.value ?: return@forEach + val allModels = mutableListOf() + val stateManager = stateManagers.apply(blockId) ?: return@forEach + for (resource in original[BlockStatesLoader.FINDER.toResourcePath(it.block)] ?: return@forEach) { + try { + resource.reader.use { reader -> + val jsonElement = JsonParser.parseReader(reader) + val blockModelDefinition = + BlockModelDefinition.CODEC.parse(JsonOps.INSTANCE, jsonElement) + .getOrThrow { msg: String? -> JsonParseException(msg) } + allModels.add( + BlockStatesLoader.LoadedBlockStateDefinition( + resource.getPackId(), + blockModelDefinition + ) + ) + } + } catch (exception: Exception) { + ErrorUtil.softError( + "Failed to load custom blockstate definition ${it.block} from pack ${resource.packId}", + exception + ) + } + } + + try { + it.unbakedBlockStateMap = BlockStatesLoader.combine( + blockId, + stateManager, + allModels + ).models + } catch (exception: Exception) { + ErrorUtil.softError("Failed to combine custom blockstate definitions for ${it.block}", exception) + } + } + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalArmorOverrides.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalArmorOverrides.kt new file mode 100644 index 0000000..8a2bde5 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalArmorOverrides.kt @@ -0,0 +1,180 @@ +@file:UseSerializers(IdentifierSerializer::class) + +package moe.nea.firmament.features.texturepack + +import java.util.Optional +import java.util.concurrent.atomic.AtomicInteger +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlinx.serialization.UseSerializers +import net.minecraft.client.render.entity.equipment.EquipmentModel +import net.minecraft.component.type.EquippableComponent +import net.minecraft.entity.EquipmentSlot +import net.minecraft.item.ItemStack +import net.minecraft.item.equipment.EquipmentAssetKeys +import net.minecraft.registry.RegistryKey +import net.minecraft.resource.ResourceManager +import net.minecraft.resource.SinglePreparationResourceReloader +import net.minecraft.util.Identifier +import net.minecraft.util.profiler.Profiler +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.FinalizeResourceManagerEvent +import moe.nea.firmament.features.texturepack.CustomGlobalTextures.logger +import moe.nea.firmament.util.IdentifierSerializer +import moe.nea.firmament.util.collections.WeakCache +import moe.nea.firmament.util.intoOptional +import moe.nea.firmament.util.skyBlockId + +object CustomGlobalArmorOverrides { + @Serializable + data class ArmorOverride( + @SerialName("item_ids") + val itemIds: List, + val layers: List? = null, + val model: Identifier? = null, + val overrides: List = listOf(), + ) { + @Transient + lateinit var modelIdentifier: Identifier + fun bake(manager: ResourceManager) { + modelIdentifier = bakeModel(model, layers) + overrides.forEach { it.bake(manager) } + } + + init { + require(layers != null || model != null) { "Either model or layers must be specified for armor override" } + require(layers == null || model == null) { "Can't specify both model and layers for armor override" } + } + } + + @Serializable + data class ArmorOverrideLayer( + val tint: Boolean = false, + val identifier: Identifier, + val suffix: String = "", + ) + + @Serializable + data class ArmorOverrideOverride( + val predicate: FirmamentModelPredicate, + val layers: List? = null, + val model: Identifier? = null, + ) { + init { + require(layers != null || model != null) { "Either model or layers must be specified for armor override override" } + require(layers == null || model == null) { "Can't specify both model and layers for armor override override" } + } + + @Transient + lateinit var modelIdentifier: Identifier + fun bake(manager: ResourceManager) { + modelIdentifier = bakeModel(model, layers) + } + } + + + private fun resolveComponent(slot: EquipmentSlot, model: Identifier): EquippableComponent { + return EquippableComponent( + slot, + null, + Optional.of(RegistryKey.of(EquipmentAssetKeys.REGISTRY_KEY, model)), + Optional.empty(), + Optional.empty(), + false, + false, + false, + false + ) + } + + // TODO: BipedEntityRenderer.getEquippedStack create copies of itemstacks for rendering. This means this cache is essentially useless + // If i figure out how to circumvent this (maybe track the origin of those copied itemstacks in some sort of variable in the itemstack to track back the original instance) i should reenable this cache. + // Then also re add this to the cache clearing function + val overrideCache = + WeakCache.dontMemoize>("ArmorOverrides") { stack, slot -> + val id = stack.skyBlockId ?: return@dontMemoize Optional.empty() + val override = overrides[id.neuItem] ?: return@dontMemoize Optional.empty() + for (suboverride in override.overrides) { + if (suboverride.predicate.test(stack)) { + return@dontMemoize resolveComponent(slot, suboverride.modelIdentifier).intoOptional() + } + } + return@dontMemoize resolveComponent(slot, override.modelIdentifier).intoOptional() + } + + var overrides: Map = mapOf() + private var bakedOverrides: MutableMap = mutableMapOf() + private val sentinelFirmRunning = AtomicInteger() + + private fun bakeModel(model: Identifier?, layers: List?): Identifier { + require(model == null || layers == null) + if (model != null) { + return model + } else if (layers != null) { + val idNumber = sentinelFirmRunning.incrementAndGet() + val identifier = Identifier.of("firmament:sentinel/armor/$idNumber") + val equipmentLayers = layers.map { + EquipmentModel.Layer( + it.identifier, if (it.tint) { + Optional.of(EquipmentModel.Dyeable(Optional.of(0xFFA06540.toInt()))) + } else { + Optional.empty() + }, + false + ) + } + bakedOverrides[identifier] = EquipmentModel( + mapOf( + EquipmentModel.LayerType.HUMANOID to equipmentLayers, + EquipmentModel.LayerType.HUMANOID_LEGGINGS to equipmentLayers, + ) + ) + return identifier + } else { + error("Either model or layers must be non null") + } + } + + + @Subscribe + fun onStart(event: FinalizeResourceManagerEvent) { + event.resourceManager.registerReloader(object : + SinglePreparationResourceReloader>() { + override fun prepare(manager: ResourceManager, profiler: Profiler): Map { + val overrideFiles = manager.findResources("overrides/armor_models") { + it.namespace == "firmskyblock" && it.path.endsWith(".json") + } + val overrides = overrideFiles.mapNotNull { + Firmament.tryDecodeJsonFromStream(it.value.inputStream).getOrElse { ex -> + logger.error("Failed to load armor texture override at ${it.key}", ex) + null + } + } + bakedOverrides.clear() + val associatedMap = overrides.flatMap { obj -> obj.itemIds.map { it to obj } } + .toMap() + associatedMap.forEach { it.value.bake(manager) } + return associatedMap + } + + override fun apply(prepared: Map, manager: ResourceManager, profiler: Profiler) { + overrides = prepared + } + }) + } + + @JvmStatic + fun overrideArmor(itemStack: ItemStack, slot: EquipmentSlot): Optional { + if (!CustomSkyBlockTextures.TConfig.enableArmorOverrides) return Optional.empty() + return overrideCache.invoke(itemStack, slot) + } + + @JvmStatic + fun overrideArmorLayer(id: Identifier): EquipmentModel? { + if (!CustomSkyBlockTextures.TConfig.enableArmorOverrides) return null + return bakedOverrides[id] + } + +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt new file mode 100644 index 0000000..403e3bd --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt @@ -0,0 +1,143 @@ +@file:UseSerializers(IdentifierSerializer::class, FirmamentRootPredicateSerializer::class) + +package moe.nea.firmament.features.texturepack + + +import java.util.concurrent.CompletableFuture +import org.slf4j.LoggerFactory +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers +import kotlin.jvm.optionals.getOrNull +import net.minecraft.resource.ResourceManager +import net.minecraft.resource.SinglePreparationResourceReloader +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import net.minecraft.util.profiler.Profiler +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.CustomItemModelEvent +import moe.nea.firmament.events.EarlyResourceReloadEvent +import moe.nea.firmament.events.FinalizeResourceManagerEvent +import moe.nea.firmament.events.ScreenChangeEvent +import moe.nea.firmament.events.subscription.SubscriptionOwner +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.util.ErrorUtil +import moe.nea.firmament.util.IdentifierSerializer +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.json.SingletonSerializableList +import moe.nea.firmament.util.runNull + +object CustomGlobalTextures : SinglePreparationResourceReloader(), + SubscriptionOwner { + override val delegateFeature: FirmamentFeature + get() = CustomSkyBlockTextures + + class CustomGuiTextureOverride( + val classes: List + ) + + @Serializable + data class GlobalItemOverride( + val screen: @Serializable(SingletonSerializableList::class) List, + val model: Identifier, + val predicate: FirmamentModelPredicate, + ) + + @Serializable + data class ScreenFilter( + val title: StringMatcher, + ) + + data class ItemOverrideCollection( + val screenFilter: ScreenFilter, + val overrides: List, + ) + + @Subscribe + fun onStart(event: FinalizeResourceManagerEvent) { + MC.resourceManager.registerReloader(this) + } + + @Subscribe + fun onEarlyReload(event: EarlyResourceReloadEvent) { + preparationFuture = CompletableFuture + .supplyAsync( + { + prepare(event.resourceManager) + }, event.preparationExecutor) + } + + @Volatile + var preparationFuture: CompletableFuture = CompletableFuture.completedFuture( + CustomGuiTextureOverride(listOf())) + + override fun prepare(manager: ResourceManager?, profiler: Profiler?): CustomGuiTextureOverride { + return preparationFuture.join() + } + + override fun apply(prepared: CustomGuiTextureOverride, manager: ResourceManager?, profiler: Profiler?) { + guiClassOverrides = prepared + } + + val logger = LoggerFactory.getLogger(CustomGlobalTextures::class.java) + fun prepare(manager: ResourceManager): CustomGuiTextureOverride { + val overrideResources = + manager.findResources("overrides/item") { it.namespace == "firmskyblock" && it.path.endsWith(".json") } + .mapNotNull { + Firmament.tryDecodeJsonFromStream(it.value.inputStream).getOrElse { ex -> + ErrorUtil.softError("Failed to load global item override at ${it.key}", ex) + null + } + } + + val byGuiClass = overrideResources.flatMap { override -> override.screen.toSet().map { it to override } } + .groupBy { it.first } + val guiClasses = byGuiClass.entries + .mapNotNull { + val key = it.key + val guiClassResource = + manager.getResource(Identifier.of(key.namespace, "filters/screen/${key.path}.json")) + .getOrNull() + ?: return@mapNotNull runNull { + ErrorUtil.softError("Failed to locate screen filter at $key used by ${it.value.map { it.first }}") + } + val screenFilter = + Firmament.tryDecodeJsonFromStream(guiClassResource.inputStream) + .getOrElse { ex -> + ErrorUtil.softError("Failed to load screen filter at $key used by ${it.value.map { it.first }}", ex) + return@mapNotNull null + } + ItemOverrideCollection(screenFilter, it.value.map { it.second }) + } + logger.info("Loaded ${overrideResources.size} global item overrides") + return CustomGuiTextureOverride(guiClasses) + } + + var guiClassOverrides = CustomGuiTextureOverride(listOf()) + + var matchingOverrides: Set = setOf() + + @Subscribe + fun onOpenGui(event: ScreenChangeEvent) { + val newTitle = event.new?.title ?: Text.empty() + matchingOverrides = guiClassOverrides.classes + .filterTo(mutableSetOf()) { it.screenFilter.title.matches(newTitle) } + } + + @Subscribe + fun replaceGlobalModel(event: CustomItemModelEvent) { + val override = matchingOverrides + .firstNotNullOfOrNull { + it.overrides + .asSequence() + .filter { it.predicate.test(event.itemStack) } + .map { it.model } + .firstOrNull() + } + + if (override != null) + event.overrideIfExists(override) + } + + +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt new file mode 100644 index 0000000..1da840d --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt @@ -0,0 +1,123 @@ +package moe.nea.firmament.features.texturepack + +import com.google.gson.JsonObject +import com.mojang.datafixers.util.Pair +import com.mojang.serialization.Codec +import com.mojang.serialization.DataResult +import com.mojang.serialization.Decoder +import com.mojang.serialization.DynamicOps +import com.mojang.serialization.Encoder +import net.minecraft.client.render.item.model.ItemModelTypes +import net.minecraft.item.ItemStack +import net.minecraft.util.Identifier +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.FinalizeResourceManagerEvent +import moe.nea.firmament.features.texturepack.predicates.AndPredicate +import moe.nea.firmament.features.texturepack.predicates.CastPredicate +import moe.nea.firmament.features.texturepack.predicates.DisplayNamePredicate +import moe.nea.firmament.features.texturepack.predicates.ExtraAttributesPredicate +import moe.nea.firmament.features.texturepack.predicates.GenericComponentPredicate +import moe.nea.firmament.features.texturepack.predicates.ItemPredicate +import moe.nea.firmament.features.texturepack.predicates.LorePredicate +import moe.nea.firmament.features.texturepack.predicates.NotPredicate +import moe.nea.firmament.features.texturepack.predicates.OrPredicate +import moe.nea.firmament.features.texturepack.predicates.PetPredicate +import moe.nea.firmament.features.texturepack.predicates.PullingPredicate +import moe.nea.firmament.features.texturepack.predicates.SkullPredicate +import moe.nea.firmament.util.json.KJsonOps + +object CustomModelOverrideParser { + + val LEGACY_CODEC: Codec = + Codec.of( + Encoder.error("cannot encode legacy firmament model predicates"), + object : Decoder { + override fun decode( + ops: DynamicOps, + input: T + ): DataResult> { + try { + val pred = Firmament.json.decodeFromJsonElement( + FirmamentRootPredicateSerializer, + ops.convertTo(KJsonOps.INSTANCE, input)) + return DataResult.success(Pair.of(pred, ops.empty())) + } catch (ex: Exception) { + return DataResult.error { "Could not deserialize ${ex.message}" } + } + } + } + ) + + val predicateParsers = mutableMapOf() + + + fun registerPredicateParser(name: String, parser: FirmamentModelPredicateParser) { + predicateParsers[Identifier.of("firmament", name)] = parser + } + + init { + registerPredicateParser("display_name", DisplayNamePredicate.Parser) + registerPredicateParser("lore", LorePredicate.Parser) + registerPredicateParser("all", AndPredicate.Parser) + registerPredicateParser("any", OrPredicate.Parser) + registerPredicateParser("not", NotPredicate.Parser) + registerPredicateParser("item", ItemPredicate.Parser) + registerPredicateParser("extra_attributes", ExtraAttributesPredicate.Parser) + registerPredicateParser("pet", PetPredicate.Parser) + registerPredicateParser("component", GenericComponentPredicate.Parser) + registerPredicateParser("skull", SkullPredicate.Parser) + } + + private val neverPredicate = listOf( + object : FirmamentModelPredicate { + override fun test(stack: ItemStack): Boolean { + return false + } + } + ) + + fun parsePredicates(predicates: JsonObject?): List { + if (predicates == null) return neverPredicate + val parsedPredicates = mutableListOf() + for (predicateName in predicates.keySet()) { + if (predicateName == "cast") { // 1.21.4 + parsedPredicates.add(CastPredicate.Parser.parse(predicates[predicateName]) ?: return neverPredicate) + } + if (predicateName == "pull") { + parsedPredicates.add(PullingPredicate.Parser.parse(predicates[predicateName]) ?: return neverPredicate) + } + if (predicateName == "pulling") { + parsedPredicates.add(PullingPredicate.AnyPulling) + } + if (!predicateName.startsWith("firmament:")) continue + val identifier = Identifier.of(predicateName) + val parser = predicateParsers[identifier] ?: return neverPredicate + val parsedPredicate = parser.parse(predicates[predicateName]) ?: return neverPredicate + parsedPredicates.add(parsedPredicate) + } + return parsedPredicates + } + + @JvmStatic + fun parseCustomModelOverrides(jsonObject: JsonObject): Array? { + val predicates = (jsonObject["predicate"] as? JsonObject) ?: return null + val parsedPredicates = parsePredicates(predicates) + if (parsedPredicates.isEmpty()) + return null + return parsedPredicates.toTypedArray() + } + + @Subscribe + fun finalizeResources(event: FinalizeResourceManagerEvent) { + ItemModelTypes.ID_MAPPER.put( + Firmament.identifier("predicates/legacy"), + PredicateModel.Unbaked.CODEC + ) + ItemModelTypes.ID_MAPPER.put( + Firmament.identifier("head_model"), + HeadModelChooser.Unbaked.CODEC + ) + } + +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomScreenLayouts.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomScreenLayouts.kt new file mode 100644 index 0000000..4785e90 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomScreenLayouts.kt @@ -0,0 +1,224 @@ +package moe.nea.firmament.features.texturepack + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import net.minecraft.client.font.TextRenderer +import net.minecraft.client.gui.DrawContext +import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.gui.screen.ingame.HandledScreen +import net.minecraft.client.render.RenderLayer +import net.minecraft.registry.Registries +import net.minecraft.resource.ResourceManager +import net.minecraft.resource.SinglePreparationResourceReloader +import net.minecraft.screen.slot.Slot +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import net.minecraft.util.profiler.Profiler +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.FinalizeResourceManagerEvent +import moe.nea.firmament.events.ScreenChangeEvent +import moe.nea.firmament.features.texturepack.CustomScreenLayouts.Alignment.CENTER +import moe.nea.firmament.features.texturepack.CustomScreenLayouts.Alignment.LEFT +import moe.nea.firmament.features.texturepack.CustomScreenLayouts.Alignment.RIGHT +import moe.nea.firmament.mixins.accessor.AccessorHandledScreen +import moe.nea.firmament.util.ErrorUtil.intoCatch +import moe.nea.firmament.util.IdentifierSerializer + +object CustomScreenLayouts : SinglePreparationResourceReloader>() { + + @Serializable + data class CustomScreenLayout( + val predicates: Preds, + val background: BackgroundReplacer? = null, + val slots: List = listOf(), + val playerTitle: TitleReplacer? = null, + val containerTitle: TitleReplacer? = null, + val repairCostTitle: TitleReplacer? = null, + val nameField: ComponentMover? = null, + ) + + @Serializable + data class ComponentMover( + val x: Int, + val y: Int, + val width: Int? = null, + val height: Int? = null, + ) + + @Serializable + data class Preds( + val label: StringMatcher, + @Serializable(with = IdentifierSerializer::class) + val screenType: Identifier? = null, + ) { + fun matches(screen: Screen): Boolean { + // TODO: does this deserve the restriction to handled screen + val s = screen as? HandledScreen<*>? ?: return false + val typeMatches = screenType == null || s.screenHandler.type.equals(Registries.SCREEN_HANDLER + .get(screenType)); + + return label.matches(s.title) && typeMatches + } + } + + @Serializable + data class BackgroundReplacer( + @Serializable(with = IdentifierSerializer::class) + val texture: Identifier, + // TODO: allow selectively still rendering some components (recipe button, trade backgrounds, furnace flame progress, arrows) + val x: Int, + val y: Int, + val width: Int, + val height: Int, + ) { + fun renderGeneric(context: DrawContext, screen: HandledScreen<*>) { + screen as AccessorHandledScreen + val originalX: Int = (screen.width - screen.backgroundWidth_Firmament) / 2 + val originalY: Int = (screen.height - screen.backgroundHeight_Firmament) / 2 + val modifiedX = originalX + this.x + val modifiedY = originalY + this.y + val textureWidth = this.width + val textureHeight = this.height + context.drawTexture( + RenderLayer::getGuiTextured, + this.texture, + modifiedX, + modifiedY, + 0.0f, + 0.0f, + textureWidth, + textureHeight, + textureWidth, + textureHeight + ) + + } + } + + @Serializable + data class SlotReplacer( + // TODO: override getRecipeBookButtonPos as well + // TODO: is this index or id (i always forget which one is duplicated per inventory) + val index: Int, + val x: Int, + val y: Int, + ) { + fun move(slots: List) { + val slot = slots.getOrNull(index) ?: return + slot.x = x + slot.y = y + } + } + + @Serializable + enum class Alignment { + @SerialName("left") + LEFT, + + @SerialName("center") + CENTER, + + @SerialName("right") + RIGHT + } + + @Serializable + data class TitleReplacer( + val x: Int? = null, + val y: Int? = null, + val align: Alignment = Alignment.LEFT, + val replace: String? = null + ) { + @Transient + val replacedText: Text? = replace?.let(Text::literal) + + fun replaceText(text: Text): Text { + if (replacedText != null) return replacedText + return text + } + + fun replaceY(y: Int): Int { + return this.y ?: y + } + + fun replaceX(font: TextRenderer, text: Text, x: Int): Int { + val baseX = this.x ?: x + return baseX + when (this.align) { + LEFT -> 0 + CENTER -> -font.getWidth(text) / 2 + RIGHT -> -font.getWidth(text) + } + } + + /** + * Not technically part of the package, but it does allow for us to later on seamlessly integrate a color option into this class as well + */ + fun replaceColor(text: Text, color: Int): Int { + return CustomTextColors.mapTextColor(text, color) + } + } + + + @Subscribe + fun onStart(event: FinalizeResourceManagerEvent) { + event.resourceManager.registerReloader(CustomScreenLayouts) + } + + override fun prepare( + manager: ResourceManager, + profiler: Profiler + ): List { + val allScreenLayouts = manager.findResources( + "overrides/screen_layout", + { it.path.endsWith(".json") && it.namespace == "firmskyblock" }) + val allParsedLayouts = allScreenLayouts.mapNotNull { (path, stream) -> + Firmament.tryDecodeJsonFromStream(stream.inputStream) + .intoCatch("Could not read custom screen layout from $path").orNull() + } + return allParsedLayouts + } + + var customScreenLayouts = listOf() + + override fun apply( + prepared: List, + manager: ResourceManager?, + profiler: Profiler? + ) { + this.customScreenLayouts = prepared + } + + @get:JvmStatic + var activeScreenOverride = null as CustomScreenLayout? + + val DO_NOTHING_TEXT_REPLACER = TitleReplacer() + + @JvmStatic + fun getMover(selector: (CustomScreenLayout)-> (T?)) = + activeScreenOverride?.let(selector) + + @JvmStatic + fun getTextMover(selector: (CustomScreenLayout) -> (TitleReplacer?)) = + getMover(selector) ?: DO_NOTHING_TEXT_REPLACER + + @Subscribe + fun onScreenOpen(event: ScreenChangeEvent) { + if (!CustomSkyBlockTextures.TConfig.allowLayoutChanges) { + activeScreenOverride = null + return + } + activeScreenOverride = event.new?.let { screen -> + customScreenLayouts.find { it.predicates.matches(screen) } + } + + val screen = event.new as? HandledScreen<*> ?: return + val handler = screen.screenHandler + activeScreenOverride?.let { override -> + override.slots.forEach { slotReplacer -> + slotReplacer.move(handler.slots) + } + } + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt new file mode 100644 index 0000000..18949ff --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt @@ -0,0 +1,119 @@ +package moe.nea.firmament.features.texturepack + +import com.mojang.authlib.minecraft.MinecraftProfileTexture +import com.mojang.authlib.properties.Property +import java.util.Optional +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable +import kotlin.jvm.optionals.getOrNull +import net.minecraft.block.SkullBlock +import net.minecraft.client.MinecraftClient +import net.minecraft.client.render.RenderLayer +import net.minecraft.component.type.ProfileComponent +import net.minecraft.util.Identifier +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.CustomItemModelEvent +import moe.nea.firmament.events.FinalizeResourceManagerEvent +import moe.nea.firmament.events.TickEvent +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.features.debug.PowerUserTools +import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.collections.WeakCache +import moe.nea.firmament.util.mc.decodeProfileTextureProperty +import moe.nea.firmament.util.skyBlockId + +object CustomSkyBlockTextures : FirmamentFeature { + override val identifier: String + get() = "custom-skyblock-textures" + + object TConfig : ManagedConfig(identifier, Category.INTEGRATIONS) { // TODO: should this be its own thing? + val enabled by toggle("enabled") { true } + val skullsEnabled by toggle("skulls-enabled") { true } + val cacheForever by toggle("cache-forever") { true } + val cacheDuration by integer("cache-duration", 0, 100) { 1 } + val enableModelOverrides by toggle("model-overrides") { true } + val enableArmorOverrides by toggle("armor-overrides") { true } + val enableBlockOverrides by toggle("block-overrides") { true } + val enableLegacyMinecraftCompat by toggle("legacy-minecraft-path-support") { true } + val enableLegacyCIT by toggle("legacy-cit") { true } + val allowRecoloringUiText by toggle("recolor-text") { true } + val allowLayoutChanges by toggle("screen-layouts") { true } + } + + override val config: ManagedConfig + get() = TConfig + + val allItemCaches by lazy { + listOf( + skullTextureCache.cache, + CustomItemModelEvent.cache.cache, + // TODO: re-add this once i figure out how to make the cache useful again CustomGlobalArmorOverrides.overrideCache.cache + ) + } + + init { + PowerUserTools.getSkullId = ::getSkullTexture + } + + fun clearAllCaches() { + allItemCaches.forEach(WeakCache<*, *, *>::clear) + } + + @Subscribe + fun onTick(it: TickEvent) { + if (TConfig.cacheForever) return + if (TConfig.cacheDuration < 1 || it.tickCount % TConfig.cacheDuration == 0) { + clearAllCaches() + } + } + + @Subscribe + fun onStart(event: FinalizeResourceManagerEvent) { + event.registerOnApply("Clear firmament CIT caches") { + clearAllCaches() + } + } + + @Subscribe + fun onCustomModelId(it: CustomItemModelEvent) { + if (!TConfig.enabled) return + val id = it.itemStack.skyBlockId ?: return + it.overrideIfEmpty(Identifier.of("firmskyblock", id.identifier.path)) + } + + private val skullTextureCache = + WeakCache.memoize>("SkullTextureCache") { component -> + val id = getSkullTexture(component) ?: return@memoize Optional.empty() + if (!MinecraftClient.getInstance().resourceManager.getResource(id).isPresent) { + return@memoize Optional.empty() + } + return@memoize Optional.of(id) + } + + private val mcUrlRegex = "https?://textures.minecraft.net/texture/([a-fA-F0-9]+)".toRegex() + + fun getSkullId(textureProperty: Property): String? { + val texture = decodeProfileTextureProperty(textureProperty) ?: return null + val textureUrl = + texture.textures[MinecraftProfileTexture.Type.SKIN]?.url ?: return null + val mcUrlData = mcUrlRegex.matchEntire(textureUrl) ?: return null + return mcUrlData.groupValues[1] + } + + fun getSkullTexture(profile: ProfileComponent): Identifier? { + val id = getSkullId(profile.properties["textures"].firstOrNull() ?: return null) ?: return null + return Identifier.of("firmskyblock", "textures/placedskull/$id.png") + } + + fun modifySkullTexture( + type: SkullBlock.SkullType?, + component: ProfileComponent?, + cir: CallbackInfoReturnable + ) { + if (type != SkullBlock.Type.PLAYER) return + if (!TConfig.skullsEnabled) return + if (component == null) return + + val n = skullTextureCache.invoke(component).getOrNull() ?: return + cir.returnValue = RenderLayer.getEntityTranslucent(n) + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomTextColors.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomTextColors.kt new file mode 100644 index 0000000..3ac895a --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomTextColors.kt @@ -0,0 +1,80 @@ +package moe.nea.firmament.features.texturepack + +import java.util.Optional +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlin.jvm.optionals.getOrNull +import net.minecraft.resource.ResourceManager +import net.minecraft.resource.SinglePreparationResourceReloader +import net.minecraft.text.Text +import net.minecraft.util.Identifier +import net.minecraft.util.profiler.Profiler +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.FinalizeResourceManagerEvent +import moe.nea.firmament.util.collections.WeakCache + +object CustomTextColors : SinglePreparationResourceReloader() { + @Serializable + data class TextOverrides( + val defaultColor: Int, + val overrides: List = listOf() + ) { + /** + * Stub custom text color to allow always returning a text override + */ + @Transient + val baseOverride = TextOverride( + StringMatcher.Equals("", false), + defaultColor, + 0, + 0 + ) + } + + @Serializable + data class TextOverride( + val predicate: StringMatcher, + val override: Int, + val x: Int = 0, + val y: Int = 0, + ) + + @Subscribe + fun registerTextColorReloader(event: FinalizeResourceManagerEvent) { + event.resourceManager.registerReloader(this) + } + + val cache = WeakCache.memoize>("CustomTextColor") { text -> + val override = textOverrides ?: return@memoize Optional.empty() + Optional.ofNullable(override.overrides.find { it.predicate.matches(text) }) + } + + fun mapTextColor(text: Text, oldColor: Int): Int { + val override = cache(text).orElse(null) + return override?.override ?: textOverrides?.defaultColor ?: oldColor + } + + override fun prepare( + manager: ResourceManager, + profiler: Profiler + ): TextOverrides? { + val resource = manager.getResource(Identifier.of("firmskyblock", "overrides/text_colors.json")).getOrNull() + ?: return null + return Firmament.tryDecodeJsonFromStream(resource.inputStream) + .getOrElse { + Firmament.logger.error("Could not parse text_colors.json", it) + null + } + } + + var textOverrides: TextOverrides? = null + + override fun apply( + prepared: TextOverrides?, + manager: ResourceManager, + profiler: Profiler + ) { + textOverrides = prepared + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicate.kt new file mode 100644 index 0000000..e020d66 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicate.kt @@ -0,0 +1,11 @@ +package moe.nea.firmament.features.texturepack + +import kotlinx.serialization.Serializable +import net.minecraft.entity.LivingEntity +import net.minecraft.item.ItemStack + +@Serializable(with = FirmamentRootPredicateSerializer::class) +interface FirmamentModelPredicate { + fun test(stack: ItemStack, holder: LivingEntity?): Boolean = test(stack) + fun test(stack: ItemStack): Boolean = test(stack, null) +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicateParser.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicateParser.kt new file mode 100644 index 0000000..3ed0c67 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicateParser.kt @@ -0,0 +1,8 @@ + +package moe.nea.firmament.features.texturepack + +import com.google.gson.JsonElement + +interface FirmamentModelPredicateParser { + fun parse(jsonElement: JsonElement): FirmamentModelPredicate? +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentRootPredicateSerializer.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentRootPredicateSerializer.kt new file mode 100644 index 0000000..0b8ae8e --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentRootPredicateSerializer.kt @@ -0,0 +1,23 @@ +package moe.nea.firmament.features.texturepack + +import com.google.gson.JsonObject +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import moe.nea.firmament.features.texturepack.predicates.AndPredicate + +object FirmamentRootPredicateSerializer : KSerializer { + val delegateSerializer = kotlinx.serialization.json.JsonObject.serializer() + override val descriptor: SerialDescriptor + get() = SerialDescriptor("FirmamentModelRootPredicate", delegateSerializer.descriptor) + + override fun deserialize(decoder: Decoder): FirmamentModelPredicate { + val json = decoder.decodeSerializableValue(delegateSerializer).intoGson() as JsonObject + return AndPredicate(CustomModelOverrideParser.parsePredicates(json).toTypedArray()) + } + + override fun serialize(encoder: Encoder, value: FirmamentModelPredicate) { + TODO("Cannot serialize firmament predicates") + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/HeadModelChooser.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/HeadModelChooser.kt new file mode 100644 index 0000000..3e8cc4e --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/HeadModelChooser.kt @@ -0,0 +1,90 @@ +package moe.nea.firmament.features.texturepack + +import com.google.gson.JsonObject +import com.mojang.serialization.MapCodec +import com.mojang.serialization.codecs.RecordCodecBuilder +import net.minecraft.client.item.ItemModelManager +import net.minecraft.client.render.item.ItemRenderState +import net.minecraft.client.render.item.model.BasicItemModel +import net.minecraft.client.render.item.model.ItemModel +import net.minecraft.client.render.item.model.ItemModelTypes +import net.minecraft.client.render.model.ResolvableModel +import net.minecraft.client.world.ClientWorld +import net.minecraft.entity.LivingEntity +import net.minecraft.item.ItemDisplayContext +import net.minecraft.item.ItemStack +import net.minecraft.util.Identifier + +object HeadModelChooser { + val IS_CHOOSING_HEAD_MODEL = ThreadLocal.withInitial { false } + + interface HasExplicitHeadModelMarker { + fun markExplicitHead_Firmament() + fun isExplicitHeadModel_Firmament(): Boolean + companion object{ + @JvmStatic + fun cast(state: ItemRenderState) = state as HasExplicitHeadModelMarker + } + } + + data class Baked(val head: ItemModel, val regular: ItemModel) : ItemModel { + override fun update( + state: ItemRenderState, + stack: ItemStack?, + resolver: ItemModelManager?, + displayContext: ItemDisplayContext, + world: ClientWorld?, + user: LivingEntity?, + seed: Int + ) { + val instance = + if (IS_CHOOSING_HEAD_MODEL.get()) { + HasExplicitHeadModelMarker.cast(state).markExplicitHead_Firmament() + head + } else { + regular + } + instance.update(state, stack, resolver, displayContext, world, user, seed) + } + } + + data class Unbaked( + val head: ItemModel.Unbaked, + val regular: ItemModel.Unbaked, + ) : ItemModel.Unbaked { + override fun getCodec(): MapCodec { + return CODEC + } + + override fun bake(context: ItemModel.BakeContext): ItemModel { + return Baked( + head.bake(context), + regular.bake(context) + ) + } + + override fun resolve(resolver: ResolvableModel.Resolver) { + head.resolve(resolver) + regular.resolve(resolver) + } + + companion object { + @JvmStatic + fun fromLegacyJson(jsonObject: JsonObject, unbakedModel: ItemModel.Unbaked): ItemModel.Unbaked { + val model = jsonObject["firmament:head_model"] ?: return unbakedModel + val modelUrl = model.asJsonPrimitive.asString + val headModel = BasicItemModel.Unbaked(Identifier.of(modelUrl), listOf()) + return Unbaked(headModel, unbakedModel) + } + + val CODEC = RecordCodecBuilder.mapCodec { + it.group( + ItemModelTypes.CODEC.fieldOf("head") + .forGetter(Unbaked::head), + ItemModelTypes.CODEC.fieldOf("regular") + .forGetter(Unbaked::regular), + ).apply(it, ::Unbaked) + } + } + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/PredicateModel.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/PredicateModel.kt new file mode 100644 index 0000000..e6b5bcf --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/PredicateModel.kt @@ -0,0 +1,105 @@ +package moe.nea.firmament.features.texturepack + +import com.google.gson.JsonObject +import com.mojang.serialization.Codec +import com.mojang.serialization.MapCodec +import com.mojang.serialization.codecs.RecordCodecBuilder +import net.minecraft.client.item.ItemModelManager +import net.minecraft.client.render.item.ItemRenderState +import net.minecraft.client.render.item.model.BasicItemModel +import net.minecraft.client.render.item.model.ItemModel +import net.minecraft.client.render.item.model.ItemModelTypes +import net.minecraft.client.render.model.ResolvableModel +import net.minecraft.client.world.ClientWorld +import net.minecraft.entity.LivingEntity +import net.minecraft.item.ItemDisplayContext +import net.minecraft.item.ItemStack +import net.minecraft.util.Identifier +import moe.nea.firmament.features.texturepack.predicates.AndPredicate + +class PredicateModel { + data class Baked( + val fallback: ItemModel, + val overrides: List + ) : ItemModel { + data class Override( + val model: ItemModel, + val predicate: FirmamentModelPredicate, + ) + + override fun update( + state: ItemRenderState?, + stack: ItemStack, + resolver: ItemModelManager?, + displayContext: ItemDisplayContext?, + world: ClientWorld?, + user: LivingEntity?, + seed: Int + ) { + val model = + overrides + .findLast { it.predicate.test(stack, user) } + ?.model + ?: fallback + model.update(state, stack, resolver, displayContext, world, user, seed) + } + } + + data class Unbaked( + val fallback: ItemModel.Unbaked, + val overrides: List, + ) : ItemModel.Unbaked { + companion object { + @JvmStatic + fun fromLegacyJson(jsonObject: JsonObject, fallback: ItemModel.Unbaked): ItemModel.Unbaked { + val legacyOverrides = jsonObject.getAsJsonArray("overrides") ?: return fallback + val newOverrides = ArrayList() + for (legacyOverride in legacyOverrides) { + legacyOverride as JsonObject + val overrideModel = Identifier.tryParse(legacyOverride.get("model")?.asString ?: continue) ?: continue + val predicate = CustomModelOverrideParser.parsePredicates(legacyOverride.getAsJsonObject("predicate")) + newOverrides.add(Override( + BasicItemModel.Unbaked(overrideModel, listOf()), + AndPredicate(predicate.toTypedArray()) + )) + } + return Unbaked(fallback, newOverrides) + } + + val OVERRIDE_CODEC: Codec = RecordCodecBuilder.create { + it.group( + ItemModelTypes.CODEC.fieldOf("model").forGetter(Override::model), + CustomModelOverrideParser.LEGACY_CODEC.fieldOf("predicate").forGetter(Override::predicate), + ).apply(it, Unbaked::Override) + } + val CODEC: MapCodec = + RecordCodecBuilder.mapCodec { + it.group( + ItemModelTypes.CODEC.fieldOf("fallback").forGetter(Unbaked::fallback), + OVERRIDE_CODEC.listOf().fieldOf("overrides").forGetter(Unbaked::overrides), + ).apply(it, ::Unbaked) + } + } + + data class Override( + val model: ItemModel.Unbaked, + val predicate: FirmamentModelPredicate, + ) + + override fun resolve(resolver: ResolvableModel.Resolver) { + fallback.resolve(resolver) + overrides.forEach { it.model.resolve(resolver) } + } + + override fun getCodec(): MapCodec { + return CODEC + } + + override fun bake(context: ItemModel.BakeContext): ItemModel { + return Baked( + fallback.bake(context), + overrides.map { Baked.Override(it.model.bake(context), it.predicate) } + ) + } + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/RarityMatcher.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/RarityMatcher.kt new file mode 100644 index 0000000..634a171 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/RarityMatcher.kt @@ -0,0 +1,69 @@ + +package moe.nea.firmament.features.texturepack + +import com.google.gson.JsonElement +import io.github.moulberry.repo.data.Rarity +import moe.nea.firmament.util.useMatch + +abstract class RarityMatcher { + abstract fun match(rarity: Rarity): Boolean + + companion object { + fun parse(jsonElement: JsonElement): RarityMatcher { + val string = jsonElement.asString + val range = parseRange(string) + if (range != null) return range + return Exact(Rarity.valueOf(string)) + } + + private val allRarities = Rarity.entries.joinToString("|", "(?:", ")") + private val intervalSpec = + "(?[\\[\\(])(?$allRarities)?,(?$allRarities)?(?[\\]\\)])" + .toPattern() + + fun parseRange(string: String): RangeMatcher? { + intervalSpec.useMatch(string) { + // Open in the set-theory sense, meaning does not include its end. + val beginningOpen = group("beginningOpen") == "(" + val endingOpen = group("endingOpen") == ")" + val beginning = group("beginning")?.let(Rarity::valueOf) + val ending = group("ending")?.let(Rarity::valueOf) + return RangeMatcher(beginning, !beginningOpen, ending, !endingOpen) + } + return null + } + + } + + data class Exact(val expected: Rarity) : RarityMatcher() { + override fun match(rarity: Rarity): Boolean { + return rarity == expected + } + } + + data class RangeMatcher( + val beginning: Rarity?, + val beginningInclusive: Boolean, + val ending: Rarity?, + val endingInclusive: Boolean, + ) : RarityMatcher() { + override fun match(rarity: Rarity): Boolean { + if (beginning != null) { + if (beginningInclusive) { + if (rarity < beginning) return false + } else { + if (rarity <= beginning) return false + } + } + if (ending != null) { + if (endingInclusive) { + if (rarity > ending) return false + } else { + if (rarity >= ending) return false + } + } + return true + } + } + +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/StringMatcher.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/StringMatcher.kt new file mode 100644 index 0000000..dd28d9f --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/StringMatcher.kt @@ -0,0 +1,160 @@ + +package moe.nea.firmament.features.texturepack + +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonNull +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import com.google.gson.internal.LazilyParsedNumber +import java.util.function.Predicate +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlin.jvm.optionals.getOrNull +import net.minecraft.nbt.NbtString +import net.minecraft.text.Text +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.removeColorCodes + +@Serializable(with = StringMatcher.Serializer::class) +interface StringMatcher { + fun matches(string: String): Boolean + fun matches(text: Text): Boolean { + return matches(text.string) + } + + fun matches(nbt: NbtString): Boolean { + val string = nbt.value + val jsonStart = string.indexOf('{') + val stringStart = string.indexOf('"') + val isString = stringStart >= 0 && string.subSequence(0, stringStart).isBlank() + val isJson = jsonStart >= 0 && string.subSequence(0, jsonStart).isBlank() + if (isString || isJson) + return matches(Text.Serialization.fromJson(string, MC.defaultRegistries) ?: return false) + return matches(string) + } + + class Equals(input: String, val stripColorCodes: Boolean) : StringMatcher { + private val expected = if (stripColorCodes) input.removeColorCodes() else input + override fun matches(string: String): Boolean { + return expected == (if (stripColorCodes) string.removeColorCodes() else string) + } + + override fun toString(): String { + return "Equals($expected, stripColorCodes = $stripColorCodes)" + } + } + + class Pattern(val patternWithColorCodes: String, val stripColorCodes: Boolean) : StringMatcher { + private val regex: Predicate = patternWithColorCodes.toPattern().asMatchPredicate() + override fun matches(string: String): Boolean { + return regex.test(if (stripColorCodes) string.removeColorCodes() else string) + } + + override fun toString(): String { + return "Pattern($patternWithColorCodes, stripColorCodes = $stripColorCodes)" + } + } + + object Serializer : KSerializer { + val delegateSerializer = kotlinx.serialization.json.JsonElement.serializer() + override val descriptor: SerialDescriptor + get() = SerialDescriptor("StringMatcher", delegateSerializer.descriptor) + + override fun deserialize(decoder: Decoder): StringMatcher { + val delegate = decoder.decodeSerializableValue(delegateSerializer) + val gsonDelegate = delegate.intoGson() + return parse(gsonDelegate) + } + + override fun serialize(encoder: Encoder, value: StringMatcher) { + encoder.encodeSerializableValue(delegateSerializer, serialize(value).intoKotlinJson()) + } + + } + + companion object { + fun serialize(stringMatcher: StringMatcher): JsonElement { + TODO("Cannot serialize string matchers rn") + } + + fun parse(jsonElement: JsonElement): StringMatcher { + if (jsonElement is JsonPrimitive) { + return Equals(jsonElement.asString, true) + } + if (jsonElement is JsonObject) { + val regex = jsonElement["regex"] as JsonPrimitive? + val text = jsonElement["equals"] as JsonPrimitive? + val shouldStripColor = when (val color = (jsonElement["color"] as JsonPrimitive?)?.asString) { + "preserve" -> false + "strip", null -> true + else -> error("Unknown color preservation mode: $color") + } + if ((regex == null) == (text == null)) error("Could not parse $jsonElement as string matcher") + if (regex != null) + return Pattern(regex.asString, shouldStripColor) + if (text != null) + return Equals(text.asString, shouldStripColor) + } + error("Could not parse $jsonElement as a string matcher") + } + } +} + +fun JsonElement.intoKotlinJson(): kotlinx.serialization.json.JsonElement { + when (this) { + is JsonNull -> return kotlinx.serialization.json.JsonNull + is JsonObject -> { + return kotlinx.serialization.json.JsonObject(this.entrySet() + .associate { it.key to it.value.intoKotlinJson() }) + } + + is JsonArray -> { + return kotlinx.serialization.json.JsonArray(this.map { it.intoKotlinJson() }) + } + + is JsonPrimitive -> { + if (this.isString) + return kotlinx.serialization.json.JsonPrimitive(this.asString) + if (this.isBoolean) + return kotlinx.serialization.json.JsonPrimitive(this.asBoolean) + return kotlinx.serialization.json.JsonPrimitive(this.asNumber) + } + + else -> error("Unknown json variant $this") + } +} + +fun kotlinx.serialization.json.JsonElement.intoGson(): JsonElement { + when (this) { + is kotlinx.serialization.json.JsonNull -> return JsonNull.INSTANCE + is kotlinx.serialization.json.JsonPrimitive -> { + if (this.isString) + return JsonPrimitive(this.content) + if (this.content == "true") + return JsonPrimitive(true) + if (this.content == "false") + return JsonPrimitive(false) + return JsonPrimitive(LazilyParsedNumber(this.content)) + } + + is kotlinx.serialization.json.JsonObject -> { + val obj = JsonObject() + for ((k, v) in this) { + obj.add(k, v.intoGson()) + } + return obj + } + + is kotlinx.serialization.json.JsonArray -> { + val arr = JsonArray() + for (v in this) { + arr.add(v.intoGson()) + } + return arr + } + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/TintOverrides.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/TintOverrides.kt new file mode 100644 index 0000000..53df184 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/TintOverrides.kt @@ -0,0 +1,75 @@ +package moe.nea.firmament.features.texturepack + +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import moe.nea.firmament.util.ErrorUtil + +data class TintOverrides( + val layerMap: Map = mapOf() +) { + val hasOverrides by lazy { layerMap.values.any { it !is Reset } } + + companion object { + val EMPTY = TintOverrides() + private val threadLocal = object : ThreadLocal() {} + fun enter(overrides: TintOverrides?) { + ErrorUtil.softCheck("Double entered tintOverrides", + threadLocal.get() == null) + threadLocal.set(overrides ?: EMPTY) + } + + fun exit(overrides: TintOverrides?) { + ErrorUtil.softCheck("Exited with non matching enter tintOverrides", + threadLocal.get() == (overrides ?: EMPTY)) + threadLocal.remove() + } + + fun getCurrentOverrides(): TintOverrides { + return ErrorUtil.notNullOr(threadLocal.get(), "Got current tintOverrides without entering") { + EMPTY + } + } + + fun parse(jsonObject: JsonObject): TintOverrides { + val map = mutableMapOf() + for ((key, value) in jsonObject.entrySet()) { + val layerIndex = + ErrorUtil.notNullOr(key.toIntOrNull(), + "Unknown layer index $value. Should be integer") { continue } + if (value.isJsonNull) { + map[layerIndex] = Reset + continue + } + val override = (value as? JsonPrimitive) + ?.takeIf(JsonPrimitive::isNumber) + ?.asInt + ?.let(TintOverrides::Fixed) + if (override == null) { + ErrorUtil.softError("Invalid tint override for a layer: $value") + continue + } + map[layerIndex] = override + } + return TintOverrides(map) + } + } + + fun mergeWithParent(parent: TintOverrides): TintOverrides { + val mergedMap = parent.layerMap.toMutableMap() + mergedMap.putAll(this.layerMap) + return TintOverrides(mergedMap) + } + + fun hasOverrides(): Boolean = hasOverrides + fun getOverride(tintIndex: Int): Int? { + return when (val tint = layerMap[tintIndex]) { + is Reset -> null + is Fixed -> tint.color + null -> null + } + } + + sealed interface TintOverride + data object Reset : TintOverride + data class Fixed(val color: Int) : TintOverride +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AlwaysPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AlwaysPredicate.kt new file mode 100644 index 0000000..7e0ddb1 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AlwaysPredicate.kt @@ -0,0 +1,19 @@ + +package moe.nea.firmament.features.texturepack.predicates + +import com.google.gson.JsonElement +import moe.nea.firmament.features.texturepack.FirmamentModelPredicate +import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser +import net.minecraft.item.ItemStack + +object AlwaysPredicate : FirmamentModelPredicate { + override fun test(stack: ItemStack): Boolean { + return true + } + + object Parser : FirmamentModelPredicateParser { + override fun parse(jsonElement: JsonElement): FirmamentModelPredicate { + return AlwaysPredicate + } + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AndPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AndPredicate.kt new file mode 100644 index 0000000..70eb814 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AndPredicate.kt @@ -0,0 +1,29 @@ +package moe.nea.firmament.features.texturepack.predicates + +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import net.minecraft.entity.LivingEntity +import moe.nea.firmament.features.texturepack.CustomModelOverrideParser +import moe.nea.firmament.features.texturepack.FirmamentModelPredicate +import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser +import net.minecraft.item.ItemStack + +class AndPredicate(val children: Array) : FirmamentModelPredicate { + override fun test(stack: ItemStack, holder: LivingEntity?): Boolean { + return children.all { it.test(stack, holder) } + } + + object Parser : FirmamentModelPredicateParser { + override fun parse(jsonElement: JsonElement): FirmamentModelPredicate { + val children = + (jsonElement as JsonArray) + .flatMap { + CustomModelOverrideParser.parsePredicates(it as JsonObject) + } + .toTypedArray() + return AndPredicate(children) + } + + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/CastPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/CastPredicate.kt new file mode 100644 index 0000000..321f87c --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/CastPredicate.kt @@ -0,0 +1,25 @@ +package moe.nea.firmament.features.texturepack.predicates + +import com.google.gson.JsonElement +import net.minecraft.entity.LivingEntity +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.item.ItemStack +import moe.nea.firmament.features.texturepack.FirmamentModelPredicate +import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser + +class CastPredicate : FirmamentModelPredicate { + object Parser : FirmamentModelPredicateParser { + override fun parse(jsonElement: JsonElement): FirmamentModelPredicate? { + if (jsonElement.asDouble >= 1) return CastPredicate() + return NotPredicate(arrayOf(CastPredicate())) + } + } + + override fun test(stack: ItemStack, holder: LivingEntity?): Boolean { + return (holder as? PlayerEntity)?.fishHook != null && holder.mainHandStack === stack + } + + override fun test(stack: ItemStack): Boolean { + return false + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/DisplayNamePredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/DisplayNamePredicate.kt new file mode 100644 index 0000000..04c7a2b --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/DisplayNamePredicate.kt @@ -0,0 +1,22 @@ + +package moe.nea.firmament.features.texturepack.predicates + +import com.google.gson.JsonElement +import moe.nea.firmament.features.texturepack.FirmamentModelPredicate +import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser +import moe.nea.firmament.features.texturepack.StringMatcher +import net.minecraft.item.ItemStack +import moe.nea.firmament.util.mc.displayNameAccordingToNbt + +data class DisplayNamePredicate(val stringMatcher: StringMatcher) : FirmamentModelPredicate { + override fun test(stack: ItemStack): Boolean { + val display = stack.displayNameAccordingToNbt + return stringMatcher.matches(display) + } + + object Parser : FirmamentModelPredicateParser { + override fun parse(jsonElement: JsonElement): FirmamentModelPredicate { + return DisplayNamePredicate(StringMatcher.parse(jsonElement)) + } + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ExtraAttributesPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ExtraAttributesPredicate.kt new file mode 100644 index 0000000..8115739 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ExtraAttributesPredicate.kt @@ -0,0 +1,241 @@ + +package moe.nea.firmament.features.texturepack.predicates + +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonPrimitive +import kotlin.jvm.optionals.getOrDefault +import kotlin.jvm.optionals.getOrNull +import moe.nea.firmament.features.texturepack.FirmamentModelPredicate +import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser +import moe.nea.firmament.features.texturepack.StringMatcher +import net.minecraft.item.ItemStack +import net.minecraft.nbt.NbtByte +import net.minecraft.nbt.NbtDouble +import net.minecraft.nbt.NbtElement +import net.minecraft.nbt.NbtFloat +import net.minecraft.nbt.NbtInt +import net.minecraft.nbt.NbtLong +import net.minecraft.nbt.NbtShort +import moe.nea.firmament.util.extraAttributes +import moe.nea.firmament.util.mc.NbtPrism + +fun interface NbtMatcher { + fun matches(nbt: NbtElement): Boolean + + object Parser { + fun parse(jsonElement: JsonElement): NbtMatcher? { + if (jsonElement is JsonPrimitive) { + if (jsonElement.isString) { + val string = jsonElement.asString + return MatchStringExact(string) + } + if (jsonElement.isNumber) { + return MatchNumberExact(jsonElement.asLong) // TODO: parse generic number + } + } + if (jsonElement is JsonObject) { + var encounteredParser: NbtMatcher? = null + for (entry in ExclusiveParserType.entries) { + val data = jsonElement[entry.key] ?: continue + if (encounteredParser != null) { + // TODO: warn + return null + } + encounteredParser = entry.parse(data) ?: return null + } + return encounteredParser + } + return null + } + + enum class ExclusiveParserType(val key: String) { + STRING("string") { + override fun parse(element: JsonElement): NbtMatcher? { + return MatchString(StringMatcher.parse(element)) + } + }, + INT("int") { + override fun parse(element: JsonElement): NbtMatcher? { + return parseGenericNumber( + element, + { it.asInt }, + { (it as? NbtInt)?.intValue() }, + { a, b -> + if (a == b) Comparison.EQUAL + else if (a < b) Comparison.LESS_THAN + else Comparison.GREATER + }) + } + }, + FLOAT("float") { + override fun parse(element: JsonElement): NbtMatcher? { + return parseGenericNumber( + element, + { it.asFloat }, + { (it as? NbtFloat)?.floatValue() }, + { a, b -> + if (a == b) Comparison.EQUAL + else if (a < b) Comparison.LESS_THAN + else Comparison.GREATER + }) + } + }, + DOUBLE("double") { + override fun parse(element: JsonElement): NbtMatcher? { + return parseGenericNumber( + element, + { it.asDouble }, + { (it as? NbtDouble)?.doubleValue() }, + { a, b -> + if (a == b) Comparison.EQUAL + else if (a < b) Comparison.LESS_THAN + else Comparison.GREATER + }) + } + }, + LONG("long") { + override fun parse(element: JsonElement): NbtMatcher? { + return parseGenericNumber( + element, + { it.asLong }, + { (it as? NbtLong)?.longValue() }, + { a, b -> + if (a == b) Comparison.EQUAL + else if (a < b) Comparison.LESS_THAN + else Comparison.GREATER + }) + } + }, + SHORT("short") { + override fun parse(element: JsonElement): NbtMatcher? { + return parseGenericNumber( + element, + { it.asShort }, + { (it as? NbtShort)?.shortValue() }, + { a, b -> + if (a == b) Comparison.EQUAL + else if (a < b) Comparison.LESS_THAN + else Comparison.GREATER + }) + } + }, + BYTE("byte") { + override fun parse(element: JsonElement): NbtMatcher? { + return parseGenericNumber( + element, + { it.asByte }, + { (it as? NbtByte)?.byteValue() }, + { a, b -> + if (a == b) Comparison.EQUAL + else if (a < b) Comparison.LESS_THAN + else Comparison.GREATER + }) + } + }, + ; + + abstract fun parse(element: JsonElement): NbtMatcher? + } + + enum class Comparison { + LESS_THAN, EQUAL, GREATER + } + + inline fun parseGenericNumber( + jsonElement: JsonElement, + primitiveExtractor: (JsonPrimitive) -> T?, + crossinline nbtExtractor: (NbtElement) -> T?, + crossinline compare: (T, T) -> Comparison + ): NbtMatcher? { + if (jsonElement is JsonPrimitive) { + val expected = primitiveExtractor(jsonElement) ?: return null + return NbtMatcher { + val actual = nbtExtractor(it) ?: return@NbtMatcher false + compare(actual, expected) == Comparison.EQUAL + } + } + if (jsonElement is JsonObject) { + val minElement = jsonElement.getAsJsonPrimitive("min") + val min = if (minElement != null) primitiveExtractor(minElement) ?: return null else null + val minExclusive = jsonElement.get("minExclusive")?.asBoolean ?: false + val maxElement = jsonElement.getAsJsonPrimitive("max") + val max = if (maxElement != null) primitiveExtractor(maxElement) ?: return null else null + val maxExclusive = jsonElement.get("maxExclusive")?.asBoolean ?: true + if (min == null && max == null) return null + return NbtMatcher { + val actual = nbtExtractor(it) ?: return@NbtMatcher false + if (max != null) { + val comp = compare(actual, max) + if (comp == Comparison.GREATER) return@NbtMatcher false + if (comp == Comparison.EQUAL && maxExclusive) return@NbtMatcher false + } + if (min != null) { + val comp = compare(actual, min) + if (comp == Comparison.LESS_THAN) return@NbtMatcher false + if (comp == Comparison.EQUAL && minExclusive) return@NbtMatcher false + } + return@NbtMatcher true + } + } + return null + + } + } + + class MatchNumberExact(val number: Long) : NbtMatcher { + override fun matches(nbt: NbtElement): Boolean { + return when (nbt) { + is NbtByte -> nbt.byteValue().toLong() == number + is NbtInt -> nbt.intValue().toLong() == number + is NbtShort -> nbt.shortValue().toLong() == number + is NbtLong -> nbt.longValue().toLong() == number + else -> false + } + } + + } + + class MatchStringExact(val string: String) : NbtMatcher { + override fun matches(nbt: NbtElement): Boolean { + return nbt.asString().getOrNull() == string + } + + override fun toString(): String { + return "MatchNbtStringExactly($string)" + } + } + + class MatchString(val string: StringMatcher) : NbtMatcher { + override fun matches(nbt: NbtElement): Boolean { + return nbt.asString().map(string::matches).getOrDefault(false) + } + + override fun toString(): String { + return "MatchNbtString($string)" + } + } +} + +data class ExtraAttributesPredicate( + val path: NbtPrism, + val matcher: NbtMatcher, +) : FirmamentModelPredicate { + + object Parser : FirmamentModelPredicateParser { + override fun parse(jsonElement: JsonElement): FirmamentModelPredicate? { + if (jsonElement !is JsonObject) return null + val path = jsonElement.get("path") ?: return null + val prism = NbtPrism.fromElement(path) ?: return null + val matcher = NbtMatcher.Parser.parse(jsonElement.get("match") ?: jsonElement) + ?: return null + return ExtraAttributesPredicate(prism, matcher) + } + } + + override fun test(stack: ItemStack): Boolean { + return path.access(stack.extraAttributes) + .any { matcher.matches(it) } + } +} + diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/GenericComponentPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/GenericComponentPredicate.kt new file mode 100644 index 0000000..71392ef --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/GenericComponentPredicate.kt @@ -0,0 +1,58 @@ +package moe.nea.firmament.features.texturepack.predicates + +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.mojang.serialization.Codec +import kotlin.jvm.optionals.getOrNull +import net.minecraft.component.ComponentType +import net.minecraft.component.type.NbtComponent +import net.minecraft.entity.LivingEntity +import net.minecraft.item.ItemStack +import net.minecraft.nbt.NbtOps +import net.minecraft.registry.RegistryKey +import net.minecraft.registry.RegistryKeys +import net.minecraft.util.Identifier +import moe.nea.firmament.features.texturepack.FirmamentModelPredicate +import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.mc.NbtPrism + +data class GenericComponentPredicate( + val componentType: ComponentType, + val codec: Codec, + val path: NbtPrism, + val matcher: NbtMatcher, +) : FirmamentModelPredicate { + constructor(componentType: ComponentType, path: NbtPrism, matcher: NbtMatcher) + : this(componentType, componentType.codecOrThrow, path, matcher) + + override fun test(stack: ItemStack, holder: LivingEntity?): Boolean { + val component = stack.get(componentType) ?: return false + // TODO: cache this + val nbt = + if (component is NbtComponent) component.nbt + else codec.encodeStart(NbtOps.INSTANCE, component) + .resultOrPartial().getOrNull() ?: return false + return path.access(nbt).any { matcher.matches(it) } + } + + object Parser : FirmamentModelPredicateParser { + override fun parse(jsonElement: JsonElement): GenericComponentPredicate<*>? { + if (jsonElement !is JsonObject) return null + val path = jsonElement.get("path") ?: return null + val prism = NbtPrism.fromElement(path) ?: return null + val matcher = NbtMatcher.Parser.parse(jsonElement.get("match") ?: jsonElement) + ?: return null + val component = MC.currentOrDefaultRegistries + .getOrThrow(RegistryKeys.DATA_COMPONENT_TYPE) + .getOrThrow( + RegistryKey.of( + RegistryKeys.DATA_COMPONENT_TYPE, + Identifier.of(jsonElement.get("component").asString) + ) + ).value() + return GenericComponentPredicate(component, prism, matcher) + } + } + +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ItemPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ItemPredicate.kt new file mode 100644 index 0000000..4833dc0 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ItemPredicate.kt @@ -0,0 +1,34 @@ + +package moe.nea.firmament.features.texturepack.predicates + +import com.google.gson.JsonElement +import com.google.gson.JsonPrimitive +import moe.nea.firmament.features.texturepack.FirmamentModelPredicate +import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser +import kotlin.jvm.optionals.getOrNull +import net.minecraft.item.Item +import net.minecraft.item.ItemStack +import net.minecraft.registry.RegistryKey +import net.minecraft.registry.RegistryKeys +import net.minecraft.util.Identifier +import moe.nea.firmament.util.MC + +class ItemPredicate( + val item: Item +) : FirmamentModelPredicate { + override fun test(stack: ItemStack): Boolean { + return stack.isOf(item) + } + + object Parser : FirmamentModelPredicateParser { + override fun parse(jsonElement: JsonElement): ItemPredicate? { + if (jsonElement is JsonPrimitive && jsonElement.isString) { + val itemKey = RegistryKey.of(RegistryKeys.ITEM, + Identifier.tryParse(jsonElement.asString) + ?: return null) + return ItemPredicate(MC.defaultItems.getOptional(itemKey).getOrNull()?.value() ?: return null) + } + return null + } + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/LorePredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/LorePredicate.kt new file mode 100644 index 0000000..f0b4737 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/LorePredicate.kt @@ -0,0 +1,22 @@ + +package moe.nea.firmament.features.texturepack.predicates + +import com.google.gson.JsonElement +import moe.nea.firmament.features.texturepack.FirmamentModelPredicate +import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser +import moe.nea.firmament.features.texturepack.StringMatcher +import net.minecraft.item.ItemStack +import moe.nea.firmament.util.mc.loreAccordingToNbt + +class LorePredicate(val matcher: StringMatcher) : FirmamentModelPredicate { + object Parser : FirmamentModelPredicateParser { + override fun parse(jsonElement: JsonElement): FirmamentModelPredicate { + return LorePredicate(StringMatcher.parse(jsonElement)) + } + } + + override fun test(stack: ItemStack): Boolean { + val lore = stack.loreAccordingToNbt + return lore.any { matcher.matches(it) } + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/NotPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/NotPredicate.kt new file mode 100644 index 0000000..4986ad9 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/NotPredicate.kt @@ -0,0 +1,21 @@ + +package moe.nea.firmament.features.texturepack.predicates + +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import moe.nea.firmament.features.texturepack.CustomModelOverrideParser +import moe.nea.firmament.features.texturepack.FirmamentModelPredicate +import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser +import net.minecraft.item.ItemStack + +class NotPredicate(val children: Array) : FirmamentModelPredicate { + override fun test(stack: ItemStack): Boolean { + return children.none { it.test(stack) } + } + + object Parser : FirmamentModelPredicateParser { + override fun parse(jsonElement: JsonElement): FirmamentModelPredicate { + return NotPredicate(CustomModelOverrideParser.parsePredicates(jsonElement as JsonObject).toTypedArray()) + } + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/NumberMatcher.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/NumberMatcher.kt new file mode 100644 index 0000000..b0d5178 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/NumberMatcher.kt @@ -0,0 +1,124 @@ +package moe.nea.firmament.features.texturepack.predicates + +import com.google.gson.JsonElement +import com.google.gson.JsonPrimitive +import moe.nea.firmament.util.useMatch + +abstract class NumberMatcher { + abstract fun test(number: Number): Boolean + + + companion object { + fun parse(jsonElement: JsonElement): NumberMatcher? { + if (jsonElement is JsonPrimitive) { + if (jsonElement.isString) { + val string = jsonElement.asString + return parseRange(string) ?: parseOperator(string) + } + if (jsonElement.isNumber) { + val number = jsonElement.asNumber + val hasDecimals = (number.toString().contains(".")) + return MatchNumberExact(if (hasDecimals) number.toLong() else number.toDouble()) + } + } + return null + } + + private val intervalSpec = + "(?[\\[\\(])(?[0-9.]+)?,(?[0-9.]+)?(?[\\]\\)])" + .toPattern() + + fun parseRange(string: String): RangeMatcher? { + intervalSpec.useMatch(string) { + // Open in the set-theory sense, meaning does not include its end. + val beginningOpen = group("beginningOpen") == "(" + val endingOpen = group("endingOpen") == ")" + val beginning = group("beginning")?.toDouble() + val ending = group("ending")?.toDouble() + return RangeMatcher(beginning, !beginningOpen, ending, !endingOpen) + } + return null + } + + enum class Operator(val operator: String) { + LESS("<") { + override fun matches(comparisonResult: Int): Boolean { + return comparisonResult < 0 + } + }, + LESS_EQUALS("<=") { + override fun matches(comparisonResult: Int): Boolean { + return comparisonResult <= 0 + } + }, + GREATER(">") { + override fun matches(comparisonResult: Int): Boolean { + return comparisonResult > 0 + } + }, + GREATER_EQUALS(">=") { + override fun matches(comparisonResult: Int): Boolean { + return comparisonResult >= 0 + } + }, + ; + + abstract fun matches(comparisonResult: Int): Boolean + } + + private val operatorPattern = + "(?${Operator.entries.joinToString("|") { it.operator }})(?[0-9.]+)".toPattern() + + fun parseOperator(string: String): OperatorMatcher? { + return operatorPattern.useMatch(string) { + val operatorName = group("operator") + val operator = Operator.entries.find { it.operator == operatorName }!! + val value = group("value").toDouble() + OperatorMatcher(operator, value) + } + } + + data class OperatorMatcher(val operator: Operator, val value: Double) : NumberMatcher() { + override fun test(number: Number): Boolean { + return operator.matches(number.toDouble().compareTo(value)) + } + } + + + data class MatchNumberExact(val number: Number) : NumberMatcher() { + override fun test(number: Number): Boolean { + return when (this.number) { + is Double -> number.toDouble() == this.number.toDouble() + else -> number.toLong() == this.number.toLong() + } + } + } + + data class RangeMatcher( + val beginning: Double?, + val beginningInclusive: Boolean, + val ending: Double?, + val endingInclusive: Boolean, + ) : NumberMatcher() { + override fun test(number: Number): Boolean { + val value = number.toDouble() + if (beginning != null) { + if (beginningInclusive) { + if (value < beginning) return false + } else { + if (value <= beginning) return false + } + } + if (ending != null) { + if (endingInclusive) { + if (value > ending) return false + } else { + if (value >= ending) return false + } + } + return true + } + } + } + +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/OrPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/OrPredicate.kt new file mode 100644 index 0000000..e3093cd --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/OrPredicate.kt @@ -0,0 +1,29 @@ + +package moe.nea.firmament.features.texturepack.predicates + +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import moe.nea.firmament.features.texturepack.CustomModelOverrideParser +import moe.nea.firmament.features.texturepack.FirmamentModelPredicate +import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser +import net.minecraft.item.ItemStack + +class OrPredicate(val children: Array) : FirmamentModelPredicate { + override fun test(stack: ItemStack): Boolean { + return children.any { it.test(stack) } + } + + object Parser : FirmamentModelPredicateParser { + override fun parse(jsonElement: JsonElement): FirmamentModelPredicate { + val children = + (jsonElement as JsonArray) + .flatMap { + CustomModelOverrideParser.parsePredicates(it as JsonObject) + } + .toTypedArray() + return OrPredicate(children) + } + + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/PetPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/PetPredicate.kt new file mode 100644 index 0000000..b30b7c9 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/PetPredicate.kt @@ -0,0 +1,66 @@ + +package moe.nea.firmament.features.texturepack.predicates + +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import moe.nea.firmament.features.texturepack.FirmamentModelPredicate +import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser +import moe.nea.firmament.features.texturepack.RarityMatcher +import moe.nea.firmament.features.texturepack.StringMatcher +import net.minecraft.item.ItemStack +import moe.nea.firmament.repo.ExpLadders +import moe.nea.firmament.util.petData + +data class PetPredicate( + val petId: StringMatcher?, + val tier: RarityMatcher?, + val exp: NumberMatcher?, + val candyUsed: NumberMatcher?, + val level: NumberMatcher?, +) : FirmamentModelPredicate { + + override fun test(stack: ItemStack): Boolean { + val petData = stack.petData ?: return false + if (petId != null) { + if (!petId.matches(petData.type)) return false + } + if (exp != null) { + if (!exp.test(petData.exp)) return false + } + if (candyUsed != null) { + if (!candyUsed.test(petData.candyUsed)) return false + } + if (tier != null) { + if (!tier.match(petData.tier)) return false + } + val levelData by lazy(LazyThreadSafetyMode.NONE) { + ExpLadders.getExpLadder(petData.type, petData.tier) + .getPetLevel(petData.exp) + } + if (level != null) { + if (!level.test(levelData.currentLevel)) return false + } + return true + } + + object Parser : FirmamentModelPredicateParser { + override fun parse(jsonElement: JsonElement): FirmamentModelPredicate? { + if (jsonElement.isJsonPrimitive) { + return PetPredicate(StringMatcher.Equals(jsonElement.asString, false), null, null, null, null) + } + if (jsonElement !is JsonObject) return null + val idMatcher = jsonElement["id"]?.let(StringMatcher::parse) + val expMatcher = jsonElement["exp"]?.let(NumberMatcher::parse) + val levelMatcher = jsonElement["level"]?.let(NumberMatcher::parse) + val candyMatcher = jsonElement["candyUsed"]?.let(NumberMatcher::parse) + val tierMatcher = jsonElement["tier"]?.let(RarityMatcher::parse) + return PetPredicate( + idMatcher, + tierMatcher, + expMatcher, + candyMatcher, + levelMatcher, + ) + } + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/PullingPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/PullingPredicate.kt new file mode 100644 index 0000000..fa46a70 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/PullingPredicate.kt @@ -0,0 +1,26 @@ +package moe.nea.firmament.features.texturepack.predicates + +import com.google.gson.JsonElement +import net.minecraft.entity.LivingEntity +import net.minecraft.item.BowItem +import net.minecraft.item.ItemStack +import moe.nea.firmament.features.texturepack.FirmamentModelPredicate +import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser + +class PullingPredicate(val percentage: Double) : FirmamentModelPredicate { + companion object { + val AnyPulling = PullingPredicate(0.1) + } + + object Parser : FirmamentModelPredicateParser { + override fun parse(jsonElement: JsonElement): FirmamentModelPredicate? { + return PullingPredicate(jsonElement.asDouble) + } + } + + override fun test(stack: ItemStack, holder: LivingEntity?): Boolean { + if (holder == null) return false + return BowItem.getPullProgress(holder.itemUseTime) >= percentage + } + +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/SkullPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/SkullPredicate.kt new file mode 100644 index 0000000..416e86c --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/SkullPredicate.kt @@ -0,0 +1,63 @@ +package moe.nea.firmament.features.texturepack.predicates + +import com.google.gson.JsonElement +import com.mojang.authlib.minecraft.MinecraftProfileTexture +import java.util.UUID +import kotlin.jvm.optionals.getOrNull +import net.minecraft.component.DataComponentTypes +import net.minecraft.entity.LivingEntity +import net.minecraft.item.ItemStack +import net.minecraft.item.Items +import moe.nea.firmament.features.texturepack.FirmamentModelPredicate +import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser +import moe.nea.firmament.features.texturepack.StringMatcher +import moe.nea.firmament.util.mc.decodeProfileTextureProperty +import moe.nea.firmament.util.parsePotentiallyDashlessUUID + +class SkullPredicate( + val profileId: UUID?, + val textureProfileId: UUID?, + val skinUrl: StringMatcher?, + val textureValue: StringMatcher?, +) : FirmamentModelPredicate { + object Parser : FirmamentModelPredicateParser { + override fun parse(jsonElement: JsonElement): FirmamentModelPredicate? { + val obj = jsonElement.asJsonObject + val profileId = obj.getAsJsonPrimitive("profileId") + ?.asString?.let(::parsePotentiallyDashlessUUID) + val textureProfileId = obj.getAsJsonPrimitive("textureProfileId") + ?.asString?.let(::parsePotentiallyDashlessUUID) + val textureValue = obj.get("textureValue")?.let(StringMatcher::parse) + val skinUrl = obj.get("skinUrl")?.let(StringMatcher::parse) + return SkullPredicate(profileId, textureProfileId, skinUrl, textureValue) + } + } + + override fun test(stack: ItemStack, holder: LivingEntity?): Boolean { + if (!stack.isOf(Items.PLAYER_HEAD)) return false + val profile = stack.get(DataComponentTypes.PROFILE) ?: return false + val textureProperty = profile.properties["textures"].firstOrNull() + val textureMode = lazy(LazyThreadSafetyMode.NONE) { + decodeProfileTextureProperty(textureProperty ?: return@lazy null) + } + when { + profileId != null + && profileId != profile.id.getOrNull() -> + return false + + textureValue != null + && !textureValue.matches(textureProperty?.value ?: "") -> + return false + + skinUrl != null + && !skinUrl.matches(textureMode.value?.textures?.get(MinecraftProfileTexture.Type.SKIN)?.url ?: "") -> + return false + + textureProfileId != null + && textureProfileId != textureMode.value?.profileId -> + return false + + else -> return true + } + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ApplyHeadModelInItemRenderer.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ApplyHeadModelInItemRenderer.java new file mode 100644 index 0000000..4665829 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ApplyHeadModelInItemRenderer.java @@ -0,0 +1,23 @@ + +package moe.nea.firmament.mixins.custommodels; + +import net.minecraft.client.render.entity.LivingEntityRenderer; +import net.minecraft.client.render.entity.model.EntityModel; +import net.minecraft.client.render.entity.state.LivingEntityRenderState; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.decoration.DisplayEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(LivingEntityRenderer.class) +public class ApplyHeadModelInItemRenderer> { + // TODO: replace head_model with a condition model (if possible, automatically) + // TODO: ItemAsset.CODEC should upgrade partials + @Inject(method = "updateRenderState(Lnet/minecraft/entity/LivingEntity;Lnet/minecraft/client/render/entity/state/LivingEntityRenderState;F)V", + at = @At("TAIL")) + private void updateHeadState(T livingEntity, S livingEntityRenderState, float f, CallbackInfo ci) { + + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/BuildExtraBlockStateModels.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/BuildExtraBlockStateModels.java new file mode 100644 index 0000000..6b3c929 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/BuildExtraBlockStateModels.java @@ -0,0 +1,24 @@ +package moe.nea.firmament.mixins.custommodels; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import com.llamalad7.mixinextras.sugar.Local; +import moe.nea.firmament.features.texturepack.CustomBlockTextures; +import net.minecraft.client.render.model.Baker; +import net.minecraft.client.render.model.ModelBaker; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +@Mixin(ModelBaker.class) +public class BuildExtraBlockStateModels { + @ModifyReturnValue(method = "bake", at = @At("RETURN")) + private CompletableFuture injectMoreBlockModels(CompletableFuture original, @Local ModelBaker.BakerImpl baker, @Local(argsOnly = true) Executor executor) { + Baker b = baker; + return original.thenCombine( + CustomBlockTextures.createBakedModels(b, executor), + (a, _void) -> a + ); + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/CustomSkullTexturePatch.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/CustomSkullTexturePatch.java new file mode 100644 index 0000000..fede766 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/CustomSkullTexturePatch.java @@ -0,0 +1,26 @@ + + +package moe.nea.firmament.mixins.custommodels; + +import moe.nea.firmament.features.texturepack.CustomSkyBlockTextures; +import net.minecraft.block.SkullBlock; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.block.entity.SkullBlockEntityRenderer; +import net.minecraft.component.type.ProfileComponent; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(SkullBlockEntityRenderer.class) +public class CustomSkullTexturePatch { + @Inject( + method = "getRenderLayer(Lnet/minecraft/block/SkullBlock$SkullType;Lnet/minecraft/component/type/ProfileComponent;Lnet/minecraft/util/Identifier;)Lnet/minecraft/client/render/RenderLayer;", + at = @At("HEAD"), + cancellable = true + ) + private static void onGetRenderLayer(SkullBlock.SkullType type, ProfileComponent profile, Identifier texture, CallbackInfoReturnable cir) { + CustomSkyBlockTextures.INSTANCE.modifySkullTexture(type, profile, cir); + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/InsertExtraBlockModelDependencies.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/InsertExtraBlockModelDependencies.java new file mode 100644 index 0000000..91779e7 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/InsertExtraBlockModelDependencies.java @@ -0,0 +1,28 @@ +package moe.nea.firmament.mixins.custommodels; + +import com.llamalad7.mixinextras.sugar.Local; +import moe.nea.firmament.features.texturepack.CustomBlockTextures; +import net.minecraft.client.item.ItemAssetsLoader; +import net.minecraft.client.render.model.BakedModelManager; +import net.minecraft.client.render.model.BlockStatesLoader; +import net.minecraft.client.render.model.ReferencedModelsCollector; +import net.minecraft.client.render.model.UnbakedModel; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.Map; + +@Mixin(BakedModelManager.class) +public class InsertExtraBlockModelDependencies { + @Inject(method = "collect", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/model/ReferencedModelsCollector;addSpecialModel(Lnet/minecraft/util/Identifier;Lnet/minecraft/client/render/model/UnbakedModel;)V", shift = At.Shift.AFTER)) + private static void insertExtraModels( + Map modelMap, + BlockStatesLoader.LoadedModels stateDefinition, + ItemAssetsLoader.Result result, + CallbackInfoReturnable cir, @Local ReferencedModelsCollector modelsCollector) { + CustomBlockTextures.collectExtraModels(modelsCollector); + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ItemRenderStateExtraInfo.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ItemRenderStateExtraInfo.java new file mode 100644 index 0000000..2872dd1 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ItemRenderStateExtraInfo.java @@ -0,0 +1,28 @@ +package moe.nea.firmament.mixins.custommodels; + +import moe.nea.firmament.features.texturepack.HeadModelChooser; +import net.minecraft.client.render.item.ItemRenderState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ItemRenderState.class) +public class ItemRenderStateExtraInfo implements HeadModelChooser.HasExplicitHeadModelMarker { + boolean hasExplicitHead_firmament = false; + + @Inject(method = "clear", at = @At("HEAD")) + private void clear(CallbackInfo ci) { + hasExplicitHead_firmament = false; + } + + @Override + public void markExplicitHead_Firmament() { + hasExplicitHead_firmament = true; + } + + @Override + public boolean isExplicitHeadModel_Firmament() { + return hasExplicitHead_firmament; + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/LoadExtraBlockStates.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/LoadExtraBlockStates.java new file mode 100644 index 0000000..c33fd04 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/LoadExtraBlockStates.java @@ -0,0 +1,34 @@ +package moe.nea.firmament.mixins.custommodels; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.sugar.Local; +import moe.nea.firmament.features.texturepack.CustomBlockTextures; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.client.render.model.BlockStatesLoader; +import net.minecraft.resource.Resource; +import net.minecraft.state.StateManager; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.Function; + +@Mixin(BlockStatesLoader.class) +public class LoadExtraBlockStates { + @ModifyExpressionValue(method = "load", at = @At(value = "INVOKE", target = "Ljava/util/concurrent/CompletableFuture;supplyAsync(Ljava/util/function/Supplier;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;")) + private static CompletableFuture>> loadExtraModels( + CompletableFuture>> x, + @Local(argsOnly = true) Executor executor, + @Local Function> stateManagers + ) { + return x.thenCombineAsync(CustomBlockTextures.getPreparationFuture(), (original, extra) -> { + CustomBlockTextures.collectExtraBlockStateMaps(extra, original, stateManagers); + return original; + }, executor); + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchArmorTexture.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchArmorTexture.java new file mode 100644 index 0000000..669da63 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchArmorTexture.java @@ -0,0 +1,30 @@ + +package moe.nea.firmament.mixins.custommodels; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import moe.nea.firmament.features.texturepack.CustomGlobalArmorOverrides; +import net.minecraft.client.render.entity.feature.ArmorFeatureRenderer; +import net.minecraft.component.ComponentType; +import net.minecraft.component.type.EquippableComponent; +import net.minecraft.entity.EquipmentSlot; +import net.minecraft.item.ItemStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(ArmorFeatureRenderer.class) +public class PatchArmorTexture { + @ModifyExpressionValue( + method = "renderArmor", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/item/ItemStack;get(Lnet/minecraft/component/ComponentType;)Ljava/lang/Object;")) + private Object overrideLayers( + Object original, @Local(argsOnly = true) ItemStack itemStack, @Local(argsOnly = true) EquipmentSlot slot + ) { + var overrides = CustomGlobalArmorOverrides.overrideArmor(itemStack, slot); + return overrides.orElse((EquippableComponent) original); + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyArmorLayerSupport.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyArmorLayerSupport.java new file mode 100644 index 0000000..951e3be --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyArmorLayerSupport.java @@ -0,0 +1,22 @@ +package moe.nea.firmament.mixins.custommodels; + +import moe.nea.firmament.features.texturepack.CustomGlobalArmorOverrides; +import net.minecraft.client.render.entity.equipment.EquipmentModel; +import net.minecraft.client.render.entity.equipment.EquipmentModelLoader; +import net.minecraft.item.equipment.EquipmentAsset; +import net.minecraft.registry.RegistryKey; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +// TODO: auto import legacy models, maybe!!! in a later patch tho +@Mixin(EquipmentModelLoader.class) +public class PatchLegacyArmorLayerSupport { + @Inject(method = "get", at = @At(value = "HEAD"), cancellable = true) + private void patchModelLayers(RegistryKey assetKey, CallbackInfoReturnable cir) { + var modelOverride = CustomGlobalArmorOverrides.overrideArmorLayer(assetKey.getValue()); + if (modelOverride != null) + cir.setReturnValue(modelOverride); + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyTexturePathsIntoArmorLayers.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyTexturePathsIntoArmorLayers.java new file mode 100644 index 0000000..0fb6bf8 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyTexturePathsIntoArmorLayers.java @@ -0,0 +1,37 @@ +package moe.nea.firmament.mixins.custommodels; + + +import moe.nea.firmament.features.texturepack.CustomSkyBlockTextures; +import moe.nea.firmament.util.MC; +import net.minecraft.client.render.entity.equipment.EquipmentModel; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(EquipmentModel.Layer.class) +public class PatchLegacyTexturePathsIntoArmorLayers { + @Shadow + @Final + private Identifier textureId; + + @Inject(method = "getFullTextureId", at = @At("HEAD"), cancellable = true) + private void replaceWith1201TextureIfExists(EquipmentModel.LayerType layerType, CallbackInfoReturnable cir) { + if (!CustomSkyBlockTextures.TConfig.INSTANCE.getEnableLegacyMinecraftCompat()) + return; + var resourceManager = MC.INSTANCE.getResourceManager(); + // legacy format: "assets/{identifier.namespace}/textures/models/armor/{identifier.path}_layer_{isLegs ? 2 : 1}{suffix}.png" + // suffix is sadly not available to us here. this means leather armor will look a bit shite + var legacyIdentifier = this.textureId.withPath((textureName) -> { + return "textures/models/armor/" + textureName + "_layer_" + + (layerType == EquipmentModel.LayerType.HUMANOID_LEGGINGS ? 2 : 1) + + ".png"; + }); + if (resourceManager.getResource(legacyIdentifier).isPresent()) { + cir.setReturnValue(legacyIdentifier); + } + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockBreakSoundPatch.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockBreakSoundPatch.java new file mode 100644 index 0000000..9401889 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockBreakSoundPatch.java @@ -0,0 +1,27 @@ +package moe.nea.firmament.mixins.custommodels; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import moe.nea.firmament.features.texturepack.CustomBlockTextures; +import net.minecraft.block.BlockState; +import net.minecraft.client.render.WorldRenderer; +import net.minecraft.sound.BlockSoundGroup; +import net.minecraft.sound.SoundEvent; +import net.minecraft.util.math.BlockPos; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(WorldRenderer.class) +public class ReplaceBlockBreakSoundPatch { +// Sadly hypixel does not send a world event here and instead plays the sound on the server directly +// @WrapOperation(method = "processWorldEvent", at = @At(value = "INVOKE", target = "Lnet/minecraft/sound/BlockSoundGroup;getBreakSound()Lnet/minecraft/sound/SoundEvent;")) +// private SoundEvent replaceBreakSoundEvent(BlockSoundGroup instance, Operation original, +// @Local(argsOnly = true) BlockPos pos, @Local BlockState blockState) { +// var replacement = CustomBlockTextures.getReplacement(blockState, pos); +// if (replacement != null && replacement.getSound() != null) { +// return SoundEvent.of(replacement.getSound()); +// } +// return original.call(instance); +// } +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockHitSoundPatch.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockHitSoundPatch.java new file mode 100644 index 0000000..95e7dce --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockHitSoundPatch.java @@ -0,0 +1,31 @@ +package moe.nea.firmament.mixins.custommodels; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import moe.nea.firmament.features.texturepack.CustomBlockTextures; +import net.minecraft.block.BlockState; +import net.minecraft.client.network.ClientPlayerInteractionManager; +import net.minecraft.client.sound.PositionedSoundInstance; +import net.minecraft.sound.SoundCategory; +import net.minecraft.sound.SoundEvent; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.random.Random; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(ClientPlayerInteractionManager.class) +public class ReplaceBlockHitSoundPatch { + @WrapOperation(method = "updateBlockBreakingProgress", + at = @At(value = "NEW", target = "(Lnet/minecraft/sound/SoundEvent;Lnet/minecraft/sound/SoundCategory;FFLnet/minecraft/util/math/random/Random;Lnet/minecraft/util/math/BlockPos;)Lnet/minecraft/client/sound/PositionedSoundInstance;")) + private PositionedSoundInstance replaceSound( + SoundEvent sound, SoundCategory category, float volume, float pitch, + Random random, BlockPos pos, Operation original, + @Local BlockState blockState) { + var replacement = CustomBlockTextures.getReplacement(blockState, pos); + if (replacement != null && replacement.getSound() != null) { + sound = SoundEvent.of(replacement.getSound()); + } + return original.call(sound, category, volume, pitch, random, pos); + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockRenderManagerBlockModel.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockRenderManagerBlockModel.java new file mode 100644 index 0000000..8d2ba38 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockRenderManagerBlockModel.java @@ -0,0 +1,37 @@ +package moe.nea.firmament.mixins.custommodels; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import moe.nea.firmament.features.texturepack.CustomBlockTextures; +import net.minecraft.block.BlockState; +import net.minecraft.client.render.block.BlockRenderManager; +import net.minecraft.client.render.chunk.SectionBuilder; +import net.minecraft.client.render.model.BlockStateModel; +import net.minecraft.util.math.BlockPos; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(SectionBuilder.class) +public class ReplaceBlockRenderManagerBlockModel { + @WrapOperation(method = "build", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/block/BlockRenderManager;getModel(Lnet/minecraft/block/BlockState;)Lnet/minecraft/client/render/model/BlockStateModel;")) + private BlockStateModel replaceModelInRenderBlock(BlockRenderManager instance, BlockState state, Operation original, @Local(ordinal = 2) BlockPos pos) { + var replacement = CustomBlockTextures.getReplacementModel(state, pos); + if (replacement != null) return replacement; + CustomBlockTextures.enterFallbackCall(); + var fallback = original.call(instance, state); + CustomBlockTextures.exitFallbackCall(); + return fallback; + } +//TODO: cover renderDamage model +// @WrapOperation(method = "renderDamage", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/block/BlockModels;getModel(Lnet/minecraft/block/BlockState;)Lnet/minecraft/client/render/model/BakedModel;")) +// private BakedModel replaceModelInRenderDamage( +// BlockModels instance, BlockState state, Operation original, @Local(argsOnly = true) BlockPos pos) { +// var replacement = CustomBlockTextures.getReplacementModel(state, pos); +// if (replacement != null) return replacement; +// CustomBlockTextures.enterFallbackCall(); +// var fallback = original.call(instance, state); +// CustomBlockTextures.exitFallbackCall(); +// return fallback; +// } +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceFallbackBlockModel.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceFallbackBlockModel.java new file mode 100644 index 0000000..455fbf1 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceFallbackBlockModel.java @@ -0,0 +1,21 @@ +package moe.nea.firmament.mixins.custommodels; + +import moe.nea.firmament.features.texturepack.CustomBlockTextures; +import net.minecraft.block.BlockState; +import net.minecraft.client.render.block.BlockModels; +import net.minecraft.client.render.model.BlockStateModel; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(BlockModels.class) +public class ReplaceFallbackBlockModel { + // TODO: add check to BlockDustParticle + @Inject(method = "getModel", at = @At("HEAD"), cancellable = true) + private void getModel(BlockState state, CallbackInfoReturnable cir) { + var replacement = CustomBlockTextures.getReplacementModel(state, null); + if (replacement != null) + cir.setReturnValue(replacement); + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceHeadModel.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceHeadModel.java new file mode 100644 index 0000000..f445f02 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceHeadModel.java @@ -0,0 +1,51 @@ +package moe.nea.firmament.mixins.custommodels; + +import moe.nea.firmament.features.texturepack.HeadModelChooser; +import net.minecraft.client.item.ItemModelManager; +import net.minecraft.client.render.entity.LivingEntityRenderer; +import net.minecraft.client.render.entity.model.EntityModel; +import net.minecraft.client.render.entity.state.LivingEntityRenderState; +import net.minecraft.client.render.item.ItemRenderState; +import net.minecraft.entity.EquipmentSlot; +import net.minecraft.entity.LivingEntity; +import net.minecraft.item.ItemDisplayContext; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(LivingEntityRenderer.class) +public class ReplaceHeadModel> { + @Shadow + @Final + protected ItemModelManager itemModelResolver; + + @Unique + private ItemRenderState tempRenderState = new ItemRenderState(); + + @Inject( + method = "updateRenderState(Lnet/minecraft/entity/LivingEntity;Lnet/minecraft/client/render/entity/state/LivingEntityRenderState;F)V", + at = @At("TAIL") + ) + private void replaceHeadModel( + T livingEntity, S livingEntityRenderState, float f, CallbackInfo ci + ) { + var headItemStack = livingEntity.getEquippedStack(EquipmentSlot.HEAD); + + HeadModelChooser.INSTANCE.getIS_CHOOSING_HEAD_MODEL().set(true); + tempRenderState.clear(); + this.itemModelResolver.updateForLivingEntity(tempRenderState, headItemStack, ItemDisplayContext.HEAD, livingEntity); + HeadModelChooser.INSTANCE.getIS_CHOOSING_HEAD_MODEL().set(false); + + if (HeadModelChooser.HasExplicitHeadModelMarker.cast(tempRenderState) + .isExplicitHeadModel_Firmament()) { + livingEntityRenderState.wearingSkullType = null; + var temp = livingEntityRenderState.headItemRenderState; + livingEntityRenderState.headItemRenderState = tempRenderState; + tempRenderState = temp; + } + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceItemModelPatch.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceItemModelPatch.java new file mode 100644 index 0000000..f2a7409 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceItemModelPatch.java @@ -0,0 +1,43 @@ +package moe.nea.firmament.mixins.custommodels; + + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import moe.nea.firmament.events.CustomItemModelEvent; +import moe.nea.firmament.util.mc.IntrospectableItemModelManager; +import net.minecraft.client.item.ItemModelManager; +import net.minecraft.client.render.item.model.ItemModel; +import net.minecraft.client.render.item.model.MissingItemModel; +import net.minecraft.component.ComponentType; +import net.minecraft.item.ItemStack; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.NotNull; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; + +import java.util.function.Function; + +@Mixin(ItemModelManager.class) +public class ReplaceItemModelPatch implements IntrospectableItemModelManager { + @Shadow + @Final + private Function modelGetter; + + @WrapOperation( + method = "update", + at = @At(value = "INVOKE", target = "Lnet/minecraft/item/ItemStack;get(Lnet/minecraft/component/ComponentType;)Ljava/lang/Object;")) + private Object replaceItemModelByIdentifier(ItemStack instance, ComponentType componentType, Operation original) { + var override = CustomItemModelEvent.getModelIdentifier(instance, this); + if (override != null && hasModel_firmament(override)) { + return override; + } + return original.call(instance, componentType); + } + + @Override + public boolean hasModel_firmament(@NotNull Identifier identifier) { + return !(modelGetter.apply(identifier) instanceof MissingItemModel); + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/SupplyFakeModelPatch.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/SupplyFakeModelPatch.java new file mode 100644 index 0000000..75cedf8 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/SupplyFakeModelPatch.java @@ -0,0 +1,97 @@ +package moe.nea.firmament.mixins.custommodels; + +import com.google.gson.JsonObject; +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import com.llamalad7.mixinextras.sugar.Local; +import moe.nea.firmament.Firmament; +import moe.nea.firmament.features.texturepack.CustomSkyBlockTextures; +import moe.nea.firmament.features.texturepack.HeadModelChooser; +import moe.nea.firmament.features.texturepack.PredicateModel; +import moe.nea.firmament.util.ErrorUtil; +import net.minecraft.client.item.ItemAsset; +import net.minecraft.client.item.ItemAssetsLoader; +import net.minecraft.client.render.item.model.BasicItemModel; +import net.minecraft.client.render.item.model.ItemModel; +import net.minecraft.resource.Resource; +import net.minecraft.resource.ResourceManager; +import net.minecraft.resource.ResourcePack; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +import java.io.InputStreamReader; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.stream.Collector; +import java.util.stream.Collectors; + +@Mixin(ItemAssetsLoader.class) +public class SupplyFakeModelPatch { + + @ModifyReturnValue( + method = "load", + at = @At("RETURN") + ) + private static CompletableFuture injectFakeGeneratedModels( + CompletableFuture original, + @Local(argsOnly = true) ResourceManager resourceManager, + @Local(argsOnly = true) Executor executor + ) { + return original.thenCompose(oldModels -> CompletableFuture.supplyAsync(() -> supplyExtraModels(resourceManager, oldModels), executor)); + } + + private static ItemAssetsLoader.Result supplyExtraModels(ResourceManager resourceManager, ItemAssetsLoader.Result oldModels) { + if (!CustomSkyBlockTextures.TConfig.INSTANCE.getEnableLegacyMinecraftCompat()) return oldModels; + Map newModels = new HashMap<>(oldModels.contents()); + var resources = resourceManager.findResources( + "models/item", + id -> (id.getNamespace().equals("firmskyblock") || id.getNamespace().equals("cittofirmgenerated")) + && id.getPath().endsWith(".json")); + for (Map.Entry model : resources.entrySet()) { + var resource = model.getValue(); + var itemModelId = model.getKey().withPath(it -> it.substring("models/item/".length(), it.length() - ".json".length())); + var genericModelId = itemModelId.withPrefixedPath("item/"); + var itemAssetId = itemModelId.withPrefixedPath("items/"); + // TODO: inject tint indexes based on the json data here + ItemModel.Unbaked unbakedModel = new BasicItemModel.Unbaked(genericModelId, List.of()); + // TODO: add a filter using the pack.mcmeta to opt out of this behaviour + try (var is = resource.getInputStream()) { + var jsonObject = Firmament.INSTANCE.getGson().fromJson(new InputStreamReader(is), JsonObject.class); + unbakedModel = PredicateModel.Unbaked.fromLegacyJson(jsonObject, unbakedModel); + unbakedModel = HeadModelChooser.Unbaked.fromLegacyJson(jsonObject, unbakedModel); + } catch (Exception e) { + ErrorUtil.INSTANCE.softError("Could not create resource for fake model supplication: " + model.getKey(), e); + } + if (resourceManager.getResource(itemAssetId.withSuffixedPath(".json")) + .map(Resource::getPack) + .map(it -> isResourcePackNewer(resourceManager, it, resource.getPack())) + .orElse(true)) { + newModels.put(itemModelId, new ItemAsset( + unbakedModel, + new ItemAsset.Properties(true) + )); + } + } + return new ItemAssetsLoader.Result(newModels); + } + + private static boolean isResourcePackNewer( + ResourceManager manager, + ResourcePack null_, ResourcePack proposal) { + var pack = manager.streamResourcePacks() + .filter(it -> it == null_ || it == proposal) + .collect(findLast()); + return pack.orElse(null_) != null_; + } + + private static Collector> findLast() { + return Collectors.reducing(Optional.empty(), Optional::of, + (left, right) -> right.isPresent() ? right : left); + + } + +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ExpandScreenBoundaries.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ExpandScreenBoundaries.java new file mode 100644 index 0000000..e2cae45 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ExpandScreenBoundaries.java @@ -0,0 +1,21 @@ +package moe.nea.firmament.mixins.custommodels.screenlayouts; + +import moe.nea.firmament.features.texturepack.CustomScreenLayouts; +import net.minecraft.client.gui.screen.ingame.HandledScreen; +import net.minecraft.client.gui.screen.ingame.RecipeBookScreen; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin({HandledScreen.class, RecipeBookScreen.class}) +public class ExpandScreenBoundaries { + @Inject(method = "isClickOutsideBounds", at = @At("HEAD"), cancellable = true) + private void onClickOutsideBounds(double mouseX, double mouseY, int left, int top, int button, CallbackInfoReturnable cir) { + var background = CustomScreenLayouts.getMover(CustomScreenLayouts.CustomScreenLayout::getBackground); + if (background == null) return; + var x = background.getX() + left; + var y = background.getY() + top; + cir.setReturnValue(mouseX < (double) x || mouseY < (double) y || mouseX >= (double) (x + background.getWidth()) || mouseY >= (double) (y + background.getHeight())); + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceAnvilScreen.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceAnvilScreen.java new file mode 100644 index 0000000..7c5dc45 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceAnvilScreen.java @@ -0,0 +1,55 @@ +package moe.nea.firmament.mixins.custommodels.screenlayouts; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import moe.nea.firmament.features.texturepack.CustomScreenLayouts; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.ingame.AnvilScreen; +import net.minecraft.client.gui.screen.ingame.ForgingScreen; +import net.minecraft.client.gui.widget.TextFieldWidget; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.screen.AnvilScreenHandler; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(AnvilScreen.class) +public abstract class ReplaceAnvilScreen extends ForgingScreen { + @Shadow + private TextFieldWidget nameField; + + public ReplaceAnvilScreen(AnvilScreenHandler handler, PlayerInventory playerInventory, Text title, Identifier texture) { + super(handler, playerInventory, title, texture); + } + + @Inject(method = "setup", at = @At("TAIL")) + private void moveNameField(CallbackInfo ci) { + var override = CustomScreenLayouts.getMover(CustomScreenLayouts.CustomScreenLayout::getNameField); + if (override == null) return; + int baseX = (this.width - this.backgroundWidth) / 2; + int baseY = (this.height - this.backgroundHeight) / 2; + nameField.setX(baseX + override.getX()); + nameField.setY(baseY + override.getY()); + if (override.getWidth() != null) + nameField.setWidth(override.getWidth()); + if (override.getHeight() != null) + nameField.setHeight(override.getHeight()); + } + + @WrapOperation(method = "drawForeground", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawTextWithShadow(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/text/Text;III)I"), + allow = 1) + private int onDrawRepairCost(DrawContext instance, TextRenderer textRenderer, Text text, int x, int y, int color, Operation original) { + var textOverride = CustomScreenLayouts.getTextMover(CustomScreenLayouts.CustomScreenLayout::getRepairCostTitle); + return original.call(instance, textRenderer, + textOverride.replaceText(text), + textOverride.replaceX(textRenderer, text, x), + textOverride.replaceY(y), + textOverride.replaceColor(text, color)); + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceForgingScreen.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceForgingScreen.java new file mode 100644 index 0000000..6e9023d --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceForgingScreen.java @@ -0,0 +1,9 @@ +package moe.nea.firmament.mixins.custommodels.screenlayouts; + +import net.minecraft.client.gui.screen.ingame.ForgingScreen; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.Inject; + +@Mixin(ForgingScreen.class) +public class ReplaceForgingScreen { +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceFurnaceBackgrounds.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceFurnaceBackgrounds.java new file mode 100644 index 0000000..6b076db --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceFurnaceBackgrounds.java @@ -0,0 +1,31 @@ +package moe.nea.firmament.mixins.custommodels.screenlayouts; + +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import moe.nea.firmament.features.texturepack.CustomScreenLayouts; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.ingame.AbstractFurnaceScreen; +import net.minecraft.client.gui.screen.ingame.RecipeBookScreen; +import net.minecraft.client.gui.screen.recipebook.RecipeBookWidget; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.screen.AbstractFurnaceScreenHandler; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import java.util.function.Function; + +@Mixin(AbstractFurnaceScreen.class) +public abstract class ReplaceFurnaceBackgrounds extends RecipeBookScreen { + public ReplaceFurnaceBackgrounds(T handler, RecipeBookWidget recipeBook, PlayerInventory inventory, Text title) { + super(handler, recipeBook, inventory, title); + } + + @WrapWithCondition(method = "drawBackground", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawTexture(Ljava/util/function/Function;Lnet/minecraft/util/Identifier;IIFFIIII)V"), allow = 1) + private boolean onDrawBackground(DrawContext instance, Function renderLayers, Identifier sprite, int x, int y, float u, float v, int width, int height, int textureWidth, int textureHeight) { + final var override = CustomScreenLayouts.getActiveScreenOverride(); + if (override == null || override.getBackground() == null) return true; + override.getBackground().renderGeneric(instance, this); + return false; + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceGenericBackgrounds.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceGenericBackgrounds.java new file mode 100644 index 0000000..bd12177 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceGenericBackgrounds.java @@ -0,0 +1,28 @@ +package moe.nea.firmament.mixins.custommodels.screenlayouts; + +import moe.nea.firmament.features.texturepack.CustomScreenLayouts; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.ingame.*; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.text.Text; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin({CraftingScreen.class, CrafterScreen.class, Generic3x3ContainerScreen.class, GenericContainerScreen.class, HopperScreen.class, ShulkerBoxScreen.class,}) +public abstract class ReplaceGenericBackgrounds extends HandledScreen { + // TODO: split out screens with special background components like flames, arrows, etc. (maybe arrows deserve generic handling tho) + public ReplaceGenericBackgrounds(ScreenHandler handler, PlayerInventory inventory, Text title) { + super(handler, inventory, title); + } + + @Inject(method = "drawBackground", at = @At("HEAD"), cancellable = true) + private void replaceDrawBackground(DrawContext context, float deltaTicks, int mouseX, int mouseY, CallbackInfo ci) { + final var override = CustomScreenLayouts.getActiveScreenOverride(); + if (override == null || override.getBackground() == null) return; + override.getBackground().renderGeneric(context, this); + ci.cancel(); + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplacePlayerBackgrounds.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplacePlayerBackgrounds.java new file mode 100644 index 0000000..e02a821 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplacePlayerBackgrounds.java @@ -0,0 +1,50 @@ +package moe.nea.firmament.mixins.custommodels.screenlayouts; + +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import moe.nea.firmament.features.texturepack.CustomScreenLayouts; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.ingame.InventoryScreen; +import net.minecraft.client.gui.screen.ingame.RecipeBookScreen; +import net.minecraft.client.gui.screen.recipebook.RecipeBookWidget; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.screen.PlayerScreenHandler; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +import java.util.function.Function; + +@Mixin(InventoryScreen.class) +public abstract class ReplacePlayerBackgrounds extends RecipeBookScreen { + public ReplacePlayerBackgrounds(PlayerScreenHandler handler, RecipeBookWidget recipeBook, PlayerInventory inventory, Text title) { + super(handler, recipeBook, inventory, title); + } + + + @WrapOperation(method = "drawForeground", + allow = 1, + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawText(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/text/Text;IIIZ)I")) + private int onDrawForegroundText(DrawContext instance, TextRenderer textRenderer, Text text, int x, int y, int color, boolean shadow, Operation original) { + var textOverride = CustomScreenLayouts.getTextMover(CustomScreenLayouts.CustomScreenLayout::getContainerTitle); + return original.call(instance, textRenderer, + textOverride.replaceText(text), + textOverride.replaceX(textRenderer, text, x), + textOverride.replaceY(y), + textOverride.replaceColor(text, color), + shadow); + } + + @WrapWithCondition(method = "drawBackground", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawTexture(Ljava/util/function/Function;Lnet/minecraft/util/Identifier;IIFFIIII)V")) + private boolean onDrawBackground(DrawContext instance, Function renderLayers, Identifier sprite, int x, int y, float u, float v, int width, int height, int textureWidth, int textureHeight) { + final var override = CustomScreenLayouts.getActiveScreenOverride(); + if (override == null || override.getBackground() == null) return true; + override.getBackground().renderGeneric(instance, this); + return false; + } + // TODO: allow moving the player +} diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceTextColorInHandledScreen.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceTextColorInHandledScreen.java new file mode 100644 index 0000000..4f0905a --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceTextColorInHandledScreen.java @@ -0,0 +1,65 @@ +package moe.nea.firmament.mixins.custommodels.screenlayouts; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import moe.nea.firmament.features.texturepack.CustomScreenLayouts; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.ingame.AnvilScreen; +import net.minecraft.client.gui.screen.ingame.BeaconScreen; +import net.minecraft.client.gui.screen.ingame.CreativeInventoryScreen; +import net.minecraft.client.gui.screen.ingame.HandledScreen; +import net.minecraft.client.gui.screen.ingame.InventoryScreen; +import net.minecraft.client.gui.screen.ingame.MerchantScreen; +import net.minecraft.text.Text; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Slice; + +@Mixin(HandledScreen.class) +// TODO: MerchantScreen.class, BeaconScreen.class +public class ReplaceTextColorInHandledScreen { + + @WrapOperation( + method = "drawForeground", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/gui/DrawContext;drawText(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/text/Text;IIIZ)I"), + slice = @Slice( + from = @At(value = "FIELD", target = "Lnet/minecraft/client/gui/screen/ingame/HandledScreen;title:Lnet/minecraft/text/Text;", opcode = Opcodes.GETFIELD), + to = @At(value = "FIELD", target = "Lnet/minecraft/client/gui/screen/ingame/HandledScreen;playerInventoryTitle:Lnet/minecraft/text/Text;", opcode = Opcodes.GETFIELD) + ), + allow = 1, + require = 1) + private int replaceContainerTitle(DrawContext instance, TextRenderer textRenderer, Text text, int x, int y, int color, boolean shadow, Operation original) { + var textOverride = CustomScreenLayouts.getTextMover(CustomScreenLayouts.CustomScreenLayout::getContainerTitle); + return original.call(instance, textRenderer, + textOverride.replaceText(text), + textOverride.replaceX(textRenderer, text, x), + textOverride.replaceY(y), + textOverride.replaceColor(text, color), + shadow); + } + + @WrapOperation( + method = "drawForeground", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/gui/DrawContext;drawText(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/text/Text;IIIZ)I"), + slice = @Slice( + from = @At(value = "FIELD", target = "Lnet/minecraft/client/gui/screen/ingame/HandledScreen;playerInventoryTitle:Lnet/minecraft/text/Text;", opcode = Opcodes.GETFIELD), + to = @At(value = "TAIL") + ), + allow = 1, + require = 1) + private int replacePlayerTitle(DrawContext instance, TextRenderer textRenderer, Text text, int x, int y, int color, boolean shadow, Operation original) { + var textOverride = CustomScreenLayouts.getTextMover(CustomScreenLayouts.CustomScreenLayout::getPlayerTitle); + return original.call(instance, textRenderer, + textOverride.replaceText(text), + textOverride.replaceX(textRenderer, text, x), + textOverride.replaceY(y), + textOverride.replaceColor(text, color), + shadow); + } +} diff --git a/symbols/build.gradle.kts b/symbols/build.gradle.kts new file mode 100644 index 0000000..2084af4 --- /dev/null +++ b/symbols/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + kotlin("jvm") + id("com.google.devtools.ksp") + id("firmament.common") +} + +dependencies { + ksp("dev.zacsweers.autoservice:auto-service-ksp:1.1.0") + implementation("com.google.auto.service:auto-service-annotations:1.1.1") + implementation("com.google.devtools.ksp:symbol-processing-api:1.9.23-1.0.20") + implementation("com.google.code.gson:gson:2.11.0") +} diff --git a/symbols/src/main/kotlin/Subscribe.kt b/symbols/src/main/kotlin/Subscribe.kt new file mode 100644 index 0000000..a6b173f --- /dev/null +++ b/symbols/src/main/kotlin/Subscribe.kt @@ -0,0 +1,7 @@ + +package moe.nea.firmament.annotations + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.SOURCE) +annotation class Subscribe + diff --git a/symbols/src/main/kotlin/process/CompatMetaProcessor.kt b/symbols/src/main/kotlin/process/CompatMetaProcessor.kt new file mode 100644 index 0000000..0753e4c --- /dev/null +++ b/symbols/src/main/kotlin/process/CompatMetaProcessor.kt @@ -0,0 +1,63 @@ +package moe.nea.firmament.annotations.process + +import com.google.auto.service.AutoService +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.Dependencies +import com.google.devtools.ksp.processing.KSPLogger +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSName + +class CompatMetaProcessor(val logger: KSPLogger, val codeGenerator: CodeGenerator, val sourceSetName: String) : + SymbolProcessor { + override fun process(resolver: Resolver): List { + val files = resolver.getAllFiles().toList() + val packages = files.mapTo(mutableSetOf()) { it.packageName.asString() } + packages.add("moe.nea.firmament.annotations.generated.$sourceSetName") + val compatMeta = resolver.getSymbolsWithAnnotation("moe.nea.firmament.util.compatloader.CompatMeta") + .singleOrNull() as KSClassDeclaration? ?: return listOf() + val dependencies = Dependencies(aggregating = true, *files.toTypedArray()) + val generatedFileName = "GeneratedCompat${sourceSetName.replaceFirstChar { it.uppercaseChar() }}" + val compatFile = + codeGenerator.createNewFile(dependencies, "moe.nea.firmament.annotations.generated.$sourceSetName", generatedFileName) + .bufferedWriter() + compatFile.appendLine("// This file is @generated by SubscribeAnnotationProcessor") + compatFile.appendLine("// Do not edit") + compatFile.appendLine("package moe.nea.firmament.annotations.generated.$sourceSetName") + compatFile.appendLine("class $generatedFileName : moe.nea.firmament.util.compatloader.ICompatMetaGen {") + compatFile.appendLine(""" + override fun owns(className: String): Boolean { + return moe.nea.firmament.util.compatloader.CompatHelper.isOwnedByPackage(className, ${ + packages.joinToString { "\"" + it + "\"" } + }) + } + + override val meta: moe.nea.firmament.util.compatloader.ICompatMeta + get() = ${compatMeta.qualifiedName!!.asString()} +""") + compatFile.appendLine("}") + compatFile.close() + val metaInf = codeGenerator.createNewFileByPath( + dependencies, + "META-INF/services/moe.nea.firmament.util.compatloader.ICompatMetaGen", extensionName = "") + .bufferedWriter() + metaInf.append("moe.nea.firmament.annotations.generated.$sourceSetName.") + metaInf.appendLine(generatedFileName) + metaInf.close() + return listOf() + } + + + @AutoService(SymbolProcessorProvider::class) + class Provider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + return CompatMetaProcessor(environment.logger, + environment.codeGenerator, + environment.options["firmament.sourceset"] ?: "main") + } + } +} diff --git a/symbols/src/main/kotlin/process/GameTestContainingClassProcessor.kt b/symbols/src/main/kotlin/process/GameTestContainingClassProcessor.kt new file mode 100644 index 0000000..4fcf91f --- /dev/null +++ b/symbols/src/main/kotlin/process/GameTestContainingClassProcessor.kt @@ -0,0 +1,80 @@ +package moe.nea.firmament.annotations.process + +import com.google.auto.service.AutoService +import com.google.devtools.ksp.containingFile +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.Dependencies +import com.google.devtools.ksp.processing.KSPLogger +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSFile +import com.google.gson.Gson +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import java.io.OutputStreamWriter +import java.nio.charset.StandardCharsets +import java.util.TreeSet + +class GameTestContainingClassProcessor( + val logger: KSPLogger, + val codeGenerator: CodeGenerator, + val sourceSetName: String, +) : SymbolProcessor { + + + @AutoService(SymbolProcessorProvider::class) + class Provider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + return GameTestContainingClassProcessor( + environment.logger, + environment.codeGenerator, + environment.options["firmament.sourceset"] ?: "main") + } + } + + val allClasses: MutableSet = TreeSet() + val allSources = mutableSetOf() + + override fun process(resolver: Resolver): List { + val annotated = resolver.getSymbolsWithAnnotation("net.minecraft.test.GameTest").toList() + annotated.forEach { + val containingClass = it.parent as KSClassDeclaration + allClasses.add(containingClass.qualifiedName!!.asString()) + allSources.add(it.containingFile!!) + } + return emptyList() + } + + fun createJson(): JsonObject { + return JsonObject().apply { + addProperty("schemaVersion", 1) + addProperty("id", "firmament-gametest") + addProperty("name", "Firmament Gametest") + addProperty("version", "1.0.0") + addProperty("environment", "*") + add("entrypoints", JsonObject().apply { + add("fabric-gametest", JsonArray().apply { + allClasses.forEach { + add(it) + } + }) + }) + } + } + + override fun finish() { + if (allClasses.isEmpty()) return + val stream = codeGenerator.createNewFile(Dependencies(aggregating = true, *allSources.toTypedArray()), + "", + "fabric.mod", + "json") + val output = OutputStreamWriter(stream, StandardCharsets.UTF_8) + Gson().toJson(createJson(), output) + output.close() + } + +} diff --git a/symbols/src/main/kotlin/process/SubscribeAnnotationProcessor.kt b/symbols/src/main/kotlin/process/SubscribeAnnotationProcessor.kt new file mode 100644 index 0000000..3eaf3d6 --- /dev/null +++ b/symbols/src/main/kotlin/process/SubscribeAnnotationProcessor.kt @@ -0,0 +1,136 @@ +package moe.nea.firmament.annotations.process + +import com.google.auto.service.AutoService +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.Dependencies +import com.google.devtools.ksp.processing.KSPLogger +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider +import com.google.devtools.ksp.symbol.ClassKind +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSFunctionDeclaration +import com.google.devtools.ksp.symbol.KSType +import com.google.devtools.ksp.symbol.Nullability +import com.google.devtools.ksp.validate +import moe.nea.firmament.annotations.Subscribe + +class SubscribeAnnotationProcessor( + val logger: KSPLogger, + val codeGenerator: CodeGenerator, + val sourceSetName: String, +) : SymbolProcessor { + override fun finish() { + subscriptions.sort() + if (subscriptions.isEmpty()) return + val subscriptionSet = subscriptions.mapTo(mutableSetOf()) { it.cf } + val dependencies = Dependencies( + aggregating = true, + *subscriptionSet.toTypedArray()) + val generatedFileName = "AllSubscriptions${sourceSetName.replaceFirstChar { it.uppercaseChar() }}" + val subscriptionsFile = + codeGenerator + .createNewFile(dependencies, "moe.nea.firmament.annotations.generated.$sourceSetName", generatedFileName) + .bufferedWriter() + subscriptionsFile.apply { + appendLine("// This file is @generated by SubscribeAnnotationProcessor") + appendLine("// Do not edit") + appendLine("package moe.nea.firmament.annotations.generated.$sourceSetName") + appendLine() + appendLine("import moe.nea.firmament.events.subscription.*") + appendLine() + appendLine("@Suppress()") + appendLine("class $generatedFileName : SubscriptionList {") + appendLine(" override fun provideSubscriptions(addSubscription: (Subscription<*>) -> Unit) {") + for (subscription in subscriptions) { + val owner = subscription.pQName.asString() + val method = subscription.child.simpleName.asString() + val type = subscription.type.declaration.qualifiedName!!.asString() + appendLine(" addSubscription(Subscription<$type>(") + appendLine(" ${owner},") + appendLine(" ${owner}::${method},") + appendLine(" ${type},") + appendLine(" \"${method}\"))") + } + appendLine(" }") + appendLine("}") + } + subscriptionsFile.close() + val metaInf = codeGenerator.createNewFileByPath( + dependencies, + "META-INF/services/moe.nea.firmament.events.subscription.SubscriptionList", extensionName = "") + .bufferedWriter() + metaInf.append("moe.nea.firmament.annotations.generated.$sourceSetName.") + metaInf.appendLine(generatedFileName) + metaInf.close() + } + + data class Subscription( + val parent: KSClassDeclaration, + val child: KSFunctionDeclaration, + val type: KSType, + ) : Comparable { + val cf = parent.containingFile!! + val pQName = parent.qualifiedName!! + val tName = type.declaration.qualifiedName!! + override fun compareTo(other: Subscription): Int { + var compare = pQName.asString().compareTo(other.pQName.asString()) + if (compare != 0) return compare + compare = other.child.simpleName.asString().compareTo(child.simpleName.asString()) + if (compare != 0) return compare + compare = other.tName.asString().compareTo(tName.asString()) + if (compare != 0) return compare + return 0 + } + } + + val subscriptions = mutableListOf() + + fun processCandidates(list: List) { + for (element in list) { + if (element !is KSFunctionDeclaration) { + logger.error("@Subscribe annotation on a not-function", element) + continue + } + if (element.isAbstract) { + logger.error("@Subscribe annotation on an abstract function", element) + continue + } + val parent = element.parentDeclaration + if (parent !is KSClassDeclaration || parent.classKind != ClassKind.OBJECT) { + logger.error("@Subscribe on a non-object", element) + continue + } + val param = element.parameters.singleOrNull() + if (param == null) { + logger.error("@Subscribe annotated functions need to take exactly one parameter", element) + continue + } + val type = param.type.resolve() + if (type.nullability != Nullability.NOT_NULL) { + logger.error("@Subscribe annotated functions cannot take a nullable event", element) + continue + } + subscriptions.add(Subscription(parent, element, type)) + } + } + + override fun process(resolver: Resolver): List { + val candidates = resolver.getSymbolsWithAnnotation(Subscribe::class.qualifiedName!!).toList() + val valid = candidates.filter { it.validate() } + val invalid = candidates.filter { !it.validate() } + processCandidates(valid) + return invalid + } +} + +@AutoService(SymbolProcessorProvider::class) +class SubscribeAnnotationProcessorProvider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + return SubscribeAnnotationProcessor(environment.logger, + environment.codeGenerator, + environment.options["firmament.sourceset"] ?: "main") + } +} diff --git a/testagent/build.gradle.kts b/testagent/build.gradle.kts new file mode 100644 index 0000000..cf7c15b --- /dev/null +++ b/testagent/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + java + id("firmament.common") + id("com.gradleup.shadow") +} +dependencies { + implementation(libs.asm) +} +tasks.withType { + val agentMain = "moe.nea.firmament.testagent.AgentMain" + manifest.attributes( + "Agent-Class" to agentMain, + "Premain-Class" to agentMain, + ) +} diff --git a/testagent/src/main/java/moe/nea/firmament/testagent/AgentMain.java b/testagent/src/main/java/moe/nea/firmament/testagent/AgentMain.java new file mode 100644 index 0000000..79023d8 --- /dev/null +++ b/testagent/src/main/java/moe/nea/firmament/testagent/AgentMain.java @@ -0,0 +1,21 @@ +package moe.nea.firmament.testagent; + +import java.lang.instrument.Instrumentation; + +public class AgentMain { + + public static void premain( + String agentArgs, Instrumentation inst) { + System.out.println("Pre-Main Firmament Test Agent"); + AgentMain.inject(inst); + } + + public static void agentmain( + String agentArgs, Instrumentation inst) { + System.out.println("Injected Firmament Test Agent"); + AgentMain.inject(inst); + } + + private static void inject(Instrumentation inst) { + inst.addTransformer(new ProtectedToPublicClassTransformer(inst)); } +} diff --git a/testagent/src/main/java/moe/nea/firmament/testagent/ProtectedToPublicClassRewriter.java b/testagent/src/main/java/moe/nea/firmament/testagent/ProtectedToPublicClassRewriter.java new file mode 100644 index 0000000..7d9aa56 --- /dev/null +++ b/testagent/src/main/java/moe/nea/firmament/testagent/ProtectedToPublicClassRewriter.java @@ -0,0 +1,31 @@ +package moe.nea.firmament.testagent; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +public class ProtectedToPublicClassRewriter extends ClassVisitor { + public ProtectedToPublicClassRewriter(ClassWriter writer) { + super(Opcodes.ASM9, writer); + } + + int makePublic(int flags) { + if ((flags & Opcodes.ACC_PROTECTED) != 0) + return (flags & ~Opcodes.ACC_PROTECTED) | Opcodes.ACC_PUBLIC; + if ((flags & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED | Opcodes.ACC_PRIVATE)) == 0) + return flags | Opcodes.ACC_PUBLIC; + return flags; + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + return super.visitField(makePublic(access), name, descriptor, signature, value); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + return super.visitMethod(makePublic(access), name, descriptor, signature, exceptions); + } +} diff --git a/testagent/src/main/java/moe/nea/firmament/testagent/ProtectedToPublicClassTransformer.java b/testagent/src/main/java/moe/nea/firmament/testagent/ProtectedToPublicClassTransformer.java new file mode 100644 index 0000000..5d59035 --- /dev/null +++ b/testagent/src/main/java/moe/nea/firmament/testagent/ProtectedToPublicClassTransformer.java @@ -0,0 +1,30 @@ +package moe.nea.firmament.testagent; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.lang.instrument.Instrumentation; +import java.security.ProtectionDomain; + +public class ProtectedToPublicClassTransformer implements ClassFileTransformer { + public ProtectedToPublicClassTransformer(Instrumentation inst) { + } + + @Override + public byte[] transform(ClassLoader loader, + String className, + Class classBeingRedefined, + ProtectionDomain protectionDomain, + byte[] classfileBuffer) + throws IllegalClassFormatException { + if (!className.startsWith("net/minecraft/")) return classfileBuffer; + if (classfileBuffer == null) return null; + var reader = new ClassReader(classfileBuffer); + var writer = new ClassWriter(0); + var transformer = new ProtectedToPublicClassRewriter(writer); + reader.accept(transformer, 0); + return writer.toByteArray(); + } +} diff --git a/translations/en_us.json b/translations/en_us.json new file mode 100644 index 0000000..b902206 --- /dev/null +++ b/translations/en_us.json @@ -0,0 +1,552 @@ +{ + "firmament.carnival.tutorial.minesweeper": "§eClick here to check out Firmaments Tutorial for this minigame!", + "firmament.command.toggle.no-config-found": "Could not find config %s", + "firmament.command.toggle.no-property-found": "Could not find property %s", + "firmament.command.toggle.not-a-toggle": "Property %s is not a toggle", + "firmament.command.toggle.toggled": "Toggled %s / %s %s", + "firmament.command.waypoint.added": "Added waypoint %s %s %s.", + "firmament.command.waypoint.clear": "Cleared waypoints.", + "firmament.command.waypoint.import": "Imported %s waypoints from clipboard.", + "firmament.command.waypoint.ordered.toggle.false": "Disabled ordered waypoints", + "firmament.command.waypoint.ordered.toggle.true": "Enabled ordered waypoints", + "firmament.command.waypoint.remove": "Removed waypoint %s. Other waypoints may have different indexes now.", + "firmament.command.waypoint.remove.error": "Could not find waypoint with that index to delete.", + "firmament.command.waypoint.skip": "Skipped 1 waypoint", + "firmament.command.waypoint.skip.error": "Could not skip a waypoint. Are you in ordered waypoint mode with waypoints loaded?", + "firmament.config.all-configs": "- All Configs -", + "firmament.config.anniversary": "Anniversary Features", + "firmament.config.anniversary.pig-hud": "Pig Tracker Hud", + "firmament.config.anniversary.pig-hud.description": "A HUD showing rewards pulled from shiny pigs", + "firmament.config.anniversary.shiny-pigs": "Shiny Pigs Tracker", + "firmament.config.anniversary.shiny-pigs.description": "Track rewards from shiny pigs, as well as how much time you have left to collect your pig.", + "firmament.config.auto-completions": "Hypixel Command Improvements", + "firmament.config.auto-completions.warp-complete": "Auto Complete /warp", + "firmament.config.auto-completions.warp-complete.description": "Auto complete warp destinations in chat. This may include warps you have not yet unlocked.", + "firmament.config.auto-completions.warp-is": "Redirect /warp is to /warp island", + "firmament.config.auto-completions.warp-is.description": "Redirects /warp is to /warp island, since hypixel does not recognize /warp is as a warp destination.", + "firmament.config.block-zapper-overlay": "Block Zapper Overlay", + "firmament.config.block-zapper-overlay.block-zapper-overlay": "Block Zapper Overlay", + "firmament.config.block-zapper-overlay.block-zapper-overlay.description": "Shows what blocks will be zapped", + "firmament.config.block-zapper-overlay.color": "Colour", + "firmament.config.block-zapper-overlay.color.description": "The color that the blocks will be highlighted in", + "firmament.config.block-zapper-overlay.undo-key": "Undo Keybind", + "firmament.config.block-zapper-overlay.undo-key.description": "Keybind to undo your zap", + "firmament.config.bonemerang-overlay": "Bonemerang Overlay", + "firmament.config.bonemerang-overlay.bonemerang-overlay": "Bonemerang Overlay", + "firmament.config.bonemerang-overlay.bonemerang-overlay-hud": "Bonemerang Overlay Hud", + "firmament.config.bonemerang-overlay.bonemerang-overlay-hud.description": "Shows how many targets your bonemerang will hit", + "firmament.config.bonemerang-overlay.bonemerang-overlay.description": "Display an overlay that tells you what block you will warp to.", + "firmament.config.bonemerang-overlay.bonemerang-overlay.display": "Bonemerang Targets: %s", + "firmament.config.bonemerang-overlay.highlight-hit-entities": "Highlight Target Entities", + "firmament.config.bonemerang-overlay.highlight-hit-entities.description": "Highlight entities that will be hit", + "firmament.config.carnival": "Carnival Features", + "firmament.config.carnival.bombs-solver": "Minesweeper Helper", + "firmament.config.carnival.bombs-solver.description": "Display bombs surrounding each block in minesweeper.", + "firmament.config.carnival.tutorials": "Tutorial Reminder", + "firmament.config.carnival.tutorials.description": "Show a tutorial hint every time you start a game with a Firmament tutorial.", + "firmament.config.category.chat": "Chat", + "firmament.config.category.chat.description": "Chat related features", + "firmament.config.category.dev": "Developer & Debug", + "firmament.config.category.dev.description": "Settings for texture pack devs and programmers", + "firmament.config.category.events": "Events", + "firmament.config.category.events.description": "Settings for temporary or repeating events", + "firmament.config.category.garden": "Garden", + "firmament.config.category.garden.description": "Features for the No. 1 Macro Free Island on SkyBlock", + "firmament.config.category.integrations": "Integrations & Textures", + "firmament.config.category.integrations.description": "Integrations with other mods, as well as texture packs", + "firmament.config.category.inventory": "Inventory", + "firmament.config.category.inventory.description": "Features for anything that happens in a chest or inventory", + "firmament.config.category.items": "Items", + "firmament.config.category.items.description": "Features for items", + "firmament.config.category.meta": "Meta & Firmament", + "firmament.config.category.meta.description": "Settings for Firmament and the item repo", + "firmament.config.category.mining": "Mining", + "firmament.config.category.mining.description": "Mining related features", + "firmament.config.category.misc": "Miscellaneous", + "firmament.config.category.misc.description": "Miscellaneous features that don't fit elsewhere", + "firmament.config.centuryraffle": "Century Raffle", + "firmament.config.centuryraffle.highlight-cake-players": "Highlight Players in Team", + "firmament.config.centuryraffle.highlight-cake-players.description": "Highlight the players you can feed while holding a Slice of Century Cake", + "firmament.config.chat-links": "Chat Links", + "firmament.config.chat-links.allow-all-hosts": "Allow all Image Hosts", + "firmament.config.chat-links.allow-all-hosts.description": "Allow displaying images no matter where it is hosted.", + "firmament.config.chat-links.allowed-hosts": "Allowed Image Hosts", + "firmament.config.chat-links.allowed-hosts.description": "Prevent yourself from requesting images from other servers, to prevent your IP from being leaked.", + "firmament.config.chat-links.image-enabled": "Enable Image Preview", + "firmament.config.chat-links.image-enabled.description": "Show a preview of images when hovering over links in chat", + "firmament.config.chat-links.links-enabled": "Enable Clickable Links", + "firmament.config.chat-links.links-enabled.description": "Make links in chat clickable", + "firmament.config.chat-links.position": "Chat Image Preview", + "firmament.config.chat-links.position.description": "Edit where the images are shown", + "firmament.config.commissions": "Commissions", + "firmament.config.commissions.highlight-completed": "Highlight Completed", + "firmament.config.commissions.highlight-completed.description": "Highlight completed commissions in the commission menu", + "firmament.config.compatibility": "Intermod Features", + "firmament.config.compatibility.explosion-enabled": "Redirect Enhanced Explosions", + "firmament.config.compatibility.explosion-enabled.description": "Redirect explosion particles to be rendered by enhanced explosions.", + "firmament.config.compatibility.explosion-power": "Enhanced Explosion Power", + "firmament.config.compatibility.explosion-power.description": "Choose how big explosions will be rendered by enhanced explosions", + "firmament.config.composter": "Composter", + "firmament.config.composter.no-more-noises": "Mute Composter", + "firmament.config.composter.no-more-noises.description": "Muffle all noises and sounds made by the composter", + "firmament.config.configconfig": "Firmaments Config", + "firmament.config.configconfig.enable-moulconfig": "Use MoulConfig", + "firmament.config.configconfig.enable-moulconfig.description": "Uses the MoulConfig config UI. Turn off to fall back to the built in config.", + "firmament.config.configconfig.enable-yacl": "Use YACL Config", + "firmament.config.configconfig.enable-yacl.description": "Uses the YACL config UI. Turn off to fall back to the built in config. Needs YACL to be installed separately.", + "firmament.config.configconfig.wide-moulconfig": "Wide MoulConfig", + "firmament.config.configconfig.wide-moulconfig.description": "Use a wider editor for MoulConfig", + "firmament.config.copy-chat": "Copy Chat", + "firmament.config.copy-chat.copy-chat": "Copy Chat", + "firmament.config.copy-chat.copy-chat.description": "Right click a message to copy", + "firmament.config.custom-skyblock-textures": "Custom SkyBlock Item Textures", + "firmament.config.custom-skyblock-textures.armor-overrides": "Enable Armor re-texturing", + "firmament.config.custom-skyblock-textures.armor-overrides.description": "Allows texture pack authors to re-texture (but not re-model) SkyBlock armors.", + "firmament.config.custom-skyblock-textures.block-overrides": "Enable Block re-modelling", + "firmament.config.custom-skyblock-textures.block-overrides.description": "Allows texture pack authors replacing block models depending on block position and SkyBlock island.", + "firmament.config.custom-skyblock-textures.cache-duration": "Model Cache Duration", + "firmament.config.custom-skyblock-textures.cache-duration.description": "How long texture models should be cached for.", + "firmament.config.custom-skyblock-textures.cache-forever": "Disable cache clearing", + "firmament.config.custom-skyblock-textures.cache-forever.description": "Disables clearing the cache entirely. Since you will be sent new item stacks from the server if something changes, this should not cause issues.", + "firmament.config.custom-skyblock-textures.enabled": "Enable Custom Item Textures", + "firmament.config.custom-skyblock-textures.enabled.description": "Allow replacing items for texture packs. Turning this off does not disable custom predicates", + "firmament.config.custom-skyblock-textures.legacy-cit": "Enable legacy CIT Resewn compat", + "firmament.config.custom-skyblock-textures.legacy-cit.description": "Allow CIT resewn texture packs written for 1.20.4 to be loaded on newer versions.", + "firmament.config.custom-skyblock-textures.legacy-minecraft-path-support": "Enable Legacy Paths", + "firmament.config.custom-skyblock-textures.legacy-minecraft-path-support.description": "Allow texture packs to load textures from some legacy paths. I.e.: Allows loading 1.21.0 armor textures on 1.21.4.", + "firmament.config.custom-skyblock-textures.model-overrides": "Enable model overrides/predicates", + "firmament.config.custom-skyblock-textures.model-overrides.description": "Enable Firmament's model predicates. This will apply to vanilla models as well, if that vanilla model has Firmament predicates.", + "firmament.config.custom-skyblock-textures.recolor-text": "Allow packs to recolor text", + "firmament.config.custom-skyblock-textures.recolor-text.description": "Allows texture packs to recolor UI texts.", + "firmament.config.custom-skyblock-textures.screen-layouts": "Allow packs screen relayouts", + "firmament.config.custom-skyblock-textures.screen-layouts.description": "Allows texture packs to move UI elements like slots around, as well as replace the background of screens.", + "firmament.config.custom-skyblock-textures.skulls-enabled": "Enable Custom Placed Skull Textures", + "firmament.config.custom-skyblock-textures.skulls-enabled.description": "Allow replacing the textures of placed skulls.", + "firmament.config.developer": "Developer Settings", + "firmament.config.developer-capes": "Developer Capes", + "firmament.config.developer-capes.show-cape": "Show Developer Capes", + "firmament.config.developer-capes.show-cape.description": "Allows you to see the developer capes.", + "firmament.config.developer.auto-rebuild": "Automatically rebuild resources", + "firmament.config.developer.auto-rebuild.description": "Executes ./gradlew processResources before F3+T is executed.", + "firmament.config.diana": "Diana", + "firmament.config.diana.ancestral-spade": "Ancestral Spade Solver", + "firmament.config.diana.ancestral-spade.description": "Automatically guess your next burrow based on particles and sounds spawned from the Ancestral Spade's Echo ability.", + "firmament.config.diana.ancestral-teleport": "Warp near guess", + "firmament.config.diana.ancestral-teleport.description": "Click to teleport near the guessed burrow.", + "firmament.config.diana.nearby-waypoints": "Nearby Waypoints Highlighter", + "firmament.config.diana.nearby-waypoints.description": "Highlight nearby diana burrows.", + "firmament.config.etherwarp-overlay": "Etherwarp Overlay", + "firmament.config.etherwarp-overlay.cube": "Cube", + "firmament.config.etherwarp-overlay.cube-colour": "Cube Color", + "firmament.config.etherwarp-overlay.cube-colour.description": "Choose the colour of the etherwarp target block.", + "firmament.config.etherwarp-overlay.cube.description": "Displays a full cube on the block", + "firmament.config.etherwarp-overlay.etherwarp-overlay": "Etherwarp Overlay", + "firmament.config.etherwarp-overlay.etherwarp-overlay.description": "Display an overlay that tells you what block you will warp to.", + "firmament.config.etherwarp-overlay.only-show-while-sneaking": "Only show while sneaking", + "firmament.config.etherwarp-overlay.only-show-while-sneaking.description": "Displays the Etherwarp overlay only while sneaking.", + "firmament.config.etherwarp-overlay.wireframe": "Outline", + "firmament.config.etherwarp-overlay.wireframe.description": "Displays a full outline on the block", + "firmament.config.fairy-souls": "Fairy Souls", + "firmament.config.fairy-souls.reset": "Reset Collected Fairy Souls", + "firmament.config.fairy-souls.reset.description": "Reset all collected fairy souls, allowing you to restart from null.", + "firmament.config.fairy-souls.show": "Show Fairy Soul Waypoints", + "firmament.config.fairy-souls.show.description": "Show unclaimed Fairy Soul waypoints in the world you are currently in.", + "firmament.config.fishing-warning": "Fishing Warning", + "firmament.config.fishing-warning.display-warning": "Display a warning when you are about to hook a fish", + "firmament.config.fishing-warning.highlight-wake-chain": "Highlight fishing particles", + "firmament.config.fixes": "Fixes", + "firmament.config.fixes.auto-sprint": "Auto Sprint", + "firmament.config.fixes.auto-sprint-hud": "Sprint State Hud", + "firmament.config.fixes.auto-sprint-hud.description": "Show your current sprint state on your screen. Only visible if no auto sprint keybind is set.", + "firmament.config.fixes.auto-sprint-keybinding": "Auto Sprint KeyBinding", + "firmament.config.fixes.auto-sprint-keybinding.description": "Toggle auto sprint via this keybinding.", + "firmament.config.fixes.auto-sprint-underwater": "Sprint Under Water", + "firmament.config.fixes.auto-sprint-underwater.description": "Also Toggle Sprint under water. Sprinting under water puts you in the swimming animation which changes your camera and hitbox, which can be confusing if you stop and start moving a lot.", + "firmament.config.fixes.auto-sprint.description": "This is different from vanilla sprint in the way that it only marks the keybinding pressed for the first tick of walking.", + "firmament.config.fixes.disable-hurt-cam": "No Hurt Cam", + "firmament.config.fixes.disable-hurt-cam.description": "Disable the damage screen shake animation.", + "firmament.config.fixes.hide-mob-effects": "Hide Potion Effects", + "firmament.config.fixes.hide-mob-effects.description": "Hide Potion effects on the right side of your player inventory.", + "firmament.config.fixes.hide-off-hand": "No Off Hand", + "firmament.config.fixes.hide-off-hand.description": "Remove the recipe slot from your inventory", + "firmament.config.fixes.hide-potion-effects-hud": "Hide Potion Effects HUD", + "firmament.config.fixes.hide-potion-effects-hud.description": "Hides the potion effects HUD in the top right.", + "firmament.config.fixes.hide-recipe-book": "No Recipe Book", + "firmament.config.fixes.hide-recipe-book.description": "Remove the recipe book from your inventory", + "firmament.config.fixes.hide-slot-highlights": "Hide Slot Highlights", + "firmament.config.fixes.hide-slot-highlights.description": "Hide slot highlights for items with disabled tooltip. This makes /sbmenu look nicer with smooth texture packs.", + "firmament.config.fixes.peek-chat": "Peek Chat", + "firmament.config.fixes.peek-chat.description": "Hold this keybinding to view the chat as if you have it opened, but while still being able to control your character.", + "firmament.config.fixes.player-skins": "Fix unsigned Player Skins", + "firmament.config.fixes.player-skins.description": "Mark all player skins as signed, preventing console spam, and some rendering issues.", + "firmament.config.hud": "HUD", + "firmament.config.hud.day-count": "Day Count", + "firmament.config.hud.day-count-hud": "Day Count HUD", + "firmament.config.hud.day-count-hud.description": "Shows day.", + "firmament.config.hud.day-count-hud.display": "Day: %s", + "firmament.config.hud.day-count.description": "A HUD showing current day.", + "firmament.config.hud.fps-count": "FPS Count", + "firmament.config.hud.fps-count-hud": "FPS Count HUD", + "firmament.config.hud.fps-count-hud.description": "Shows FPS.", + "firmament.config.hud.fps-count-hud.display": "FPS: %s", + "firmament.config.hud.fps-count.description": "A HUD showing current FPS.", + "firmament.config.hud.ping-count": "Ping Count", + "firmament.config.hud.ping-count-hud": "Ping Count HUD", + "firmament.config.hud.ping-count-hud.description": "Shows Ping.", + "firmament.config.hud.ping-count-hud.display": "Ping %s", + "firmament.config.hud.ping-count.description": "A HUD showing current Ping.", + "firmament.config.inventory-buttons-config": "Inventory Buttons", + "firmament.config.inventory-buttons-config.hover-text": "Hover Tooltip", + "firmament.config.inventory-buttons-config.hover-text.description": "Hovering over inventory buttons will show the command they run.", + "firmament.config.inventory-buttons-config.only-inv": "Inventory Only", + "firmament.config.inventory-buttons-config.only-inv.description": "Only shows buttons while in the inventory", + "firmament.config.inventory-buttons-config.open-editor": "Open Editor", + "firmament.config.inventory-buttons-config.open-editor.description": "Click anywhere to create a new inventory button or to edit one. Hold SHIFT to grid align.", + "firmament.config.item-hotkeys": "Item Hotkeys", + "firmament.config.item-hotkeys.global-trade-interface": "Search on Bazaar/AH", + "firmament.config.item-hotkeys.global-trade-interface.description": "Press this button to search the hovered item on the bazaar or auction house.", + "firmament.config.item-rarity-cosmetics": "Item Rarity Cosmetics", + "firmament.config.item-rarity-cosmetics.background": "Slot Background Rarity", + "firmament.config.item-rarity-cosmetics.background-hotbar": "Hotbar Background Rarity", + "firmament.config.item-rarity-cosmetics.background-hotbar.description": "Show item rarity background in the hotbar.", + "firmament.config.item-rarity-cosmetics.background.description": "Show a background behind each item, depending on its rarity.", + "firmament.config.jade-integration": "Jade / WAILA", + "firmament.config.jade-integration.blocks": "Enable Custom Blocks", + "firmament.config.jade-integration.blocks.description": "Show custom block descriptions and hardness levels in Jade.", + "firmament.config.jade-integration.progress": "Enable Custom Mining Progress", + "firmament.config.jade-integration.progress.description": "Show the custom mining progress in Jade, when in a world with mining fatigue.", + "firmament.config.junk-highlighter": "Junk Highlighter", + "firmament.config.junk-highlighter.description": "Highlight items using regex to search for items in your inventory", + "firmament.config.junk-highlighter.highlight": "Highlight Keybind", + "firmament.config.junk-highlighter.highlight.description": "Highlight found items when this keybind is held", + "firmament.config.junk-highlighter.regex": "Search Regex", + "firmament.config.junk-highlighter.regex.description": "The RegEx (Regular Expression) to use when searching", + "firmament.config.lore-timers": "Item Timestamps", + "firmament.config.lore-timers.format": "Time Format", + "firmament.config.lore-timers.format.choice.american": "§9Ame§cri§fcan", + "firmament.config.lore-timers.format.choice.local": "System Time Format", + "firmament.config.lore-timers.format.choice.rfc": "RFC", + "firmament.config.lore-timers.format.choice.rfcprecise": "RFC (Milliseconds)", + "firmament.config.lore-timers.format.choice.socialist": "European-ish", + "firmament.config.lore-timers.format.description": "Choose the time format in which resolved timers are displayed.", + "firmament.config.lore-timers.show": "Show Lore Timers", + "firmament.config.lore-timers.show-creation": "Show Creation", + "firmament.config.lore-timers.show-creation.description": "Shows the creation or craft timestamp of the item. Sometimes this timestamp is retained when upgrading an item, so it isn't necessarily the craft time of this specific item, but rather one of its components.", + "firmament.config.lore-timers.show.description": "Shows when a timer in a lore (such as interest, auction duration) would end.", + "firmament.config.party-commands": "Party Commands", + "firmament.config.party-commands.cooldown": "Cooldown", + "firmament.config.party-commands.cooldown.description": "Prevent people from spamming commands with a delay between party commands.", + "firmament.config.party-commands.enable": "Enable Party Commands", + "firmament.config.party-commands.enable.description": "Allow people in your party to use commands like !warp, !coords, !ptme and so on. See /firm partycommands for a list", + "firmament.config.party-commands.ignore-own": "Ignore Own Messages", + "firmament.config.party-commands.ignore-own.description": "Prevent your own messages from triggering party commands", + "firmament.config.pets": "Pets", + "firmament.config.pets.highlight-pet": "Highlight active pet", + "firmament.config.pets.highlight-pet.description": "Highlight your currently selected pet in the /pets menu.", + "firmament.config.pets.pet-overlay": "Pet Info", + "firmament.config.pets.pet-overlay-hud": "Pet Info Hud", + "firmament.config.pets.pet-overlay-hud.description": "A HUD showing current active pet and the pet exp.", + "firmament.config.pets.pet-overlay.description": "Shows current active pet and pet exp on screen.", + "firmament.config.pickaxe-info": "Pickaxes & Drills", + "firmament.config.pickaxe-info.ability-cooldown": "Pickaxe Ability Cooldown", + "firmament.config.pickaxe-info.ability-cooldown-toast": "Pickaxe Ability Ready Toast", + "firmament.config.pickaxe-info.ability-cooldown-toast.description": "Shows a toast when your pickaxe ability is ready.", + "firmament.config.pickaxe-info.ability-cooldown.description": "Show a cooldown on your cross-hair for your pickaxe ability.", + "firmament.config.pickaxe-info.ability-scale": "Ability Cooldown Scale", + "firmament.config.pickaxe-info.ability-scale.description": "Resize the cooldown around your cross-hair for your pickaxe ability.", + "firmament.config.pickaxe-info.block-on-dynamic": "Block on Private Island", + "firmament.config.pickaxe-info.block-on-dynamic.choice.always": "Always Block", + "firmament.config.pickaxe-info.block-on-dynamic.choice.never": "Never Block", + "firmament.config.pickaxe-info.block-on-dynamic.choice.only_destructive": "Only with dangerous", + "firmament.config.pickaxe-info.block-on-dynamic.description": "Block pickaxe abilities on private islands by preventing you from right clicking.", + "firmament.config.pickaxe-info.disable-in-dungeons": "Disable cross-hair cooldown in Dungeons", + "firmament.config.pickaxe-info.disable-in-dungeons.description": "Disables the cooldown around your cross-hair while in Dungeons.", + "firmament.config.pickaxe-info.fuel-bar": "Drill Fuel Durability Bar", + "firmament.config.pickaxe-info.fuel-bar.description": "Replace the item durability bar of your drills with one that shows the remaining fuel.", + "firmament.config.power-user": "Power Users", + "firmament.config.power-user.copy-item-id": "Copy SkyBlock Id", + "firmament.config.power-user.copy-item-id.description": "Press this button to copy the NEU repo SkyBlock id. This is not the raw id, but instead contains some extra transformations for things like runes, pets and enchant books.", + "firmament.config.power-user.copy-item-stack": "Copy ItemStack", + "firmament.config.power-user.copy-item-stack.description": "Copy the entire item stack metadata. This can be used in tests to recreate the item in code.", + "firmament.config.power-user.copy-lore": "Copy Name + Lore", + "firmament.config.power-user.copy-lore.description": "Copy the items name and lore as json encoded text", + "firmament.config.power-user.copy-nbt-data": "Copy ExtraAttributes data", + "firmament.config.power-user.copy-nbt-data.description": "Copy only the ExtraAttributes, so the hypixel specific item data.", + "firmament.config.power-user.copy-skull-texture": "Copy Placed Skull Id", + "firmament.config.power-user.copy-skull-texture.description": "Copy the texture location that can be used to re-texture the skull under your cross-hair.", + "firmament.config.power-user.copy-texture-pack-id": "Copy Texture Pack Id", + "firmament.config.power-user.copy-texture-pack-id.description": "Copy the texture pack id that is used for the item stack under your cursor.", + "firmament.config.power-user.copy-title": "Copy Inventory Title", + "firmament.config.power-user.copy-title.description": "Copies Inventory and Screen Titles", + "firmament.config.power-user.dont-highlight-semicolon-items": "Remove Semicolon Highlight", + "firmament.config.power-user.dont-highlight-semicolon-items.description": "Removes the highlight from items that contain a semicolon e.g Pets or Enchanted Books.", + "firmament.config.power-user.entity-data": "Show Entity Data", + "firmament.config.power-user.entity-data.description": "Print out information about the entity under your cross-hair.", + "firmament.config.power-user.export-item-stack": "Export Item Stack", + "firmament.config.power-user.export-item-stack.description": "Exports the hovered item to the repo data folder", + "firmament.config.power-user.export-npc-location": "Export NPC Location", + "firmament.config.power-user.export-npc-location.description": "Export the NPC's location to the repo data", + "firmament.config.power-user.export-recipe": "Export Recipe Data", + "firmament.config.power-user.export-recipe.description": "Export Recipe Data to the repo data", + "firmament.config.power-user.highlight-non-overlay": "Highlight Missing Items", + "firmament.config.power-user.highlight-non-overlay.description": "Highlights items that don't exist in the repo.", + "firmament.config.power-user.show-item-id": "Show SkyBlock Ids", + "firmament.config.power-user.show-item-id.description": "Show the SkyBlock id of items underneath them.", + "firmament.config.price-data": "Price Data", + "firmament.config.price-data.avg-lowest-bin-days": "AVG Lowest Bin Days", + "firmament.config.price-data.avg-lowest-bin-days.choice.off": "Off", + "firmament.config.price-data.avg-lowest-bin-days.choice.onedayavglowestbin": "1 Day", + "firmament.config.price-data.avg-lowest-bin-days.choice.sevendayavglowestbin": "7 Days", + "firmament.config.price-data.avg-lowest-bin-days.choice.threedayavglowestbin": "3 Days", + "firmament.config.price-data.avg-lowest-bin-days.description": "Select if and for how long the AVG Lowest BIN should show.", + "firmament.config.price-data.enable-always": "Enable Item Price", + "firmament.config.price-data.enable-always.description": "Show item auction/bazaar prices on SkyBlock items", + "firmament.config.price-data.enable-keybind": "Enable only with Keybinding", + "firmament.config.price-data.enable-keybind.description": "Only show auction/bazaar prices when holding this keybinding. Unbind to always show.", + "firmament.config.price-data.stack-size-keybind": "Stack Size Multiplier Keybinding", + "firmament.config.price-data.stack-size-keybind.description": "Press this key while hovering over an item to show its price multiplied by the number of items you have.", + "firmament.config.pristine-profit": "Pristine Profit Tracker", + "firmament.config.pristine-profit.fine-gemstones": "Use Fine Gemstones", + "firmament.config.pristine-profit.fine-gemstones.description": "Use the (more stable) price of fine gemstones, instead of flawed gemstones.", + "firmament.config.pristine-profit.position": "Pristine Profit HUD", + "firmament.config.pristine-profit.position.description": "Edit the pristine profit hud location.", + "firmament.config.pristine-profit.timeout": "Timeout (0 = disabled)", + "firmament.config.pristine-profit.timeout.description": "Track the profit you make from pristine gemstones while mining. Set to 0 seconds to disable the HUD.", + "firmament.config.quick-commands": "Quick Commands", + "firmament.config.quick-commands.dh": "Enable /dh", + "firmament.config.quick-commands.dh.description": "Warps you to the dungeon hub.", + "firmament.config.quick-commands.join": "Enable /join", + "firmament.config.quick-commands.join.description": "Join various types of instances like dungeons using short hands like /join f1, /join k1, /join m7", + "firmament.config.repo": "Firmament Repo Settings", + "firmament.config.repo.autoUpdate": "Auto Update", + "firmament.config.repo.autoUpdate.description": "Automatically download new items for the item list on every startup.", + "firmament.config.repo.branch": "Repo Branch", + "firmament.config.repo.branch.description": "Git Branch to pull item data from.", + "firmament.config.repo.branch.hint": "dangerous", + "firmament.config.repo.disable-item-groups": "Disable Item Groups", + "firmament.config.repo.disable-item-groups.description": "Disabling item groups can increase performance, but will no longer collect similar items (like minions, enchantments) together.", + "firmament.config.repo.enable-rei": "Enable REI", + "firmament.config.repo.enable-rei.description": "REI is required for viewing Firmaments item list. If you want to use another item list provider like SkyBlockers, you can turn it off here. Without other mods this will make you revert back to the vanilla item list.", + "firmament.config.repo.enable-super-craft": "Always use Super Craft", + "firmament.config.repo.enable-super-craft.description": "Always use super craft when clicking the craft button in REI, instead of just when holding shift.", + "firmament.config.repo.perfect-renders": "Perfect Render", + "firmament.config.repo.perfect-renders.choice.nothing": "Broken (Fastest)", + "firmament.config.repo.perfect-renders.choice.render": "Fixed Visual (Fast)", + "firmament.config.repo.perfect-renders.choice.render_and_text": "Perfect (Slowest)", + "firmament.config.repo.perfect-renders.description": "Speed up item list loading by allowing items to be loaded in partially incorrectly at first. They will be corrected down the line when the background reload completes.", + "firmament.config.repo.redownload": "Redownload Item List", + "firmament.config.repo.redownload.description": "Force re-download the item list. This is automatically done on restart.", + "firmament.config.repo.reload": "Reload Item List", + "firmament.config.repo.reload.description": "Force reload the item list. This will not download the item list again.", + "firmament.config.repo.reponame": "Repo Name", + "firmament.config.repo.reponame.description": "The GitHub repository name to pull item data from.", + "firmament.config.repo.reponame.hint": "NotEnoughUpdates-REPO", + "firmament.config.repo.reset": "Reset", + "firmament.config.repo.reset.description": "Reset the Git download location.", + "firmament.config.repo.username": "Repo Username", + "firmament.config.repo.username.description": "The GitHub repository organization to pull item data from.", + "firmament.config.repo.username.hint": "NotEnoughUpdates", + "firmament.config.repo.warn-for-missing-item-list-mod": "Warn for missing item list", + "firmament.config.repo.warn-for-missing-item-list-mod.description": "Warn if there is no compatible item list mod, like REI, installed.", + "firmament.config.save-cursor-position": "Save Cursor Position", + "firmament.config.save-cursor-position.enable": "Enable", + "firmament.config.save-cursor-position.enable.description": "Save your cursor position when switching GUIs.", + "firmament.config.save-cursor-position.tolerance": "Tolerance", + "firmament.config.save-cursor-position.tolerance.description": "Select how long your cursor position last between GUIs before resetting back to the middle of the screen.", + "firmament.config.slot-locking": "Slot Locking", + "firmament.config.slot-locking.bind": "Bind Slot", + "firmament.config.slot-locking.bind-render": "Show Slot Bindings", + "firmament.config.slot-locking.bind-render.choice.everything": "Always", + "firmament.config.slot-locking.bind-render.choice.nothing": "Only when hovered", + "firmament.config.slot-locking.bind-render.choice.only_boxes": "Only boxes", + "firmament.config.slot-locking.bind-render.description": "Disable rendering of the slot binding lines (or all of the slot binding rendering), unless the relevant slot is being hovered.", + "firmament.config.slot-locking.bind.description": "Bind a hotbar slot to another slot. This allows quick switching between the slots by shift clicking on either slot.", + "firmament.config.slot-locking.drop-in-dungeons": "Allow Dungeon Abilities", + "firmament.config.slot-locking.drop-in-dungeons.description": "Allow dropping items in dungeons, to use your dungeon ultimate abilities.", + "firmament.config.slot-locking.hunting-box": "Protect Hunting Box", + "firmament.config.slot-locking.hunting-box.description": "The Hunting Box frequently changes its UUID, and as such cannot be reliably protected using UUID locking. Instead this option can be used to block from dropping any Hunting Box.", + "firmament.config.slot-locking.lock": "Lock Slot", + "firmament.config.slot-locking.lock-uuid": "Lock UUID (Lock Item)", + "firmament.config.slot-locking.lock-uuid.description": "Lock a SkyBlock item by it's UUID. This blocks a specific item from being dropped/sold, but still allows moving it around.", + "firmament.config.slot-locking.lock.description": "Lock a slot, preventing any item from being moved from, dropped from, or placed into this slot.", + "firmament.config.slot-locking.multi-bind": "Allow Multi Binding", + "firmament.config.slot-locking.multi-bind.description": "Allow binding the same hotbar slot to multiple inventory slots.", + "firmament.config.slot-locking.require-quick-move": "Require Shift-Click for Bound Slots", + "firmament.config.slot-locking.require-quick-move.description": "If turned off, allows to switch between bound slots without holding shift.", + "firmament.config.storage-overlay": "Storage Overlay", + "firmament.config.storage-overlay.always-replace": "Always Open Overlay", + "firmament.config.storage-overlay.always-replace.description": "Always replace the ender chest with Firmament's storage overlay.", + "firmament.config.storage-overlay.block-item-scrolling": "Block Scrolling on Items", + "firmament.config.storage-overlay.block-item-scrolling.description": "Disables scrolling the storage overlay screen while you are hovering over an item. Useful if you have a tooltip scrolling mod.", + "firmament.config.storage-overlay.height": "Storage Height", + "firmament.config.storage-overlay.height.description": "The height of the scrollable storage panel.", + "firmament.config.storage-overlay.highlight-search-results": "Highlight Search Results", + "firmament.config.storage-overlay.highlight-search-results-colour": "Highlight Search Colour", + "firmament.config.storage-overlay.highlight-search-results-colour.description": "Change the colour of the highlighted search result.", + "firmament.config.storage-overlay.highlight-search-results.description": "Highlight the search results in the ender chest overlay.", + "firmament.config.storage-overlay.inverse-scroll": "Invert Scroll", + "firmament.config.storage-overlay.inverse-scroll.description": "Invert the mouse wheel scrolling in Firmament's storage overlay.", + "firmament.config.storage-overlay.margin": "Margin", + "firmament.config.storage-overlay.margin.description": "Margin inside of the storage overview.", + "firmament.config.storage-overlay.outline-active-page": "Outline Active Page", + "firmament.config.storage-overlay.outline-active-page-colour": "Outline Colour", + "firmament.config.storage-overlay.outline-active-page-colour.description": "Change the colour of the border around your selected storage page.", + "firmament.config.storage-overlay.outline-active-page.description": "Put a border around the selected storage page in the storage overlay.", + "firmament.config.storage-overlay.padding": "Padding", + "firmament.config.storage-overlay.padding.description": "Padding inside of the storage overview.", + "firmament.config.storage-overlay.retain-scroll": "Retain Scroll Position", + "firmament.config.storage-overlay.retain-scroll.description": "Retain scroll position when closing storage overlay and overview.", + "firmament.config.storage-overlay.rows": "Columns", + "firmament.config.storage-overlay.rows.description": "Max columns used by the storage overlay and overview.", + "firmament.config.storage-overlay.scroll-speed": "Scroll Speed", + "firmament.config.storage-overlay.scroll-speed.description": "Scroll speed inside of the storage overlay and overview.", + "firmament.config.wardrobe-keybinds": "Wardrobe Keybinds", + "firmament.config.wardrobe-keybinds.change-page": "Change Page", + "firmament.config.wardrobe-keybinds.change-page.description": "Changes the active page", + "firmament.config.wardrobe-keybinds.next-page": "Next Page", + "firmament.config.wardrobe-keybinds.next-page.description": "Goes to the next page", + "firmament.config.wardrobe-keybinds.previous-page": "Previous Page", + "firmament.config.wardrobe-keybinds.previous-page.description": "Goes to the previous page", + "firmament.config.wardrobe-keybinds.slot-1": "Slot 1", + "firmament.config.wardrobe-keybinds.slot-1.description": "Keybind to toggle the first set in your wardrobe", + "firmament.config.wardrobe-keybinds.slot-2": "Slot 2", + "firmament.config.wardrobe-keybinds.slot-2.description": "Keybind to toggle the second set in your wardrobe", + "firmament.config.wardrobe-keybinds.slot-3": "Slot 3", + "firmament.config.wardrobe-keybinds.slot-3.description": "Keybind to toggle the third set in your wardrobe", + "firmament.config.wardrobe-keybinds.slot-4": "Slot 4", + "firmament.config.wardrobe-keybinds.slot-4.description": "Keybind to toggle the fourth set in your wardrobe", + "firmament.config.wardrobe-keybinds.slot-5": "Slot 5", + "firmament.config.wardrobe-keybinds.slot-5.description": "Keybind to toggle the fifth set in your wardrobe", + "firmament.config.wardrobe-keybinds.slot-6": "Slot 6", + "firmament.config.wardrobe-keybinds.slot-6.description": "Keybind to toggle the sixth set in your wardrobe", + "firmament.config.wardrobe-keybinds.slot-7": "Slot 7", + "firmament.config.wardrobe-keybinds.slot-7.description": "Keybind to toggle the seventh set in your wardrobe", + "firmament.config.wardrobe-keybinds.slot-8": "Slot 8", + "firmament.config.wardrobe-keybinds.slot-8.description": "Keybind to toggle the eighth set in your wardrobe", + "firmament.config.wardrobe-keybinds.slot-9": "Slot 9", + "firmament.config.wardrobe-keybinds.slot-9.description": "Keybind to toggle the ninth set in your wardrobe", + "firmament.config.wardrobe-keybinds.wardrobe-keybinds": "Keybinds for your wardrobe", + "firmament.config.wardrobe-keybinds.wardrobe-keybinds.description": "Lets you use your number keys to quickly change your wardrobe", + "firmament.config.waypoints": "Waypoints", + "firmament.config.waypoints.reset-order-on-swap": "Reset Ordered Waypoints On Hop", + "firmament.config.waypoints.reset-order-on-swap.description": "Resets Ordered Waypoint progress after swapping to another world.", + "firmament.config.waypoints.show-index": "Show ordered waypoint indexes", + "firmament.config.waypoints.show-index.description": "Show the number of an ordered waypoint in the world.", + "firmament.config.waypoints.skip-to-nearest": "Allow skipping waypoints", + "firmament.config.waypoints.skip-to-nearest.description": "Allow skipping waypoints in an ordered list. If disabled you will need to walk to the next point, before being allowed to the next one after.", + "firmament.config.waypoints.temp-waypoint-duration": "Temporary Waypoint Duration", + "firmament.config.waypoints.temp-waypoint-duration.description": "Display waypoints sent by other players using /firm sendcoords in the world.", + "firmament.debug.skyblockid": "SkyBlock ID: %s", + "firmament.debug.skyblockid.copy": "Click to copy SkyBlock ID", + "firmament.dev.resourcerebuild.done": "Gradle resource rebuild done in %s", + "firmament.dev.resourcerebuild.start": "Invoking gradle resource rebuild (./gradlew :processResources)", + "firmament.download": "Click here to download %s", + "firmament.download.already": " (Already downloaded)", + "firmament.fixes.auto-sprint.not-sprinting": "Not Sprinting", + "firmament.fixes.auto-sprint.on": "Sprint toggled", + "firmament.fixes.auto-sprint.sprinting": "Sprinting", + "firmament.hotmpreset.copied": "Collected all HOTM perks to clipboard. Use /firm importhotm to import.", + "firmament.hotmpreset.failedimport": "Could not find a HOTM perk preset in your clipboard. You can export your current HOTM perks with /firm exporthotm", + "firmament.hotmpreset.okayimport": "Imported a HOTM perk preset.", + "firmament.hotmpreset.openinghotm": "Opening /hotm menu for export.", + "firmament.hotmpreset.scrolled": "Just scrolled. Waiting on server to update items.", + "firmament.hotmpreset.scrollprompt": "We need to scroll! Please click anywhere to continue.", + "firmament.hud.edit": "Edit %s", + "firmament.inventory-buttons.all-warps-preset": "All Warps Preset", + "firmament.inventory-buttons.delete": "Hold L-CTRL and click to delete", + "firmament.inventory-buttons.import-failed": "One of your buttons could only be imported partially", + "firmament.inventory-buttons.info": "Hold SHIFT to grid align", + "firmament.inventory-buttons.load-preset": "Load Preset", + "firmament.inventory-buttons.reset": "Reset buttons", + "firmament.inventory-buttons.save-preset": "Save Preset", + "firmament.inventory-buttons.simple-preset": "Simple Preset", + "firmament.key.category": "Firmament", + "firmament.keybinding.external": "%s", + "firmament.modapi.event": "Received mod API event: %s", + "firmament.poweruser.entity.armor": "Entity Armor:", + "firmament.poweruser.entity.armor.item": " - %s", + "firmament.poweruser.entity.fail": "No entity found under cursor", + "firmament.poweruser.entity.name": "Entity Name: %s", + "firmament.poweruser.entity.passengers": "%s Passengers", + "firmament.poweruser.entity.position": "Position: %s", + "firmament.poweruser.entity.type": "Entity Type: %s", + "firmament.price": "Checking price for %s", + "firmament.price.bazaar": "Bazaar stats:", + "firmament.price.bazaar.buy.order": "Buy orders: %d", + "firmament.price.bazaar.buy.price": "Buy Price: %s", + "firmament.price.bazaar.productid": "Stock id: %s", + "firmament.price.bazaar.sell.order": "Sell orders: %d", + "firmament.price.bazaar.sell.price": "Sell Price: %s", + "firmament.price.lowestbin": "Lowest BIN: %s", + "firmament.pristine-profit.collection": "Collection: %s/h", + "firmament.pristine-profit.money": "Money: %s/h", + "firmament.pv.lookingup": "Looking up %s", + "firmament.pv.noplayer": "%s is not a Minecraft player", + "firmament.pv.noprofile": "%s has no SkyBlock profiles", + "firmament.pv.pets": "Pets", + "firmament.pv.skills": "Skills", + "firmament.pv.skills.alchemy": "Alchemy", + "firmament.pv.skills.carpentry": "Carpentry", + "firmament.pv.skills.combat": "Combat", + "firmament.pv.skills.enchanting": "Enchanting", + "firmament.pv.skills.farming": "Farming", + "firmament.pv.skills.fishing": "Fishing", + "firmament.pv.skills.foraging": "Foraging", + "firmament.pv.skills.mining": "Mining", + "firmament.pv.skills.rift": "Rift", + "firmament.pv.skills.runecrafting": "Runecrafting", + "firmament.pv.skills.social": "Social", + "firmament.pv.skills.taming": "Taming", + "firmament.pv.skills.total": "Total Exp: %s", + "firmament.quick-commands.join.explain": "Join a dungeon or kuudra floor by using commands like /join f1, /join m7, /join fe or /join khot", + "firmament.quick-commands.join.success": "Joining: %s", + "firmament.quick-commands.join.unknown": "Could not find instance for %s", + "firmament.quick-commands.join.unknown-catacombs": "Unknown catacombs floor %s", + "firmament.quick-commands.join.unknown-kuudra": "Unknown kuudra floor %s", + "firmament.recipe.forge.time": "Forging Time: %s", + "firmament.recipe.mobs.coins": "§eCoins: %s", + "firmament.recipe.mobs.combat": "§bCombat Experience: %s", + "firmament.recipe.mobs.drops": "§e§lDrop Chance: %s", + "firmament.recipe.mobs.exp": "§6Experience: %s", + "firmament.recipe.mobs.name": "§8[§7Lv %d§8] §c%s", + "firmament.recipe.mobs.name.nolevel": "§c%s", + "firmament.recipe.novanilla": "Hypixel cannot super craft vanilla recipes", + "firmament.reiwarning": "Firmament needs RoughlyEnoughItems to display its item list!", + "firmament.reiwarning.disable": "Click here to disable this warning", + "firmament.reiwarning.disabled": "Disabled the RoughlyEnoughItems warning. Keep in mind that you will not have an item list without REI.", + "firmament.repo.brokenitem": "Failed to render item: %s", + "firmament.repo.cache": "Recaching items", + "firmament.repo.reload.disk": "Reloading repository from disk. This may lag a bit.", + "firmament.repo.reload.network": "Trying to redownload the repository", + "firmament.sbinfo.gametype": "Locraw Gametype: %s", + "firmament.sbinfo.map": "Locraw Map: %s", + "firmament.sbinfo.mode": "Locraw Mode: %s", + "firmament.sbinfo.nolocraw": "No locraw data available", + "firmament.sbinfo.profile": "Current profile cutename: %s", + "firmament.sbinfo.server": "Locraw Server: %s", + "firmament.toggle.false": "Off", + "firmament.toggle.true": "On", + "firmament.tooltip.copied.lore": "Copied Name and Lore", + "firmament.tooltip.copied.modelid": "Copied Texture Id: %s", + "firmament.tooltip.copied.modelid.fail": "Failed to copy Texture Id", + "firmament.tooltip.copied.nbt": "Copied NBT data", + "firmament.tooltip.copied.skull": "Copied Skull Id: %s", + "firmament.tooltip.copied.skull-id": "Copied Skull Id: %s", + "firmament.tooltip.copied.skull-id.fail.no-profile": "Skull has no profile", + "firmament.tooltip.copied.skull-id.fail.no-skull": "That isn't a skull", + "firmament.tooltip.copied.skull-id.fail.no-texture": "Skull has no texture", + "firmament.tooltip.copied.skull.fail": "Failed to copy skull id.", + "firmament.tooltip.copied.skyblockid": "Copied SkyBlock Id: %s", + "firmament.tooltip.copied.skyblockid.fail": "Failed to copy SkyBlock Id", + "firmament.tooltip.copied.stack": "Copied ItemStack", + "firmament.tooltip.skyblockid": "SkyBlock Id: %s", + "firmament.ursa.debugrequest.result": "Ursa request succeeded: %s", + "firmament.ursa.debugrequest.start": "Ursa request launched", + "firmament.warp-util.already-close": "Already closer to destination than /warp %s", + "firmament.warp-util.attempting-to-warp": "Trying to warp to /warp %s", + "firmament.warp-util.clear-excluded": "Marked all /warp commands as potentially available.", + "firmament.warp-util.mark-excluded": "Firmament: Tried to warp to %s, but it was not unlocked. I will avoid warping there again.", + "firmament.warp-util.no-warp-found": "Could not find an unlocked warp in %s", + "firmament.waypoint.temporary": "Temporary Waypoint: %s", + "zzzzzzzzz.lastentry": "Here so every real firmament entry has a trailing ," +} diff --git a/translations/extra.json b/translations/extra.json new file mode 100644 index 0000000..cb21fc9 --- /dev/null +++ b/translations/extra.json @@ -0,0 +1,6 @@ +{ + // These are require by jade, but i don't think they are actually rendered in game. + // Jade throws exceptions if they are not present however. + "config.jade.plugin_firmament.toolprovider": "Firmament Tool Provider", + "config.jade.plugin_firmament.custom_mining_hardness": "Firmament Mining Hardness" +} diff --git a/translations/extra_zh_cn.json b/translations/extra_zh_cn.json new file mode 100644 index 0000000..43abe89 --- /dev/null +++ b/translations/extra_zh_cn.json @@ -0,0 +1,6 @@ +{ + // These are require by jade, but i don't think they are actually rendered in game. + // Jade throws exceptions if they are not present however. + "config.jade.plugin_firmament.toolprovider": "Firmament工具提供者", + "config.jade.plugin_firmament.custom_mining_hardness": "Firmament挖掘硬度" +} diff --git a/translations/zh_cn.json b/translations/zh_cn.json new file mode 100644 index 0000000..5759b64 --- /dev/null +++ b/translations/zh_cn.json @@ -0,0 +1,552 @@ +{ + "firmament.carnival.tutorial.minesweeper": "§e点击这里查看这个小游戏的教程!", + "firmament.command.toggle.no-config-found": "找不到配置%s", + "firmament.command.toggle.no-property-found": "找不到属性%s", + "firmament.command.toggle.not-a-toggle": "属性%s不是一个开关", + "firmament.command.toggle.toggled": "已切换%s/%s%s", + "firmament.command.waypoint.added": "已添加路径点%s%s%s。", + "firmament.command.waypoint.clear": "已清除路径点。", + "firmament.command.waypoint.import": "从剪贴板导入了%s个路径点。", + "firmament.command.waypoint.ordered.toggle.false": "已禁用有序路径点", + "firmament.command.waypoint.ordered.toggle.true": "已启用有序路径点", + "firmament.command.waypoint.remove": "已移除路径点%s。其他路径点的索引可能会变化。", + "firmament.command.waypoint.remove.error": "索引对应的路径点不存在,无法删除。", + "firmament.command.waypoint.skip": "跳过了1个路径点", + "firmament.command.waypoint.skip.error": "无法跳过路径点。是不是没有使用有序路径点或没有加载了数据?", + "firmament.config.all-configs": "- 所有配置 -", + "firmament.config.anniversary": "周年活动功能", + "firmament.config.anniversary.pig-hud": "闪亮猪HUD", + "firmament.config.anniversary.pig-hud.description": "一个显示闪亮猪掉落奖励的HUD", + "firmament.config.anniversary.shiny-pigs": "闪亮猪追踪器", + "firmament.config.anniversary.shiny-pigs.description": "追踪闪亮猪的奖励,并显示抓猪的限时。", + "firmament.config.auto-completions": "Hypixel命令优化", + "firmament.config.auto-completions.warp-complete": "自动补全/warp", + "firmament.config.auto-completions.warp-complete.description": "在聊天中自动补全warp目的地。这可能包括你尚未解锁的地点。", + "firmament.config.auto-completions.warp-is": "将/warp is重定向到/warp island", + "firmament.config.auto-completions.warp-is.description": "将/warp is重定向到/warp island,因为hypixel不识别/warp is为warp目的地。", + "firmament.config.block-zapper-overlay": "Block Zapper Overlay", + "firmament.config.block-zapper-overlay.block-zapper-overlay": "Block Zapper Overlay", + "firmament.config.block-zapper-overlay.block-zapper-overlay.description": "Shows what blocks will be zapped", + "firmament.config.block-zapper-overlay.color": "Colour", + "firmament.config.block-zapper-overlay.color.description": "The color that the blocks will be highlighted in", + "firmament.config.block-zapper-overlay.undo-key": "Undo Keybind", + "firmament.config.block-zapper-overlay.undo-key.description": "Keybind to undo your zap", + "firmament.config.bonemerang-overlay": "骨回旋镖覆盖层", + "firmament.config.bonemerang-overlay.bonemerang-overlay": "骨回旋镖覆盖层", + "firmament.config.bonemerang-overlay.bonemerang-overlay-hud": "骨回旋镖覆盖层HUD", + "firmament.config.bonemerang-overlay.bonemerang-overlay-hud.description": "显示你的骨回旋镖将击中多少目标", + "firmament.config.bonemerang-overlay.bonemerang-overlay.description": "显示骨回旋镖的信息。", + "firmament.config.bonemerang-overlay.bonemerang-overlay.display": "骨回旋镖目标:%s", + "firmament.config.bonemerang-overlay.highlight-hit-entities": "高亮目标实体", + "firmament.config.bonemerang-overlay.highlight-hit-entities.description": "高亮将被击中的实体", + "firmament.config.carnival": "嘉年华功能", + "firmament.config.carnival.bombs-solver": "扫雷助手", + "firmament.config.carnival.bombs-solver.description": "在扫雷中显示每个方块周围的炸弹。", + "firmament.config.carnival.tutorials": "教程提醒", + "firmament.config.carnival.tutorials.description": "每次开始Firmament提供了教程的游戏时,显示一个教程提示。", + "firmament.config.category.chat": "聊天", + "firmament.config.category.chat.description": "聊天相关功能", + "firmament.config.category.dev": "开发与调试", + "firmament.config.category.dev.description": "纹理包开发者和程序员使用的设置", + "firmament.config.category.events": "活动", + "firmament.config.category.events.description": "各种游戏活动的设置", + "firmament.config.category.garden": "花园", + "firmament.config.category.garden.description": "花园相关功能", + "firmament.config.category.integrations": "联动与纹理", + "firmament.config.category.integrations.description": "与其他模组和纹理包的联动", + "firmament.config.category.inventory": "物品栏", + "firmament.config.category.inventory.description": "物品栏,箱子和各UI功能", + "firmament.config.category.items": "物品", + "firmament.config.category.items.description": "物品相关功能", + "firmament.config.category.meta": "元数据与Firmament", + "firmament.config.category.meta.description": "Firmament和数据仓库的设置", + "firmament.config.category.mining": "挖矿", + "firmament.config.category.mining.description": "挖矿相关功能", + "firmament.config.category.misc": "杂项", + "firmament.config.category.misc.description": "一些杂项功能", + "firmament.config.centuryraffle": "世纪抽奖", + "firmament.config.centuryraffle.highlight-cake-players": "高亮队伍中的玩家", + "firmament.config.centuryraffle.highlight-cake-players.description": "高亮可以赠与手持世纪蛋糕片的玩家", + "firmament.config.chat-links": "聊天链接", + "firmament.config.chat-links.allow-all-hosts": "允许所有图片来源", + "firmament.config.chat-links.allow-all-hosts.description": "允许显示任何来源的图片。", + "firmament.config.chat-links.allowed-hosts": "允许的图片来源", + "firmament.config.chat-links.allowed-hosts.description": "防止你从其他服务器请求图片,以防止你的IP泄露。", + "firmament.config.chat-links.image-enabled": "启用图片预览", + "firmament.config.chat-links.image-enabled.description": "在聊天中将鼠标悬停在链接上时显示图片预览", + "firmament.config.chat-links.links-enabled": "启用可点击链接", + "firmament.config.chat-links.links-enabled.description": "使聊天中的链接可点击", + "firmament.config.chat-links.position": "聊天图片预览", + "firmament.config.chat-links.position.description": "编辑图片显示位置", + "firmament.config.commissions": "委托任务", + "firmament.config.commissions.highlight-completed": "高亮已完成", + "firmament.config.commissions.highlight-completed.description": "在委托菜单中高亮已完成的委托任务", + "firmament.config.compatibility": "兼容功能", + "firmament.config.compatibility.explosion-enabled": "接管爆炸粒子", + "firmament.config.compatibility.explosion-enabled.description": "接管爆炸粒子的渲染。", + "firmament.config.compatibility.explosion-power": "增强爆炸威力", + "firmament.config.compatibility.explosion-power.description": "选择爆炸粒子的大小", + "firmament.config.composter": "堆肥桶", + "firmament.config.composter.no-more-noises": "静音堆肥桶", + "firmament.config.composter.no-more-noises.description": "消除堆肥桶发出的所有噪音和声音", + "firmament.config.configconfig": "Firmament配置", + "firmament.config.configconfig.enable-moulconfig": "使用MoulConfig", + "firmament.config.configconfig.enable-moulconfig.description": "使用MoulConfig配置UI。关闭以回退到内置配置。", + "firmament.config.configconfig.enable-yacl": "使用YACL配置", + "firmament.config.configconfig.enable-yacl.description": "使用YACL配置UI。关闭以回退到内置配置。需要单独安装YACL。", + "firmament.config.configconfig.wide-moulconfig": "更宽的MoulConfig UI", + "firmament.config.configconfig.wide-moulconfig.description": "使MoulConfig的UI变得更宽", + "firmament.config.copy-chat": "复制聊天", + "firmament.config.copy-chat.copy-chat": "复制聊天", + "firmament.config.copy-chat.copy-chat.description": "右键消息进行复制", + "firmament.config.custom-skyblock-textures": "自定义空岛物品纹理", + "firmament.config.custom-skyblock-textures.armor-overrides": "启用盔甲纹理", + "firmament.config.custom-skyblock-textures.armor-overrides.description": "允许纹理包作者修改(但不能修改模型)空岛盔甲的纹理。", + "firmament.config.custom-skyblock-textures.block-overrides": "启用方块模型", + "firmament.config.custom-skyblock-textures.block-overrides.description": "允许纹理包作者根据方块位置和所处区域替换方块模型。", + "firmament.config.custom-skyblock-textures.cache-duration": "模型缓存持续时间", + "firmament.config.custom-skyblock-textures.cache-duration.description": "纹理模型应缓存多长时间。", + "firmament.config.custom-skyblock-textures.cache-forever": "禁用缓存清除", + "firmament.config.custom-skyblock-textures.cache-forever.description": "完全不清除缓存。若物品数据变化,服务器会向你发送新的物品数据,大概不会有问题。", + "firmament.config.custom-skyblock-textures.enabled": "启用自定义物品纹理", + "firmament.config.custom-skyblock-textures.enabled.description": "允许为纹理包替换物品。关闭此选项不会禁用自定义纹理匹配", + "firmament.config.custom-skyblock-textures.legacy-cit": "启用旧版CIT Resewn兼容性", + "firmament.config.custom-skyblock-textures.legacy-cit.description": "允许在更新版本上加载为 1.20.4 编写的CIT resewn纹理包。", + "firmament.config.custom-skyblock-textures.legacy-minecraft-path-support": "启用旧版纹理包路径", + "firmament.config.custom-skyblock-textures.legacy-minecraft-path-support.description": "允许纹理包以旧版的方式加载纹理。例如:允许在 1.21.4 上加载 1.21.0 的盔甲纹理。", + "firmament.config.custom-skyblock-textures.model-overrides": "启用模型覆盖/条件", + "firmament.config.custom-skyblock-textures.model-overrides.description": "启用Firmament的模型匹配。这也适用于原版模型,如果该原版模型具有Firmament的断言。", + "firmament.config.custom-skyblock-textures.recolor-text": "允许纹理包重新着色文本", + "firmament.config.custom-skyblock-textures.recolor-text.description": "允许纹理包重新着色UI文本。", + "firmament.config.custom-skyblock-textures.screen-layouts": "允许纹理包重新布局屏幕", + "firmament.config.custom-skyblock-textures.screen-layouts.description": "允许纹理包移动槽位等UI元素,以及替换屏幕背景。", + "firmament.config.custom-skyblock-textures.skulls-enabled": "启用自定义放置头颅纹理", + "firmament.config.custom-skyblock-textures.skulls-enabled.description": "允许替换放下的头颅的纹理。", + "firmament.config.developer": "开发者设置", + "firmament.config.developer-capes": "开发者披风", + "firmament.config.developer-capes.show-cape": "显示开发者披风", + "firmament.config.developer-capes.show-cape.description": "让你看到开发者披风。", + "firmament.config.developer.auto-rebuild": "自动重建资源", + "firmament.config.developer.auto-rebuild.description": "在执行F3+T之前执行./gradlew processResources。", + "firmament.config.diana": "Diana", + "firmament.config.diana.ancestral-spade": "Diana铲解析器", + "firmament.config.diana.ancestral-spade.description": "根据铲子的能力产生的粒子和声音,自动猜测你的下一个点位。", + "firmament.config.diana.ancestral-teleport": "传送至猜测点附近", + "firmament.config.diana.ancestral-teleport.description": "点击传送至猜测点附近。", + "firmament.config.diana.nearby-waypoints": "附近路径点高亮器", + "firmament.config.diana.nearby-waypoints.description": "高亮附近的Diana猜测点。", + "firmament.config.etherwarp-overlay": "Etherwarp显示", + "firmament.config.etherwarp-overlay.cube": "实心方块", + "firmament.config.etherwarp-overlay.cube-colour": "方块颜色", + "firmament.config.etherwarp-overlay.cube-colour.description": "选择Etherwarp目标方块的颜色。", + "firmament.config.etherwarp-overlay.cube.description": "使方块整体变色", + "firmament.config.etherwarp-overlay.etherwarp-overlay": "Etherwarp显示", + "firmament.config.etherwarp-overlay.etherwarp-overlay.description": "显示一个告诉你你将传送到哪个方块的覆盖层。", + "firmament.config.etherwarp-overlay.only-show-while-sneaking": "只在潜行时显示", + "firmament.config.etherwarp-overlay.only-show-while-sneaking.description": "仅在潜行时显示Etherwarp覆盖层。", + "firmament.config.etherwarp-overlay.wireframe": "空心方框", + "firmament.config.etherwarp-overlay.wireframe.description": "在指向方块上显示的轮廓", + "firmament.config.fairy-souls": "仙女之魂", + "firmament.config.fairy-souls.reset": "重置已收集仙女之魂", + "firmament.config.fairy-souls.reset.description": "重置所有已收集的仙女之魂,允许你从头开始。", + "firmament.config.fairy-souls.show": "显示仙女之魂路径点", + "firmament.config.fairy-souls.show.description": "显示你当前所在世界中未收集的仙女之魂路径点。", + "firmament.config.fishing-warning": "钓鱼警告", + "firmament.config.fishing-warning.display-warning": "当你即将钓到鱼时显示警告", + "firmament.config.fishing-warning.highlight-wake-chain": "高亮钓鱼粒子", + "firmament.config.fixes": "修复", + "firmament.config.fixes.auto-sprint": "自动疾跑", + "firmament.config.fixes.auto-sprint-hud": "疾跑状态HUD", + "firmament.config.fixes.auto-sprint-hud.description": "在屏幕上显示你当前的疾跑状态。仅在未设置自动疾跑键位时可见。", + "firmament.config.fixes.auto-sprint-keybinding": "自动疾跑键位", + "firmament.config.fixes.auto-sprint-keybinding.description": "通过此键位切换自动疾跑。", + "firmament.config.fixes.auto-sprint-underwater": "水下疾跑", + "firmament.config.fixes.auto-sprint-underwater.description": "也在水下切换疾跑。在水下疾跑会让你进入游泳动画,这会改变你的视角和碰撞箱,如果你经常停止和移动,这可能会令人困惑。", + "firmament.config.fixes.auto-sprint.description": "这与原版疾跑不同,它只在行走的第一刻标记键位被按下。", + "firmament.config.fixes.disable-hurt-cam": "关闭受伤时视角抖动", + "firmament.config.fixes.disable-hurt-cam.description": "禁用受伤害时的屏幕抖动动画。", + "firmament.config.fixes.hide-mob-effects": "隐藏药水效果", + "firmament.config.fixes.hide-mob-effects.description": "隐藏玩家物品栏右侧的药水效果。", + "firmament.config.fixes.hide-off-hand": "No Off Hand", + "firmament.config.fixes.hide-off-hand.description": "Remove the recipe slot from your inventory", + "firmament.config.fixes.hide-potion-effects-hud": "隐藏药水效果HUD", + "firmament.config.fixes.hide-potion-effects-hud.description": "隐藏右上角的药水效果HUD。", + "firmament.config.fixes.hide-recipe-book": "移除合成书", + "firmament.config.fixes.hide-recipe-book.description": "从你的物品栏中移除合成书", + "firmament.config.fixes.hide-slot-highlights": "隐藏插槽高亮", + "firmament.config.fixes.hide-slot-highlights.description": "隐藏禁用工具提示的物品的槽位高亮。这使得/sbmenu在使用平滑纹理包时看起来更好。", + "firmament.config.fixes.peek-chat": "窥视聊天", + "firmament.config.fixes.peek-chat.description": "按住此键位可以查看聊天,就像你打开了聊天框一样,但仍然可以控制你的角色。", + "firmament.config.fixes.player-skins": "修复未签名的玩家皮肤", + "firmament.config.fixes.player-skins.description": "将所有玩家皮肤标记为已签名,防止控制台垃圾信息和一些渲染问题。", + "firmament.config.hud": "HUD", + "firmament.config.hud.day-count": "显示天数", + "firmament.config.hud.day-count-hud": "显示天数HUD", + "firmament.config.hud.day-count-hud.description": "显示天数。", + "firmament.config.hud.day-count-hud.display": "天数:%s", + "firmament.config.hud.day-count.description": "一个显示当前天数的HUD。", + "firmament.config.hud.fps-count": "显示帧数", + "firmament.config.hud.fps-count-hud": "显示帧数HUD", + "firmament.config.hud.fps-count-hud.description": "显示帧数。", + "firmament.config.hud.fps-count-hud.display": "FPS:%s", + "firmament.config.hud.fps-count.description": "一个显示当前帧数的HUD。", + "firmament.config.hud.ping-count": "显示延迟", + "firmament.config.hud.ping-count-hud": "显示延迟HUD", + "firmament.config.hud.ping-count-hud.description": "显示延迟。", + "firmament.config.hud.ping-count-hud.display": "Ping:%s", + "firmament.config.hud.ping-count.description": "一个显示当前延迟的HUD。", + "firmament.config.inventory-buttons-config": "物品栏按钮", + "firmament.config.inventory-buttons-config.hover-text": "悬停工具提示", + "firmament.config.inventory-buttons-config.hover-text.description": "将鼠标悬停在物品栏按钮上会显示它们运行的命令。", + "firmament.config.inventory-buttons-config.only-inv": "仅物品栏", + "firmament.config.inventory-buttons-config.only-inv.description": "仅在物品栏中显示按钮", + "firmament.config.inventory-buttons-config.open-editor": "打开编辑器", + "firmament.config.inventory-buttons-config.open-editor.description": "点击任意位置创建一个物品栏按钮或编辑现有按钮。按住SHIFT可按网格对齐。", + "firmament.config.item-hotkeys": "物品快捷键", + "firmament.config.item-hotkeys.global-trade-interface": "在集市/拍卖行搜索", + "firmament.config.item-hotkeys.global-trade-interface.description": "按下此按钮可在集市或拍卖行搜索鼠标所指的物品。", + "firmament.config.item-rarity-cosmetics": "物品稀有度外观", + "firmament.config.item-rarity-cosmetics.background": "物品栏背景稀有度", + "firmament.config.item-rarity-cosmetics.background-hotbar": "快捷栏背景稀有度", + "firmament.config.item-rarity-cosmetics.background-hotbar.description": "在快捷栏中显示物品稀有度背景。", + "firmament.config.item-rarity-cosmetics.background.description": "根据物品稀有度在每个物品后面显示背景。", + "firmament.config.jade-integration": "Jade/WAILA集成", + "firmament.config.jade-integration.blocks": "启用自定义方块", + "firmament.config.jade-integration.blocks.description": "在Jade中显示自定义方块描述和硬度等级。", + "firmament.config.jade-integration.progress": "启用自定义挖矿进度", + "firmament.config.jade-integration.progress.description": "在存在挖掘疲劳的世界中让Jade显示自定义挖矿进度。", + "firmament.config.junk-highlighter": "Junk Highlighter", + "firmament.config.junk-highlighter.description": "Highlight items using regex to search for items in your inventory", + "firmament.config.junk-highlighter.highlight": "Highlight Keybind", + "firmament.config.junk-highlighter.highlight.description": "Highlight found items when this keybind is held", + "firmament.config.junk-highlighter.regex": "Search Regex", + "firmament.config.junk-highlighter.regex.description": "The RegEx (Regular Expression) to use when searching", + "firmament.config.lore-timers": "物品时间戳", + "firmament.config.lore-timers.format": "时间格式", + "firmament.config.lore-timers.format.choice.american": "§9美§f式", + "firmament.config.lore-timers.format.choice.local": "系统时间格式", + "firmament.config.lore-timers.format.choice.rfc": "RFC", + "firmament.config.lore-timers.format.choice.rfcprecise": "RFC (毫秒)", + "firmament.config.lore-timers.format.choice.socialist": "欧式", + "firmament.config.lore-timers.format.description": "选择解析计时器显示的时间格式。", + "firmament.config.lore-timers.show": "显示物品描述计时器", + "firmament.config.lore-timers.show-creation": "显示创建时间", + "firmament.config.lore-timers.show-creation.description": "显示物品的创建时间戳。有时此时间戳在升级物品时会保留,因此它不一定是此特定物品的制作时间,而是其组件之一的制作时间。", + "firmament.config.lore-timers.show.description": "显示物品描述中的计时器(例如利息、拍卖持续时间)何时结束。", + "firmament.config.party-commands": "队伍命令", + "firmament.config.party-commands.cooldown": "冷却时间", + "firmament.config.party-commands.cooldown.description": "设置冷却来防止命令刷屏。", + "firmament.config.party-commands.enable": "启用队伍命令", + "firmament.config.party-commands.enable.description": "允许你队伍中的人使用 !warp、!coords、!ptme 等命令。请参阅/firm partycommands获取列表", + "firmament.config.party-commands.ignore-own": "忽略自己的消息", + "firmament.config.party-commands.ignore-own.description": "防止你自己的消息触发队伍命令", + "firmament.config.pets": "宠物", + "firmament.config.pets.highlight-pet": "高亮当前宠物", + "firmament.config.pets.highlight-pet.description": "在/pets菜单中高亮你当前选择的宠物。", + "firmament.config.pets.pet-overlay": "宠物信息", + "firmament.config.pets.pet-overlay-hud": "宠物信息HUD", + "firmament.config.pets.pet-overlay-hud.description": "一个显示当前宠物和宠物经验的HUD。", + "firmament.config.pets.pet-overlay.description": "在屏幕上显示当前宠物和宠物经验。", + "firmament.config.pickaxe-info": "镐子和钻头", + "firmament.config.pickaxe-info.ability-cooldown": "镐子能力冷却", + "firmament.config.pickaxe-info.ability-cooldown-toast": "镐子能力就绪提示", + "firmament.config.pickaxe-info.ability-cooldown-toast.description": "当你的镐子能力准备就绪时显示一个提示。", + "firmament.config.pickaxe-info.ability-cooldown.description": "在你的准星上显示镐子能力的冷却时间。", + "firmament.config.pickaxe-info.ability-scale": "能力冷却缩放", + "firmament.config.pickaxe-info.ability-scale.description": "调整准星旁镐子能力冷却时间的大小。", + "firmament.config.pickaxe-info.block-on-dynamic": "阻止在私人岛屿使用", + "firmament.config.pickaxe-info.block-on-dynamic.choice.always": "总是阻止", + "firmament.config.pickaxe-info.block-on-dynamic.choice.never": "从不阻止", + "firmament.config.pickaxe-info.block-on-dynamic.choice.only_destructive": "仅在危险时", + "firmament.config.pickaxe-info.block-on-dynamic.description": "阻止你在私人岛屿上右键镐子使用能力", + "firmament.config.pickaxe-info.disable-in-dungeons": "Disable cross-hair cooldown in Dungeons", + "firmament.config.pickaxe-info.disable-in-dungeons.description": "Disables the cooldown around your cross-hair while in Dungeons.", + "firmament.config.pickaxe-info.fuel-bar": "钻头燃料耐久度", + "firmament.config.pickaxe-info.fuel-bar.description": "用显示剩余燃料的耐久度条替换你的钻头的物品耐久度条。", + "firmament.config.power-user": "高级", + "firmament.config.power-user.copy-item-id": "复制空岛生存物品ID", + "firmament.config.power-user.copy-item-id.description": "按下此按钮复制NEU Repo空岛生存物品ID,会包含宠物品质,符文类型,附魔类型等额外信息。", + "firmament.config.power-user.copy-item-stack": "复制ItemStack", + "firmament.config.power-user.copy-item-stack.description": "复制ItemStack的数据。这可以在用于在代码中还原此组物品。", + "firmament.config.power-user.copy-lore": "复制名称+描述", + "firmament.config.power-user.copy-lore.description": "将物品名称和描述复制为JSON编码文本", + "firmament.config.power-user.copy-nbt-data": "复制ExtraAttributes数据", + "firmament.config.power-user.copy-nbt-data.description": "仅复制ExtraAttributes,即Hypixel独有的物品数据。", + "firmament.config.power-user.copy-skull-texture": "复制放置头颅ID", + "firmament.config.power-user.copy-skull-texture.description": "复制准心指向的放下的头颅的数据。", + "firmament.config.power-user.copy-texture-pack-id": "复制纹理包ID", + "firmament.config.power-user.copy-texture-pack-id.description": "复制用于鼠标指向物品的纹理包ID。", + "firmament.config.power-user.copy-title": "复制物品栏标题", + "firmament.config.power-user.copy-title.description": "复制物品栏和屏幕标题", + "firmament.config.power-user.dont-highlight-semicolon-items": "移除分号高亮", + "firmament.config.power-user.dont-highlight-semicolon-items.description": "移除ID包含分号的物品(例如宠物或附魔书)的高亮。", + "firmament.config.power-user.entity-data": "显示实体数据", + "firmament.config.power-user.entity-data.description": "打印出准星指向实体的信息。", + "firmament.config.power-user.export-item-stack": "导出ItemStack", + "firmament.config.power-user.export-item-stack.description": "将鼠标指向的档物品导出到仓库数据文件夹", + "firmament.config.power-user.export-npc-location": "导出NPC位置", + "firmament.config.power-user.export-npc-location.description": "将NPC的位置导出到数据仓库", + "firmament.config.power-user.export-recipe": "导出合成数据", + "firmament.config.power-user.export-recipe.description": "将合成数据导出到数据仓库", + "firmament.config.power-user.highlight-non-overlay": "高亮缺失物品", + "firmament.config.power-user.highlight-non-overlay.description": "高亮数据仓库中不存在的物品。", + "firmament.config.power-user.show-item-id": "显示空岛ID", + "firmament.config.power-user.show-item-id.description": "在物品下方显示其空岛物品ID。", + "firmament.config.price-data": "价格数据", + "firmament.config.price-data.avg-lowest-bin-days": "平均最低价格BIN物品天数", + "firmament.config.price-data.avg-lowest-bin-days.choice.off": "关闭", + "firmament.config.price-data.avg-lowest-bin-days.choice.onedayavglowestbin": "1天", + "firmament.config.price-data.avg-lowest-bin-days.choice.sevendayavglowestbin": "7天", + "firmament.config.price-data.avg-lowest-bin-days.choice.threedayavglowestbin": "3天", + "firmament.config.price-data.avg-lowest-bin-days.description": "选择是否以及显示最低BIN平均天数。", + "firmament.config.price-data.enable-always": "启用物品价格", + "firmament.config.price-data.enable-always.description": "在空岛物品上显示物品拍卖/集市价格", + "firmament.config.price-data.enable-keybind": "仅在按下绑定按键键时启用", + "firmament.config.price-data.enable-keybind.description": "仅在按住此绑定键时显示拍卖/集市价格。解绑以始终显示。", + "firmament.config.price-data.stack-size-keybind": "统计拥有物品总价的绑定按键", + "firmament.config.price-data.stack-size-keybind.description": "在悬停物品时按下此键可显示其价格乘以你拥有的物品数量。", + "firmament.config.pristine-profit": "Pristine收入追踪器", + "firmament.config.pristine-profit.fine-gemstones": "基于Fine宝石", + "firmament.config.pristine-profit.fine-gemstones.description": "使用Fine类型宝石(更稳定)的价格,而不是Flawed类型。", + "firmament.config.pristine-profit.position": "Pristine收入追踪器HUD", + "firmament.config.pristine-profit.position.description": "编辑Pristine收入追踪器位置。", + "firmament.config.pristine-profit.timeout": "间隔(0=禁用)", + "firmament.config.pristine-profit.timeout.description": "追踪你在挖矿时Pristine触发获得的利润。设置为0秒以禁用HUD。", + "firmament.config.quick-commands": "快捷命令", + "firmament.config.quick-commands.dh": "启用/dh", + "firmament.config.quick-commands.dh.description": "将你传送到地牢大厅。", + "firmament.config.quick-commands.join": "启用/join", + "firmament.config.quick-commands.join.description": "使用/join f1、/join k1、/join m7等简写加入各种类型的临时服务器,例如地牢。", + "firmament.config.repo": "Firmament数据仓库设置", + "firmament.config.repo.autoUpdate": "自动更新", + "firmament.config.repo.autoUpdate.description": "每次启动时自动下载新物品到物品列表。", + "firmament.config.repo.branch": "仓库分支", + "firmament.config.repo.branch.description": "从中拉取物品数据的Git分支。", + "firmament.config.repo.branch.hint": "危险/不稳定", + "firmament.config.repo.disable-item-groups": "禁用物品组", + "firmament.config.repo.disable-item-groups.description": "禁用物品组可以提高性能,但将不再把相似物品(如仆从、附魔)集合为一项显示。", + "firmament.config.repo.enable-rei": "启用REI", + "firmament.config.repo.enable-rei.description": "Firmament显示其品列表需要REI。若想使用其他物品列表提供模组(如SkyBlockers)可以在这里关闭它。如果没有此类模组启用后将显示原版物品列表。", + "firmament.config.repo.enable-super-craft": "始终使用Supercraft", + "firmament.config.repo.enable-super-craft.description": "在REI中点击合成按钮时,始终使用Supercraft,而不是只在按住shift时。", + "firmament.config.repo.perfect-renders": "完美渲染", + "firmament.config.repo.perfect-renders.choice.nothing": "损坏 (最快)", + "firmament.config.repo.perfect-renders.choice.render": "视觉修复 (快)", + "firmament.config.repo.perfect-renders.choice.render_and_text": "完美 (最慢)", + "firmament.config.repo.perfect-renders.description": "通过允许物品部分先不正确加载来加快物品列表加载速度。当后台重新加载完成时,它们稍后会恢复正常。", + "firmament.config.repo.redownload": "重新下载物品列表", + "firmament.config.repo.redownload.description": "强制重新下载物品列表。这会在重启后完成。", + "firmament.config.repo.reload": "重新加载物品列表", + "firmament.config.repo.reload.description": "强制重新加载物品列表。但不会再次下载物品列表。", + "firmament.config.repo.reponame": "数据仓库名称", + "firmament.config.repo.reponame.description": "从中拉取物品数据的GitHub仓库名称。", + "firmament.config.repo.reponame.hint": "NotEnoughUpdates-REPO", + "firmament.config.repo.reset": "重置", + "firmament.config.repo.reset.description": "重置Git拉取仓库。", + "firmament.config.repo.username": "仓库用户名", + "firmament.config.repo.username.description": "从中拉取物品数据的GitHub仓库用户。", + "firmament.config.repo.username.hint": "NotEnoughUpdates", + "firmament.config.repo.warn-for-missing-item-list-mod": "缺少物品列表模组警告", + "firmament.config.repo.warn-for-missing-item-list-mod.description": "如果未安装兼容的物品列表模组(如REI),则发出警告。", + "firmament.config.save-cursor-position": "保存光标位置", + "firmament.config.save-cursor-position.enable": "启用", + "firmament.config.save-cursor-position.enable.description": "切换GUI时保存光标位置。", + "firmament.config.save-cursor-position.tolerance": "容忍度", + "firmament.config.save-cursor-position.tolerance.description": "在复位到屏幕中间之前,选择光标在gui之间停留的时间。", + "firmament.config.slot-locking": "锁定物品栏槽位", + "firmament.config.slot-locking.bind": "绑定物品栏槽位", + "firmament.config.slot-locking.bind-render": "显示绑定的物品栏", + "firmament.config.slot-locking.bind-render.choice.everything": "总是显示", + "firmament.config.slot-locking.bind-render.choice.nothing": "仅在悬停时", + "firmament.config.slot-locking.bind-render.choice.only_boxes": "仅显示方框", + "firmament.config.slot-locking.bind-render.description": "只在相关槽位被悬停时才显示绑定的连线。", + "firmament.config.slot-locking.bind.description": "将快捷栏槽位绑定到另一个槽位。这允许通过按住 Shift 键点击任一插槽来快速切换插槽。", + "firmament.config.slot-locking.drop-in-dungeons": "允许在地牢中丢弃物品", + "firmament.config.slot-locking.drop-in-dungeons.description": "允许在地牢中丢弃物品来在地牢里开大。", + "firmament.config.slot-locking.hunting-box": "保护狩猎陷阱物品", + "firmament.config.slot-locking.hunting-box.description": "狩猎陷阱的UUID会变化,因此根据UUID去保护特定陷阱。此选项可用于阻止丢弃任何狩猎陷阱。", + "firmament.config.slot-locking.lock": "锁定槽位", + "firmament.config.slot-locking.lock-uuid": "锁定带有特定UUID的物品", + "firmament.config.slot-locking.lock-uuid.description": "通过UUID锁定空岛物品。这会阻止特定物品被丢弃/出售,但仍然允许移动它。", + "firmament.config.slot-locking.lock.description": "锁定一个槽位,防止槽位内物品被移动或丢弃,替换。", + "firmament.config.slot-locking.multi-bind": "允许多重绑定", + "firmament.config.slot-locking.multi-bind.description": "允许将同一个快捷栏槽位绑定到多个槽位。", + "firmament.config.slot-locking.require-quick-move": "只能使用Shift-Click来切换绑定的物品", + "firmament.config.slot-locking.require-quick-move.description": "如果关闭,无需按住shift就可以进行切换。", + "firmament.config.storage-overlay": "存储UI", + "firmament.config.storage-overlay.always-replace": "始终使用存储UI", + "firmament.config.storage-overlay.always-replace.description": "始终使用Firmament的存储UI替换原版末影箱显示。", + "firmament.config.storage-overlay.block-item-scrolling": "阻止滚动屏幕", + "firmament.config.storage-overlay.block-item-scrolling.description": "在你悬停在物品上时禁用存储UI的滚动。可以兼容物品描述滚动模组。", + "firmament.config.storage-overlay.height": "存储UI高度", + "firmament.config.storage-overlay.height.description": "可滚动的存储UI的高度。", + "firmament.config.storage-overlay.highlight-search-results": "高亮搜索结果", + "firmament.config.storage-overlay.highlight-search-results-colour": "高亮搜索颜色", + "firmament.config.storage-overlay.highlight-search-results-colour.description": "更改高亮搜索结果的颜色。", + "firmament.config.storage-overlay.highlight-search-results.description": "在存储UI中高亮搜索结果。", + "firmament.config.storage-overlay.inverse-scroll": "反转滚动", + "firmament.config.storage-overlay.inverse-scroll.description": "反转Firmament存储叠加中的鼠标滚轮滚动。", + "firmament.config.storage-overlay.margin": "边距", + "firmament.config.storage-overlay.margin.description": "存储概览内部的边距。", + "firmament.config.storage-overlay.outline-active-page": "选定页面轮廓", + "firmament.config.storage-overlay.outline-active-page-colour": "轮廓颜色", + "firmament.config.storage-overlay.outline-active-page-colour.description": "更改你选择的存储页面周围边框的颜色。", + "firmament.config.storage-overlay.outline-active-page.description": "在存储UI中为你选择的存储页面添加边框。", + "firmament.config.storage-overlay.padding": "填充", + "firmament.config.storage-overlay.padding.description": "存储概览内部如何填充。", + "firmament.config.storage-overlay.retain-scroll": "保留滚动位置", + "firmament.config.storage-overlay.retain-scroll.description": "关闭存储UI和概览时保留滚动位置。", + "firmament.config.storage-overlay.rows": "列", + "firmament.config.storage-overlay.rows.description": "存储UI和概览使用的最大列数。", + "firmament.config.storage-overlay.scroll-speed": "滚动速度", + "firmament.config.storage-overlay.scroll-speed.description": "存储叠加和概览内部的滚动速度。", + "firmament.config.wardrobe-keybinds": "衣柜快捷键", + "firmament.config.wardrobe-keybinds.change-page": "更改页面", + "firmament.config.wardrobe-keybinds.change-page.description": "更改当前选中的页面", + "firmament.config.wardrobe-keybinds.next-page": "下一页", + "firmament.config.wardrobe-keybinds.next-page.description": "前往下一页", + "firmament.config.wardrobe-keybinds.previous-page": "上一页", + "firmament.config.wardrobe-keybinds.previous-page.description": "前往上一页", + "firmament.config.wardrobe-keybinds.slot-1": "槽位1", + "firmament.config.wardrobe-keybinds.slot-1.description": "切换到第一套装备的键位", + "firmament.config.wardrobe-keybinds.slot-2": "槽位2", + "firmament.config.wardrobe-keybinds.slot-2.description": "切换到第二套装备的键位", + "firmament.config.wardrobe-keybinds.slot-3": "槽位3", + "firmament.config.wardrobe-keybinds.slot-3.description": "切换到第三套装备的键位", + "firmament.config.wardrobe-keybinds.slot-4": "槽位4", + "firmament.config.wardrobe-keybinds.slot-4.description": "切换到第四套装备的键位", + "firmament.config.wardrobe-keybinds.slot-5": "槽位5", + "firmament.config.wardrobe-keybinds.slot-5.description": "切换到第五套装备的键位", + "firmament.config.wardrobe-keybinds.slot-6": "槽位6", + "firmament.config.wardrobe-keybinds.slot-6.description": "切换到第六套装备的键位", + "firmament.config.wardrobe-keybinds.slot-7": "槽位7", + "firmament.config.wardrobe-keybinds.slot-7.description": "切换到第七套装备的键位", + "firmament.config.wardrobe-keybinds.slot-8": "槽位8", + "firmament.config.wardrobe-keybinds.slot-8.description": "切换到第八套装备的键位", + "firmament.config.wardrobe-keybinds.slot-9": "槽位9", + "firmament.config.wardrobe-keybinds.slot-9.description": "切换到第九套装备的键位", + "firmament.config.wardrobe-keybinds.wardrobe-keybinds": "衣柜快捷键", + "firmament.config.wardrobe-keybinds.wardrobe-keybinds.description": "使用数字按键快速穿上衣柜内的装备", + "firmament.config.waypoints": "路径点", + "firmament.config.waypoints.reset-order-on-swap": "切换世界时重置有序路径点", + "firmament.config.waypoints.reset-order-on-swap.description": "切换到另一个世界后重置有序路径点的进度。", + "firmament.config.waypoints.show-index": "显示有序路径点索引", + "firmament.config.waypoints.show-index.description": "在世界中显示有序路径点的编号。", + "firmament.config.waypoints.skip-to-nearest": "允许跳过路径点", + "firmament.config.waypoints.skip-to-nearest.description": "允许跳过有序列表中的路径点。如果禁用,显示下一个路径点前必须移动至当前路径点周围。", + "firmament.config.waypoints.temp-waypoint-duration": "临时路径点持续时间", + "firmament.config.waypoints.temp-waypoint-duration.description": "在世界中显示其他玩家使用/firm sendcoords发送的路径点。", + "firmament.debug.skyblockid": "空岛ID: %s", + "firmament.debug.skyblockid.copy": "点击复制空岛ID", + "firmament.dev.resourcerebuild.done": "Gradle资源重建完成于 %s", + "firmament.dev.resourcerebuild.start": "正在调用gradle资源重建(./gradlew :processResources)", + "firmament.download": "点击这里下载%s", + "firmament.download.already": " (已下载)", + "firmament.fixes.auto-sprint.not-sprinting": "未在疾跑", + "firmament.fixes.auto-sprint.on": "已切换疾跑状态", + "firmament.fixes.auto-sprint.sprinting": "正在疾跑", + "firmament.hotmpreset.copied": "已将所有HOTM技能点复制到剪贴板。使用/firm importhotm导入。", + "firmament.hotmpreset.failedimport": "在你的剪贴板中找不到HOTM技能点预设。你可以使用/firm exporthotm 导出你当前的HOTM技能点", + "firmament.hotmpreset.okayimport": "已导入HOTM技能点预设。", + "firmament.hotmpreset.openinghotm": "正在打开/hotm菜单进行导出。", + "firmament.hotmpreset.scrolled": "刚刚进行了一次滚动。正在等待服务器更新物品。", + "firmament.hotmpreset.scrollprompt": "我们需要滚动!请点击任意位置继续。", + "firmament.hud.edit": "编辑%s", + "firmament.inventory-buttons.all-warps-preset": "所有传送预设", + "firmament.inventory-buttons.delete": "按住L-CTRL并点击删除", + "firmament.inventory-buttons.import-failed": "你的一个按钮没有完整导入", + "firmament.inventory-buttons.info": "按住SHIFT可按网格对齐", + "firmament.inventory-buttons.load-preset": "加载预设", + "firmament.inventory-buttons.reset": "重置按钮", + "firmament.inventory-buttons.save-preset": "保存预设", + "firmament.inventory-buttons.simple-preset": "简单预设", + "firmament.key.category": "Firmament", + "firmament.keybinding.external": "%s", + "firmament.modapi.event": "收到模组API事件:%s", + "firmament.poweruser.entity.armor": "实体盔甲:", + "firmament.poweruser.entity.armor.item": " - %s", + "firmament.poweruser.entity.fail": "光标下未找到实体", + "firmament.poweruser.entity.name": "实体名称:%s", + "firmament.poweruser.entity.passengers": "%s乘客", + "firmament.poweruser.entity.position": "位置:%s", + "firmament.poweruser.entity.type": "实体类型:%s", + "firmament.price": "正在检查 %s 的价格", + "firmament.price.bazaar": "集市统计:", + "firmament.price.bazaar.buy.order": "购买订单:%d", + "firmament.price.bazaar.buy.price": "购买价格:%s", + "firmament.price.bazaar.productid": "库存ID:%s", + "firmament.price.bazaar.sell.order": "出售订单:%d", + "firmament.price.bazaar.sell.price": "出售价格:%s", + "firmament.price.lowestbin": "最低即时购买价格:%s", + "firmament.pristine-profit.collection": "收集:%s/小时", + "firmament.pristine-profit.money": "金钱:%s/小时", + "firmament.pv.lookingup": "正在查找%s", + "firmament.pv.noplayer": "%s不是一个 Minecraft 玩家", + "firmament.pv.noprofile": "%s没有空岛档案", + "firmament.pv.pets": "宠物", + "firmament.pv.skills": "技能", + "firmament.pv.skills.alchemy": "炼药", + "firmament.pv.skills.carpentry": "合成", + "firmament.pv.skills.combat": "战斗", + "firmament.pv.skills.enchanting": "附魔", + "firmament.pv.skills.farming": "农业", + "firmament.pv.skills.fishing": "钓鱼", + "firmament.pv.skills.foraging": "砍树", + "firmament.pv.skills.mining": "挖矿", + "firmament.pv.skills.rift": "裂隙", + "firmament.pv.skills.runecrafting": "符文锻造", + "firmament.pv.skills.social": "社交", + "firmament.pv.skills.taming": "宠物", + "firmament.pv.skills.total": "总经验:%s", + "firmament.quick-commands.join.explain": "使用/join f1,/join m7,/join fe或/join khot等命令加入地牢或kuudra服务器。", + "firmament.quick-commands.join.success": "正在加入:%s", + "firmament.quick-commands.join.unknown": "找不到%s的实例", + "firmament.quick-commands.join.unknown-catacombs": "未知的地牢层数%s", + "firmament.quick-commands.join.unknown-kuudra": "未知的kuudra等级%s", + "firmament.recipe.forge.time": "锻造时间:%s", + "firmament.recipe.mobs.coins": "§e金币:%s", + "firmament.recipe.mobs.combat": "§b战斗经验:%s", + "firmament.recipe.mobs.drops": "§e§l掉落几率:%s", + "firmament.recipe.mobs.exp": "§6经验:%s", + "firmament.recipe.mobs.name": "§8[§7等级%d§8]§c%s", + "firmament.recipe.mobs.name.nolevel": "§c%s", + "firmament.recipe.novanilla": "Hypixel无法对原版配方进行Supercraft", + "firmament.reiwarning": "Firmament需要RoughlyEnoughItems来显示其物品列表!", + "firmament.reiwarning.disable": "点击此处禁用此警告", + "firmament.reiwarning.disabled": "已禁用RoughlyEnoughItems警告。但没有REI物品列表将无法显示。", + "firmament.repo.brokenitem": "渲染物品失败:%s", + "firmament.repo.cache": "正在重新缓存物品", + "firmament.repo.reload.disk": "正在从磁盘重新加载仓库。这可能会有点卡顿。", + "firmament.repo.reload.network": "正在尝试重新下载仓库", + "firmament.sbinfo.gametype": "Locraw游戏类型: %s", + "firmament.sbinfo.map": "Locraw地图: %s", + "firmament.sbinfo.mode": "Locraw模式: %s", + "firmament.sbinfo.nolocraw": "无Locraw数据可用", + "firmament.sbinfo.profile": "当前档案昵称:%s", + "firmament.sbinfo.server": "Locraw服务器:%s", + "firmament.toggle.false": "关闭", + "firmament.toggle.true": "开启", + "firmament.tooltip.copied.lore": "已复制名称和描述", + "firmament.tooltip.copied.modelid": "已复制纹理ID:%s", + "firmament.tooltip.copied.modelid.fail": "复制纹理ID失败", + "firmament.tooltip.copied.nbt": "已复制NBT数据", + "firmament.tooltip.copied.skull": "已复制头颅ID:%s", + "firmament.tooltip.copied.skull-id": "已复制头颅ID:%s", + "firmament.tooltip.copied.skull-id.fail.no-profile": "头颅没有档案", + "firmament.tooltip.copied.skull-id.fail.no-skull": "这不是头颅", + "firmament.tooltip.copied.skull-id.fail.no-texture": "头颅没有纹理", + "firmament.tooltip.copied.skull.fail": "复制头颅ID失败。", + "firmament.tooltip.copied.skyblockid": "已复制空岛ID:%s", + "firmament.tooltip.copied.skyblockid.fail": "复制空岛ID失败", + "firmament.tooltip.copied.stack": "已复制ItemStack", + "firmament.tooltip.skyblockid": "空岛ID:%s", + "firmament.ursa.debugrequest.result": "Ursa请求成功:%s", + "firmament.ursa.debugrequest.start": "Ursa请求已启动", + "firmament.warp-util.already-close": "已经在目的地,无法使用/warp %s", + "firmament.warp-util.attempting-to-warp": "正在尝试传送到/warp %s", + "firmament.warp-util.clear-excluded": "已将所有/warp命令标记为可能可用。", + "firmament.warp-util.mark-excluded": "Firmament:尝试传送到%s,但它未解锁。我将避免再次传送到那里。", + "firmament.warp-util.no-warp-found": "找不到/warp %s", + "firmament.waypoint.temporary": "临时路径点:%s", + "zzzzzzzzz.lastentry": "此处为确保每个真正的Firmament条目都有一个尾随逗号" +} diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..f6f4a0b --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,28 @@ +#SPDX-FileCopyrightText: 2024 Linnea Gräf +# +#SPDX-License-Identifier: CC0-1.0 + +# build output +dist/ +# generated types +.astro/ + +# dependencies +node_modules/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + + +# environment variables +.env +.env.production + +# macOS-specific files +.DS_Store + +# jetbrains setting folder +.idea/ diff --git a/web/.vscode/extensions.json b/web/.vscode/extensions.json new file mode 100644 index 0000000..22a1505 --- /dev/null +++ b/web/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + "recommendations": ["astro-build.astro-vscode"], + "unwantedRecommendations": [] +} diff --git a/web/.vscode/launch.json b/web/.vscode/launch.json new file mode 100644 index 0000000..d642209 --- /dev/null +++ b/web/.vscode/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "command": "./node_modules/.bin/astro dev", + "name": "Development server", + "request": "launch", + "type": "node-terminal" + } + ] +} diff --git a/web/REUSE.toml b/web/REUSE.toml new file mode 100644 index 0000000..951c8b9 --- /dev/null +++ b/web/REUSE.toml @@ -0,0 +1,20 @@ +#SPDX-FileCopyrightText: 2024 Linnea Gräf +# +#SPDX-License-Identifier: CC0-1.0 + +version = 1 + +[[annotations]] +path = ["src/**/*"] +SPDX-License-Identifier = "GPL-3.0-or-later" +SPDX-FileCopyrightText = ["Linnea Gräf ", "Firmament Contributors"] + +[[annotations]] +path = ["pnpm-lock.yaml"] +SPDX-License-Identifier = "CC0-1.0" +SPDX-FileCopyrightText = ["Auto Generated"] + +[[annotations]] +path = ["*.json", "*.mjs", ".vscode/*"] +SPDX-License-Identifier = "CC0-1.0" +SPDX-FileCopyrightText = ["Linnea Gräf ", "Firmament Contributors"] diff --git a/web/astro.config.mjs b/web/astro.config.mjs new file mode 100644 index 0000000..061f596 --- /dev/null +++ b/web/astro.config.mjs @@ -0,0 +1,12 @@ +// @ts-check +import { defineConfig } from 'astro/config'; + +import tailwind from '@astrojs/tailwind'; + +// https://astro.build/config +export default defineConfig({ + integrations: [tailwind()], + redirects: { + "/discord": "https://discord.com/invite/64pFP94AWA", + }, +}); diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..0cf9347 --- /dev/null +++ b/web/package.json @@ -0,0 +1,21 @@ +{ + "packageManager": "pnpm@9.3.0", + "name": "", + "type": "module", + "version": "0.0.1", + "scripts": { + "dev": "astro dev", + "start": "astro dev", + "build": "astro check && astro build", + "preview": "astro preview", + "astro": "astro" + }, + "dependencies": { + "@astrojs/check": "^0.9.4", + "@astrojs/tailwind": "^5.1.2", + "astro": "^4.16.13", + "sharp": "^0.33.5", + "tailwindcss": "^3.4.15", + "typescript": "^5.6.3" + } +} diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml new file mode 100644 index 0000000..eb1b894 --- /dev/null +++ b/web/pnpm-lock.yaml @@ -0,0 +1,4487 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@astrojs/check': + specifier: ^0.9.4 + version: 0.9.4(typescript@5.6.3) + '@astrojs/tailwind': + specifier: ^5.1.2 + version: 5.1.2(astro@4.16.13(rollup@4.27.2)(typescript@5.6.3))(tailwindcss@3.4.15) + astro: + specifier: ^4.16.13 + version: 4.16.13(rollup@4.27.2)(typescript@5.6.3) + sharp: + specifier: ^0.33.5 + version: 0.33.5 + tailwindcss: + specifier: ^3.4.15 + version: 3.4.15 + typescript: + specifier: ^5.6.3 + version: 5.6.3 + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@astrojs/check@0.9.4': + resolution: {integrity: sha512-IOheHwCtpUfvogHHsvu0AbeRZEnjJg3MopdLddkJE70mULItS/Vh37BHcI00mcOJcH1vhD3odbpvWokpxam7xA==} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + + '@astrojs/compiler@2.10.3': + resolution: {integrity: sha512-bL/O7YBxsFt55YHU021oL+xz+B/9HvGNId3F9xURN16aeqDK9juHGktdkCSXz+U4nqFACq6ZFvWomOzhV+zfPw==} + + '@astrojs/internal-helpers@0.4.1': + resolution: {integrity: sha512-bMf9jFihO8YP940uD70SI/RDzIhUHJAolWVcO1v5PUivxGKvfLZTLTVVxEYzGYyPsA3ivdLNqMnL5VgmQySa+g==} + + '@astrojs/language-server@2.15.4': + resolution: {integrity: sha512-JivzASqTPR2bao9BWsSc/woPHH7OGSGc9aMxXL4U6egVTqBycB3ZHdBJPuOCVtcGLrzdWTosAqVPz1BVoxE0+A==} + hasBin: true + peerDependencies: + prettier: ^3.0.0 + prettier-plugin-astro: '>=0.11.0' + peerDependenciesMeta: + prettier: + optional: true + prettier-plugin-astro: + optional: true + + '@astrojs/markdown-remark@5.3.0': + resolution: {integrity: sha512-r0Ikqr0e6ozPb5bvhup1qdWnSPUvQu6tub4ZLYaKyG50BXZ0ej6FhGz3GpChKpH7kglRFPObJd/bDyf2VM9pkg==} + + '@astrojs/prism@3.1.0': + resolution: {integrity: sha512-Z9IYjuXSArkAUx3N6xj6+Bnvx8OdUSHA8YoOgyepp3+zJmtVYJIl/I18GozdJVW1p5u/CNpl3Km7/gwTJK85cw==} + engines: {node: ^18.17.1 || ^20.3.0 || >=21.0.0} + + '@astrojs/tailwind@5.1.2': + resolution: {integrity: sha512-IvOF0W/dtHElcXvhrPR35nHmhyV3cfz1EzPitMGtU7sYy9Hci3BNK1To6FWmVuuNKPxza1IgCGetSynJZL7fOg==} + peerDependencies: + astro: ^3.0.0 || ^4.0.0 || ^5.0.0-beta.0 + tailwindcss: ^3.0.24 + + '@astrojs/telemetry@3.1.0': + resolution: {integrity: sha512-/ca/+D8MIKEC8/A9cSaPUqQNZm+Es/ZinRv0ZAzvu2ios7POQSsVD+VOj7/hypWNsNM3T7RpfgNq7H2TU1KEHA==} + engines: {node: ^18.17.1 || ^20.3.0 || >=21.0.0} + + '@astrojs/yaml2ts@0.2.2': + resolution: {integrity: sha512-GOfvSr5Nqy2z5XiwqTouBBpy5FyI6DEe+/g/Mk5am9SjILN1S5fOEvYK0GuWHg98yS/dobP4m8qyqw/URW35fQ==} + + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.26.2': + resolution: {integrity: sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.26.0': + resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.26.2': + resolution: {integrity: sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.25.9': + resolution: {integrity: sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.25.9': + resolution: {integrity: sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.25.9': + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.26.0': + resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.25.9': + resolution: {integrity: sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.25.9': + resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.26.0': + resolution: {integrity: sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.26.2': + resolution: {integrity: sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-jsx@7.25.9': + resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx@7.25.9': + resolution: {integrity: sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.25.9': + resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.25.9': + resolution: {integrity: sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.26.0': + resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==} + engines: {node: '>=6.9.0'} + + '@emmetio/abbreviation@2.3.3': + resolution: {integrity: sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA==} + + '@emmetio/css-abbreviation@2.1.8': + resolution: {integrity: sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw==} + + '@emmetio/css-parser@0.4.0': + resolution: {integrity: sha512-z7wkxRSZgrQHXVzObGkXG+Vmj3uRlpM11oCZ9pbaz0nFejvCDmAiNDpY75+wgXOcffKpj4rzGtwGaZxfJKsJxw==} + + '@emmetio/html-matcher@1.3.0': + resolution: {integrity: sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ==} + + '@emmetio/scanner@1.0.4': + resolution: {integrity: sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA==} + + '@emmetio/stream-reader-utils@0.1.0': + resolution: {integrity: sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A==} + + '@emmetio/stream-reader@2.2.0': + resolution: {integrity: sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw==} + + '@emnapi/runtime@1.3.1': + resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@oslojs/encoding@1.1.0': + resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@rollup/pluginutils@5.1.3': + resolution: {integrity: sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.27.2': + resolution: {integrity: sha512-Tj+j7Pyzd15wAdSJswvs5CJzJNV+qqSUcr/aCD+jpQSBtXvGnV0pnrjoc8zFTe9fcKCatkpFpOO7yAzpO998HA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.27.2': + resolution: {integrity: sha512-xsPeJgh2ThBpUqlLgRfiVYBEf/P1nWlWvReG+aBWfNv3XEBpa6ZCmxSVnxJgLgkNz4IbxpLy64h2gCmAAQLneQ==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.27.2': + resolution: {integrity: sha512-KnXU4m9MywuZFedL35Z3PuwiTSn/yqRIhrEA9j+7OSkji39NzVkgxuxTYg5F8ryGysq4iFADaU5osSizMXhU2A==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.27.2': + resolution: {integrity: sha512-Hj77A3yTvUeCIx/Vi+4d4IbYhyTwtHj07lVzUgpUq9YpJSEiGJj4vXMKwzJ3w5zp5v3PFvpJNgc/J31smZey6g==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.27.2': + resolution: {integrity: sha512-RjgKf5C3xbn8gxvCm5VgKZ4nn0pRAIe90J0/fdHUsgztd3+Zesb2lm2+r6uX4prV2eUByuxJNdt647/1KPRq5g==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.27.2': + resolution: {integrity: sha512-duq21FoXwQtuws+V9H6UZ+eCBc7fxSpMK1GQINKn3fAyd9DFYKPJNcUhdIKOrMFjLEJgQskoMoiuizMt+dl20g==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.27.2': + resolution: {integrity: sha512-6npqOKEPRZkLrMcvyC/32OzJ2srdPzCylJjiTJT2c0bwwSGm7nz2F9mNQ1WrAqCBZROcQn91Fno+khFhVijmFA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.27.2': + resolution: {integrity: sha512-V9Xg6eXtgBtHq2jnuQwM/jr2mwe2EycnopO8cbOvpzFuySCGtKlPCI3Hj9xup/pJK5Q0388qfZZy2DqV2J8ftw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.27.2': + resolution: {integrity: sha512-uCFX9gtZJoQl2xDTpRdseYuNqyKkuMDtH6zSrBTA28yTfKyjN9hQ2B04N5ynR8ILCoSDOrG/Eg+J2TtJ1e/CSA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.27.2': + resolution: {integrity: sha512-/PU9P+7Rkz8JFYDHIi+xzHabOu9qEWR07L5nWLIUsvserrxegZExKCi2jhMZRd0ATdboKylu/K5yAXbp7fYFvA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.27.2': + resolution: {integrity: sha512-eCHmol/dT5odMYi/N0R0HC8V8QE40rEpkyje/ZAXJYNNoSfrObOvG/Mn+s1F/FJyB7co7UQZZf6FuWnN6a7f4g==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.27.2': + resolution: {integrity: sha512-DEP3Njr9/ADDln3kNi76PXonLMSSMiCir0VHXxmGSHxCxDfQ70oWjHcJGfiBugzaqmYdTC7Y+8Int6qbnxPBIQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.27.2': + resolution: {integrity: sha512-NHGo5i6IE/PtEPh5m0yw5OmPMpesFnzMIS/lzvN5vknnC1sXM5Z/id5VgcNPgpD+wHmIcuYYgW+Q53v+9s96lQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.27.2': + resolution: {integrity: sha512-PaW2DY5Tan+IFvNJGHDmUrORadbe/Ceh8tQxi8cmdQVCCYsLoQo2cuaSj+AU+YRX8M4ivS2vJ9UGaxfuNN7gmg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.27.2': + resolution: {integrity: sha512-dOlWEMg2gI91Qx5I/HYqOD6iqlJspxLcS4Zlg3vjk1srE67z5T2Uz91yg/qA8sY0XcwQrFzWWiZhMNERylLrpQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.27.2': + resolution: {integrity: sha512-euMIv/4x5Y2/ImlbGl88mwKNXDsvzbWUlT7DFky76z2keajCtcbAsN9LUdmk31hAoVmJJYSThgdA0EsPeTr1+w==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.27.2': + resolution: {integrity: sha512-RsnE6LQkUHlkC10RKngtHNLxb7scFykEbEwOFDjr3CeCMG+Rr+cKqlkKc2/wJ1u4u990urRHCbjz31x84PBrSQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.27.2': + resolution: {integrity: sha512-foJM5vv+z2KQmn7emYdDLyTbkoO5bkHZE1oth2tWbQNGW7mX32d46Hz6T0MqXdWS2vBZhaEtHqdy9WYwGfiliA==} + cpu: [x64] + os: [win32] + + '@shikijs/core@1.23.0': + resolution: {integrity: sha512-J4Fo22oBlfRHAXec+1AEzcowv+Qdf4ZQkuP/X/UHYH9+KA9LvyFXSXyS+HxuBRFfon+u7bsmKdRBjoZlbDVRkQ==} + + '@shikijs/engine-javascript@1.23.0': + resolution: {integrity: sha512-CcrppseWShG+8Efp1iil9divltuXVdCaU4iu+CKvzTGZO5RmXyAiSx668M7VbX8+s/vt1ZKu75Vn/jWi8O3G/Q==} + + '@shikijs/engine-oniguruma@1.23.0': + resolution: {integrity: sha512-gS8bZLqVvmZXX+E5JUMJICsBp+kx6gj79MH/UEpKHKIqnUzppgbmEn6zLa6mB5D+sHse2gFei3YYJxQe1EzZXQ==} + + '@shikijs/types@1.23.0': + resolution: {integrity: sha512-HiwzsihRao+IbPk7FER/EQT/D0dEEK3n5LAtHDzL5iRT+JMblA7y9uitUnjEnHeLkKigNM+ZplrP7MuEyyc5kA==} + + '@shikijs/vscode-textmate@9.3.0': + resolution: {integrity: sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.6.8': + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.6': + resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@0.7.34': + resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} + + '@types/nlcst@2.0.3': + resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@ungap/structured-clone@1.2.0': + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + + '@volar/kit@2.4.10': + resolution: {integrity: sha512-ul+rLeO9RlFDgkY/FhPWMnpFqAsjvjkKz8VZeOY5YCJMwTblmmSBlNJtFNxSBx9t/k1q80nEthLyxiJ50ZbIAg==} + peerDependencies: + typescript: '*' + + '@volar/language-core@2.4.10': + resolution: {integrity: sha512-hG3Z13+nJmGaT+fnQzAkS0hjJRa2FCeqZt6Bd+oGNhUkQ+mTFsDETg5rqUTxyzIh5pSOGY7FHCWUS8G82AzLCA==} + + '@volar/language-server@2.4.10': + resolution: {integrity: sha512-odQsgrJh8hOXfxkSj/BSnpjThb2/KDhbxZnG/XAEx6E3QGDQv4hAOz9GWuKoNs0tkjgwphQGIwDMT1JYaTgRJw==} + + '@volar/language-service@2.4.10': + resolution: {integrity: sha512-VxUiWS11rnRzakkqw5x1LPhsz+RBfD0CrrFarLGW2/voliYXEdCuSOM3r8JyNRvMvP4uwhD38ccAdTcULQEAIQ==} + + '@volar/source-map@2.4.10': + resolution: {integrity: sha512-OCV+b5ihV0RF3A7vEvNyHPi4G4kFa6ukPmyVocmqm5QzOd8r5yAtiNvaPEjl8dNvgC/lj4JPryeeHLdXd62rWA==} + + '@volar/typescript@2.4.10': + resolution: {integrity: sha512-F8ZtBMhSXyYKuBfGpYwqA5rsONnOwAVvjyE7KPYJ7wgZqo2roASqNWUnianOomJX5u1cxeRooHV59N0PhvEOgw==} + + '@vscode/emmet-helper@2.10.0': + resolution: {integrity: sha512-UHw1EQRgLbSYkyB73/7wR/IzV6zTBnbzEHuuU4Z6b95HKf2lmeTdGwBIwspWBSRrnIA1TI2x2tetBym6ErA7Gw==} + + '@vscode/l10n@0.0.18': + resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==} + + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-iterate@2.0.1: + resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==} + + astro@4.16.13: + resolution: {integrity: sha512-Mtd76+BC0zLWqoXpf9xc731AhdH4MNh5JFHYdLRvSH0Nqn48hA64dPGh/cWsJvh/DZFmC0NTZusM1Qq2gyNaVg==} + engines: {node: ^18.17.1 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'} + hasBin: true + + autoprefixer@10.4.20: + resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base-64@1.0.0: + resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + boxen@8.0.1: + resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==} + engines: {node: '>=18'} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.24.2: + resolution: {integrity: sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + camelcase@8.0.0: + resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} + engines: {node: '>=16'} + + caniuse-lite@1.0.30001680: + resolution: {integrity: sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@4.0.1: + resolution: {integrity: sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==} + engines: {node: '>= 14.16.0'} + + ci-info@4.1.0: + resolution: {integrity: sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==} + engines: {node: '>=8'} + + cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + common-ancestor-path@1.0.1: + resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cross-spawn@7.0.5: + resolution: {integrity: sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-named-character-reference@1.0.2: + resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + + deterministic-object-hash@2.0.2: + resolution: {integrity: sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ==} + engines: {node: '>=18'} + + devalue@5.1.1: + resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + diff@5.2.0: + resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} + engines: {node: '>=0.3.1'} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + dset@3.1.4: + resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} + engines: {node: '>=4'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + electron-to-chromium@1.5.62: + resolution: {integrity: sha512-t8c+zLmJHa9dJy96yBZRXGQYoiCEnHYgFwn1asvSPZSUdVxnB62A4RASd7k41ytG3ErFBA0TpHlKg9D9SQBmLg==} + + emmet@2.4.11: + resolution: {integrity: sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==} + + emoji-regex-xs@1.0.0: + resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} + + emoji-regex@10.4.0: + resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + es-module-lexer@1.5.4: + resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + + fast-uri@3.0.3: + resolution: {integrity: sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==} + + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up-simple@1.0.0: + resolution: {integrity: sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==} + engines: {node: '>=18'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-yarn-workspace-root2@1.2.16: + resolution: {integrity: sha512-hr6hb1w8ePMpPVUK39S4RlwJzi+xPLuVuG8XlwXU3KD5Yn3qgBWVfy3AzNlDhWvE1EORCE65/Qm26rFQt3VLVA==} + + flattie@1.1.1: + resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} + engines: {node: '>=8'} + + foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} + engines: {node: '>=14'} + + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.3.0: + resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} + engines: {node: '>=18'} + + github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hast-util-from-html@2.0.3: + resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} + + hast-util-from-parse5@8.0.1: + resolution: {integrity: sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==} + + hast-util-is-element@3.0.0: + resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + + hast-util-to-html@9.0.3: + resolution: {integrity: sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg==} + + hast-util-to-parse5@8.0.0: + resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==} + + hast-util-to-text@4.0.2: + resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hastscript@8.0.0: + resolution: {integrity: sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==} + + html-escaper@3.0.3: + resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + http-cache-semantics@4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + + import-meta-resolve@4.1.0: + resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} + + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.15.1: + resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} + engines: {node: '>= 0.4'} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jiti@1.21.6: + resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-parser@2.3.1: + resolution: {integrity: sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==} + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + + lilconfig@3.1.2: + resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-yaml-file@0.2.0: + resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==} + engines: {node: '>=6'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + log-symbols@6.0.0: + resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} + engines: {node: '>=18'} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.12: + resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==} + + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + mdast-util-definitions@6.0.0: + resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==} + + mdast-util-find-and-replace@3.0.1: + resolution: {integrity: sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==} + + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.0.0: + resolution: {integrity: sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.0.0: + resolution: {integrity: sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromark-core-commonmark@2.0.2: + resolution: {integrity: sha512-FKjQKbxd1cibWMM1P9N+H8TwlgGgSkWZMmfuVucLCHaYqeSvJ0hFeHsIa65pA2nYbes0f8LDHPMrd9X7Ujxg9w==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.0: + resolution: {integrity: sha512-Ub2ncQv+fwD70/l4ou27b4YzfNaCJOvyX4HxXU15m7mpYY+rjuWzsLIPZHJL253Z643RpbcP1oeIJlQ/SKW67g==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.0.2: + resolution: {integrity: sha512-xKxhkB62vwHUuuxHe9Xqty3UaAsizV2YKq5OV344u3hFBbf8zIYrhYOWhAQb94MtMPkjTOzzjJ/hid9/dR5vFA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.1: + resolution: {integrity: sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==} + + micromark@4.0.1: + resolution: {integrity: sha512-eBPdkcoCNvYcxQOAKAlceo5SNdzZWfF+FcSupREAzdAh9rRmE239CEQAiTwIgblwnoM8zzj35sZ5ZwvSEOF6Kw==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + mrmime@2.0.0: + resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + neotraverse@0.6.18: + resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==} + engines: {node: '>= 10'} + + nlcst-to-string@4.0.0: + resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==} + + node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + oniguruma-to-es@0.1.2: + resolution: {integrity: sha512-sBYKVJlIMB0WPO+tSu/NNB1ytSFeHyyJZ3Ayxfx3f/QUuXu0lvZk0VB4K7npmdlHSC0ldqanzh/sUSlAbgCTfw==} + + ora@8.1.1: + resolution: {integrity: sha512-YWielGi1XzG1UTvOaCFaNgEnuhZVMSHYkW/FQ7UX8O26PtlpdM84c0f7wLPlkvx2RfiQmnzd61d/MGxmpQeJPw==} + engines: {node: '>=18'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@6.1.0: + resolution: {integrity: sha512-H0jc0q1vOzlEk0TqAKXKZxdl7kX3OFUzCnNVUnq5Pc3DGo0kpeaMuPqxQn235HibwBEb0/pm9dgKTjXy66fBkg==} + engines: {node: '>=18'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-queue@8.0.1: + resolution: {integrity: sha512-NXzu9aQJTAzbBqOt2hwsR63ea7yvxJc0PwN/zobNAudYfb1B7R08SzB4TsLeSbUCuG467NhnoT0oO6w1qRO+BA==} + engines: {node: '>=18'} + + p-timeout@6.1.3: + resolution: {integrity: sha512-UJUyfKbwvr/uZSV6btANfb+0t/mOhKV/KXcCUTp8FcQI+v/0d+wXqH4htrW0E4rR6WiEO/EPvUFiV9D5OI4vlw==} + engines: {node: '>=14.16'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parse-latin@7.0.0: + resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==} + + parse5@7.2.1: + resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.0.1: + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@4.0.2: + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.4.49: + resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} + engines: {node: ^10 || ^12 || >=14} + + preferred-pm@4.0.0: + resolution: {integrity: sha512-gYBeFTZLu055D8Vv3cSPox/0iTPtkzxpLroSYYA7WXgRi31WCJ51Uyl8ZiPeUUjyvs2MBzK+S8v9JVUgHU/Sqw==} + engines: {node: '>=18.12'} + + prettier@2.8.7: + resolution: {integrity: sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==} + engines: {node: '>=10.13.0'} + hasBin: true + + prismjs@1.29.0: + resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} + engines: {node: '>=6'} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + property-information@6.5.0: + resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@4.0.2: + resolution: {integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==} + engines: {node: '>= 14.16.0'} + + regex-recursion@4.2.1: + resolution: {integrity: sha512-QHNZyZAeKdndD1G3bKAbBEKOSSK4KOHQrAJ01N1LJeb0SoH4DJIeFhp0uUpETgONifS4+P3sOgoA1dhzgrQvhA==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@4.4.0: + resolution: {integrity: sha512-uCUSuobNVeqUupowbdZub6ggI5/JZkYyJdDogddJr60L764oxC2pMZov1fQ3wM9bdyzUILDG+Sqx6NAKAz9rKQ==} + + rehype-parse@9.0.1: + resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==} + + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + + rehype-stringify@10.0.1: + resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==} + + rehype@13.0.2: + resolution: {integrity: sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==} + + remark-gfm@4.0.0: + resolution: {integrity: sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.1: + resolution: {integrity: sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ==} + + remark-smartypants@3.0.2: + resolution: {integrity: sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==} + engines: {node: '>=16.0.0'} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + request-light@0.5.8: + resolution: {integrity: sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg==} + + request-light@0.7.0: + resolution: {integrity: sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + retext-latin@4.0.0: + resolution: {integrity: sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==} + + retext-smartypants@6.2.0: + resolution: {integrity: sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==} + + retext-stringify@4.0.0: + resolution: {integrity: sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==} + + retext@9.0.0: + resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==} + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.27.2: + resolution: {integrity: sha512-KreA+PzWmk2yaFmZVwe6GB2uBD86nXl86OsDkt1bJS9p3vqWuEQ6HnJJ+j/mZi/q0920P99/MVRlB4L3crpF5w==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shiki@1.23.0: + resolution: {integrity: sha512-xfdu9DqPkIpExH29cmiTlgo0/jBki5la1Tkfhsv+Wu5TT3APLNHslR1acxuKJOCWqVdSc+pIbs/2ozjVRGppdg==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stdin-discarder@0.2.2: + resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} + engines: {node: '>=18'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tailwindcss@3.4.15: + resolution: {integrity: sha512-r4MeXnfBmSOuKUWmXe6h2CcyfzJCEk4F0pptO5jlnYSIViUkVmsawj80N5h2lO3gwcmSb4n3PuN+e+GC1Guylw==} + engines: {node: '>=14.0.0'} + hasBin: true + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinyexec@0.3.1: + resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tsconfck@3.1.4: + resolution: {integrity: sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-fest@4.27.0: + resolution: {integrity: sha512-3IMSWgP7C5KSQqmo1wjhKrwsvXAtF33jO3QY+Uy++ia7hqvgSK6iXbbg5PbDBc1P2ZbNEDgejOrN4YooXvhwCw==} + engines: {node: '>=16'} + + typesafe-path@0.2.2: + resolution: {integrity: sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==} + + typescript-auto-import-cache@0.3.5: + resolution: {integrity: sha512-fAIveQKsoYj55CozUiBoj4b/7WpN0i4o74wiGY5JVUEoD0XiqDk1tJqTEjgzL2/AizKQrXxyRosSebyDzBZKjw==} + + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} + hasBin: true + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-find-after@5.0.0: + resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} + + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-modify-children@4.0.0: + resolution: {integrity: sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-remove-position@5.0.0: + resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-children@3.0.0: + resolution: {integrity: sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + + update-browserslist-db@1.1.1: + resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + + vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite@5.4.11: + resolution: {integrity: sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitefu@1.0.3: + resolution: {integrity: sha512-iKKfOMBHob2WxEJbqbJjHAkmYgvFDPhuqrO82om83S8RLk+17FtyMBfcyeH8GqD0ihShtkMW/zzJgiA51hCNCQ==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0-beta.0 + peerDependenciesMeta: + vite: + optional: true + + volar-service-css@0.0.62: + resolution: {integrity: sha512-JwNyKsH3F8PuzZYuqPf+2e+4CTU8YoyUHEHVnoXNlrLe7wy9U3biomZ56llN69Ris7TTy/+DEX41yVxQpM4qvg==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-emmet@0.0.62: + resolution: {integrity: sha512-U4dxWDBWz7Pi4plpbXf4J4Z/ss6kBO3TYrACxWNsE29abu75QzVS0paxDDhI6bhqpbDFXlpsDhZ9aXVFpnfGRQ==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-html@0.0.62: + resolution: {integrity: sha512-Zw01aJsZRh4GTGUjveyfEzEqpULQUdQH79KNEiKVYHZyuGtdBRYCHlrus1sueSNMxwwkuF5WnOHfvBzafs8yyQ==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-prettier@0.0.62: + resolution: {integrity: sha512-h2yk1RqRTE+vkYZaI9KYuwpDfOQRrTEMvoHol0yW4GFKc75wWQRrb5n/5abDrzMPrkQbSip8JH2AXbvrRtYh4w==} + peerDependencies: + '@volar/language-service': ~2.4.0 + prettier: ^2.2 || ^3.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + prettier: + optional: true + + volar-service-typescript-twoslash-queries@0.0.62: + resolution: {integrity: sha512-KxFt4zydyJYYI0kFAcWPTh4u0Ha36TASPZkAnNY784GtgajerUqM80nX/W1d0wVhmcOFfAxkVsf/Ed+tiYU7ng==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-typescript@0.0.62: + resolution: {integrity: sha512-p7MPi71q7KOsH0eAbZwPBiKPp9B2+qrdHAd6VY5oTo9BUXatsOAdakTm9Yf0DUj6uWBAaOT01BSeVOPwucMV1g==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-yaml@0.0.62: + resolution: {integrity: sha512-k7gvv7sk3wa+nGll3MaSKyjwQsJjIGCHFjVkl3wjaSP2nouKyn9aokGmqjrl39mi88Oy49giog2GkZH526wjig==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + vscode-css-languageservice@6.3.1: + resolution: {integrity: sha512-1BzTBuJfwMc3A0uX4JBdJgoxp74cjj4q2mDJdp49yD/GuAq4X0k5WtK6fNcMYr+FfJ9nqgR6lpfCSZDkARJ5qQ==} + + vscode-html-languageservice@5.3.1: + resolution: {integrity: sha512-ysUh4hFeW/WOWz/TO9gm08xigiSsV/FOAZ+DolgJfeLftna54YdmZ4A+lIn46RbdO3/Qv5QHTn1ZGqmrXQhZyA==} + + vscode-json-languageservice@4.1.8: + resolution: {integrity: sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg==} + engines: {npm: '>=7.0.0'} + + vscode-jsonrpc@6.0.0: + resolution: {integrity: sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==} + engines: {node: '>=8.0.0 || >=10.0.0'} + + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageserver-protocol@3.16.0: + resolution: {integrity: sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + + vscode-languageserver-types@3.16.0: + resolution: {integrity: sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-languageserver@7.0.0: + resolution: {integrity: sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==} + hasBin: true + + vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + + vscode-nls@5.2.0: + resolution: {integrity: sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==} + + vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + + which-pm-runs@1.1.0: + resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} + engines: {node: '>=4'} + + which-pm@3.0.0: + resolution: {integrity: sha512-ysVYmw6+ZBhx3+ZkcPwRuJi38ZOTLJJ33PSHaitLxSKUMsh0LkKd0nC69zZCwt5D+AYUcMK2hhw4yWny20vSGg==} + engines: {node: '>=18.12'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + widest-line@5.0.0: + resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} + engines: {node: '>=18'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrap-ansi@9.0.0: + resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} + engines: {node: '>=18'} + + xxhash-wasm@1.0.2: + resolution: {integrity: sha512-ibF0Or+FivM9lNrg+HGJfVX8WJqgo+kCLDc4vx6xMeTce7Aj+DLttKbxxRR/gNLSAelRc1omAPlJ77N/Jem07A==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml-language-server@1.15.0: + resolution: {integrity: sha512-N47AqBDCMQmh6mBLmI6oqxryHRzi33aPFPsJhYy3VTUGCdLHYjGh4FZzpUjRlphaADBBkDmnkM/++KNIOHi5Rw==} + hasBin: true + + yaml@2.2.2: + resolution: {integrity: sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==} + engines: {node: '>= 14'} + + yaml@2.6.0: + resolution: {integrity: sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==} + engines: {node: '>= 14'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@1.1.1: + resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==} + engines: {node: '>=12.20'} + + zod-to-json-schema@3.23.5: + resolution: {integrity: sha512-5wlSS0bXfF/BrL4jPAbz9da5hDlDptdEppYfe+x4eIJ7jioqKG9uUxOwPzqof09u/XeVdrgFu29lZi+8XNDJtA==} + peerDependencies: + zod: ^3.23.3 + + zod-to-ts@1.2.0: + resolution: {integrity: sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA==} + peerDependencies: + typescript: ^4.9.4 || ^5.0.2 + zod: ^3 + + zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + + '@astrojs/check@0.9.4(typescript@5.6.3)': + dependencies: + '@astrojs/language-server': 2.15.4(typescript@5.6.3) + chokidar: 4.0.1 + kleur: 4.1.5 + typescript: 5.6.3 + yargs: 17.7.2 + transitivePeerDependencies: + - prettier + - prettier-plugin-astro + + '@astrojs/compiler@2.10.3': {} + + '@astrojs/internal-helpers@0.4.1': {} + + '@astrojs/language-server@2.15.4(typescript@5.6.3)': + dependencies: + '@astrojs/compiler': 2.10.3 + '@astrojs/yaml2ts': 0.2.2 + '@jridgewell/sourcemap-codec': 1.5.0 + '@volar/kit': 2.4.10(typescript@5.6.3) + '@volar/language-core': 2.4.10 + '@volar/language-server': 2.4.10 + '@volar/language-service': 2.4.10 + fast-glob: 3.3.2 + muggle-string: 0.4.1 + volar-service-css: 0.0.62(@volar/language-service@2.4.10) + volar-service-emmet: 0.0.62(@volar/language-service@2.4.10) + volar-service-html: 0.0.62(@volar/language-service@2.4.10) + volar-service-prettier: 0.0.62(@volar/language-service@2.4.10) + volar-service-typescript: 0.0.62(@volar/language-service@2.4.10) + volar-service-typescript-twoslash-queries: 0.0.62(@volar/language-service@2.4.10) + volar-service-yaml: 0.0.62(@volar/language-service@2.4.10) + vscode-html-languageservice: 5.3.1 + vscode-uri: 3.0.8 + transitivePeerDependencies: + - typescript + + '@astrojs/markdown-remark@5.3.0': + dependencies: + '@astrojs/prism': 3.1.0 + github-slugger: 2.0.0 + hast-util-from-html: 2.0.3 + hast-util-to-text: 4.0.2 + import-meta-resolve: 4.1.0 + mdast-util-definitions: 6.0.0 + rehype-raw: 7.0.0 + rehype-stringify: 10.0.1 + remark-gfm: 4.0.0 + remark-parse: 11.0.0 + remark-rehype: 11.1.1 + remark-smartypants: 3.0.2 + shiki: 1.23.0 + unified: 11.0.5 + unist-util-remove-position: 5.0.0 + unist-util-visit: 5.0.0 + unist-util-visit-parents: 6.0.1 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@astrojs/prism@3.1.0': + dependencies: + prismjs: 1.29.0 + + '@astrojs/tailwind@5.1.2(astro@4.16.13(rollup@4.27.2)(typescript@5.6.3))(tailwindcss@3.4.15)': + dependencies: + astro: 4.16.13(rollup@4.27.2)(typescript@5.6.3) + autoprefixer: 10.4.20(postcss@8.4.49) + postcss: 8.4.49 + postcss-load-config: 4.0.2(postcss@8.4.49) + tailwindcss: 3.4.15 + transitivePeerDependencies: + - ts-node + + '@astrojs/telemetry@3.1.0': + dependencies: + ci-info: 4.1.0 + debug: 4.3.7 + dlv: 1.1.3 + dset: 3.1.4 + is-docker: 3.0.0 + is-wsl: 3.1.0 + which-pm-runs: 1.1.0 + transitivePeerDependencies: + - supports-color + + '@astrojs/yaml2ts@0.2.2': + dependencies: + yaml: 2.6.0 + + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.26.2': {} + + '@babel/core@7.26.0': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helpers': 7.26.0 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + convert-source-map: 2.0.0 + debug: 4.3.7 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.26.2': + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.0.2 + + '@babel/helper-annotate-as-pure@7.25.9': + dependencies: + '@babel/types': 7.26.0 + + '@babel/helper-compilation-targets@7.25.9': + dependencies: + '@babel/compat-data': 7.26.2 + '@babel/helper-validator-option': 7.25.9 + browserslist: 4.24.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-module-imports@7.25.9': + dependencies: + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.25.9': {} + + '@babel/helper-string-parser@7.25.9': {} + + '@babel/helper-validator-identifier@7.25.9': {} + + '@babel/helper-validator-option@7.25.9': {} + + '@babel/helpers@7.26.0': + dependencies: + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 + + '@babel/parser@7.26.2': + dependencies: + '@babel/types': 7.26.0 + + '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) + '@babel/types': 7.26.0 + transitivePeerDependencies: + - supports-color + + '@babel/template@7.25.9': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + + '@babel/traverse@7.25.9': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 + debug: 4.3.7 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.26.0': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + '@emmetio/abbreviation@2.3.3': + dependencies: + '@emmetio/scanner': 1.0.4 + + '@emmetio/css-abbreviation@2.1.8': + dependencies: + '@emmetio/scanner': 1.0.4 + + '@emmetio/css-parser@0.4.0': + dependencies: + '@emmetio/stream-reader': 2.2.0 + '@emmetio/stream-reader-utils': 0.1.0 + + '@emmetio/html-matcher@1.3.0': + dependencies: + '@emmetio/scanner': 1.0.4 + + '@emmetio/scanner@1.0.4': {} + + '@emmetio/stream-reader-utils@0.1.0': {} + + '@emmetio/stream-reader@2.2.0': {} + + '@emnapi/runtime@1.3.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.3.1 + optional: true + + '@img/sharp-win32-ia32@0.33.5': + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + + '@oslojs/encoding@1.1.0': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@rollup/pluginutils@5.1.3(rollup@4.27.2)': + dependencies: + '@types/estree': 1.0.6 + estree-walker: 2.0.2 + picomatch: 4.0.2 + optionalDependencies: + rollup: 4.27.2 + + '@rollup/rollup-android-arm-eabi@4.27.2': + optional: true + + '@rollup/rollup-android-arm64@4.27.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.27.2': + optional: true + + '@rollup/rollup-darwin-x64@4.27.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.27.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.27.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.27.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.27.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.27.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.27.2': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.27.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.27.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.27.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.27.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.27.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.27.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.27.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.27.2': + optional: true + + '@shikijs/core@1.23.0': + dependencies: + '@shikijs/engine-javascript': 1.23.0 + '@shikijs/engine-oniguruma': 1.23.0 + '@shikijs/types': 1.23.0 + '@shikijs/vscode-textmate': 9.3.0 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.3 + + '@shikijs/engine-javascript@1.23.0': + dependencies: + '@shikijs/types': 1.23.0 + '@shikijs/vscode-textmate': 9.3.0 + oniguruma-to-es: 0.1.2 + + '@shikijs/engine-oniguruma@1.23.0': + dependencies: + '@shikijs/types': 1.23.0 + '@shikijs/vscode-textmate': 9.3.0 + + '@shikijs/types@1.23.0': + dependencies: + '@shikijs/vscode-textmate': 9.3.0 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@9.3.0': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.6 + + '@types/babel__generator@7.6.8': + dependencies: + '@babel/types': 7.26.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + + '@types/babel__traverse@7.20.6': + dependencies: + '@babel/types': 7.26.0 + + '@types/cookie@0.6.0': {} + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 0.7.34 + + '@types/estree@1.0.6': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@0.7.34': {} + + '@types/nlcst@2.0.3': + dependencies: + '@types/unist': 3.0.3 + + '@types/unist@3.0.3': {} + + '@ungap/structured-clone@1.2.0': {} + + '@volar/kit@2.4.10(typescript@5.6.3)': + dependencies: + '@volar/language-service': 2.4.10 + '@volar/typescript': 2.4.10 + typesafe-path: 0.2.2 + typescript: 5.6.3 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + + '@volar/language-core@2.4.10': + dependencies: + '@volar/source-map': 2.4.10 + + '@volar/language-server@2.4.10': + dependencies: + '@volar/language-core': 2.4.10 + '@volar/language-service': 2.4.10 + '@volar/typescript': 2.4.10 + path-browserify: 1.0.1 + request-light: 0.7.0 + vscode-languageserver: 9.0.1 + vscode-languageserver-protocol: 3.17.5 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + + '@volar/language-service@2.4.10': + dependencies: + '@volar/language-core': 2.4.10 + vscode-languageserver-protocol: 3.17.5 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + + '@volar/source-map@2.4.10': {} + + '@volar/typescript@2.4.10': + dependencies: + '@volar/language-core': 2.4.10 + path-browserify: 1.0.1 + vscode-uri: 3.0.8 + + '@vscode/emmet-helper@2.10.0': + dependencies: + emmet: 2.4.11 + jsonc-parser: 2.3.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-languageserver-types: 3.17.5 + vscode-uri: 3.0.8 + + '@vscode/l10n@0.0.18': {} + + acorn@8.14.0: {} + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@5.0.2: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + aria-query@5.3.2: {} + + array-iterate@2.0.1: {} + + astro@4.16.13(rollup@4.27.2)(typescript@5.6.3): + dependencies: + '@astrojs/compiler': 2.10.3 + '@astrojs/internal-helpers': 0.4.1 + '@astrojs/markdown-remark': 5.3.0 + '@astrojs/telemetry': 3.1.0 + '@babel/core': 7.26.0 + '@babel/plugin-transform-react-jsx': 7.25.9(@babel/core@7.26.0) + '@babel/types': 7.26.0 + '@oslojs/encoding': 1.1.0 + '@rollup/pluginutils': 5.1.3(rollup@4.27.2) + '@types/babel__core': 7.20.5 + '@types/cookie': 0.6.0 + acorn: 8.14.0 + aria-query: 5.3.2 + axobject-query: 4.1.0 + boxen: 8.0.1 + ci-info: 4.1.0 + clsx: 2.1.1 + common-ancestor-path: 1.0.1 + cookie: 0.7.2 + cssesc: 3.0.0 + debug: 4.3.7 + deterministic-object-hash: 2.0.2 + devalue: 5.1.1 + diff: 5.2.0 + dlv: 1.1.3 + dset: 3.1.4 + es-module-lexer: 1.5.4 + esbuild: 0.21.5 + estree-walker: 3.0.3 + fast-glob: 3.3.2 + flattie: 1.1.1 + github-slugger: 2.0.0 + gray-matter: 4.0.3 + html-escaper: 3.0.3 + http-cache-semantics: 4.1.1 + js-yaml: 4.1.0 + kleur: 4.1.5 + magic-string: 0.30.12 + magicast: 0.3.5 + micromatch: 4.0.8 + mrmime: 2.0.0 + neotraverse: 0.6.18 + ora: 8.1.1 + p-limit: 6.1.0 + p-queue: 8.0.1 + preferred-pm: 4.0.0 + prompts: 2.4.2 + rehype: 13.0.2 + semver: 7.6.3 + shiki: 1.23.0 + tinyexec: 0.3.1 + tsconfck: 3.1.4(typescript@5.6.3) + unist-util-visit: 5.0.0 + vfile: 6.0.3 + vite: 5.4.11 + vitefu: 1.0.3(vite@5.4.11) + which-pm: 3.0.0 + xxhash-wasm: 1.0.2 + yargs-parser: 21.1.1 + zod: 3.23.8 + zod-to-json-schema: 3.23.5(zod@3.23.8) + zod-to-ts: 1.2.0(typescript@5.6.3)(zod@3.23.8) + optionalDependencies: + sharp: 0.33.5 + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - rollup + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - typescript + + autoprefixer@10.4.20(postcss@8.4.49): + dependencies: + browserslist: 4.24.2 + caniuse-lite: 1.0.30001680 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + + axobject-query@4.1.0: {} + + bail@2.0.2: {} + + balanced-match@1.0.2: {} + + base-64@1.0.0: {} + + binary-extensions@2.3.0: {} + + boxen@8.0.1: + dependencies: + ansi-align: 3.0.1 + camelcase: 8.0.0 + chalk: 5.3.0 + cli-boxes: 3.0.0 + string-width: 7.2.0 + type-fest: 4.27.0 + widest-line: 5.0.0 + wrap-ansi: 9.0.0 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.24.2: + dependencies: + caniuse-lite: 1.0.30001680 + electron-to-chromium: 1.5.62 + node-releases: 2.0.18 + update-browserslist-db: 1.1.1(browserslist@4.24.2) + + camelcase-css@2.0.1: {} + + camelcase@8.0.0: {} + + caniuse-lite@1.0.30001680: {} + + ccount@2.0.1: {} + + chalk@5.3.0: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@4.0.1: + dependencies: + readdirp: 4.0.2 + + ci-info@4.1.0: {} + + cli-boxes@3.0.0: {} + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-spinners@2.9.2: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clsx@2.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + + comma-separated-tokens@2.0.3: {} + + commander@4.1.1: {} + + common-ancestor-path@1.0.1: {} + + convert-source-map@2.0.0: {} + + cookie@0.7.2: {} + + cross-spawn@7.0.5: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + decode-named-character-reference@1.0.2: + dependencies: + character-entities: 2.0.2 + + dequal@2.0.3: {} + + detect-libc@2.0.3: {} + + deterministic-object-hash@2.0.2: + dependencies: + base-64: 1.0.0 + + devalue@5.1.1: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + didyoumean@1.2.2: {} + + diff@5.2.0: {} + + dlv@1.1.3: {} + + dset@3.1.4: {} + + eastasianwidth@0.2.0: {} + + electron-to-chromium@1.5.62: {} + + emmet@2.4.11: + dependencies: + '@emmetio/abbreviation': 2.3.3 + '@emmetio/css-abbreviation': 2.1.8 + + emoji-regex-xs@1.0.0: {} + + emoji-regex@10.4.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + entities@4.5.0: {} + + es-module-lexer@1.5.4: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escalade@3.2.0: {} + + escape-string-regexp@5.0.0: {} + + esprima@4.0.1: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.6 + + eventemitter3@5.0.1: {} + + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + + extend@3.0.2: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.2: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-uri@3.0.3: {} + + fastq@1.17.1: + dependencies: + reusify: 1.0.4 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up-simple@1.0.0: {} + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-yarn-workspace-root2@1.2.16: + dependencies: + micromatch: 4.0.8 + pkg-dir: 4.2.0 + + flattie@1.1.1: {} + + foreground-child@3.3.0: + dependencies: + cross-spawn: 7.0.5 + signal-exit: 4.1.0 + + fraction.js@4.3.7: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.3.0: {} + + github-slugger@2.0.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.4.5: + dependencies: + foreground-child: 3.3.0 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + globals@11.12.0: {} + + graceful-fs@4.2.11: {} + + gray-matter@4.0.3: + dependencies: + js-yaml: 3.14.1 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hast-util-from-html@2.0.3: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + hast-util-from-parse5: 8.0.1 + parse5: 7.2.1 + vfile: 6.0.3 + vfile-message: 4.0.2 + + hast-util-from-parse5@8.0.1: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 8.0.0 + property-information: 6.5.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + + hast-util-is-element@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-raw@9.1.0: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + '@ungap/structured-clone': 1.2.0 + hast-util-from-parse5: 8.0.1 + hast-util-to-parse5: 8.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + parse5: 7.2.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-html@9.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-to-parse5@8.0.0: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-text@4.0.2: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + hast-util-is-element: 3.0.0 + unist-util-find-after: 5.0.0 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hastscript@8.0.0: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + + html-escaper@3.0.3: {} + + html-void-elements@3.0.0: {} + + http-cache-semantics@4.1.1: {} + + import-meta-resolve@4.1.0: {} + + is-arrayish@0.3.2: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.15.1: + dependencies: + hasown: 2.0.2 + + is-docker@3.0.0: {} + + is-extendable@0.1.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-interactive@2.0.0: {} + + is-number@7.0.0: {} + + is-plain-obj@4.1.0: {} + + is-unicode-supported@1.3.0: {} + + is-unicode-supported@2.1.0: {} + + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jiti@1.21.6: {} + + js-tokens@4.0.0: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.0.2: {} + + json-schema-traverse@1.0.0: {} + + json5@2.2.3: {} + + jsonc-parser@2.3.1: {} + + jsonc-parser@3.3.1: {} + + kind-of@6.0.3: {} + + kleur@3.0.3: {} + + kleur@4.1.5: {} + + lilconfig@2.1.0: {} + + lilconfig@3.1.2: {} + + lines-and-columns@1.2.4: {} + + load-yaml-file@0.2.0: + dependencies: + graceful-fs: 4.2.11 + js-yaml: 3.14.1 + pify: 4.0.1 + strip-bom: 3.0.0 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + lodash@4.17.21: {} + + log-symbols@6.0.0: + dependencies: + chalk: 5.3.0 + is-unicode-supported: 1.3.0 + + longest-streak@3.1.0: {} + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.12: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + magicast@0.3.5: + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + source-map-js: 1.2.1 + + markdown-table@3.0.4: {} + + mdast-util-definitions@6.0.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + unist-util-visit: 5.0.0 + + mdast-util-find-and-replace@3.0.1: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.1 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.0.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.0.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.0 + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.2.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + merge2@1.4.1: {} + + micromark-core-commonmark@2.0.2: + dependencies: + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.0.2 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.2 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-extension-gfm-table@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.1 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.0 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.1 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.0.2 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.1 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.0.2: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.1: {} + + micromark@4.0.1: + dependencies: + '@types/debug': 4.1.12 + debug: 4.3.7 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.2 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.0.2 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + transitivePeerDependencies: + - supports-color + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mimic-function@5.0.1: {} + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minipass@7.1.2: {} + + mrmime@2.0.0: {} + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.7: {} + + neotraverse@0.6.18: {} + + nlcst-to-string@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + + node-releases@2.0.18: {} + + normalize-path@3.0.0: {} + + normalize-range@0.1.2: {} + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + oniguruma-to-es@0.1.2: + dependencies: + emoji-regex-xs: 1.0.0 + regex: 4.4.0 + regex-recursion: 4.2.1 + + ora@8.1.1: + dependencies: + chalk: 5.3.0 + cli-cursor: 5.0.0 + cli-spinners: 2.9.2 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 6.0.0 + stdin-discarder: 0.2.2 + string-width: 7.2.0 + strip-ansi: 7.1.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@6.1.0: + dependencies: + yocto-queue: 1.1.1 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-queue@8.0.1: + dependencies: + eventemitter3: 5.0.1 + p-timeout: 6.1.3 + + p-timeout@6.1.3: {} + + p-try@2.2.0: {} + + package-json-from-dist@1.0.1: {} + + parse-latin@7.0.0: + dependencies: + '@types/nlcst': 2.0.3 + '@types/unist': 3.0.3 + nlcst-to-string: 4.0.0 + unist-util-modify-children: 4.0.0 + unist-util-visit-children: 3.0.0 + vfile: 6.0.3 + + parse5@7.2.1: + dependencies: + entities: 4.5.0 + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.2: {} + + pify@2.3.0: {} + + pify@4.0.1: {} + + pirates@4.0.6: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + postcss-import@15.1.0(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.8 + + postcss-js@4.0.1(postcss@8.4.49): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.4.49 + + postcss-load-config@4.0.2(postcss@8.4.49): + dependencies: + lilconfig: 3.1.2 + yaml: 2.6.0 + optionalDependencies: + postcss: 8.4.49 + + postcss-nested@6.2.0(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + postcss-selector-parser: 6.1.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.4.49: + dependencies: + nanoid: 3.3.7 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + preferred-pm@4.0.0: + dependencies: + find-up-simple: 1.0.0 + find-yarn-workspace-root2: 1.2.16 + which-pm: 3.0.0 + + prettier@2.8.7: + optional: true + + prismjs@1.29.0: {} + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + property-information@6.5.0: {} + + queue-microtask@1.2.3: {} + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + readdirp@4.0.2: {} + + regex-recursion@4.2.1: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@4.4.0: {} + + rehype-parse@9.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-from-html: 2.0.3 + unified: 11.0.5 + + rehype-raw@7.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-raw: 9.1.0 + vfile: 6.0.3 + + rehype-stringify@10.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.3 + unified: 11.0.5 + + rehype@13.0.2: + dependencies: + '@types/hast': 3.0.4 + rehype-parse: 9.0.1 + rehype-stringify: 10.0.1 + unified: 11.0.5 + + remark-gfm@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.0.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + micromark-util-types: 2.0.1 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.0 + unified: 11.0.5 + vfile: 6.0.3 + + remark-smartypants@3.0.2: + dependencies: + retext: 9.0.0 + retext-smartypants: 6.2.0 + unified: 11.0.5 + unist-util-visit: 5.0.0 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + request-light@0.5.8: {} + + request-light@0.7.0: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve@1.22.8: + dependencies: + is-core-module: 2.15.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + retext-latin@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + parse-latin: 7.0.0 + unified: 11.0.5 + + retext-smartypants@6.2.0: + dependencies: + '@types/nlcst': 2.0.3 + nlcst-to-string: 4.0.0 + unist-util-visit: 5.0.0 + + retext-stringify@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + nlcst-to-string: 4.0.0 + unified: 11.0.5 + + retext@9.0.0: + dependencies: + '@types/nlcst': 2.0.3 + retext-latin: 4.0.0 + retext-stringify: 4.0.0 + unified: 11.0.5 + + reusify@1.0.4: {} + + rollup@4.27.2: + dependencies: + '@types/estree': 1.0.6 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.27.2 + '@rollup/rollup-android-arm64': 4.27.2 + '@rollup/rollup-darwin-arm64': 4.27.2 + '@rollup/rollup-darwin-x64': 4.27.2 + '@rollup/rollup-freebsd-arm64': 4.27.2 + '@rollup/rollup-freebsd-x64': 4.27.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.27.2 + '@rollup/rollup-linux-arm-musleabihf': 4.27.2 + '@rollup/rollup-linux-arm64-gnu': 4.27.2 + '@rollup/rollup-linux-arm64-musl': 4.27.2 + '@rollup/rollup-linux-powerpc64le-gnu': 4.27.2 + '@rollup/rollup-linux-riscv64-gnu': 4.27.2 + '@rollup/rollup-linux-s390x-gnu': 4.27.2 + '@rollup/rollup-linux-x64-gnu': 4.27.2 + '@rollup/rollup-linux-x64-musl': 4.27.2 + '@rollup/rollup-win32-arm64-msvc': 4.27.2 + '@rollup/rollup-win32-ia32-msvc': 4.27.2 + '@rollup/rollup-win32-x64-msvc': 4.27.2 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + section-matter@1.0.0: + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + + semver@6.3.1: {} + + semver@7.6.3: {} + + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.0.3 + semver: 7.6.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shiki@1.23.0: + dependencies: + '@shikijs/core': 1.23.0 + '@shikijs/engine-javascript': 1.23.0 + '@shikijs/engine-oniguruma': 1.23.0 + '@shikijs/types': 1.23.0 + '@shikijs/vscode-textmate': 9.3.0 + '@types/hast': 3.0.4 + + signal-exit@4.1.0: {} + + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + + sisteransi@1.0.5: {} + + source-map-js@1.2.1: {} + + space-separated-tokens@2.0.2: {} + + sprintf-js@1.0.3: {} + + stdin-discarder@0.2.2: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.4.0 + get-east-asian-width: 1.3.0 + strip-ansi: 7.1.0 + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + strip-bom-string@1.0.0: {} + + strip-bom@3.0.0: {} + + sucrase@3.35.0: + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + commander: 4.1.1 + glob: 10.4.5 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.6 + ts-interface-checker: 0.1.13 + + supports-preserve-symlinks-flag@1.0.0: {} + + tailwindcss@3.4.15: + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.2 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.6 + lilconfig: 2.1.0 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.4.49 + postcss-import: 15.1.0(postcss@8.4.49) + postcss-js: 4.0.1(postcss@8.4.49) + postcss-load-config: 4.0.2(postcss@8.4.49) + postcss-nested: 6.2.0(postcss@8.4.49) + postcss-selector-parser: 6.1.2 + resolve: 1.22.8 + sucrase: 3.35.0 + transitivePeerDependencies: + - ts-node + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinyexec@0.3.1: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + trim-lines@3.0.1: {} + + trough@2.2.0: {} + + ts-interface-checker@0.1.13: {} + + tsconfck@3.1.4(typescript@5.6.3): + optionalDependencies: + typescript: 5.6.3 + + tslib@2.8.1: + optional: true + + type-fest@4.27.0: {} + + typesafe-path@0.2.2: {} + + typescript-auto-import-cache@0.3.5: + dependencies: + semver: 7.6.3 + + typescript@5.6.3: {} + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-find-after@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-modify-children@4.0.0: + dependencies: + '@types/unist': 3.0.3 + array-iterate: 2.0.1 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-remove-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-visit: 5.0.0 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-children@3.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + update-browserslist-db@1.1.1(browserslist@4.24.2): + dependencies: + browserslist: 4.24.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + util-deprecate@1.0.2: {} + + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + + vfile-message@4.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.2 + + vite@5.4.11: + dependencies: + esbuild: 0.21.5 + postcss: 8.4.49 + rollup: 4.27.2 + optionalDependencies: + fsevents: 2.3.3 + + vitefu@1.0.3(vite@5.4.11): + optionalDependencies: + vite: 5.4.11 + + volar-service-css@0.0.62(@volar/language-service@2.4.10): + dependencies: + vscode-css-languageservice: 6.3.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + optionalDependencies: + '@volar/language-service': 2.4.10 + + volar-service-emmet@0.0.62(@volar/language-service@2.4.10): + dependencies: + '@emmetio/css-parser': 0.4.0 + '@emmetio/html-matcher': 1.3.0 + '@vscode/emmet-helper': 2.10.0 + vscode-uri: 3.0.8 + optionalDependencies: + '@volar/language-service': 2.4.10 + + volar-service-html@0.0.62(@volar/language-service@2.4.10): + dependencies: + vscode-html-languageservice: 5.3.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + optionalDependencies: + '@volar/language-service': 2.4.10 + + volar-service-prettier@0.0.62(@volar/language-service@2.4.10): + dependencies: + vscode-uri: 3.0.8 + optionalDependencies: + '@volar/language-service': 2.4.10 + + volar-service-typescript-twoslash-queries@0.0.62(@volar/language-service@2.4.10): + dependencies: + vscode-uri: 3.0.8 + optionalDependencies: + '@volar/language-service': 2.4.10 + + volar-service-typescript@0.0.62(@volar/language-service@2.4.10): + dependencies: + path-browserify: 1.0.1 + semver: 7.6.3 + typescript-auto-import-cache: 0.3.5 + vscode-languageserver-textdocument: 1.0.12 + vscode-nls: 5.2.0 + vscode-uri: 3.0.8 + optionalDependencies: + '@volar/language-service': 2.4.10 + + volar-service-yaml@0.0.62(@volar/language-service@2.4.10): + dependencies: + vscode-uri: 3.0.8 + yaml-language-server: 1.15.0 + optionalDependencies: + '@volar/language-service': 2.4.10 + + vscode-css-languageservice@6.3.1: + dependencies: + '@vscode/l10n': 0.0.18 + vscode-languageserver-textdocument: 1.0.12 + vscode-languageserver-types: 3.17.5 + vscode-uri: 3.0.8 + + vscode-html-languageservice@5.3.1: + dependencies: + '@vscode/l10n': 0.0.18 + vscode-languageserver-textdocument: 1.0.12 + vscode-languageserver-types: 3.17.5 + vscode-uri: 3.0.8 + + vscode-json-languageservice@4.1.8: + dependencies: + jsonc-parser: 3.3.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-languageserver-types: 3.17.5 + vscode-nls: 5.2.0 + vscode-uri: 3.0.8 + + vscode-jsonrpc@6.0.0: {} + + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.16.0: + dependencies: + vscode-jsonrpc: 6.0.0 + vscode-languageserver-types: 3.16.0 + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.16.0: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver@7.0.0: + dependencies: + vscode-languageserver-protocol: 3.16.0 + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + + vscode-nls@5.2.0: {} + + vscode-uri@3.0.8: {} + + web-namespaces@2.0.1: {} + + which-pm-runs@1.1.0: {} + + which-pm@3.0.0: + dependencies: + load-yaml-file: 0.2.0 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + widest-line@5.0.0: + dependencies: + string-width: 7.2.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + wrap-ansi@9.0.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 7.2.0 + strip-ansi: 7.1.0 + + xxhash-wasm@1.0.2: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yaml-language-server@1.15.0: + dependencies: + ajv: 8.17.1 + lodash: 4.17.21 + request-light: 0.5.8 + vscode-json-languageservice: 4.1.8 + vscode-languageserver: 7.0.0 + vscode-languageserver-textdocument: 1.0.12 + vscode-languageserver-types: 3.17.5 + vscode-nls: 5.2.0 + vscode-uri: 3.0.8 + yaml: 2.2.2 + optionalDependencies: + prettier: 2.8.7 + + yaml@2.2.2: {} + + yaml@2.6.0: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@1.1.1: {} + + zod-to-json-schema@3.23.5(zod@3.23.8): + dependencies: + zod: 3.23.8 + + zod-to-ts@1.2.0(typescript@5.6.3)(zod@3.23.8): + dependencies: + typescript: 5.6.3 + zod: 3.23.8 + + zod@3.23.8: {} + + zwitch@2.0.4: {} diff --git a/web/src/components/Hero.astro b/web/src/components/Hero.astro new file mode 100644 index 0000000..94c7e34 --- /dev/null +++ b/web/src/components/Hero.astro @@ -0,0 +1,19 @@ +--- +import {type ImageMetadata} from "astro"; +import {Picture} from "astro:assets"; + +export type Props = { + image: ImageMetadata, + alt: string +} +--- +
+
+ +
+
+
+ +
+
diff --git a/web/src/components/NavBar.astro b/web/src/components/NavBar.astro new file mode 100644 index 0000000..d98094f --- /dev/null +++ b/web/src/components/NavBar.astro @@ -0,0 +1,26 @@ +--- +import NavLink from "./NavLink.astro"; +export type Props = { + navStyle?: 'transparent' | 'full' +} + +const navbar = Astro.props.navStyle ?? 'full'; +--- + + diff --git a/web/src/components/NavLink.astro b/web/src/components/NavLink.astro new file mode 100644 index 0000000..dce002b --- /dev/null +++ b/web/src/components/NavLink.astro @@ -0,0 +1,11 @@ +--- +type Props = { + link: string +}; +--- + +
  • + + + +
  • diff --git a/web/src/env.d.ts b/web/src/env.d.ts new file mode 100644 index 0000000..e16c13c --- /dev/null +++ b/web/src/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/web/src/layouts/Base.astro b/web/src/layouts/Base.astro new file mode 100644 index 0000000..da71f41 --- /dev/null +++ b/web/src/layouts/Base.astro @@ -0,0 +1,13 @@ +--- +import Head, {type Props as HeadProps} from "./Head.astro"; +type Props = { +} & HeadProps; +--- + + + + + +
    + + diff --git a/web/src/layouts/Content.astro b/web/src/layouts/Content.astro new file mode 100644 index 0000000..51ad0b8 --- /dev/null +++ b/web/src/layouts/Content.astro @@ -0,0 +1,63 @@ +--- +--- + +
    + +
    + diff --git a/web/src/layouts/Head.astro b/web/src/layouts/Head.astro new file mode 100644 index 0000000..2732105 --- /dev/null +++ b/web/src/layouts/Head.astro @@ -0,0 +1,12 @@ +--- +export type Props = { + title: string +} +--- + + + + + + {Astro.props.title} + diff --git a/web/src/pages/docs/_texture-pack-format.md b/web/src/pages/docs/_texture-pack-format.md new file mode 100644 index 0000000..3575cfc --- /dev/null +++ b/web/src/pages/docs/_texture-pack-format.md @@ -0,0 +1,832 @@ + + +# Custom SkyBlock Items Texture Pack Format + +## Items by internal id (ExtraAttributes) + +Find the internal id of the item. This is usually stored in the ExtraAttributes tag (Check the Power User Config for +keybinds). Once you found it, create an item model in a resource pack like you would for +a vanilla item model, but at the coordinate `firmskyblock:`. So for an aspect of the end, this would be +`firmskyblock:models/item/aspect_of_the_end.json` (or `assets/firmskyblock/models/item/aspect_of_the_end.json`). Then, +just use a normal minecraft item model. See https://github.com/nea89o/BadSkyblockTP/blob/master/assets/firmskyblock/models/item/magma_rod.json +as an example. The id is first turned to lower case, then gets `:` replaced with `___`, `;` with `__` and all other +characters that cannot be used in a minecraft resource location with `__XXXX` where `XXXX` is the 4 digit hex code for +the character. + +## (Placed) Skulls by texture id + +Find the texture id of a skull. This is the hash part of an url like +`https://textures.minecraft.net/texture/bc8ea1f51f253ff5142ca11ae45193a4ad8c3ab5e9c6eec8ba7a4fcb7bac40` (so after the +/texture/). You can find it in game for placed skulls using the keybinding in the Power User Config. Then place the +replacement texture at `firmskyblock:textures/placedskulls/.png`. Keep in mind that you will probably replace +the texture with another skin texture, meaning that skin texture has its own hash. Do not mix those up, you need to use +the hash of the old skin. + +## Armor Skull Models + +You can replace the models of skull items (or other items) by specifying the `firmament:head_model` property on your +model. Note that this is resolved *after* all [overrides](#predicates) and further predicates are not resolved on the +head model. + +```json5 +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "firmskyblock:item/regular_texture" + }, + "firmament:head_model": "minecraft:block/diamond_block" // when wearing on the head render a diamond block instead (can be any item model, including custom ones) +} +``` + +## Tint Overrides + +Some items get naturally tinted by Minecraft's rendering. Examples include leather armour, spawn eggs, potions and more. +If you want to avoid your textures getting tinted, one thing you can do is use a higher texture layer: + +```json +{ + "parent": "minecraft:item/generated", + "textures": { + // Notice the layer1 instead of layer0 here + "layer1": "firmskyblock:item/regular_texture" + } +} +``` + +Some items, however, tint *all* layers. For those items you can instead specify a tint override: + +```json +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "firmskyblock:item/regular_texture" + }, + "firmament:tint_overrides": { + "0": -1 + } +} +``` + +This forces layer 0 to be tinted with the color `-1` (pure white, aka no tint). This property is inherited, so if you +attach it to one of your root models that you `"parent"` other models to, all those models will have their tints +overridden. When the property is inherited, only layers specified in the child actually overwrite the parent layers. +You can use `"0": null` to remove the tint override in a child, which will cause a fallback to the vanilla tinting +behaviour. + +## Predicates + +Firmament adds the ability for more complex [item model predicates](https://minecraft.wiki/w/Tutorials/Models#Item_predicates). +Those predicates work on any model, including models for vanilla items, but they don't mix very well with vanilla model overrides. +Vanilla predicates only ever get parsed at the top level, so including a vanilla predicate inside of a more complex +firmament parser will result in an ignored predicate. + +### Example usage + +```json +{ + "parent": "minecraft:item/handheld", + "textures": { + "layer0": "firmskyblock:item/bat_wand" + }, + "overrides": [ + { + "predicate": { + "firmament:display_name": { + "regex": ".*§d.*", + "color": "preserve" + } + }, + "model": "firmskyblock:item/recombobulated_bat_wand" + } + ] +} +``` + +You specify an override like normally, with a `model` that will replace the current model and a list of `predicate`s +that must match before that override takes place. + +At the top level `predicate` you can still use all the normal vanilla predicates, as well as the custom ones, which are +all prefixed with `firmament:`. + +#### Display Name + +Matches the display name against a [string matcher](#string-matcher) + +```json +"firmament:display_name": "Display Name Test" +``` + +#### Lore + +Tries to find at least one lore line that matches the given [string matcher](#string-matcher). + +```json +"firmament:lore": { + "regex": "Mode: Red Mushrooms", + "color": "strip" +} +``` + +#### Item type + +Filter by item type: + +```json +"firmament:item": "minecraft:clock" +``` + +#### Skulls + +You can match skulls using the skull textures and other properties using the skull predicate. If there are no properties specified this is equivalent to checking if the item is a `minecraft:player_head`. + +```json +"firmament:skull": { + "profileId": "cca2d452-c6d3-39cb-b695-5ec92b2d6729", + "textureProfileId": "1d5233d388624bafb00e3150a7aa3a89", + "skinUrl": "http://textures.minecraft.net/texture/7bf01c198f6e16965e230235cd22a5a9f4a40e40941234478948ff9a56e51775", + "textureValue": "ewogICJ0aW1lc3RhbXAiIDogMTYxODUyMTY2MzY1NCwKICAicHJvZmlsZUlkIiA6ICIxZDUyMzNkMzg4NjI0YmFmYjAwZTMxNTBhN2FhM2E4OSIsCiAgInByb2ZpbGVOYW1lIiA6ICIwMDAwMDAwMDAwMDAwMDBKIiwKICAic2lnbmF0dXJlUmVxdWlyZWQiIDogdHJ1ZSwKICAidGV4dHVyZXMiIDogewogICAgIlNLSU4iIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzdiZjAxYzE5OGY2ZTE2OTY1ZTIzMDIzNWNkMjJhNWE5ZjRhNDBlNDA5NDEyMzQ0Nzg5NDhmZjlhNTZlNTE3NzUiLAogICAgICAibWV0YWRhdGEiIDogewogICAgICAgICJtb2RlbCIgOiAic2xpbSIKICAgICAgfQogICAgfQogIH0KfQ" +} +``` + +| Name | Type | Description | +|--------------------|---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `profileId` | UUID | Match the uuid of the profile component directly. | +| `textureProfileId` | UUID | Match the uuid of the skin owner in the encoded texture value. This is more expensive, but can deviate from the profile id of the profile owner. | +| `skinUrl` | [string](#string-matcher) | Match the texture url of the skin. This starts with `http://`, not with `https:/` in most cases. | +| `textureValue` | [string](#string-matcher) | Match the texture value. This is the encoded base64 string of the texture url along with metadata. It is faster to query than the `skinUrl`, but it can out of changed without causing any semantic changes, and is less readable than the skinUrl. | + +#### Extra attributes + +Filter by extra attribute NBT data: + +Specify a `path` (using an [nbt prism](#nbt-prism)) to look at, separating sub elements with a `.`. You can use a `*` to check any child. + +Then either specify a `match` sub-object or directly inline that object in the format of an [nbt matcher](#nbt-matcher). + +Inlined match: + +```json5 +"firmament:extra_attributes": { + "path": "gems.JADE_0", + "string": "PERFECT" +} +``` + +Sub object match: + +```json5 +"firmament:extra_attributes": { + "path": "gems.JADE_0", + "match": { + "string": "PERFECT" + } +} +``` + +#### Components + +You can match generic components similarly to [extra attributes](#extra-attributes). If you want to match an extra +attribute match directly using that, for better performance. + +You can specify a `path` (using an [nbt prism](#nbt-prism)) and match similar to extra attributes, but in addition you can also specify a `component`. This +variable is the identifier of a component type that will then be encoded to nbt and matched according to the `match` +using a [nbt matcher](#nbt-matcher). + +```json5 +"firmament:component": { + "path": "rgb", + "component": "minecraft:dyed_color", + "int": 255 +} +// Alternatively +"firmament:component": { + "path": "rgb", + "component": "minecraft:dyed_color", + "match": { + "int": 255 + } +} +``` + + +#### Pet Data + +Filter by pet information. While you can already filter by the skyblock id for pet type and tier, this allows you to +further filter by level and some other pet info. + +```json5 +"firmament:pet" { + "id": "WOLF", + "exp": ">=25353230", + "tier": "[RARE,LEGENDARY]", + "level": "[50,)", + "candyUsed": 0 +} +``` + +| Name | Type | Description | +|-------------|------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------| +| `id` | [String](#string-matcher) | The id of the pet | +| `exp` | [Number](#number-matcher) | The total experience of the pet | +| `tier` | Rarity (like [Number](#number-matcher), but with rarity names instead) | The total experience of the pet | +| `level` | [Number](#number-matcher) | The current level of the pet | +| `candyUsed` | [Number](#number-matcher) | The number of pet candies used on the pet. This is present even if they are not shown in game (such as on a level 100 legendary pet) | + +Every part of this matcher is optional. + + +#### Logic Operators + +Logic operators allow to combine other firmament predicates into one. This is done by building boolean operators: + +```json5 +"firmament:any": [ + { + "firmament:display_name": "SkyBlock Menu (Click)" + }, + { + "firmament:display_name": "SkyBlock", + "firmament:lore": "Some Lore Requirement" + } +] +``` + +This `firmament:any` test if the display name is either "SkyBlock Menu (Click)" or "SkyBlock" (aka any of the child predicates match). + +Similarly, there is `firmament:all`, which requires all of its children to match. + +There is also `firmament:not`, which requires none of its children to match. Unlike `any` or `all`, however, `not` +only takes in one predicate `{}` directly, not an array of predicates `[{}]`. + +Note also that by default all predicate dictionaries require all predicates in it to match, so you can imagine that all +things are wrapped in an implicit `firmament:all` element. + +### String Matcher + +A string matcher allows you to match almost any string. Whenever a string matcher is expected, you can use any of these +styles of creating one. + +#### Direct + +```json +"firmament:display_name": "Test" +``` + +Directly specifying a raw string value expects the string to be *exactly* equal, after removing all formatting codes. + +#### Complex + +A complex string matcher allows you to specify whether the string will get its color codes removed or not before matching + + +```json5 +"firmament:display_name": { + "color": "strip", + "color": "preserve", + // When omitting the color property alltogether, you will fall back to "strip" +} +``` +In that same object you can then also specify how the string will be matched using another property. You can only ever +specify one of these other matchers and one color preserving property. + +```json5 +"firmament:display_name": { + "color": "strip", + // You can use a "regex" property to use a java.util.Pattern regex. It will try to match the entire string. + "regex": "So[me] Regex", + // You can use an "equals" property to test if the entire string is equal to some value. + // Equals is faster than regex, but also more limited. + "equals": "Some Text" +} +``` + +### Number Matchers + +This matches a number against either a range or a specific number. + +#### Direct number + +You can directly specify a number using that value directly: +```json5 +"firmament:pet": { + "level": 100 +} +``` + +This is best for whole numbers, since decimal numbers can be really close together but still be different. + +#### Intervals + +For ranges you can instead use an interval. This uses the standard mathematical notation for those as a string: + + +```json5 +"firmament:pet": { + "level": "(50,100]" +} +``` + +This is in the format of `(min,max)` or `[min,max]`. Either min or max can be omitted, which results in that boundary +being ignored (so `[50,)` would be 50 until infinity). You can also vary the parenthesis on either side independently. + +Specifying round parenthesis `()` means the number is exclusive, so not including this number. For example `(50,100)` +would not match just the number `50` or `100`, but would match `51`. + +Specifying square brackets `[]` means the number is inclusive. For example `[50,100]` would match both `50` and `100`. + +You can mix and match parenthesis and brackets, they only ever affect the number next to it. + +For more information in intervals check out [Wikipedia](https://en.wikipedia.org/wiki/Interval_(mathematics)). + +#### Operators + +If instead of specifying a range you just need to specify one boundary you can also use the standard operators to +compare your number: + +```json5 +"firmament:pet": { + "level": "<50" +} +``` + +This example would match if the level is less than fifty. The available operators are `<`, `>`, `<=` and `>=`. The +operator needs to be specified on the left. The versions of the operator with `=` also allow the number to be equal. + +### Nbt Prism + +An nbt prism (or path) is used to specify where in a complex nbt construct to look for a value. A basic prism just looks +like a dot-separated path (`parent.child.grandchild`), but more complex paths can be constructed. + +First the specified path is split into dot separated chunks: `"a.b.c"` -> `["a", "b", "c"]`. You can also directly +specify the list if you would like. Any entry in that list not starting with a `*` is treated as an attribute name or +an index: + +```json +{ + "propA": { + "propB": { + "propC": 100, + "propD": 1000 + } + }, + "someOtherProp": "hello", + "someThirdProp": "{\"innerProp\": true}", + "someFourthProp": "aGlkZGVuIHZhbHVl" +} +``` + +In this example json (which is supposed to represent a corresponding nbt object), you can use a path like +`propA.propB.propC` to directly extract the value `100`. + +If you want to extract all of the innermost values of `propB` +(for example if `propB` was an array instead), you could use `propA.propB.*`. You can use the `*` at any level: +`*.*.*` for example extracts all properties that are exactly at the third level. In that case you would try to match any +of the values of `[100, 1000]` to your match object. + +Sometimes values are encoded in a non-nbt format inside a string. For those you can use other star based directives like +`*base64` or `*json` to decode those entries. + +`*base64` turns a base64 encoded string into the base64 decoded counterpart. `*json` decodes a string into the json +object represented by that string. Note that json to nbt conversion isn't always straightforwards and the types can +end up being mangled (for example what could have been a byte ends up an int). + +| Path | Result | +|---------------------------------|---------------------------------| +| `propA.propB` | `{"propC": 100, "propD": 1000}` | +| `propA.propB.propC` | `100` | +| `propA.*.propC` | `100` | +| `propA.propB.*` | `100`, `1000` | +| `someOtherProp` | `"hello"` | +| `someThirdProp` | "{\"innerProp\": true}" | +| `someThirdProp.*json` | {"innerProp": true} | +| `someThirdProp.*json.innerProp` | true | +| `someFourthProp` | `"aGlkZGVuIHZhbHVl"` | +| `someFourthProp.*base64` | `"hidden value"` | + + +### Nbt Matcher + +This matches a single nbt element. + +Have the type of the nbt element as json key. Can be `string`, `int`, `float`, `double`, `long`, `short` and `byte`. + +The `string` type matches like a regular [string matcher](#string-matcher): + +```json +"string": { + "color": "strip", + "regex": "^aaa bbb$" +} +``` + +The other (numeric) types can either be matched directly against a number: + +```json +"int": 10 +``` + +Or as a range: + +```json +"long": { + "min": 0, + "max": 1000 +} +``` + +Min and max are both optional, but you need to specify at least one. By default `min` is inclusive and `max` is exclusive. +You can override that like so: + +```json +"short": { + "min": 0, + "max": 1000, + "minExclusive": true, + "maxExclusive": false +} +``` + + +> [!WARNING] +> This syntax for numbers is *just* for **NBT values**. This is also why specifying the type of the number is necessary. +> For other number matchers, use [the number matchers](#number-matchers) + +## Armor textures + +You can re-*texture* armors, but not re-*model* them with firmament. + +To retexture a piece of armor place a json file at `assets/firmskyblock/overrides/armor_models/*.json`. + +```json +{ + "item_ids": [ + "TARANTULA_BOOTS", + "TARANTULA_LEGGINGS", + // ETC + ], + "layers": [ + { + "identifier": "firmskyblock:tarantula" + } + ] +} +``` + +Only one such file can exist per item id, but multiple item ids can share one texture file this way. + +The `item_ids` is the items to which this override will apply when worn. Those are neu repo ids (so what will be shown +in game as the regular SkyBlock id, not the resource pack identifier). + +### Layers + +The `layers` specify the multiple texture layers that will be used when rendering. For non leather armor, or armor +ignoring the leather armor tint just one layer is enough. + +If you want to apply armor tint to the texture you will usually want two layers. The first layer has a tint applied: + +```json +{ + "identifier": "firmskyblock:angler", + "tint": true +} +``` + +This will tint the texture before it is being rendered. + +The second layer will have no tint applied, but will have a suffix: + +```json +{ + "identifier": "firmskyblock:angler", + "suffix": "_overlay" +} +``` + +This second layer is used for the countours of the armor. + +The layer identifier will resolve to a texture file path according to vanilla armor texture rules like so: + +`assets/{identifier.namespace}/textures/models/armor/{identifier.path}_layer_{isLegs ? 2 : 1}{suffix}.png` + +Note that there is no automatic underscore insertion for suffix, so you will need to manually specify it if you want. + +The leg armor piece uses a different texture, same as with vanilla. + +### Overrides + +You can also apply overrides to these layers. These work similar to item predicate overrides, but only the custom +Firmament predicates will work. You will also just directly specify new layers instead of delegating to another file. + +```json +{ + "item_ids": [ + "TARANTULA_BOOTS", + "TARANTULA_LEGGINGS", + // ETC + ], + "layers": [ + { + "identifier": "firmskyblock:tarantula" + } + ], + "overrides": [ + { + "layers": [ + { + "identifier": "firmskyblock:tarantula_maxed" + } + ], + "predicate": { + "firmament:lore": { + "regex": "Piece Bonus: +285.*" + } + } + } + ] +} +``` + +## UI Text Color Replacement + +This allows you to replace the color of text in your inventory. This includes inventory UIs like chests and anvils, but +not screens from other mods. You can also target specific texts via a [string matcher](#string-matcher). + +```json +// This file is at assets/firmskyblock/overrides/text_colors.json +{ + "defaultColor": -10496, + "overrides": [ + { + "predicate": "Crafting", + "override": -16711936 + } + ] +} +``` + +| Field | Required | Description | +|-----------------------|----------|----------------------------------------------------------------------------------------------------| +| `defaultColor` | true | The default color to use in case no override matches | +| `overrides` | false | Allows you to replace colors for specific strings. Is an array. | +| `overrides.predicate` | true | This is a [string matcher](#string-matcher) that allows you to match on the text you are replacing | +| `overrides.override` | true | This is the replacement color that will be used if the predicate matches. | + +## Screen Layout Replacement + +You can change the layout of an entire screen by using screen layout overrides. These get placed in `firmskyblock:overrides/screen_layout/*.json`, with one file per screen. You can match on the title of a screen, the type of screen, replace the background texture (including extending the background canvas further than vanilla allows you) and move slots around. + +### Selecting a screen + +```json +{ + "predicates": { + "label": { + "regex": "Hyper Furnace" + }, + "screenType": "minecraft:furnace" + } +} +``` + +The `label` property is a regular [string matcher](#string-matcher) and matches against the screens title (typically the chest title, or "Crafting" for the players inventory). + +The `screenType` property is an optional namespaced identifier that allows matching to a [screen type](https://minecraft.wiki/w/Java_Edition_protocol/Inventory#Types). + +### Changing the background + +```json +{ + "predicates": { + "label": { + "regex": "Hyper Furnace" + } + }, + "background": { + "texture": "firmskyblock:textures/furnace.png", + "x": -21, + "y": -30, + "width": 197, + "height": 196 + } +} +``` + +You need to specify an x and y offset relative to where the regular screen would render. This means you just check where the upper left corner of the UI texture would be in your texture (and turn it into a negative number). You also need to specify a width and height of your texture. This is the width in pixels rendered. If you want a higher or lower resolution texture, you can scale the actual texture up (tho it is expected to meet the same aspect ratio as the one defined here). + +### Moving slots around + +```json +{ + "predicates": { + "label": { + "regex": "Hyper Furnace" + } + }, + "slots": [ + { + "index": 10, + "x": -5000, + "y": -5000 + } + ] +} +``` + +You can move slots around by a specific index. This is not the index in the inventory, but rather the index in the screen (so if you have a chest screen then all the player inventory slots would be a higher index since the chest slots move them down the list). The x and y are relative to where the regular screen top left would be. Set to large values to effectively "delete" a slot by moving it offscreen. + +### Moving text around + +```json +{ + "predicates": { + "label": { + "regex": "Hyper Furnace" + } + }, + "playerTitle": { + "x": 0, + "y": 0, + "align": "left", + "replace": "a" + } +} +``` + +You can move the window title around. The x and y are relative to the top left of the regular screen (like slots). Set to large values to effectively "delete" a slot by moving it offscreen. + +The align only specifies the direction the text grows in, it does not the actual anchor point, so if you want right aligned text you will also need to move the origin of the text to the right (or it will just grow out of the left side of your screen). + +You can replace the text with another text to render instead. + +Available titles are + +- `containerTitle` for the title of the open container, typically at the very top. +- `playerTitle` for the players inventory title. Note that in the player inventory without a chest or something open, the `containerTitle` is also used for the "Crafting" text. +- `repairCostTitle` for the repair cost label in anvils. + +### Moving components around + +```json +{ + "predicates": { + "label": { + "regex": "Hyper Furnace" + } + }, + "nameField": { + "x": 10, + "y": 10, + "width": 100, + "height": 12 + } +} +``` + +Some other components can also be moved. These components might be buttons, text inputs or other things not fitting into any category. They can have a x, y (relative to the top left of the screen), as well as sometimes a width, height, and other properties. This is more of a wild card category, and which options work depends on the type of object. + +Available options + +- `nameField`: x, y, width & height are all available to move the field to set the name of the item in an anvil. + +### All together + +| Field | Required | Description | +|---------------------------|----------|--------------------------------------------------------------------------------------------------------------------------| +| `predicates` | true | A list of predicates that need to match in order to change the layout of a screen | +| `predicates.label` | true | A [string matcher](#string-matcher) for the screen title | +| `background` | false | Allows replacing the background texture | +| `background.texture` | true | The texture of the background as an identifier | +| `background.x` | true | The x offset of the background relative to where the regular background would be rendered. | +| `background.y` | true | The y offset of the background relative to where the regular background would be rendered. | +| `background.width` | true | The width of the background texture. | +| `background.height` | true | The height of the background texture. | +| `slots` | false | An array of slots to move around. | +| `slots[*].index` | true | The index in the array of all slots on the screen (not inventory). | +| `slots[*].x` | true | The x coordinate of the slot relative to the top left of the screen | +| `slots[*].y` | true | The y coordinate of the slot relative to the top left of the screen | +| `Title` | false | The title mover (see above for valid options) | +| `Title.x` | false | The x coordinate of text relative to the top left of the screen | +| `Title.y` | false | The y coordinate of text relative to the top left of the screen | +| `Title.align` | false | How you want the text to align. "left", "center" or "right". This only changes the text direction, not its anchor point. | +| `Title.replace` | false | Replace the text with your own text | +| `` | false | Allows you to move button components and similar around | +| `.x` | true | The new x coordinate of the component relative to the top left of the screen | +| `.x` | true | The new y coordinate of the component relative to the top left of the screen | +| `.width` | false | The new width of the component | +| `.height` | false | The new height of the component | + + + +## Global Item Texture Replacement + +Most texture replacement is done based on the SkyBlock id of the item. However, some items you might want to re-texture +do not have an id. The next best alternative you had before was just to replace the vanilla item and add a bunch of +predicates. This tries to fix this problem, at the cost of being more performance intensive than the other re-texturing +methods. + +The entrypoint to global overrides is `firmskyblock:overrides/item`. Put your overrides into that folder, with one file +per override. + +```json5 +{ + "screen": "testrp:chocolate_factory", + "model": "testrp:time_tower", + "predicate": { + "firmament:display_name": { + "regex": "Time Tower.*" + } + } +} +``` + +There are three parts to the override. + +The `model` is an *item id* that the item will be replaced with. This means the +model will be loaded from `assets//models/item/.json`. Make sure to use your own namespace to +avoid collisions with other texture packs that might use the same id for a gui. + +The `predicate` is just a normal [predicate](#predicates). This one does not support the vanilla predicates. You can +still use vanilla predicates in the resolved model, but this will not allow you to fall back to other global overrides. + +The `screen` specifies which screens your override will work on. This is purely for performance reasons, your filter +should work purely based on predicates if possible. You can specify multiply screens by using a json array. + +### Global item texture Screens + +In order to improve performance not all overrides are tested all the time. Instead you can prefilter by the screen that +is open. First the gui is resolved to `assets//filters/screen/.json`. Make sure to use your own namespace +to avoid collisions with other texture packs that might use the same id for a screen. + +```json +{ + "title": "Chocolate Factory" +} +``` + +Currently, the only supported filter is `title`, which accepts a [string matcher](#string-matcher). You can also use +`firmament:always` as an always on filter (this is the recommended way). + +## Block Model Replacements + +Firmament adds the ability to retexture block models. Supported renderers are vanilla, indigo (fabric), sodium (and +anything sodium based). Firmament performs gentle world reloading so that even when the world data gets updated very +late by the server there should be no flicker. + +If you want to replace block textures in the world you can do so using block overrides. Those are stored in +`assets/firmskyblock/overrides/blocks/.json`. The id does not matter, all overrides are loaded. This file specifies +which block models are replaced under which conditions: + +```json +{ + "modes": [ + "mining_3" + ], + "area": [ + { + "min": [ + -31, + 200, + -117 + ], + "max": [ + 12, + 223, + -95 + ] + } + ], + "replacements": { + "minecraft:blue_wool": "firmskyblock:mithril_deep", + "minecraft:light_blue_wool": { + "block": "firmskyblock:mithril_deep", + "sound": "minecraft:block.wet_sponge.hit" + } + } +} +``` + +The referenced `block` can either be a regular json block model (like the ones in `assets/minecraft/blocks/`), or it can +reference a blockstates json like in `assets//blockstates/.json`. The blockstates.json is prefered and +needs to match the vanilla format, so it is best to copy over the vanilla blockstates.json for the block you are editing +and replace all block model paths with your own custom block models. + +| Field | Required | Description | +|-------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `modes` | yes | A list of `/locraw` mode names. | +| `area` | no | A list of areas. Blocks outside of the coordinate range will be ignored. If the block is in *any* range it will be considered inside | +| `area.min` | yes | The lowest coordinate in the area. Is included in the area. | +| `area.max` | yes | The highest coordinate in the area. Is included in the area. | +| `replacements` | yes | A map of block id to replacement mappings | +| `replacements` (string) | yes | You can directly specify a string. Equivalent to just setting `replacements.block`. | +| `replacements.block` | yes | You can specify a block model to be used instead of the regular one. The model will be loaded from `assets//models/block/.json` like regular block models. | +| `replacements.sound` | no | You can also specify a sound override. This is only used for the "hit" sound effect that repeats while the block is mined. The "break" sound effect played after a block was finished mining is sadly sent by hypixel directly and cannot be replaced reliably. | + +> A quick note about optimization: Not specifying an area (by just omitting the `area` field) is quicker than having an +> area encompass the entire map. +> +> If you need to use multiple `area`s for unrelated sections of the world it might be a performance improvement to move +> unrelated models to different files to reduce the amount of area checks being done for each block. diff --git a/web/src/pages/docs/texture-pack-format.astro b/web/src/pages/docs/texture-pack-format.astro new file mode 100644 index 0000000..0714aea --- /dev/null +++ b/web/src/pages/docs/texture-pack-format.astro @@ -0,0 +1,15 @@ +--- +import Base from "../../layouts/Base.astro"; +import NavBar from "../../components/NavBar.astro"; +import Content from "../../layouts/Content.astro"; +import MarkDown from './_texture-pack-format.md'; +--- + + + + + + + + + diff --git a/web/src/pages/index.astro b/web/src/pages/index.astro new file mode 100644 index 0000000..4baa867 --- /dev/null +++ b/web/src/pages/index.astro @@ -0,0 +1,20 @@ +--- +import Hero from "../components/Hero.astro" +import Base from "../layouts/Base.astro"; +import Image from "../panorama.png"; +import NavBar from "../components/NavBar.astro"; +import Content from "../layouts/Content.astro"; +--- + + + +

    Firmament

    +

    Hypixel SkyBlock Utility Mod

    + Download now +
    + + + Firmament is a 1.21 Minecraft mod for Hypixel SkyBlock. It has a variety of features such as an item list, + mining features. + + diff --git a/web/src/pages/texture-packs.astro b/web/src/pages/texture-packs.astro new file mode 100644 index 0000000..ee511f9 --- /dev/null +++ b/web/src/pages/texture-packs.astro @@ -0,0 +1,9 @@ +--- +import Base from "../layouts/Base.astro"; +import NavBar from "../components/NavBar.astro"; +--- + + + + + diff --git a/web/src/panorama.png b/web/src/panorama.png new file mode 100644 index 0000000..d1ef600 Binary files /dev/null and b/web/src/panorama.png differ diff --git a/web/tailwind.config.mjs b/web/tailwind.config.mjs new file mode 100644 index 0000000..19f0614 --- /dev/null +++ b/web/tailwind.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'], + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..fbc2f5f --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "astro/tsconfigs/strictest" +}